Polymarket streams live crypto prices over its Real-Time Data Socket (RTDS) at wss://ws-live-data.polymarket.com. If you're building a bot on it, two things will bite you that the docs don't prepare you for. Here's both, with fixes, and a small open-source client that handles them.
Gotcha 1: the documented symbol filter is rejected by the live server
The docs show filtering like this:
{ "topic": "crypto_prices", "type": "update", "filters": "btcusdt,ethusdt" }
Send that comma-separated string to the live endpoint and you get:
invalid Subscription.Filters: value does not match regex ...
A JSON-array value is accepted but then silently returns nothing. The approach that actually works: subscribe with no server-side filter (it streams every symbol fine) and filter client-side.
sub = {"action": "subscribe", "subscriptions": [{"topic": "crypto_prices", "type": "update"}]}
# then keep only the symbols you care about as messages arrive
Gotcha 2: the feed can stall while staying "connected"
The docs reassure you:
Connection errors will trigger automatic reconnection attempts.
The problem is the failure that hurts most isn't a connection error. The RTDS server sometimes stops pushing data but keeps the socket open — no close frame, no exception:
ws.closed # False
# ...and no message has arrived in 90 seconds
Since there's no error, error-based auto-reconnect never fires. Your bot thinks it has a live price; it's frozen on a value from minutes ago. On Polymarket up/down markets that means you never lock the period's reference price and the strategy silently stops working.
You can't trust the transport to tell you it's alive. Track liveness yourself:
last_data_ts = time.time() # update on every valid price
# Chainlink pushes every 2-30s, so 45s of silence = stalled:
if connected and time.time() - last_data_ts > 45:
await force_reconnect() # close the socket; let your loop reconnect
Bonus: push latency
Chainlink ticks arrive 2–30s apart. Near a settlement boundary, a 25-second-old tick is a stale price. Gate on the message's own timestamp:
if now_ms - payload_timestamp_ms > 30_000:
skip() # too old to act on
The client
I packaged the connect/subscribe/parse/reconnect parts (verified against the live feed) into a small MIT module — including the no-filter-then-filter-client-side fix above:
import asyncio
from polymarket_rtds_client import RTDSClient
client = RTDSClient(topics=["crypto_prices"], symbols=["btcusdt", "ethusdt"])
asyncio.run(client.stream_prices(lambda s, v, ts: print(s, v, ts)))
Repo (free, MIT, tests + live demo):
https://github.com/BlueWhale-Quant-Lab/polymarket-realtime-price-websocket-rtds
The liveness guards (silent-stall detection, a staleness-gated price store) are in a complete version for when you're trading on the feed — but the free client fully covers reading live prices.
Takeaway
On RTDS: don't use the documented comma filter, and don't trust the socket's liveness — track your own last_data_ts and reconnect on silence, not just on errors.
Top comments (0)