Skip to content

WS price_change event semantics ​

Captured: 2026-05-12 (run_id 1778604182, UTC ms timestamps in payload) Capture window: 75 seconds Subscription: WS Market channel wss://ws-subscriptions-clob.polymarket.com/ws/market, initial_dump=true, custom_feature_enabled=true, 12 high-volume binary tokens. Source artifact: .tmp/capture/ws_events_1778604182.jsonlCapture tool: crates/pm-bot/examples/capture_ws_price_change.rs (uncommitted)

Tokens subscribed (12) ​

Tokens span 7 active binary markets — top of volume24hr. Notable hot pair:

asset_id (first YES side)marketrole
19957195...8739115Internazionali BNL d'Italia: Rublev vs Basilashvilimost active (9008 price_change refs in 75s)
25144992...409482108IPL: Gujarat Titans vs Sunrisers Hyderabad2825 refs
13915689...637394586Bitcoin $150k by June 30, 2026721 refs
9 moreVarious sports/politics≤ 200 each

Event tallies (75s, 12 tokens) ​

event_typecount
price_change12 963
book107 (12 initial dumps + periodic re-snapshots)
best_bid_ask199
last_trade_price37
new_market22

Payload structure ​

Outer event:

json
{
  "event_type": "price_change",
  "market": "0x5bac908f07bc554c9d75cc7d9a7ee6dccc96e5a8b7b59308ec9063766380407e",
  "timestamp": "1778604183329",
  "price_changes": [ /* exactly 2 entries — see below */ ]
}

Inner entry (always exactly 2 per event):

json
{
  "asset_id": "38001902...888335398",
  "best_ask": "0.74",
  "best_bid": "0.73",
  "hash":     "9f39322f7fa23b8bf495e68646352a0016869024",
  "price":    "0.03",
  "side":     "BUY",
  "size":     "35420"
}

Fields observed ​

FieldTypeNotes
event_typestringalways "price_change"
marketstring (0x…hex)condition_id of the binary market
timestampstring of milliseconds since epoche.g. "1778604183329"; matches Date.now() ms
price_changesarray, length == 2 in 100% of eventsone entry per asset_id in the binary pair
price_changes[].asset_idstring (uint256)YES or NO token id
price_changes[].side"BUY" or "SELL"side of the orderbook level being updated
price_changes[].pricestring decimal (e.g. "0.03", tick=0.01 or 0.001)the single level being changed
price_changes[].sizestring decimalnew aggregate size at that level; "0" = remove
price_changes[].hashhex string (40 chars, sha1-shaped)per-asset book hash AFTER applying this update
price_changes[].best_bid / best_askstring decimalNBBO snapshot for that asset after the update

Pair invariant (binary mirror) ​

In every observed event:

  • price_changes[0].price + price_changes[1].price == 1.000000 (exact, no FP drift — 12963/12963)
  • Sides are opposite: 11744 events BUY,SELL, 1219 events SELL,BUY.
  • size is identical across both entries (they are the same level expressed on YES vs NO).

This is NOT a multi-level snapshot — it is a single price-level update, mirrored to the complementary asset via p_NO = 1 − p_YES. Patches for token A and its complement A' are coalesced into one event so a consumer subscribed to either side sees both.

hash ​

  • Differs between the two entries (per-asset orderbook hash after this patch).
  • 12 444 distinct values across 12 963 events → ~96 % unique. Repeats happen when the same level is later replaced by an equal-shape book state, i.e. the hash is a deterministic content-hash of the orderbook state, not an event nonce. Can be used to assert local-replay parity (see experiment below).

size == "0" semantics ​

476 occurrences across the run (3.7 %). Always interpretable as level removal — confirmed empirically by the patch experiment below: applying size=0 as a delete reproduces server state byte-for-byte.

Patch vs Mirror experiment ​

Asset: 19957195...8739115 (Rublev vs Basilashvili, tennis live).

  • Initial book event for asset: bids=20, asks=48, hash=69db51bd…3503b, ts=1778604183310.
  • Server re-emitted book 31 more times during the window (every few seconds); used the last one (ts=1778604257171, hash=97860f8a…b37e1, bids=24, asks=45) as ground truth for the local replay.
  • Between those two book events: 8 949 price_change entries for this asset (one per event, the matching one of the two array slots).

Two reconstruction strategies were applied:

