Updated November 3, 2026. This article separates what Polymarket’s official documentation says about data access from the design we propose for handling it as research data. Responses and numbers in the code are hypothetical examples shaped like the documented formats, not real market data or trading results.
A prediction-market price is both a probability estimate and a trade price
On Polymarket each outcome share (Yes/No) trades between $0 and $1, and the official documentation describes that price as the market’s belief in the probability of the outcome. A winning share is redeemable for $1 and a losing share is worth $0. Trading happens on a CLOB (central limit order book) where users’ orders are matched with each other; matching is offchain and settlement runs through smart contracts on Polygon.
The first thing to know when using it as data is that the displayed probability is one specific calculation. According to the documentation, the displayed price is the midpoint of the bid-ask spread, and if the spread is wider than $0.10 the last traded price is shown instead. In a thin market the same “probability” may reflect a single stale trade.
It is also computed differently from other probability gauges. CME FedWatch backs out meeting-by-meeting rate-change probabilities from 30-Day Fed Funds futures prices, assuming moves in 25bp increments. A prediction-market price is a direct trade price on an outcome defined by a specific sentence. This article does not draw a general conclusion that one is more accurate; comparing them requires aligning the same event, time, and outcome definition and then testing.
Public data is split across three APIs with different purposes
The official documentation says market data can be read without credentials. Private requests such as creating or cancelling orders need a wallet signature (L1) and an HMAC signature based on API credentials (L2). This article covers read-only access only.
| API | Base URL | Use in this article |
|---|---|---|
| Gamma API | https://gamma-api.polymarket.com |
Discover events and markets, metadata, outcome identifiers |
| CLOB API | https://clob.polymarket.com |
Current book (/book), best price (/price), midpoint (/midpoint), spread (/spread) |
| Data API | https://data-api.polymarket.com |
Price history (/v2/prices-history), activity and position analytics |
The price history endpoint has constraints to check. An absolute range set with start/end spans at most 15 days per request, and you choose exactly one of a relative window (interval), an absolute range, or a point-in-time read (as_of). The documentation states that finer resolutions are retained for shorter periods, so if you need high-resolution data for long backtests you need a plan to collect and store it yourself. Per-endpoint request limits are listed separately in the official Rate Limits page.
Keep market IDs and outcome token IDs apart
Polymarket’s data model has three levels: event, market, and outcome. An event groups one or more markets, each market is a question with Yes and No outcomes, and prices and order books exist per outcome.
idandconditionIdidentify the market.- The
token_idin price and book requests takes the outcome’s identifier. For existing (CTF) markets, Gamma’sclobTokenIdsis a string containing a JSON array; Polymarket Protocol V2 markets use apositionIdsarray instead. Theversionfield tells them apart.
If you pass a market’s conditionId where a price request expects token_id, or fail to record whether a series is the Yes or the No price, the meaning of the time series cannot be recovered later.
Separate request building from response parsing and test them
Mixing network calls with interpretation logic makes it hard to tell where things broke when a response format changes. The code below contains only URL building and response parsing, and tests them on hypothetical responses modeled on the documented formats. For real requests, send these URLs with any HTTP client.
import json
from decimal import Decimal
from urllib.parse import urlencode
GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
DATA = "https://data-api.polymarket.com"
def markets_url(tag_id, limit=20, cursor=None):
params = {"tag_id": tag_id, "closed": "false", "limit": limit}
if cursor:
params["after_cursor"] = cursor
return f"{GAMMA}/markets/keyset?{urlencode(params)}"
def book_url(asset_id):
return f"{CLOB}/book?{urlencode({'token_id': asset_id})}"
def history_url(asset_id, start, end, bucket_seconds=3600):
if end - start > 15 * 24 * 3600:
raise ValueError("explicit ranges are limited to 15 days per request")
params = {"token_id": asset_id, "start": start, "end": end,
"bucket_seconds": bucket_seconds}
return f"{DATA}/v2/prices-history?{urlencode(params)}"
def outcome_ids(market):
"""CTF markets use the JSON-string clobTokenIds; V2 markets use the positionIds array."""
if market.get("version") == "v2":
ids = market["positionIds"]
else:
ids = json.loads(market["clobTokenIds"])
return {"market_id": market["id"], "condition_id": market["conditionId"],
"yes": ids[0], "no": ids[1]}
def book_summary(book, depth_band=Decimal("0.03")):
bids = sorted((Decimal(l["price"]), Decimal(l["size"])) for l in book["bids"])
asks = sorted((Decimal(l["price"]), Decimal(l["size"])) for l in book["asks"])
best_bid, best_ask = bids[-1][0], asks[0][0]
mid = (best_bid + best_ask) / 2
near = lambda side: sum(s for p, s in side if abs(p - mid) <= depth_band)
return {"bid": best_bid, "ask": best_ask, "mid": mid,
"spread": best_ask - best_bid,
"depth_near_mid": near(bids) + near(asks)}
def parse_history(payload):
return [(int(p["timestamp"]), Decimal(str(p["price"]))) for p in payload["data"]]
# --- Hypothetical responses in the documented format. No network access. ---
sample_market = {
"id": "1001", "version": "v1", "conditionId": "0xabc",
"question": "Hypothetical event?",
"clobTokenIds": "[\"111\", \"222\"]", "positionIds": None,
}
sample_book = {
"asset_id": "111",
"bids": [{"price": "0.40", "size": "500"}, {"price": "0.42", "size": "120"}],
"asks": [{"price": "0.47", "size": "80"}, {"price": "0.60", "size": "900"}],
}
sample_history = {"data": [
{"timestamp": 1788354000, "price": 0.255, "resolution_seconds": 3600},
{"timestamp": 1788357600, "price": 0.262, "resolution_seconds": 3600},
]}
ids = outcome_ids(sample_market)
summary = book_summary(sample_book)
history = parse_history(sample_history)
assert ids == {"market_id": "1001", "condition_id": "0xabc", "yes": "111", "no": "222"}
assert outcome_ids({"id": "2", "version": "v2", "conditionId": "0xdef",
"positionIds": ["9", "10"]})["no"] == "10"
assert book_url(ids["yes"]) == "https://clob.polymarket.com/book?token_id=111"
assert "tag_id=745" in markets_url(745) and "closed=false" in markets_url(745)
assert summary["mid"] == Decimal("0.445") and summary["spread"] == Decimal("0.05")
assert summary["depth_near_mid"] == Decimal("200") # bid 120 at 0.42 + ask 80 at 0.47
assert history[1] == (1788357600, Decimal("0.262"))
try:
history_url("111", 0, 16 * 24 * 3600)
raise AssertionError("range check failed")
except ValueError:
pass
Prices and sizes arrive as strings, so reading them as Decimal allows comparisons without rounding error. In real collection code, storing the collection time, the outcome identifier, and a hash of the raw response alongside each value makes results easier to reproduce.
Define candidate signals as changes that pass a quote-quality filter
That changes in probability may show new information arriving more directly than its level is a hypothesis to test. Two things need to be settled first: filtering out observations where the book is wide or thin, and attaching the asset return for the window after each probability observation.
import pandas as pd
# Hypothetical hourly probability series with the book state at the same times
prob = pd.DataFrame({
"ts": pd.to_datetime(["2026-10-01 00:00", "2026-10-01 01:00", "2026-10-01 02:00",
"2026-10-01 03:00", "2026-10-01 04:00"], utc=True),
"mid": [0.40, 0.41, 0.55, 0.56, 0.70],
"spread": [0.02, 0.02, 0.03, 0.12, 0.02],
"depth": [5000, 5200, 4800, 300, 5100],
})
def usable_changes(df, max_spread=0.05, min_depth=1000):
"""Drop times with a wide or thin book, then compute changes between the remaining observations."""
ok = df[(df["spread"] <= max_spread) & (df["depth"] >= min_depth)].copy()
ok["d_prob"] = ok["mid"].diff()
return ok.dropna(subset=["d_prob"])
changes = usable_changes(prob)
assert list(changes["ts"].dt.hour) == [1, 2, 4] # 03:00 is filtered out
assert abs(changes.loc[changes["ts"].dt.hour == 4, "d_prob"].item() - 0.15) < 1e-12
# Attach the asset return for the window that starts after the probability observation
btc = pd.DataFrame({
"ts": pd.to_datetime(["2026-10-01 01:30", "2026-10-01 02:30", "2026-10-01 04:30"], utc=True),
"ret_next_1h": [0.001, -0.004, 0.002],
})
btc["btc_ts"] = btc["ts"]
joined = pd.merge_asof(changes.sort_values("ts"), btc.sort_values("ts"),
on="ts", direction="forward",
tolerance=pd.Timedelta("45min"), allow_exact_matches=False)
assert joined["ret_next_1h"].notna().all()
assert (joined["btc_ts"] > joined["ts"]).all() # the return window starts after the observation
assert list(joined["ret_next_1h"]) == [0.001, -0.004, 0.002]
The filter thresholds (spread 0.05, depth 1,000) are illustrative. Set real thresholds from each market’s book distribution and fix them before looking at results. Filtering leaves gaps between observations that affect the size of each change, so record the gap as well.
A fixed rule such as “add risk when the probability crosses a level” is not presented here because we could not find support for it. To study how probability changes relate to asset returns, collect samples as an event study comparing before and after, control for other information in the same window (economic releases, rate-futures moves), and then apply the chronological validation from the overfitting checks article.
Limits to check before interpreting or using the data
- Liquidity: the documentation notes that there are no order size limits but large orders can move the price significantly, and advises checking book depth. In a thin market, the probability can move on a few participants’ orders.
- Outcome definition and resolution: the probability is a price on the outcome written in the market’s question. Markets using the UMA Optimistic Oracle are settled through a proposed outcome, a challenge period, and UMA resolution if disputed. Check that the resolution rules match the economic event you want to study.
- Events with several outcomes: when one event contains several markets, the Yes prices may not sum to 1. If you use them as a probability distribution, state whether and how you normalize.
- Geographic restrictions and terms: the official geographic restrictions page blocks OFAC-sanctioned regions completely and lists many countries, including the United States, the United Kingdom, and France, as close-only (no new orders, existing positions can be closed) on both the frontend and the API. South Korea is listed as close-only on the frontend only, with the API itself not restricted. The list can change, so check the official geographic restrictions page, the terms of use, and the laws of your country of residence before any use. This article covers reading public data only and does not encourage trading.
Sources and scope
- Polymarket API overview: Gamma, CLOB, and Data API base URLs; public reads versus L1/L2 authentication.
- Polymarket Prices & Orderbook: price as probability, displayed price (midpoint or last trade), offchain matching and onchain settlement.
- Polymarket Prices and Order Books: parameters of
/book,/price, and/v2/prices-history, with range and resolution limits. - Polymarket Discover Markets: event and market structure,
clobTokenIdsandpositionIds, tag filters. - Polymarket Geographic Restrictions: order restrictions by jurisdiction.
- CME FedWatch methodology: how meeting probabilities are derived from Fed Funds futures.
- This article does not claim that prediction-market prices are more accurate than other indicators, or that probability changes predict asset returns.

