DEV Community

Markus
Markus

Posted on AI-assisted

Comparing Polymarket prices with bookmaker odds in Python

Prediction markets and sportsbooks price the same games, but they rarely agree exactly. Polymarket might have a team at 62 cents while the bookmakers imply 58%. Whether that gap means something depends on what you're doing with it, but you can't look at it without both prices side by side.

This post pulls both for one match and lines them up as implied probabilities. It uses the Odds-API.io Python SDK, which returns Polymarket and Kalshi prices in the same format as regular bookmakers. You need an API key (paid plans).

Setup

pip install odds-api-io
Enter fullscreen mode Exit fullscreen mode
from odds_api import OddsAPIClient

client = OddsAPIClient(api_key="YOUR_API_KEY")
Enter fullscreen mode Exit fullscreen mode

Find a match

events = client.get_events(sport="football", league="england-premier-league", status="pending")
event = events[0]
print(event["id"], event["home"], "vs", event["away"], event["date"])
Enter fullscreen mode Exit fullscreen mode

Get odds from both worlds

books = "Polymarket,Kalshi,Bet365,Unibet,DraftKings,Betfair Exchange"
odds = client.get_event_odds(event_id=event["id"], bookmakers=books)
Enter fullscreen mode Exit fullscreen mode

Polymarket and Kalshi count toward your plan's bookmaker limit like any other source, and names are case-sensitive (bet365 gets a 400). Every source comes back under odds["bookmakers"], keyed by name, each with a list of markets. For a football match, the ML market has home, draw and away as decimal odds. Prices arrive as strings. For Polymarket and Kalshi the API converts cents to decimal odds, so 60 cents comes back as 1.67.

Turn odds into probabilities

Decimal odds of 2.00 imply 50%. Bookmaker prices include a margin, so the three outcomes add up to more than 100%. Normalising removes it:

def implied(ml):
    raw = {k: 1 / float(ml[k]) for k in ("home", "draw", "away") if ml.get(k)}
    total = sum(raw.values())
    return {k: v / total for k, v in raw.items()}

rows = {}
for book in books.split(","):
    markets = odds["bookmakers"].get(book, [])
    ml = next((m for m in markets if m["name"] == "ML"), None)
    if ml and ml["odds"]:
        rows[book] = implied(ml["odds"][0])
Enter fullscreen mode Exit fullscreen mode

Compare

sportsbooks = [b for b in rows if b not in ("Polymarket", "Kalshi", "Betfair Exchange")]
consensus = {
    k: sum(rows[b][k] for b in sportsbooks) / len(sportsbooks)
    for k in ("home", "draw", "away")
}

print(f"{'source':<18}{'home':>8}{'draw':>8}{'away':>8}")
for book, p in rows.items():
    print(f"{book:<18}" + "".join(f"{p.get(k, 0):>8.1%}" for k in ("home", "draw", "away")))
print(f"{'consensus':<18}" + "".join(f"{consensus[k]:>8.1%}" for k in ("home", "draw", "away")))
Enter fullscreen mode Exit fullscreen mode

Betfair Exchange is an exchange rather than a bookmaker, so it stays in the table but out of the bookmaker average. Here is the output for Manchester City vs Fulham (Premier League, 21 November 2026), run on 12 November:

72221406 Manchester City vs Fulham FC 2026-11-21T12:30:00Z
source                home    draw    away
Polymarket           67.3%   18.8%   13.9%
Kalshi               67.6%   18.6%   13.7%
Bet365               65.9%   19.7%   14.4%
Unibet               66.3%   19.5%   14.2%
DraftKings           65.7%   18.9%   15.4%
Betfair Exchange     67.4%   18.6%   14.1%
consensus            66.0%   19.4%   14.7%
Enter fullscreen mode Exit fullscreen mode

Both prediction markets rate City about 1.5 points higher than the bookmaker average, and they sit close to the exchange. Differences of one or two points are normal noise. Bigger gaps usually come from timing (one side moved on news first) or from thin liquidity on the prediction market. For Polymarket and Kalshi the API also returns the other side of the book (layHome, layDraw, layAway) and the size at each price (depthHome, depthDraw, depthAway, plus depthLay...), which tells you how much is available at that price. On this match Kalshi showed 2,377 behind the City price and 16 on the draw, so a thin side can move on very little money.

Where to take it

  • Run it on a schedule and log the gap over time, then check it against the result.
  • Swap the REST call for the WebSocket feed if you want updates as prices move.
  • Use historical closing lines to see whether the prediction market or the bookmakers were closer to the final price.

Docs for the prediction-market fields: https://docs.odds-api.io/guides/prediction-markets

If you want the bigger picture, we ran the same comparison on closing prices for 13,688 games: Polymarket vs Sportsbooks: How Often Do They Disagree?

I run Odds-API.io, so I'm biased toward the API used here.

Top comments (0)