Skip to main content

Price Feed

WS /v1/ws/prices?token=<jwt>
Streams real-time price updates for subscribed markets. All numeric values are strings.

Subscribe

After connecting, send a subscribe message:
{"action": "subscribe", "markets": ["0xabc123...", "0xdef456..."]}
Server confirms:
{"type": "subscribed", "markets": ["0xabc123...", "0xdef456..."]}

Price Updates

The server pushes price changes automatically. All fields are at the top level (not nested under a data key):
{
  "type": "price",
  "market_id": "0xabc123...",
  "condition_id": "0xabc123...",
  "buy": "0.67",
  "sell": "0.33",
  "best_bid": "0.66",
  "best_ask": "0.68",
  "last_trade": "0.67",
  "volume": "125000.50",
  "outcomes": [
    {"label": "Yes", "price": "0.67", "token_id": "71321..."},
    {"label": "No", "price": "0.33", "token_id": "71322..."}
  ],
  "tokens": {
    "71321...": {"label": "Yes", "price": "0.67"},
    "71322...": {"label": "No", "price": "0.33"}
  },
  "active": true,
  "closed": false,
  "source": "websocket",
  "emit_ts_ms": 1715518800250,
  "updated_at": "2026-05-11T20:30:45.250000+00:00",
  "ws_updated_at": "2026-05-11T20:30:45.250000+00:00"
}
The server broadcasts the cached price:{condition_id} payload with type and market_id stamped on top. Most fields are conditional — the exact set depends on whether the price came from the Gamma poller (REST), the CLOB WebSocket, the RTDS feed, or on-demand fetches during order placement. Always treat extra/missing fields defensively.
FieldTypeAlways presentDescription
typestringyesAlways "price" for price updates
market_idstringyesPolymarket condition_id
condition_idstringyesSame value as market_id — emitted for backward-compat with consumers that key off condition_id
emit_ts_msintyesServer wall-clock (milliseconds since epoch) stamped at the moment the frame is broadcast. Integer (not stringified). Purpose-built for HFT latency — compute end-to-end lag as recv_ts_ms - emit_ts_ms. See Latency telemetry below.
sourcestringyesProvenance of the cached snapshot — see Source values below.
updated_atstringyesISO 8601 backend wall-clock when the price was written to the cache. Not an upstream Polymarket event timestamp.
outcomesarrayyesPer-outcome {label, price, token_id} breakdown
volumestringyes24h trading volume in USD
buystringusuallyYes (first outcome) price (0–1). Absent if the outcome had no resolvable price.
sellstringusuallyNo (second outcome) price (0–1). Absent if the outcome had no resolvable price.
tokensobjectusuallyPer-outcome data keyed by token_id — convenience index for bots that already track tokens. Each value is {label, price}. Present whenever the cache writer had token_ids available (Gamma poller, CLOB WS); absent on a small number of Up/Down discovery writes.
best_bidstringsometimesBest bid price on the order book. Set by CLOB WS writes and Gamma poller when bestBid is present upstream.
best_askstringsometimesBest ask price on the order book. Same provenance as best_bid.
last_tradestringsometimesMost recent trade price observed on the CLOB WS feed. Absent until the first trade is seen for the market.
activeboolsometimesMarket active flag (mirrors the Gamma metadata). Present when the writer had it; drop frames where active=false if you care.
closedboolsometimesMarket closed flag (mirrors the Gamma metadata). Frames may still arrive briefly after resolution; ignore them.
ws_updated_atstringsometimesISO 8601 wall-clock of the most recent WS-source cache write. Present when source="websocket" or "rtds_websocket"; absent for Gamma-poll-only snapshots.

Source values

The source field marks which writer last touched the cache key:
ValueWriter
gamma_pollerDefault for the Gamma REST poller — used when no later WS write happened.
websocketCLOB WebSocket write (book updates, last-trade events, on-demand midpoint poll for viewed conditions).
rtds_websocketPolymarket RTDS WS write (Up/Down side-keyed price stream).
clob_on_demandOne-off CLOB fetch during order placement (lazy refresh when the cached snapshot is stale).
updown_discovery_clobInitial Up/Down market discovery — written when the poller first surfaces a new Up/Down nested market.
updown_resolved_*Settled Up/Down market — the suffix records the resolution source (e.g. updown_resolved_chainlink_onchain). Both buy and sell will be 0 or 1; new orders should not be placed against the market.
Bots that want only CLOB-WS-fresh prices can filter on source === "websocket" (or "rtds_websocket"); bots that just want “some price” can accept any source but drop updown_resolved_* frames.

Latency telemetry for HFT bots

Every price frame carries emit_ts_ms — an always-present integer wall-clock (milliseconds since epoch) stamped at the moment the frame is broadcast. It is purpose-built for end-to-end latency measurement: subtract it from your local receive time directly, with no parsing.
import json, time