StrategyAfter replaySymmetric diff vs ground-truth book
PATCH — for each entry: bids[price]=size (BUY) or asks[price]=size (SELL); delete if size=="0"bids=24, asks=450 bid diffs, 0 ask diffs
MIRROR (per-side full replace) — for each entry: clear bids OR asks, set only the one levelbids=0, asks=124 bid diffs, 44 ask diffs

Verdict: per-level PATCH semantics (not mirror) ​

price_change is a single price-level orderbook patch for both sides of the binary pair simultaneously. Each event carries exactly one level on one side, expressed twice (once on YES, once on NO at 1 − p). best_bid / best_ask and hash are post-patch snapshots provided for convenience / integrity verification — the level data itself is delta, not full book.

size=0 is the canonical level-removal signal.

Operational implications ​

  1. Local book maintenance: feasible and verified — a consumer can keep an in-memory BTreeMap<price, size> per asset and apply each price_change entry (matching its asset_id) as if size=="0" { remove } else { insert/replace }. After 8 949 patches the result matches the server's periodic re-snapshot to the entry — no drift.
  2. Hash check: every entry's hash is the post-state hash of that asset's full book. Implementations can recompute the same hash locally after each patch and detect desync immediately.
  3. Pair coalescing: even when subscribing to only YES, the server already emits the NO complement in price_changes[1]. Subscribing to both YES and NO of the same market is wasteful (will get duplicate frames? requires further test — out of scope here).
  4. Throughput: 12 963 events / 75 s across 12 markets ≈ 173 events/s aggregate. Single asset (Rublev tennis live) sustained ~120 events/s peak.
  5. best_bid / best_ask lag: included on every entry; in spot checks they reflect the post-patch NBBO, but this was not exhaustively verified — for absolute correctness derive NBBO from the maintained local book, not the field.
  6. Periodic book re-snapshots: the server resends full book events for an asset every few seconds even after the initial dump (32× over 74 s for the tennis asset). These can serve as ground-truth resync points or be used to bound reconstruction error.

Raw events sample (5 consecutive, asset 19957195/38001902 pair) ​

json
{"event_type":"price_change","market":"0x5bac908f...","price_changes":[{"asset_id":"38001902...","best_ask":"0.74","best_bid":"0.73","hash":"9f39322f...","price":"0.03","side":"BUY","size":"35420"},{"asset_id":"19957195...","best_ask":"0.27","best_bid":"0.26","hash":"709728c7...","price":"0.97","side":"SELL","size":"35420"}],"timestamp":"1778604183329"}
{"event_type":"price_change","market":"0x5bac908f...","price_changes":[{"asset_id":"38001902...","best_ask":"0.74","best_bid":"0.73","hash":"c8c09ba3...","price":"0.69","side":"BUY","size":"22797.36"},{"asset_id":"19957195...","best_ask":"0.27","best_bid":"0.26","hash":"84424b0e...","price":"0.31","side":"SELL","size":"22797.36"}],"timestamp":"1778604183343"}
{"event_type":"price_change","market":"0x5bac908f...","price_changes":[{"asset_id":"38001902...","best_ask":"0.74","best_bid":"0.73","hash":"7233ae48...","price":"0.03","side":"BUY","size":"33920"},{"asset_id":"19957195...","best_ask":"0.27","best_bid":"0.26","hash":"e96f7354...","price":"0.97","side":"SELL","size":"33920"}],"timestamp":"1778604183345"}
{"event_type":"price_change","market":"0x5bac908f...","price_changes":[{"asset_id":"19957195...","best_ask":"0.27","best_bid":"0.26","hash":"854a0813...","price":"0.22","side":"BUY","size":"5062.42"},{"asset_id":"38001902...","best_ask":"0.74","best_bid":"0.73","hash":"3c105c29...","price":"0.78","side":"SELL","size":"5062.42"}],"timestamp":"1778604183346"}
{"event_type":"price_change","market":"0x5bac908f...","price_changes":[{"asset_id":"19957195...","best_ask":"0.27","best_bid":"0.26","hash":"5b9ea869...","price":"0.01","side":"BUY","size":"55827.19"},{"asset_id":"38001902...","best_ask":"0.74","best_bid":"0.73","hash":"a6503033...","price":"0.99","side":"SELL","size":"55827.19"}],"timestamp":"1778604183356"}

Reproduction ​

bash
cargo run -p pm-bot --example capture_ws_price_change -- \
  "<token1>,<token2>,...,<tokenN>" 75
# writes .tmp/capture/ws_events_<unix_ts>.jsonl
python3 .tmp/capture/patch_vs_mirror.py .tmp/capture/ws_events_<unix_ts>.jsonl <asset_id>