tickfoundry
capture livesign inget the data →
§ order book

Polymarket order book data
every book update, 25 levels a side.

L2 · parquet / csvfree sampleTickFoundry · numbers from the free sample, no account

Every Polymarket outcome trades on its own central limit order book: resting bids and asks for one token, priced in cents of probability. The L2 table records that book on every change the venue broadcasts, as a full snapshot of the best 25 price levels on each side, with the size at each level and nanosecond receive timestamps. Below is what the table holds, what one day of one book looks like, and how deep it actually was, measured on the free sample with a script you can run yourself.

The sample: BTC Up or Down 4h · 2026-08-19 · full UTC day · 640,735 book rows across 26 outcome tokens.
↓ parquet bundle (23.0 MB)↓ pm_depth.py

What the public API gives you

Polymarket's CLOB API answers questions about now. /book returns the current resting bids and asks for one outcome token, with a hash so you can tell whether the book changed between two reads; it takes no time parameter. /prices-history returns timestamp and price pairs only, with no bid, ask, size or depth, and when we asked it for one-minute fidelity over a two-week-old market's whole life it returned points ten minutes apart. The market websocket does push every book change as it happens, but only to whoever is connected at that moment.

So a book's history exists only if someone was recording it. We have been, continuously, since 2026-05-11, and order-book history reaches back to February 2026. That is the data on this page.

What is in the file

One row per book update per outcome token. Each row is the book after the update, not a diff: the 25 best bids and the 25 best asks, price and size, padded with NaN past the last level present. On the sample day the venue resent a whole book 9,819 times and sent 630,916 incremental changes; both land as full snapshots, told apart by event_kind. Sizes are in shares (contracts), so a level's dollar depth is price times size.

l2.parquet · columnstext
l2.parquet — one row per book update per outcome token
ts_recv_ns int64 our receive time, ns since epoch (authoritative order)
ts_src_ms int64 the venue's own timestamp, ms
condition_id str the market
token_id str the outcome (YES or NO leg) — read as a string
event_kind str "snapshot" (venue resent the whole book) | "update"
source_event_type str upstream message: book | price_change
msg_seq int64 capture sequence number
bid_levels int16 price levels in the WHOLE bid book (the file keeps 25)
ask_levels int16 price levels in the whole ask book
best_bid, best_ask float64 top of book, rebuilt from the levels
payload_best_bid float64 top of book as the venue's message declared it
payload_best_ask float64
bid_ask_reconciles bool rebuilt top of book == declared top of book
bid_px_1 … bid_px_25 float64 price per level, best first, NaN past the book
bid_sz_1 … bid_sz_25 float64 size per level, in shares (contracts)
ask_px_1 … ask_px_25 float64
ask_sz_1 … ask_sz_25 float64

Premium and Custom plans carry all 25 levels; Explorer and the free market·days carry the best 10. Bundles also carry L1 (top of book on each change) and, from 2026-04-13, the trade tape. Full reference in the schema docs.

One day of one book

The YES side of "Bitcoin Up or Down - August 19, 4:00PM-8:00PM ET" over the four hours its question covers. Each cell is one minute and one cent of price; its brightness is the dollar size that rested there, averaged second by second. Bids are green below the mid, asks red above it, and circles are fills.

Depth heatmap of the Polymarket order book for the YES outcome of Bitcoin Up or Down, August 19 4PM to 8PM ET, 20:00 to 24:00 UTC: resting bids in green below the mid and asks in red above it. The mid climbs from 52¢ to 99.8¢, with a jump from 68¢ to 93¢ at 20:50.
2026-08-19 20:00–24:00 UTC · resting USDC per 1¢ bucket · the 25 levels a side the file keeps · blank means nothing rested there within those levels.

Read the jump just before 21:00. At 20:49:44 UTC the mid was 68¢; sixty seconds later it was 93¢. In that minute the YES book changed 2,049 times and 15 fills printed on it, about $1,400 in all. A trade tape tells you that much. The book tells you the rest: when the minute began, $5,680 of asks rested between 69¢ and 93¢, and when it ended the best ask was 94¢. A price chart shows the jump. Only the book shows whether you could have traded through it, and at what size.

How deep was it

