Polymarket Negative Risk: Size a 42-Leg Basket at the Bid
What a Negative Risk Market Actually Is
Polymarket groups mutually exclusive outcomes into one event and flags it negRisk: true. Exactly one leg resolves YES. Every other leg resolves NO. That single guarantee is the whole mechanism, and it hands you an arithmetic check that works on any basket in the feed.
Buy NO on all 42 legs of a 42-leg basket and 41 of them pay out a dollar. Your payout is fixed at 41 before a ball is kicked. So the basket is profitable when the cost of those 42 NO shares comes in under 41, and the whole question is what you pay.
Buying NO at the ask on a leg costs 1 - bestBid, because the NO ask and the YES bid are the same price viewed from opposite sides. Sum it across the basket and the algebra collapses to one line:
cost = n - sum(bestBid)
payout = n - 1
edge = sum(bestBid) - 1
Your edge is the sum of the YES bids minus one. Nothing else in the basket matters.
The Screen Says 305%. The Book Says 99.8%.
Here is the Champions League 2027 winner market, 42 legs, half a million dollars of volume in a day, measured at 15:03 UTC on 16 September 2026.
import ast, requests
GAMMA = "https://gamma-api.polymarket.com"
def basket(slug):
event = requests.get(f"{GAMMA}/events", params={"slug": slug}, timeout=60).json()[0]
legs = [m for m in event["markets"] if m.get("bestBid") is not None]
rows = []
for m in legs:
prices = m["outcomePrices"]
if isinstance(prices, str):
prices = ast.literal_eval(prices)
rows.append({
"name": m["groupItemTitle"],
"mid": float(prices[0]),
"bid": m["bestBid"],
"ask": m["bestAsk"],
})
return event, rows
event, rows = basket("uefa-champions-league-2027-champion-20260701202025549")
n = len(rows)
sum_mid = sum(r["mid"] for r in rows)
sum_bid = sum(r["bid"] for r in rows)
no_bid = [r for r in rows if r["bid"] == 0]
print(f'{event["slug"]} negRisk={event["negRisk"]} legs={n}')
print(f" sum of mid prices {sum_mid:.4f}")
print(f" sum of best bids {sum_bid:.4f}")
print(f" legs with no bid {len(no_bid)} {[r['name'] for r in no_bid]}")
print(f"\nbuy NO on every leg at the ask: costs {n - sum_bid:.4f}, pays {n - 1}")
print(f"edge per basket: {sum_bid - 1:+.4f}")
uefa-champions-league-2027-champion negRisk=True legs=42
sum of mid prices 4.0490
sum of best bids 0.9980
legs with no bid 6 ['Other', 'Team H', 'Team I', 'Team J', 'Team K', 'Team L']
buy NO on every leg at the ask: costs 41.0020, pays 41
edge per basket: -0.0020
Sum the outcomePrices field and you get 4.0490 across 42 legs. Read that as a book and it looks like a 305% overround sitting in public with nobody taking it.
Sum the bids and you get 0.9980. The market is priced at 99.8 cents on the dollar, which is tighter than any sportsbook three-way you will find this week.
The gap is six legs called Other, Team H, Team I, Team J, Team K and Team L. Those are placeholders for clubs that have not qualified yet. Each has an empty order book: bid 0, ask 1, and a mid of exactly 0.500 that no trader ever quoted. Six phantom halves add 3.000 to the sum, and 4.0490 minus 3.000 lands back at 1.049.
Run the same check across the live baskets and the pattern holds. Counts are legs with no bid at all:
| Basket | Legs | Sum of mids | Sum of bids | No bid | Edge |
|---|---|---|---|---|---|
| Champions League 2027 | 42 | 4.049 | 0.9980 | 6 | -0.0020 |
| LaLiga 2027 | 21 | 2.504 | 0.9860 | 3 | -0.0140 |
| NFL 2027 champion | 33 | 1.039 | 1.0080 | 1 | +0.0080 |
| Ballon d’Or 2026 | 70 | 1.012 | 1.0010 | 58 | +0.0010 |
| Republican nominee 2028 | 118 | 0.858 | 0.8410 | 85 | -0.1590 |
| Fed decision, September | 4 | 1.000 | 0.9890 | 0 | -0.0110 |
The two baskets with a positive number are worth 0.8 and 0.1 cents against a payout of 32 and 69 dollars. Before fees, before gas, before the price moves while you are filling 70 legs one at a time.
So count the empty books first. It costs one pass over the rows and it tells you whether the number on your screen came from traders or from a placeholder.
Every Three-Way Football Match Is a Three-Leg Basket
The same flag sits on match-day markets, and there the basket is small enough to trade. Barcelona against Racing Santander in LaLiga, measured at 15:03 UTC with kick-off at 19:30:
| Leg | Bid | Ask | Ask as decimal odds |
|---|---|---|---|
| FC Barcelona | 0.910 | 0.920 | 1.087 |
| Draw | 0.050 | 0.060 | 16.667 |
| Real Racing Club | 0.028 | 0.029 | 34.483 |
Bids sum to 0.9880, so the NO basket loses 1.2 cents and there is no arb inside Polymarket. Read the same three numbers as a sportsbook price and Polymarket is quoting a 1X2 at a 1.2% margin against a board that runs at 5.60%.
Check the Depth Before You Check the Price
A price with no size behind it is a screenshot. Pull the order book for each leg and read the top rung:
import ast, requests
GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
event = requests.get(f"{GAMMA}/events",
params={"slug": "lal-bar-rrc-2026-09-16"}, timeout=60).json()[0]
print(f'{event["slug"]} negRisk={event["negRisk"]} legs={len(event["markets"])}')
print(f'sum of best bids {sum(m["bestBid"] for m in event["markets"]):.4f}\n')
for m in event["markets"]:
tokens = m["clobTokenIds"]
if isinstance(tokens, str):
tokens = ast.literal_eval(tokens)
book = requests.get(f"{CLOB}/book", params={"token_id": tokens[0]}, timeout=60).json()
asks = sorted(({"price": float(a["price"]), "size": float(a["size"])} for a in book["asks"]),
key=lambda a: a["price"])
top = asks[0]
print(f'{m["groupItemTitle"][:34]:36} ask {top["price"]:.3f} '
f'= decimal {1 / top["price"]:7.3f} '
f'{int(top["size"]):>9,} shares = ${top["price"] * top["size"]:>10,.0f}')
FC Barcelona ask 0.920 = decimal 1.087 601,663 shares = $553,530
Draw ask 0.060 = decimal 16.667 65,626 shares = $ 3,938
Real Racing Club ask 0.029 = decimal 34.483 1,241 shares = $ 36
Half a million dollars sits on the favourite at 1.087. Thirty-six dollars sits on Racing at 34.483. Same market, same second, and the long shot that makes every arbitrage calculator light up is the one rung that cannot take a real stake.
Where the Gap Actually Lives
Polymarket prices its own basket to within 1.2 cents, so the money is not inside the event. It is between Polymarket and the 285 bookmakers pricing the same match.
import os, requests, statistics
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE_URL = "https://api.oddspapi.io/v4"
FIXTURE = "id1000000872478576" # FC Barcelona v Racing Santander, LaLiga
ONE_X_TWO = "101"
books = requests.get(f"{BASE_URL}/odds",
params={"apiKey": API_KEY, "fixtureId": FIXTURE}).json()["bookmakerOdds"]
quotes = {}
for slug, book in books.items():
market = book["markets"].get(ONE_X_TWO)
# prizepicks ships a row with only "markets", so read every flag with .get()
if book.get("suspended") or not market or not market.get("marketActive"):
continue
prices = {oid: o["players"]["0"]["price"] for oid, o in market["outcomes"].items()
if o["players"]["0"]["active"] and o["players"]["0"]["price"] > 1.0}
if len(prices) == 3:
quotes[slug] = prices
margins = {s: sum(1 / v for v in p.values()) - 1 for s, p in quotes.items()}
print(f"{len(books)} books on the fixture, {len(quotes)} complete 1X2 quotes")
print(f"board median margin {statistics.median(margins.values()) * 100:.2f}%")
for slug in ("polymarket", "kalshi", "betfair-ex", "pinnacle"):
if slug in quotes:
print(f' {slug:12} {quotes[slug]} margin {margins[slug] * 100:5.2f}%')
best = {}
for outcome_id in ("101", "102", "103"):
slug, price = max(((s, p[outcome_id]) for s, p in quotes.items()), key=lambda x: x[1])
best[outcome_id] = (slug, price)
total = sum(1 / p for _, p in best.values())
print(f"\nbest price on each leg:")
for outcome_id, (slug, price) in best.items():
print(f" {outcome_id}: {price:7.3f} at {slug}")
print(f" implied total {total:.4f}")
285 books on the fixture, 266 complete 1X2 quotes
board median margin 5.60%
polymarket {'101': 1.087, '102': 16.667, '103': 34.483} margin 0.90%
kalshi {'101': 1.087, '102': 20.0, '103': 33.333} margin -0.00%
betfair-ex {'101': 1.09, '102': 17.5, '103': 32.0} margin 0.58%
pinnacle {'101': 1.082, '102': 13.27, '103': 23.04} margin 4.30%
best price on each leg:
101: 1.110 at coral
102: 20.000 at kalshi
103: 34.483 at polymarket
implied total 0.9799
An implied total under 1.0000 is an arb, and 0.9799 is a 2.01% one: Coral on Barcelona, Kalshi on the draw, Polymarket on Racing. Restrict it to the 256 sportsbooks and you still get 0.9900, a 1.00% gap between Coral, atg.se and 1xbet.
Then read the sizes above and decide what it is worth. The Racing leg tops out at 36 dollars on the exchange, which sets the stake for the whole position; the other two legs get scaled to it. That is the number the calculators leave out, and it is why the 2% you screenshot at 15:03 is not the 2% you bank.
The prediction markets earn their place on the long shots rather than the favourite. Pinnacle has the draw at 13.27 and Racing at 23.04, against a board median of 13.00 and 23.00, while Polymarket pays 16.667 and 34.483 on the same two legs. On Barcelona at 1.08 the three venues agree to within a cent. Line shopping across 350+ books and the two exchanges is the same query either way, so run it on the whole board rather than on the venue you happen to like.
Three Checks Before You Trust a Basket
- Count the legs with no bid. An empty book reports a 0.500 mid that no trader quoted. Six of them turned a 1.05 market into a 4.05 screenshot.
- Sum the bids, and treat the mid as decoration. Your edge on a NO basket is
sum(bestBid) - 1. On every sports basket measured here that number came out between -1.6 cents and +0.8 cents. - Price the size, not the rung. Pull
/bookper token and multiply the top ask by its size. Thirty-six dollars of depth is not a trade, whatever the decimal odds say.
FAQ
What does negRisk mean on Polymarket?
It flags an event whose legs are mutually exclusive: exactly one resolves YES and the rest resolve NO. Buying NO on all n legs therefore pays n - 1 whatever happens, which is what makes the basket arithmetic work.
How do I find arbitrage on a Polymarket negative risk market?
Sum the best bid across every leg. Buying NO at the ask costs n - sum(bestBid) against a fixed payout of n - 1, so your edge is sum(bestBid) - 1. On the Champions League 2027 basket that came to -0.0020 across 42 legs on 16 September 2026.
Why do Polymarket prices sum to more than 1?
Because the mid price of a leg with an empty order book is 0.500 by construction, not by trading. The Champions League basket carried six placeholder legs with a bid of 0 and an ask of 1, which pushed the sum of mids to 4.05 while the bids summed to 0.998.
Is Polymarket cheaper than a sportsbook?
On that LaLiga fixture, yes on the margin and no on the size. Polymarket quoted the three-way at 0.90% against a 5.60% board median across 266 complete quotes, and its Racing price of 34.483 beat every sportsbook. The top rung held 1,241 shares, about 36 dollars.
Which endpoint gives me Polymarket order book depth?
The CLOB API at https://clob.polymarket.com/book?token_id=.... Token IDs arrive as a stringified array in the clobTokenIds field on each Gamma market, so parse it before you use it.
Can I pull Polymarket and sportsbook prices in one call?
Yes. Polymarket and Kalshi ship as bookmaker slugs inside the OddsPapi /odds payload, so one call to /odds returned all 285 venues on that fixture with the two prediction markets among them.
Run It on Tonight’s Board
The basket check above is fifteen lines of Python against a public endpoint. The half that needs a key is the other side: 350+ bookmakers, free historical odds for the backtest, and a WebSocket feed when you want the gap as it opens rather than as it closes.
Get your free API key and point the cross-venue snippet at tonight’s fixtures. No credit card, no sales call.
Related reading: the full Polymarket versus sportsbook scanner, Gamma versus CLOB JSON shapes, Kalshi against Polymarket, and building a consensus line from the board.