msg = json.loads(raw_frame)
recv_ts_ms = int(time.time() * 1000)
observed_lag_ms = recv_ts_ms - msg["emit_ts_ms"]
Typical observed_lag_ms is 80–500 ms; sustained values above ~2,000 ms indicate backend overload or a TCP retransmission storm on the client side. updated_at is a different timestamp — it is stamped backend-side when the price was written to the cache, which precedes the broadcast. Use it only for backend-internal lag: the gap between the cache write and the broadcast is emit_ts_ms - (updated_at parsed to ms). For latency that matters to your strategy, prefer emit_ts_ms. For the upstream lag (Polymarket book event → PolySim cache write), watch the server-side gauge polysim_clob_token_last_msg_received_timestamp_seconds in Prometheus rather than computing it client-side. Recommended bot logic: drop any quote where observed_lag_ms > 2000. For 5-min Up/Down crypto-timer markets, tighten that to 500 ms — these markets resolve in minutes and you don’t want to trade on stale signal.

Tick-size changes

Polymarket shrinks the minimum price tick from 0.01 to 0.001 when a market crosses certain thresholds (price > 0.96 or < 0.04). UpDown 5-minute crypto markets — our headline product — spend much of their last-minute lives in those ranges, so unhandled tick changes mean bot quoters silently emit orders at the wrong precision.
tick_size_change frames are NOT delivered on this /v1/ws/prices WebSocket. When the upstream CLOB WS publishes a tick_size_change, the backend broadcasts it only to subscribers of the SSE /prices/stream feed — a separate manager. The JWT /v1/ws/prices feed emits type:"price" frames only and has no tick-change path.For tick changes on a WS bot, poll GET /v1/tick-size/{token_id} (it consults the WS-fresh cache first, then the DB-synced value) or consume the SSE /prices/stream feed alongside your WS price feed.
On the SSE /prices/stream feed, the tick_size_change frame looks like this (shown for reference — this is the SSE shape, not a /v1/ws/prices frame):
{
  "event": "tick_size_change",
  "data": {
    "type": "tick_size_change",
    "market": "0xabc123...",
    "asset_id": "71321...",
    "tick_size": 0.001,
    "old_tick_size": 0.01,
    "side": "BUY",
    "ts_ms": 1715518800123
  }
}
FieldTypeDescription
typestringAlways "tick_size_change"
marketstringCondition_id of the market the change applies to. Nullable if the upstream payload didn’t carry it.
asset_idstringCLOB token_id — the per-outcome key. The tick change is per-token, not per-market.
tick_sizenumberPost-change minimum tick (e.g. 0.001). Use this for all subsequent quote rounding.
old_tick_sizenumber|nullPre-change tick if the upstream payload included it; otherwise null.
sidestring|null"BUY", "SELL", or null. PM optionally narrows the change to one side.
ts_msintServer wall-clock at broadcast (milliseconds since epoch).
The two directions have asymmetric correctness consequences:
  • Grow (0.001 → 0.01) — previously-valid 0.001-step quotes are no longer multiples of the new tick. PM rejects with INVALID_ORDER_MIN_TICK_SIZE. This is the case where missing the change silently breaks your bot.
  • Shrink (0.01 → 0.001) — 0.01-step quotes are still valid multiples of 0.001, so orders aren’t rejected. The downside is quote-competitiveness: other bots that respect the finer grid can undercut you by 0.001 increments while you still quote at 0.01.
PolySim’s matching engine reads the same markets.minimum_tick_size column that gets refreshed on Gamma sync, so off-grid orders on grow transitions are usually rejected here too — but with WS-fresh values you avoid the gap between the price moving and the next column refresh.
You can also read the current tick at any time via GET /v1/tick-size/{token_id} — that endpoint consults the WS-fresh cache first, then falls back to the DB-synced value.

Unsubscribe

{"action": "unsubscribe", "markets": ["0xabc123..."]}

Complete Example

import asyncio
import json
import requests
import websockets

API_KEY = "ps_live_..."
BASE = "https://api.polysimulator.com/v1"

# Mint WS token
ws_token = requests.post(
    f"{BASE}/keys/ws-token",
    headers={"X-API-Key": API_KEY},
).json()["token"]

async def stream_prices():
    async with websockets.connect(
        f"wss://api.polysimulator.com/v1/ws/prices?token={ws_token}"
    ) as ws:
        # Subscribe to markets
        await ws.send(json.dumps({
            "action": "subscribe",
            "markets": ["0xabc123...", "0xdef456..."],
        }))

        async for raw in ws:
            msg = json.loads(raw)
            if msg["type"] == "price":
                mid = msg["market_id"][:16]
                buy = msg["buy"]
                print(f"{mid}... → Yes: {buy}")

asyncio.run(stream_prices())

Next Steps