The same market and window, sampled once a second so that a burst of updates counts for the time it lasted, not for how many messages it produced. Spread is taken as best ask minus best bid.

spread≤ 1¢1–2¢2–5¢> 5¢median
share of time63.9%20.6%15.5%0.0%1.0¢

14,274 two-sided seconds · 2026-08-19 20:00–24:00 UTC

resting withinbids · medianbids · p10asks · medianasks · p10
±1¢ of mid$256$0$141$0
±2¢ of mid$944$102$294$76
±5¢ of mid$2,260$719$2,918$419

USDC (price × size) · p10 = the thinnest tenth of seconds

levelbid at levelbids throughask at levelasks throughask level exists
1$110$110$77$77100%
2$130$305$95$19693%
3$99$568$88$30493%
5$98$936$98$60685%
10$66$2,428$74$2,37877%
25$40$4,736$53$10,48025%

median USDC at that level, and summed from the touch through it · the bid side had all 25 levels in 99.9% of seconds

Three things stand out. The touch is small: a median $110 on the best bid and $77 on the best ask, so anything bigger than a small order walks the ladder. Depth near the mid comes and goes: in at least one second in ten a given side had nothing resting within 1¢ of the mid. And the two sides were not alike: with the price above 90¢ for two-thirds of the window there was little room between the mid and 100¢, and the median book had 111 bid levels against 17 ask levels. None of this can be read off a price series.

Reproduce it in pandas

The script behind all three tables, unchanged. Unzip the sample, run it in the bundle directory, and point it at any other market·day you download: it picks the busiest market in the file and its own window.

pm_depth.pypython
# pm_depth.py — how deep was a Polymarket order book, level by level and near the mid?
# Runs on the free sample bundle: https://tickfoundry.com/samples
import numpy as np
import pandas as pd
l2 = pd.read_parquet("l2.parquet") # one full 25-level snapshot per book update
markets = pd.read_csv("reference/markets.csv")
# The YES leg of the day's busiest market, over the four hours its question covers.
busiest = l2.groupby("condition_id").size().idxmax()
m = markets.set_index("condition_id").loc[busiest]
book = l2[l2.token_id == str(m.yes_token_id)].sort_values("ts_recv_ns").reset_index(drop=True)
end = pd.Timestamp(m.end_time)
start = end - pd.Timedelta(hours=4)
# Sample the book once a second so every statistic is time-weighted:
# a burst of 50 updates inside one second counts as one second, not as 50.
grid = np.arange(start.value, end.value, 1_000_000_000)
at = np.searchsorted(book.ts_recv_ns.to_numpy(), grid, side="right") - 1
snap = book.iloc[at[at >= 0]]
snap = snap[snap.best_bid.notna() & snap.best_ask.notna()] # drop one-sided seconds
px = {s: snap[[f"{s}_px_{i}" for i in range(1, 26)]].to_numpy() for s in ("bid", "ask")}
usd = {s: px[s] * snap[[f"{s}_sz_{i}" for i in range(1, 26)]].to_numpy() for s in ("bid", "ask")}
mid = ((snap.best_bid + snap.best_ask) / 2).to_numpy()[:, None]
spread = (snap.best_ask - snap.best_bid).to_numpy() * 100 # cents; never the rounded spread column
print(m.question, f"· {start:%Y-%m-%d %H:%M} to {end:%H:%M} UTC")
print(f"{len(book):,} book updates for this token on the day, "
f"{((book.ts_recv_ns >= start.value) & (book.ts_recv_ns < end.value)).sum():,} inside the window; "
f"{len(snap):,} two-sided seconds sampled\n")
print("spread (¢) median", round(np.median(spread), 2),
"| ≤1¢", f"{np.mean(spread <= 1.0001):.1%}",
"| 1–2¢", f"{np.mean((spread > 1.0001) & (spread <= 2.0001)):.1%}",
"| 2–5¢", f"{np.mean((spread > 2.0001) & (spread <= 5.0001)):.1%}",
"| >5¢", f"{np.mean(spread > 5.0001):.1%}\n")
rows = []
for c in (1, 2, 5): # USDC resting within c cents of the mid
bid = np.nansum(np.where(px["bid"] >= mid - c / 100 - 1e-9, usd["bid"], 0), axis=1)
ask = np.nansum(np.where(px["ask"] <= mid + c / 100 + 1e-9, usd["ask"], 0), axis=1)
rows.append({"within": f"±{c}¢", "bid median $": np.median(bid), "bid p10 $": np.percentile(bid, 10),
"ask median $": np.median(ask), "ask p10 $": np.percentile(ask, 10)})
print(pd.DataFrame(rows).round(0).to_string(index=False), "\n")
rows = []
for lvl in (1, 2, 3, 5, 10, 25): # size at each level, and through it
rows.append({"level": lvl,
"bid $ at level": np.nanmedian(usd["bid"][:, lvl - 1]),
"bid $ through": np.median(np.nansum(usd["bid"][:, :lvl], axis=1)),
"ask $ at level": np.nanmedian(usd["ask"][:, lvl - 1]),
"ask $ through": np.median(np.nansum(usd["ask"][:, :lvl], axis=1)),
"ask levels present": f"{np.mean(~np.isnan(px['ask'][:, lvl - 1])):.0%}"})
print(pd.DataFrame(rows).round(0).to_string(index=False), "\n")
print("price levels in the whole book (the file keeps the best 25 a side): median bid",
int(np.median(snap.bid_levels)), "· ask", int(np.median(snap.ask_levels)))

