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.
Field
Type
Always present
Description
type
string
yes
Always "price" for price updates
market_id
string
yes
Polymarket condition_id
condition_id
string
yes
Same value as market_id — emitted for backward-compat with consumers that key off condition_id
emit_ts_ms
int
yes
Server 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.
source
string
yes
Provenance of the cached snapshot — see Source values below.
updated_at
string
yes
ISO 8601 backend wall-clock when the price was written to the cache. Not an upstream Polymarket event timestamp.
outcomes
array
yes
Per-outcome {label, price, token_id} breakdown
volume
string
yes
24h trading volume in USD
buy
string
usually
Yes (first outcome) price (0–1). Absent if the outcome had no resolvable price.
sell
string
usually
No (second outcome) price (0–1). Absent if the outcome had no resolvable price.
tokens
object
usually
Per-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_bid
string
sometimes
Best bid price on the order book. Set by CLOB WS writes and Gamma poller when bestBid is present upstream.
best_ask
string
sometimes
Best ask price on the order book. Same provenance as best_bid.
last_trade
string
sometimes
Most recent trade price observed on the CLOB WS feed. Absent until the first trade is seen for the market.
active
bool
sometimes
Market active flag (mirrors the Gamma metadata). Present when the writer had it; drop frames where active=false if you care.
closed
bool
sometimes
Market closed flag (mirrors the Gamma metadata). Frames may still arrive briefly after resolution; ignore them.
ws_updated_at
string
sometimes
ISO 8601 wall-clock of the most recent WS-source cache write. Present when source="websocket" or "rtds_websocket"; absent for Gamma-poll-only snapshots.
One-off CLOB fetch during order placement (lazy refresh when the cached snapshot is stale).
updown_discovery_clob
Initial 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.
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.
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.
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):
Condition_id of the market the change applies to. Nullable if the upstream payload didn’t carry it.
asset_id
string
CLOB token_id — the per-outcome key. The tick change is per-token, not per-market.
tick_size
number
Post-change minimum tick (e.g. 0.001). Use this for all subsequent quote rounding.
old_tick_size
number|null
Pre-change tick if the upstream payload included it; otherwise null.
side
string|null
"BUY", "SELL", or null. PM optionally narrows the change to one side.
ts_ms
int
Server 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.