If you want historical Polymarket order book depth, most archives offer the same thing: a stream of book snapshots and price_change deltas, with instructions to seed from a snapshot and apply the deltas to rebuild the book at any past moment. We tried it. Across 381,093 checkpoints, the replay disagreed with the archive's own snapshots 67.8% of the time, and 6.4% of the books it produced were crossed — a bid above an ask, which cannot happen in a real market.
This is not a criticism of any particular archive. The snapshots themselves are accurate; we checked 18,087 of them and not one was crossed. The problem is structural, it applies to any event archive of a venue this busy, and it has a mechanism worth understanding before you plan a backtest around reconstructed depth.
The test grades itself
The neat thing about this data is that you do not need an external reference. The archive contains periodic book snapshots and the deltas between them. So seed from one snapshot, apply every following price_change, and when the next snapshot arrives, compare. The source marks its own homework.
from decimal import Decimal
import json
def levels(raw):
out = {}
for price, size in json.loads(raw):
size = Decimal(size)
if size > 0:
out[Decimal(price)] = size
return out
bids, asks, seeded = {}, {}, False
checkpoints = divergences = crossed = 0
for kind, b, a, price, size, side in rows: # one token, ordered by time
if kind == "book":
src_bids, src_asks = levels(b), levels(a)
if seeded: # compare replay against the source
checkpoints += 1
if bids != src_bids or asks != src_asks:
divergences += 1
if bids and asks and max(bids) >= min(asks):
crossed += 1
bids, asks, seeded = src_bids, src_asks, True
elif kind == "price_change":
book = bids if side == "BUY" else asks
amount, key = Decimal(size), Decimal(price)
if amount <= 0:
book.pop(key, None) # a zero size removes the level
else:
book[key] = amountWe ran this over the 25 most active tokens in each of twelve hours, spread from April to August 2026.
hour checkpoints diverged crossed extra:missing
2026-04-20T09 577 100.0% 40.6% 273:20
2026-05-02T15 1,498 99.6% 36.6% 657:69
2026-05-24T12 221 95.9% 80.5% 168:1
2026-06-03T21 78,815 67.8% 4.7% 9985:2850
2026-06-11T12 71,766 69.3% 7.9% 10469:2560
2026-06-18T06 14,575 56.7% 2.1% 593:183
2026-06-24T18 19,455 57.9% 0.2% 889:303
2026-07-05T03 52,224 66.1% 6.9% 7829:1754
2026-07-14T11 2,548 69.5% 3.6% 767:87
2026-07-22T16 55,383 70.5% 7.1% 7074:1172
2026-08-01T08 40,700 67.5% 7.1% 6300:1529
2026-08-09T23 43,331 71.0% 7.4% 7711:2022
POOLED 381,093 67.8% 6.4% 52715:12550What the numbers say
Divergence is stable and high. Every high-volume hour lands between 57% and 71%. Two thirds of the time, replaying the deltas does not reproduce the book the source says existed.
Crossed books are the visible symptom. 6.4% pooled, though the range is wide: 0.2% in a quiet hour, 80.5% in one sparse hour where snapshots were far apart. If your reconstruction produces a bid above an ask, you are not looking at a market state that ever existed.
The asymmetry is the diagnosis. When the replay disagrees, it usually holds levels the source does not: 52,715 cases of extra levels against 12,550 of missing ones, a ratio of 4.2 to 1. That rules out a bug in the delta logic. A mistake in applying updates would drop levels roughly as often as it kept them. One-directional accumulation means removal events are missing from the stream — orders that were cancelled with no price_change to say so.
Those orphaned levels sit in the reconstructed book at prices nobody is quoting any more. Eventually one of them crosses the other side.
Why this is nobody's fault
Polymarket's websocket carries around 24,000 events a second across all markets. At that rate every collector drops messages, and a dropped removal is invisible: the stream does not number its events, so there is no gap to detect. The archive is recording faithfully; it simply cannot record what it never received.
Live systems survive this because they can ask again. A collector re-reads the full book from the REST API on a timer, and every re-read silently repairs whatever drifted since the last one.
# What a live collector can do that a replay cannot
while running:
apply_websocket_events() # the delta stream
if now() - last_resync > 60: # ask the venue what the book really is
book = rest_client.get_book(token_id)
registry.replace(token_id, book)
last_resync = now()A replay has nothing to re-read against. The market closed months ago. Whatever was not captured at the time is not recoverable at any price, from any vendor.
The interval is the whole story
We changed our own reconcile interval from five minutes to sixty seconds and measured crossed books on Polymarket's five-minute BTC markets, bucketed by how close each snapshot was to settlement:
time to close before after
0-60s 11.3% 0.0%
60-120s 13.3% 1.2%
120-180s 11.7% 0.5%Nothing else changed. The same websocket, the same parsing, the same storage. Only how often we asked the venue what the book actually looked like. That is also why the archive hours with sparse snapshots are so much worse than the dense ones — the checkpoint interval is doing the same work, after the fact.
What to do with this
If you need prices, use Polymarket's own API. It is free, it serves 1-minute buckets on markets resolved years ago, and you should not pay anyone for it. We wrote up exactly what it does and does not cover on the Polymarket API pricing page.
If you need depth — slippage against a real ladder, fill probability, how liquidity behaved before a resolution — then the ladder has to have been captured, with reconciliation running, while the market was live. Reconstruction from an event stream will give you something that looks like a book two thirds of the time and is provably impossible 6% of the time.
That is what PolyOrderbooks records: Polymarket crypto order books captured at 1-second resolution with a 60-second reconcile, resolved markets kept alongside their winning outcome. You can download a free 1-second L2 sample and run the crossed-book check on it yourself. It is three lines of pandas, and we would rather you checked.
FAQ
Can you reconstruct a Polymarket order book from historical data?
Only approximately, and not reliably. Seeding from a book snapshot and applying every subsequent price_change disagreed with the source's own next snapshot at 67.8% of 381,093 checkpoints we tested. 6.4% of the reconstructions were crossed, meaning they described a state no real market was ever in.
Why are reconstructed order books crossed?
Because removal events go missing. When an order is cancelled the stream should carry aprice_change with size zero. At roughly 24,000 events a second some of those are dropped, and the stream is not sequenced, so there is no gap to detect. The replay keeps a level the venue removed. Extra levels outnumbered missing ones 4.2 to 1 in our test, which is the signature of a lossy feed rather than a faulty replay.
Does Polymarket provide historical order book depth?
No. The CLOB API returns the current book only, and there is no documented endpoint for the ladder at a past timestamp. Historical prices are a different matter and are served free down to 1-minute buckets, even on markets resolved years ago — see Polymarket API pricing for what the official API does and does not cover.
How do I check whether an order book dataset is correct?
Count crossed books. A real book can never have a bid at or above the best ask, so any row where max(bids) >= min(asks) is impossible. It is four lines of pandas and it is the first thing worth running on any sample, including ours. The provider comparison has the snippet.