To price an order against this book instead of describing it, see how to backtest Polymarket, which walks the same ladder for $100, $500 and $2,000 market buys.

What people use order book data for

Execution-aware backtests
Fill every simulated order against the ladder that was resting at its receive time, instead of at the mid or the last trade.
Market making and queue studies
Spread regimes, how fast the touch refills after a sweep, how size is distributed across levels and how that changes into resolution.
Liquidity around news
What the book did in the seconds a price moved: which side was pulled, how far the spread opened, how much could actually have traded.
Microstructure research
Tick-level books across thousands of markets, from sports and elections to crypto, with the trade tape on the same clock.

Two worked examples from the archive: the Spain–Belgium quarterfinal, where the spread opened to 31¢ while the price moved 64 points, and the World Cup final through the order book.

FAQ

Does Polymarket have an API for historical order book data?
Not for past books. The CLOB's /book endpoint returns the order book as it is now, for one outcome token, with a hash so you can tell whether it changed between two reads; it takes no time parameter. /prices-history returns a list of timestamp and price pairs with no bid, ask, size or depth, and asked for one-minute fidelity over a market's whole life it returned points ten minutes apart. The market websocket pushes every book change as it happens, but only to whoever is connected at the time. A book's history exists only if someone was recording it.
How many levels of the book are in the data?
The best 25 price levels on each side, on every update, with price and size per level. Two more columns, bid_levels and ask_levels, count the price levels in the whole book, so you can see when it extends past what the file keeps: on the sample's busiest market the median book had 111 bid levels and 17 ask levels. Premium and Custom plans carry all 25 levels; Explorer and the free market·days carry the best 10.
How often does the book update?
On every change the venue broadcasts, each stored as a full 25-level snapshot after the change. In the sample's busiest four hours one outcome token took 92,325 updates: 6.4 a second on average, up to 180 in a single second, with at least one update in two seconds out of three.
Do I need both the YES and the NO book?
For a binary market the NO book is the YES book mirrored at one minus the price: in every one of the 14,400 seconds of the sample market's window, the YES best bid equalled one minus the NO best ask, at identical size. Both legs are in the file, so you can use whichever is convenient; fills print on both tokens.
What does bid_ask_reconciles mean?
Each row carries the top of book rebuilt from the levels and the best bid and ask the venue's message declared. Where they differ the row is flagged, not dropped: 1.3% of rows on the sample day, mostly moments when one side of the book was empty and the message quoted a 0¢ or 100¢ placeholder.
How far back does it go, and in what format?
Order-book history runs back to February 2026, and since 2026-05-11 our own collectors have captured the venue continuously, raw websocket messages included. Data is delivered per market per UTC day as parquet with a CSV twin, through signed download links, the REST API, or SFTP on Premium.
Get the book for the market you trade.

A free account claims 5 market·days from any day in July 2026, in the same schema, and pm_depth.py runs on each of them unchanged. No card.

Choose your markets →Plans with the full 25 levels