Skip to content

Capture Polymarket prediction markets

Polymarket runs prediction markets settled in USDC. Each market asks a question with two outcomes, such as Yes and No, and a share of the winning outcome pays $1. ob-analytics captures a Polymarket market through the ccxt source and replays it through the L2 path. You do not need an account or an API key: the order book and the trades stream over Polymarket's public websocket.

What this feed shows

Property Value
Level L2
Depth shown whole book
Update form changes, merged, and a whole-book snapshot every second or two while trading
What can be missed merged changes
After a lost message the next snapshot corrects it
Sequence none
Clocks venue + receive
Crossing price levels
Taker side venue
Trade tape gaps repeated trades removed
Price grid 0.01 or 0.001, finer near 0 and 1; can change during the capture, and the capture finds the finer step from the prices
What the book means one book per outcome
Access public

Use it for one outcome's book, probability and trades. Don't use it for the sequence check: Polymarket sends no sequence number. What each feed shows explains each property and compares every source.

pip install "ob-analytics[ccxt]"

ob-analytics capture ccxt --exchange polymarket --pair <token id> --minutes 10 --out /tmp/poly
ob-analytics process /tmp/poly --source depth_csv --gallery --output /tmp/poly_out

Find a token id

Every Polymarket outcome has its own order book, and --pair names one outcome by its token id: a number about 77 digits long. ccxt also has a readable outcome symbol, but it cannot look one up from the symbol alone, so the capture needs the token id.

Polymarket's public Gamma API lists the open markets. For each market, clobTokenIds holds the token ids in the same order as outcomes:

curl -s "https://gamma-api.polymarket.com/markets?active=true&closed=false&order=volume24hr&ascending=false&limit=10"

To print the token ids of one market, take the market's slug from the end of its URL on polymarket.com:

import json
import urllib.request

slug = "will-the-fed-increase-interest-rates-by-25-bps-after-the-september-2026-meeting-649"
url = f"https://gamma-api.polymarket.com/markets?slug={slug}"
# The API refuses Python's default User-Agent, so name the client.
request = urllib.request.Request(url, headers={"User-Agent": "ob-analytics"})
market = json.load(urllib.request.urlopen(request))[0]
for outcome, token in zip(json.loads(market["outcomes"]), json.loads(market["clobTokenIds"])):
    print(outcome, token)

Each outcome is its own book

A Yes share and a No share of one market together pay $1, so the two books mirror each other: a bid for Yes at 0.63 matches an ask for No at 0.37. Polymarket publishes both books, and prices each outcome's trades in that outcome. So in one capture the book and the trades always agree, whichever outcome you pick.

The Yes book The No book
best bid 0.63, best ask 0.64 best bid 0.36, best ask 0.37

Capture the Yes outcome to read the book as the market's probability of Yes. This is the same view a Kalshi capture gives.

Some questions have more than two answers, such as who will win a nomination. Polymarket lists each answer as a Yes/No market of its own, so you capture each answer separately.

Prices are in USDC, between 0 and 1. Sizes are numbers of shares, and can be fractional.

Tick sizes

A Polymarket market's price step is usually 0.01 or 0.001, and Polymarket makes it finer as a price nears 0 or 1. A capture starts from the tick size in ccxt's market data and records it as tick_size in meta.json. If a later price arrives between two ticks, the capture divides the tick size by ten until the price fits, and counts each change in tick_size_changes. The pipeline reads the final value, in Python and in ob-analytics process and ob-analytics audit, so every price in the file replays exactly. Sizes work the same way: the capture records the size step as lot_size, and makes it finer if a size arrives between two steps (lot_size_changes).

from ob_analytics import Pipeline

result = Pipeline.from_source("depth_csv").run("/tmp/poly")
result.config.tick_size  # 0.001, for example

Depth bins

The depth summary's rings are 25 basis points wide by default, which is too narrow for prices between 0 and 1. Use wider rings, as for Kalshi.

A depth heatmap of flat lines

The depth heatmap leaves out each price level that does not change during the capture. A short capture of a quiet market can change no level at all. The heatmap then draws every level as a flat line, with a note that no level changed. Capture for longer to see the book move, or pass show_all_depth=True to draw the unchanged levels, as for Kalshi.

What a capture can and cannot see

The capture streams Polymarket's public websocket, so it sees the book change and the trades as they happen. Each row has two times: timestamp, when the capture received it, and exchange_timestamp, Polymarket's own time. Replay orders the rows by timestamp. The capture has two limits:

  • There is no sequence number, so audit cannot check for dropped data.
  • ccxt keeps the book up to date from the websocket, and the capture records each price level whose size differs from the last book it saw. Two changes to one price level that arrive together are recorded as one.

See also