Skip to main content

Polymarket-compatible WebSocket

These routes mirror Polymarket’s CLOB WS contract field-for-field so an SDK port of Polymarket (py-clob-client, etc.) can be pointed at PolySim by changing only the host. No event-handler rewrites required. If you’re starting fresh, the polysim-native Price Feed is more compact and slightly cheaper to parse. The PM-compat layer exists for SDK porters — pick this route when you have an existing book / price_change event consumer and don’t want to re-key on condition_id.

Subscribe message

The subscribe key differs by channel, matching Polymarket:
  • /ws/market keys by CLOB token id (assets_ids — long decimal strings, two per binary market, one per outcome). This is PM’s market channel contract.
  • /ws/user keys by condition id (markets). This is PM’s user channel contract — “the user channel subscribes by condition IDs (market identifiers), not asset IDs.” A faithful PM SDK port sends markets to its user channel.
The polysim backend accepts either field on both routes (reverse-resolving token ↔ condition transparently), so a single client can use whichever it already has.

Auth block

Alternative: pass ?token=<jwt> on connect (the polysim-native auth model). The JWT comes from POST /v1/keys/ws-token and expires in 60 seconds. Pick exactly one auth path — passing both is fine but the JWT takes precedence.

Events emitted

All four PM event types are emitted with PM’s exact field names:

book — full L2 snapshot

Emitted once per token on subscribe (with the current CLOB orderbook snapshot). Subsequent updates flow through price_change / best_bid_ask — the book event is not re-emitted on every level change.

price_change — orderbook delta

Two PM deviations:
  • PolySim adds a convenience top-level asset_id that PM’s price_change does not send (PM puts market / price_changes[] / timestamp / event_type at the top level; the asset id lives only inside each price_changes[] entry).
  • The inner size, side, and hash fields are stubbed ("0" / "" / ""). PolySim’s price cache is a top-of-book-only payload with no per-level deltas or rolling book hash, so these are emitted for schema parity but carry no data. PM populates them.

last_trade_price — fill broadcast

best_bid_ask — top-of-book change

Two PM deviations:
  • PolySim omits the spread field that PM’s best_bid_ask carries (compute it yourself as best_ask - best_bid if you need it).
  • PM gates best_bid_ask (plus new_market / market_resolved) behind custom_feature_enabled: true in the subscribe message. PolySim ignores custom_feature_enabled and emits best_bid_ask unconditionally — you do not need to set the flag.
All prices are strings (no JSON numbers) to match PM verbatim and avoid float-precision drift.

Error frames

The PM-compat layer emits structured error frames (PM does the same):
UNKNOWN_ASSET and INVALID_SUBSCRIBE are non-fatal — the connection stays open so you can send a corrected subscribe. The three rows marked “yes” above are terminal: the server sends the error frame, then closes the socket so the client knows to re-handshake (with corrected auth) or back off.

Dynamic subscribe/unsubscribe

PM SDKs mutate subscriptions without reconnecting by sending an operation frame (the initial subscribe uses type; subsequent updates use operation). PolySim accepts the same shape:
The per-connection 50-token cap is cumulative across all subscribe operations — exceeding it returns a MAX_ASSETS_EXCEEDED error frame (none of the new tokens are added). Send unsubscribe to free room first.

Ping/pong

PolySim accepts both heartbeat shapes:
In both cases the server replies with a JSON pong frame:
Deviation from PM for porters: Polymarket replies to a plain-string PING with a plain-string PONG. PolySim always replies with the JSON {"event_type": "pong", "ts": <ms>} frame — even when you sent a plain-string ping. A PM bot waiting for a literal PONG string never sees it; key your heartbeat handler off the JSON pong frame instead.
The PM-compat shape uses event_type (not type) on the pong frame for consistency with the rest of the protocol.

Complete example

Python (websockets)
For py-clob-client port-overs, the only change is the WS URL:
Event handlers — including the event_type field name and PM-style stringified prices — work without modification.

What’s NOT in the PM-compat layer (yet)

  • PM’s order PLACEMENT / UPDATE / CANCELLATION events on /ws/userfill (trade) events ARE emitted: when one of your orders fills on a subscribed market, /ws/user pushes PM’s user-channel trade frame (event_type: "trade", type: "TRADE", status: "MATCHED", unix-string timestamps). Divergences: paper trades terminate at MATCHED (no MINED/CONFIRMED lifecycle — no chain), maker_orders is empty, and owner/trade_owner are empty strings. PM’s order placement/cancel events are still NOT emitted — poll GET /v1/data/orders for order state, or use the polysim-native Execution Feed (/v1/ws/executions).
  • L2 signature auth — PM’s full L2 contract requires POLY_ADDRESS / POLY_SIGNATURE / POLY_TIMESTAMP / POLY_API_KEY / POLY_PASSPHRASE. PolySim is single-secret — we accept the apiKey field and ignore the others. SDK code that calls a PM signer to build the L2 block doesn’t need changes; the signer’s output is simply ignored.

Compatibility matrix