0

PolyCryptoUpDown

Fresh

Degenmaxx on Polymarket's short term crypto binary options, in a long-horizon RL environment.

Type
RL Env
Runtime
ORS
License
unknown
Size
9 tasks
Published
Jun 2026
Updated
Nov 2026

Cite

Notes

Only stored in your browser.

Polymarket Realtime Crypto Binaries

OpenReward environment that lets an agent paper-trade Polymarket's 5-minute and 15-minute crypto "Up or Down" binary markets in real time, across every underlying Polymarket lists them for (BTC, ETH, SOL, XRP, DOGE, BNB, HYPE, and any new ones — matched by slug, not a fixed asset list). The env server connects to Polymarket's live REST + WebSocket APIs, simulates taker fills against its own cached order books, and emits per-settled-market USD-PnL reward.

The agent never places real orders on Polymarket — execution is fully simulated. The agent's sandbox includes a pre-installed Polymarket L2 helper so it can subscribe to live order books directly for low-latency decision making. Polymarket's public L2 market data feed is unauthenticated, so no API token is provisioned to the sandbox.

What the agent sees

Market operations are not tools — they're REST endpoints on the env server (server.py) that the agent's sandbox code calls through the pre-installed polymarket_env_sandbox.bridge client. The only model-visible tools are the sandbox CLI toolset (bash/read/write/edit/…) and collect_reward; the agent writes and runs code to trade rather than issuing trade tool calls.

The bridge (and the routes behind it) expose:

  • active_markets(kind) — current crypto 5m/15m markets (all underlyings) with token ids, expiries, mids. kind filters by asset ('BTC', 'SOL', ...), duration ('5m'/'15m'), exact 'ASSET-DURATION', or 'all'.
  • place_trade(side, market_id, quantity) — FAK taker order against the live book; returns fills, VWAP, fee, new position.
  • quote(side, market_id, quantity) — dry-run pricing of the same order (VWAP, fee, budget impact) without executing or touching the rate limit/budget.
  • positions() — open positions with marks and unrealized PnL.
  • balance() — running realized + unrealized PnL since session start.
  • settlement_history() — markets settled this session and their PnL.

Each Polymarket binary has two outcome tokens (Up and Down); they are listed and traded as separate positions. There is no shorting — SELL only closes existing positions. To bet the other way, BUY the opposite outcome's token.

Reward

Reward is realized USD PnL, booked whenever it happens: at settlement (shares × (outcome_price − avg_cost)) and at every fill (a SELL's (vwap − avg_cost) × qty, and any fee), so the sum of rewards always equals balance().realized_pnl. Market ops are REST calls, not tools, so reward can't ride on them; instead PnL realized between tool calls is attached to the next toolset tool call's ToolOutput.reward (bash/read/…), with breakdowns in metadata.settlements and metadata.trade_pnl. This makes reward delivery independent of any harness knowing about collect_reward, which remains as an explicit force-a-drain. Multiple intervening settlements combine into a single emission.

Difficulty

Two knobs in the session's task_spec:

fee_multiplier (Θ ∈ [0, 1]) — a taker fee charged per fill leg:

fee_leg = fee_multiplier × 0.07 × size × p × (1 − p)

fee_multiplier=0 disables fees; 1.0 matches Polymarket's full crypto-market taker fee (0.07 × p × (1−p)).

duration_filter ("all" | "5m" | "15m") — restricts the tradeable universe to one market duration. "all" admits both; "5m"/"15m" admit only that duration. Enforced server-side: active_markets() shows only in-scope markets, and place_trade()/quote() on an out-of-scope market are rejected with OUT_OF_SCOPE.

Preset tasks (split default) are the duration × fee cross-product — nine tasks, ordered so the both-durations (all) tasks come first and, within each duration, full fees come first, i.e. task[0] is the widest, full-fee universe:

#task_idduration_filterfee_multiplier
0all_full_feesall1.0
1all_half_feesall0.5
2all_zero_feesall0.0
35m_full_fees5m1.0
45m_half_fees5m0.5
55m_zero_fees5m0.0
615m_full_fees15m1.0
715m_half_fees15m0.5
815m_zero_fees15m0.0

Settlement buffer

All sessions enforce a fixed 60 s no-trade buffer before each market's end_time (settlement_buffer_s in task_spec, defaulting to 60). Trades that would land inside the window are rejected with SETTLEMENT_BUFFER. Without it, agents harvest near-certain outcomes off the laggy order book in the final seconds — giving a ~100% win rate that fees barely dent. Forcing entry ≥60 s before close leaves real residual price uncertainty, so the edge has to come from forecasting rather than picking up already-decided markets.

Local development

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/python -m pytest tests/                    # 55 tests, no network
.venv/bin/python scripts/local_verify.py             # smoke test against live Polymarket
.venv/bin/python server.py                           # run the OpenReward server

Layout

env.py                       PolymarketEnvironment subclass + per-session state
server.py                    `python server.py` entrypoint + market-op REST routes
specs.py                     Pydantic models (config, request/result, settlement records)
prompt.py                    System prompt presented at session start
tasks.py                     Preset tasks / splits
polymarket_env/
    polymarket_client.py     REST + WS wrapper over Polymarket public APIs
    market_tracker.py        Polls Gamma every 30s for active crypto 5m/15m up/down markets (all underlyings)
    book_feed.py             Maintains per-token OrderBook cache from WS deltas
    book.py                  OrderBook + L2 diff application
    execution_sim.py         FAK ladder-walking matcher
    fees.py                  Per-leg taker fee
    positions.py             Portfolio, Position, realized/unrealized PnL
    settlement.py            Per-session settlement handler (fan-out target)
    reward_queue.py          Peek-then-ack settlement queue
    shared.py                Process-global Polymarket singleton + resolution detector
    tools.py                 Market-op methods (REST-exposed) + collect_reward tool + reward drain
sandbox/
    Dockerfile               Image baked with the L2 helper module
    polymarket_env_sandbox/  Agent-side helpers: L2 read client, price feed, trade bridge
tests/                       Unit tests + e2e env test (no network)
scripts/
    local_verify.py          Live-Polymarket smoke test (one BUY against a real book)
    live_settlement_test.py  Live-Polymarket settlement-loop check

Open items

  • SANDBOX_IMAGE in env.py is built and pushed to GHCR by .github/workflows/build-sandbox.yml (tagged sha-<gitsha>) and pinned to an immutable digest tag. Repin it to the new sha-… the workflow prints after each sandbox rebuild.
  • scripts/local_verify.py requires outbound network to gamma-api.polymarket.com, clob.polymarket.com, and ws-subscriptions-clob.polymarket.com.
  • Settlement resolution relies on Polymarket's outcomePrices returning to ["1","0"] or ["0","1"]. UMA can lag market endDate by minutes; the settlement loop honors a 30 s grace period and retries every 5 s.