Polymarket API: Trace 28 NFL Tokens From Gamma to CLOB
Polymarket Has Two APIs, and the Order Book Reads Upside Down
Search polymarket api and you land on two subdomains and a docs site that assumes you know what a condition ID is. Polymarket splits its public data across two services. Gamma is the catalogue. CLOB is the order book. They do not share a schema, and you need both.
This post maps the two services with payloads captured on 15 October 2026. It then traces one token from Gamma to CLOB to a decimal-odds feed. We ran that trace on all 14 NFL Week 7 fixtures, 28 outcomes in total, and every top-of-book price and size matched to the share. You will learn:
- What Gamma and CLOB each return, and which one holds the number you want.
- Three parser traps we hit on live data: the stringified arrays, the worst-first order book, and a
/priceendpoint whoseBUYside returns the bid. - How to convert share prices (0 to 1) to decimal odds and back.
- How to pull Polymarket next to Pinnacle and bet365 in one call, then check that feed against CLOB yourself.
Reads need no key and no wallet. You only need requests.
The Landscape: Gamma vs CLOB
Polymarket settles on Polygon, but you do not query the chain. Two off-chain APIs wrap the market state:
| Service | Base URL | Purpose | Auth |
|---|---|---|---|
| Gamma | https://gamma-api.polymarket.com |
Discovery: events, markets, metadata, volume, mid price. The catalogue. | None |
| CLOB | https://clob.polymarket.com |
Order book depth, best bid and ask, midpoint, price history, order placement. | None for reads. Signed EIP-712 payloads plus L2 headers for writes. |
Rule of thumb: Gamma tells you what exists. CLOB tells you what you can trade at. Most developers start at CLOB and then stall, because every CLOB endpoint wants a 77-digit token_id. Gamma hands you the token IDs.
The four ID types
| ID | Example (Chargers v Chiefs) | Where it comes from | What it refers to |
|---|---|---|---|
| Event slug | nfl-lac-kc-2026-10-18 |
Gamma /events |
The fixture. This event holds 308 markets: moneyline, spreads, totals. |
| Market ID | 4329891 |
Gamma /markets |
One question, here “Chargers vs. Chiefs”. |
| Condition ID | 0xa70da744…b546 |
Gamma markets[].conditionId |
The on-chain market hash. CLOB returns it as market. |
Token ID (asset_id) |
58167714289864… (77 digits) |
Gamma markets[].clobTokenIds |
One outcome. Every CLOB endpoint wants this. |
A two-way market has one condition ID and two token IDs. On an NFL moneyline the tokens are the two participants, not Yes and No.
Gamma API: The Catalogue
Gamma is plain REST. You will use /markets and /events most of the time.
Listing active markets
import requests, json
GAMMA = "https://gamma-api.polymarket.com"
r = requests.get(f"{GAMMA}/markets", params={
"limit": 3,
"active": "true",
"closed": "false",
})
for m in r.json():
# outcomes / outcomePrices / clobTokenIds are stringified JSON arrays
outcomes = json.loads(m["outcomes"])
prices = json.loads(m["outcomePrices"])
tokens = json.loads(m["clobTokenIds"])
print(m["question"])
for o, p, t in zip(outcomes, prices, tokens):
print(f" {o}: {p} (token {t[:12]}...)")
Output on 15 October 2026:
Xi Jinping out before 2027?
Yes: 0.0285 (token 323382201900...)
No: 0.9715 (token 256593106749...)
Will Gavin Newsom win the 2028 Democratic presidential nomination?
Yes: 0.1545 (token 545330438199...)
No: 0.8455 (token 878541741480...)
Will Alexandria Ocasio-Cortez win the 2028 Democratic presidential nomination?
Yes: 0.1685 (token 107064985435...)
No: 0.8315 (token 651760722613...)
Trap 1: the stringified arrays. Gamma ships outcomes, outcomePrices and clobTokenIds as JSON inside strings. Skip the json.loads and you iterate over characters, so the first outcome becomes the letter [.
The Chiefs moneyline as Gamma returns it (captured 15 Oct 2026, 13:13 UTC, trimmed from 90+ keys):
{
"id": "4329891",
"question": "Chargers vs. Chiefs",
"conditionId": "0xa70da744210533416edb26fb4ee19ea7a7acdaef6daabb5077c4dec89494b546",
"slug": "nfl-lac-kc-2026-10-18",
"outcomes": "[\"Chargers\", \"Chiefs\"]",
"outcomePrices": "[\"0.215\", \"0.785\"]",
"clobTokenIds": "[\"25975613657668487250673927756557799100593582472155421789800705293576427824049\", \"58167714289864717825087991545150213770744040987591942526721915426213133665357\"]",
"volume": "3512.5369280000004",
"liquidity": "149784.2895",
"lastTradePrice": 0.21,
"bestBid": 0.21,
"bestAsk": 0.22,
"spread": 0.01,
"negRisk": false,
"active": true,
"closed": false,
"orderPriceMinTickSize": 0.01,
"orderMinSize": 5
}
Three details matter here. outcomePrices holds the midpoint (0.785 for the Chiefs), not a price you can fill. bestBid and bestAsk belong to the first outcome only, so 0.21 / 0.22 is the Chargers. And Gamma lists the away participant first: “Chargers vs. Chiefs” for a Chiefs home fixture. Gamma did this on 14 of 14 Week 7 fixtures. If you zip Gamma outcomes against a feed that puts the home side first, you flip every price. Match on the token ID, never on position.
Finding an event from its slug
import requests
GAMMA = "https://gamma-api.polymarket.com"
# fixturePath from OddsPapi ends in the Gamma event slug
ev = requests.get(f"{GAMMA}/events", params={"slug": "nfl-lac-kc-2026-10-18"}).json()[0]
print(ev["title"], "|", len(ev["markets"]), "markets")
for m in ev["markets"][:4]:
print(" -", m["question"], "| condition", m["conditionId"][:10] + "...")
Chargers vs. Chiefs | 308 markets
- Chargers vs. Chiefs | condition 0xa70da744...
- Spread: Chiefs (-0.5) | condition 0x97aa81e2...
- Spread: Chiefs (-1.5) | condition 0x1f349328...
- Spread: Chiefs (-2.5) | condition 0xd0071583...
Gamma notes:
active=true&closed=falsefilters to tradeable markets.activealone still returns resolved ones.- Market
volumeis a string, but eventvolumeis a number. Wrap both infloat()before you sort. /markets?clob_token_ids=<token>takes you from a token back to its market. It resolved 28 of 28 NFL tokens in our run.- Paginate with
limitandoffset, and do not assume a stable order between calls.
CLOB API: The Order Book
CLOB serves depth and history for one token at a time. Read endpoints need no auth.
import requests
CLOB = "https://clob.polymarket.com"
CHIEFS = "58167714289864717825087991545150213770744040987591942526721915426213133665357"
book = requests.get(f"{CLOB}/book", params={"token_id": CHIEFS}).json()
# both sides arrive worst-first: the touch is the LAST element
best_bid, best_ask = book["bids"][-1], book["asks"][-1]
print("tick", book["tick_size"], "| min order", book["min_order_size"], "| neg_risk", book["neg_risk"])
print("best bid", best_bid, "| best ask", best_ask)
# side=BUY returns the best BID, side=SELL the best ASK
buy = requests.get(f"{CLOB}/price", params={"token_id": CHIEFS, "side": "BUY"}).json()
sell = requests.get(f"{CLOB}/price", params={"token_id": CHIEFS, "side": "SELL"}).json()
mid = requests.get(f"{CLOB}/midpoint", params={"token_id": CHIEFS}).json()
print("BUY", buy["price"], "| SELL", sell["price"], "| mid", mid["mid"])
hist = requests.get(f"{CLOB}/prices-history", params={
"market": CHIEFS, # named "market", takes a token_id
"interval": "1d",
"fidelity": "60", # 60-minute buckets
}).json()["history"]
print(len(hist), "points, first", hist[0])
tick 0.01 | min order 5 | neg_risk False
best bid {'price': '0.78', 'size': '39472.87'} | best ask {'price': '0.79', 'size': '2495.56'}
BUY 0.78 | SELL 0.79 | mid 0.785
24 points, first {'t': 1791986416, 'p': 0.785}
Trap 2: the book reads worst-first. bids run from 0.01 up to 0.78, and asks run from 0.99 down to 0.79. The first rows you print are deep, far-from-the-money orders. The touch sits at index -1 on both sides. Every number is a string, so cast before you do math.
Trap 3: side=BUY returns the bid. The side names the resting orders, not your trade. If you want to buy the Chiefs now, you pay the SELL price, 0.79. A bot that reads BUY as “my cost to buy” quotes one tick too cheap on every market.
/prices-history returns {t, p} points, where p is the midpoint. interval accepts 1h, 6h, 1d, 1w, 1m and max. fidelity is the bucket size in minutes.
Order placement is out of scope. It needs a funded Polygon wallet, an EIP-712 signature and L2 headers; Polymarket documents the flow in Place Orders. Baskets with neg_risk: true (multi-outcome events) behave differently again; our negative-risk post sizes one at the bid.
One Price, Three Shapes
| Source | Endpoint | Path to the price you can fill | Units |
|---|---|---|---|
| Gamma | /markets?slug=… |
None. outcomePrices is the mid; bestAsk covers outcome 0 only. |
Share (0 to 1) |
| CLOB | /book?token_id=… |
book["asks"][-1]["price"] (string) |
Share (0 to 1) |
| OddsPapi | /v4/odds?fixtureId=… |
bookmakerOdds["polymarket"]["markets"]["141"]["outcomes"][oid]["players"]["0"]["price"] |
Decimal (1 or more) |
Share Prices to Decimal Odds
Polymarket quotes shares. Sportsbooks quote decimal odds. One line converts each way:
def share_to_decimal(share: float) -> float:
"""0.79 -> 1.2658..."""
return 1 / share
def decimal_to_share(decimal: float) -> float:
"""1.266 -> 0.7899..."""
return 1 / decimal
def implied_pct(price, is_decimal=True):
"""Implied probability as a percentage."""
return round((1 / price if is_decimal else price) * 100, 2)
print(implied_pct(0.79, is_decimal=False)) # -> 79.0
print(round(share_to_decimal(0.79), 3)) # -> 1.266
print(implied_pct(1.244)) # -> 80.39 (Pinnacle, same outcome)
The math is the easy part. The work is doing it for every outcome on every fixture, matching Polymarket slugs to sportsbook fixtures, and keeping the home and away sides straight when Gamma lists them in reverse. For a converter that also covers American and fractional odds, see Odds Converter in Python.
Skip the Plumbing: Polymarket Next to 350+ Books
OddsPapi carries polymarket as a bookmaker slug among 350+ books and converts every price to decimal odds. It keys Polymarket on the same market IDs as Pinnacle and bet365:
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
r = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY,
"fixtureId": "id1400003171515918", # Chiefs v Chargers, NFL Week 7
"bookmakers": "polymarket,pinnacle,bet365",
})
bo = r.json()["bookmakerOdds"]
# NFL moneyline is market 141. Outcome 141 = participant 1 (Chiefs), 142 = Chargers.
for slug in ("polymarket", "pinnacle", "bet365"):
outcomes = bo.get(slug, {}).get("markets", {}).get("141", {}).get("outcomes", {})
for oid in ("141", "142"):
p = outcomes.get(oid, {}).get("players", {}).get("0", {})
print(f"{slug:11s} {oid}: {p.get('price')} limit={p.get('limit')}")
polymarket 141: 1.266 limit=1971.49
polymarket 142: 4.545 limit=8684.03
pinnacle 141: 1.244 limit=18442.0
pinnacle 142: 4.28 limit=4500.0
bet365 141: 1.18 limit=None
bet365 142: 4.6 limit=None
On that board Polymarket ran the moneyline at a 0.99% margin, Pinnacle at 3.75% and bet365 at 6.48%. Polymarket held the best Chiefs price and bet365 the best Chargers price. limit on Polymarket is the stake you can place at the top price; bet365 publishes none. For margins and depth across 88 fixtures and three prediction venues, read Polymarket API & Kalshi API: Compare 1% Markets to Pinnacle.
The CLOB book lives in exchangeMeta
Each Polymarket outcome carries a three-level ladder:
"exchangeMeta": {
"back": [ {"cents": 0.79, "price": 1.266, "size": 2495.56, "limit": 1971.49}, ... x3 ],
"lay": [ {"cents": 0.22, "price": 4.545, "size": 39472.87, "limit": 8684.03}, ... x3 ],
"bookmakerLayOutcomeId": "25975613657668487250673927756557799100593582472155421789800705293576427824049"
}
Read it like this. cents is the native share price and price is 1 / cents. size is shares, which is also the payout in dollars. limit is the stake: size × cents. The outcome’s bookmakerOutcomeId is its CLOB token, and bookmakerLayOutcomeId is the other participant’s token. Other exchanges on OddsPapi use different ladder shapes, so parse each venue on its own terms.
Check the feed against CLOB yourself
The token IDs let you audit the decimal feed against the source book:
import requests
API_KEY = "YOUR_API_KEY"
CLOB = "https://clob.polymarket.com"
odds = requests.get("https://api.oddspapi.io/v4/odds", params={
"apiKey": API_KEY,
"fixtureId": "id1400003171515918",
"bookmakers": "polymarket",
}).json()
pm = odds["bookmakerOdds"]["polymarket"]
print("event:", pm["fixturePath"])
for oid, oc in pm["markets"]["141"]["outcomes"].items():
p = oc["players"]["0"]
meta = p["exchangeMeta"]
back, lay = meta["back"][0], meta["lay"][0]
book = requests.get(f"{CLOB}/book", params={"token_id": p["bookmakerOutcomeId"]}).json()
ask, bid = book["asks"][-1], book["bids"][-1]
print(f"outcome {oid}: back {back['cents']} x {back['size']:,.2f} | CLOB ask {ask['price']} x {ask['size']}")
print(f" lay {lay['cents']} x {lay['size']:,.2f} | CLOB bid {bid['price']} x {bid['size']}"
f" (1 - bid = {1 - float(bid['price']):.2f})")
print(f" limit {back['limit']:,.2f} = size x cents {back['size'] * back['cents']:,.2f}")
event: https://polymarket.com/event/nfl-lac-kc-2026-10-18
outcome 141: back 0.79 x 2,495.56 | CLOB ask 0.79 x 2495.56
lay 0.22 x 39,472.87 | CLOB bid 0.78 x 39472.87 (1 - bid = 0.22)
limit 1,971.49 = size x cents 1,971.49
outcome 142: back 0.22 x 39,472.87 | CLOB ask 0.22 x 39472.87
lay 0.79 x 2,495.56 | CLOB bid 0.21 x 2495.56 (1 - bid = 0.79)
limit 8,684.03 = size x cents 8,684.03
The output shows how Polymarket builds a two-way market. A bid for the Chiefs at 0.78 is the same order as an ask for the Chargers at 0.22, so the two tokens share one book. OddsPapi’s back ladder is the token’s ask side. Its lay ladder is the token’s bid side, quoted as the other participant’s price.
We ran this check on all 14 NFL Week 7 fixtures on 15 October 2026, 13:09 UTC, with about one second between the OddsPapi call and each CLOB call:
| Check | Matched |
|---|---|
bookmakerOutcomeId answers on CLOB /book |
28 of 28 outcomes |
Top back cents and size = CLOB best ask |
28 of 28 |
Top lay cents = 1 minus CLOB best bid, size = bid size |
28 of 28 |
| Back levels 2 and 3 = CLOB asks 2 and 3 | 54 of 56 (two sizes moved inside the one-second gap) |
limit = size × cents |
168 of 168 ladder levels |
price = 1 / cents, three decimals |
28 of 28 |
bookmakerLayOutcomeId = the other outcome’s token |
28 of 28 |
Gamma /markets?clob_token_ids= finds the market |
28 of 28 |
For ladder depth across seven prediction venues on the same sport, see NFL Prediction Market Liquidity. If you want those ladders pushed to you instead of polled, the WebSocket odds API streams them.
Free Historical Polymarket Odds
CLOB’s /prices-history gives you midpoints for one token. OddsPapi’s /v4/historical-odds gives you every price change with the stake behind it, on the free tier. Polymarket has one rule: pass one bookmaker and exactly one outcomeId per call. A call with bookmakers=polymarket,pinnacle returns HTTP 400 INVALID_PARAMETER, so pull Pinnacle in a second call.
import requests
API_KEY = "YOUR_API_KEY"
r = requests.get("https://api.oddspapi.io/v4/historical-odds", params={
"apiKey": API_KEY,
"fixtureId": "id1400003171515892", # Packers v Bears, 11 Oct 2026, 31-7
"bookmakers": "polymarket", # polymarket: one book per call...
"outcomeId": 141, # ...and exactly one outcome
})
snaps = r.json()["bookmakers"]["polymarket"]["markets"]["141"]["outcomes"]["141"]["players"]["0"]
snaps.sort(key=lambda s: s["createdAt"])
live = [s for s in snaps if s["active"] and s["price"] > 1]
close = [s for s in live if s["createdAt"] < "2026-10-11T17:00:00"][-1]
print(len(snaps), "snapshots,", len(live), "active")
print("first active:", live[0])
print("close: ", close)
18191 snapshots, 17337 active
first active: {'createdAt': '2026-08-31T12:42:37.650Z', 'price': 1.031, 'limit': 21.06, 'active': True, 'exchangeMeta': None}
close: {'createdAt': '2026-10-11T16:59:57.490Z', 'price': 2.0, 'limit': 80312.12, 'active': True, 'exchangeMeta': None}
The Packers book opened 41 days out at 1.031 with $21 behind it, a placeholder price. It closed three seconds before kick-off at 2.0 with $80,312. Pinnacle closed the same outcome at 2.0 with a $56,250 limit (a second call, bookmakers=pinnacle). Two things to know: exchangeMeta is null on every historical snapshot, so use the snapshot limit for depth, and one Polymarket outcome on one NFL fixture is about 1.9 MB. To score closing prices against results, follow the prediction market accuracy backtest.
Where to Go From Here
- Kalshi too? Kalshi API vs Polymarket API: A Python Developer's Comparison.
- Cross-venue arbitrage? Polymarket Arbitrage: A 0.44% Median Edge on $4,023 of Depth.
- Quoting on Polymarket? Market Making on Polymarket.
- A live tracker? Prediction Market Dashboard with Python & Streamlit.
- Pricing? Odds API Pricing 2026.
One Schema for Polymarket and the Sportsbooks
Gamma plus CLOB is the right stack if you trade inside Polymarket. If you compare Polymarket against sportsbooks, you want the prices in decimal, keyed on shared market IDs, with history you can backtest for free. That is what OddsPapi gives you, and the token IDs let you audit every Polymarket price against CLOB.
Get your free API key and pull polymarket next to Pinnacle, bet365, DraftKings, FanDuel and 350+ more books in one call.
FAQ
What is the difference between Gamma and CLOB?
Gamma is Polymarket's catalogue API: events, markets, volume, midpoint prices and the clobTokenIds for each outcome. CLOB is the order book API: depth, best bid and ask, midpoint, price history and order placement. You use Gamma to find a market's token IDs, then CLOB to read the book for each token.
Is the Polymarket API free?
Yes, for reads. Gamma and the CLOB read endpoints need no key. Placing orders needs a funded Polygon wallet and EIP-712 signed payloads.
Why does CLOB /price with side=BUY return a lower price than I can buy at?
The side parameter names the resting orders. side=BUY returns the best bid and side=SELL returns the best ask. On the Chiefs token on 15 October 2026, BUY returned 0.78 and SELL returned 0.79. To buy now, you pay the SELL price.
Can I place Polymarket trades through OddsPapi?
No. OddsPapi is a read-only data API. For execution you sign orders against Polymarket's CLOB yourself. OddsPapi gives you the bookmakerOutcomeId token, which is the ID CLOB expects.
Where do I get historical Polymarket odds?
Polymarket's CLOB /prices-history returns midpoint points for one token. OddsPapi's /v4/historical-odds returns every Polymarket price change with its stake limit, in decimal odds, on the free tier. For Polymarket, pass one bookmaker and one outcomeId per call; a Packers outcome from 11 October 2026 returned 18,191 snapshots.
What format are Polymarket prices in?
Share prices from 0 to 1, read as implied probability. A Chiefs ask of 0.79 means 79%, or decimal odds of 1 / 0.79 = 1.266. Gamma and CLOB send prices as strings, so cast them before you do math. OddsPapi sends the decimal price and keeps the share price as cents on each ladder level.
Why are Gamma outcomes in a different order from my sportsbook feed?
Gamma lists the away participant first on NFL markets ("Chargers vs. Chiefs" for a Chiefs home fixture); it did so on all 14 Week 7 fixtures. OddsPapi puts participant 1 (the home side) in outcome 141. Match outcomes on the token ID, never on array position.