Тема
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) | market | role |
|---|---|---|
19957195...8739115 | Internazionali BNL d'Italia: Rublev vs Basilashvili | most active (9008 price_change refs in 75s) |
25144992...409482108 | IPL: Gujarat Titans vs Sunrisers Hyderabad | 2825 refs |
13915689...637394586 | Bitcoin $150k by June 30, 2026 | 721 refs |
| 9 more | Various sports/politics | ≤ 200 each |
Event tallies (75s, 12 tokens)
| event_type | count |
|---|---|
price_change | 12 963 |
book | 107 (12 initial dumps + periodic re-snapshots) |
best_bid_ask | 199 |
last_trade_price | 37 |
new_market | 22 |
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
| Field | Type | Notes |
|---|---|---|
event_type | string | always "price_change" |
market | string (0x…hex) | condition_id of the binary market |
timestamp | string of milliseconds since epoch | e.g. "1778604183329"; matches Date.now() ms |
price_changes | array, length == 2 in 100% of events | one entry per asset_id in the binary pair |
price_changes[].asset_id | string (uint256) | YES or NO token id |
price_changes[].side | "BUY" or "SELL" | side of the orderbook level being updated |
price_changes[].price | string decimal (e.g. "0.03", tick=0.01 or 0.001) | the single level being changed |
price_changes[].size | string decimal | new aggregate size at that level; "0" = remove |
price_changes[].hash | hex string (40 chars, sha1-shaped) | per-asset book hash AFTER applying this update |
price_changes[].best_bid / best_ask | string decimal | NBBO 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 eventsSELL,BUY. sizeis 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
bookevent for asset: bids=20, asks=48, hash=69db51bd…3503b, ts=1778604183310. - Server re-emitted
book31 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:
| Strategy | After replay | Symmetric 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=45 | 0 bid diffs, 0 ask diffs |
| MIRROR (per-side full replace) — for each entry: clear bids OR asks, set only the one level | bids=0, asks=1 | 24 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
- Local book maintenance: feasible and verified — a consumer can keep an in-memory
BTreeMap<price, size>per asset and apply eachprice_changeentry (matching itsasset_id) asif size=="0" { remove } else { insert/replace }. After 8 949 patches the result matches the server's periodic re-snapshot to the entry — no drift. - Hash check: every entry's
hashis the post-state hash of that asset's full book. Implementations can recompute the same hash locally after each patch and detect desync immediately. - 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). - Throughput: 12 963 events / 75 s across 12 markets ≈ 173 events/s aggregate. Single asset (Rublev tennis live) sustained ~120 events/s peak.
best_bid/best_asklag: 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.- Periodic
bookre-snapshots: the server resends fullbookevents 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>