If you build on Polymarket's CLOB, you've read this in the API docs for the order book endpoint (GET https://clob.polymarket.com/book?token_id=...):
bids: List of bid orders (sorted by price descending)
asks: List of ask orders (sorted by price ascending)
So you do the obvious thing — best ask is asks[0], best bid is bids[-1] — ship it, and it works on every mock you test. Then it quietly costs you money in production. Here's why, and the one-line fix.
The arrays aren't reliably sorted
In practice the /book arrays do not come back dependably sorted. The case that bites everyone: a stale long-shot order priced at 0.99 sits at asks[0], while the genuinely best ask — say 0.53 — is somewhere in the middle of the array.
{
"asks": [
{"price": "0.99", "size": "3"},
{"price": "0.53", "size": "80"},
{"price": "0.55", "size": "40"},
{"price": "0.53", "size": "20"}
],
"bids": [
{"price": "0.51", "size": "120"},
{"price": "0.48", "size": "300"}
]
}
Trust the documented sort and your bot believes the best ask is 0.99. It mis-quotes, mis-sizes, skips fills it should take, or sends a marketable order priced off a number that isn't the real top of book. Nothing throws. The docs told you it was sorted; reality disagreed.
The fix: derive it, don't index it
Never trust array order. Compute the top of book from the data:
best_ask = min(float(a["price"]) for a in book["asks"]) # 0.53, not 0.99
best_bid = max(float(b["price"]) for b in book["bids"]) # 0.51
The depth gotcha right behind it
There's a second mistake hiding here. A market order can only immediately fill against the best price level. So "how much can I take right now" is the size summed at the best price — not the sum of every level (which pads in size you can't touch without walking the book).
ask_depth = sum(float(a["size"]) for a in book["asks"]
if abs(float(a["price"]) - best_ask) < 1e-9) # 80 + 20 = 100
I packaged the correct parse
Rather than re-deriving this in every project, I open-sourced a tiny, zero-dependency parser (MIT): best_ask, best_bid, best_ask_depth, best_bid_depth, spread, mid_price, and a one-call top_of_book — with safe handling for empty and one-sided books.
from polymarket_book_parser import top_of_book
top_of_book(book)
# {'best_ask': 0.53, 'best_bid': 0.51, 'ask_depth': 100.0,
# 'bid_depth': 120.0, 'spread': 0.02, 'mid': 0.52}
Repo (free, MIT, tests included):
https://github.com/BlueWhale-Quant-Lab/polymarket-order-book-best-bid-ask-parser
Where it gets harder
A single /book read is fine for a glance. A live bot keeps the book in memory over the CLOB WebSocket, and that's where the real edge cases live: merging the book snapshot and price_change delta events without drifting, detecting when the socket goes quiet but stays connected (your "best ask" is now minutes stale), and spotting liquidity traps before you fire an order into an empty book. I put that in a complete version, but the free parser above fully solves the snapshot case — start there.
Takeaway
On Polymarket's /book, compute best bid/ask with max/min, never by array index, and measure depth at the best level only. One mindset change, a whole class of silent bugs gone.
Top comments (0)