There are four major sources of prediction market data for sports events: Polymarket, Kalshi, ESPN, and sportsbook odds aggregators. Each has a different API, authentication model, data format, and failure mode. This guide compares their published contracts with scoped Python examples. For a wider survey that also covers OddsJam, Sportradar, BetFair, and prediction APIs, see our sports odds API comparison.
Documentation check: October 8, 2026. This is a source-based comparison, not a measured latency or integration benchmark. Three anonymous Polymarket GETs checked one NBA tag, one five-market page and one book. The remaining API examples, authenticated operations and provider service levels were not exercised in this review. Vendor prices, account eligibility, quotas and data licenses can change.
1. Polymarket CLOB API
Polymarket runs a Central Limit Order Book (CLOB) on Polygon. You can read orderbooks, place trades, and stream real-time prices.
Official docs: docs.polymarket.com
Python client: unified polymarket-client recommended for new projects; the retained CLOB example uses py-clob-client-v2 1.2.0
Authentication: Public reads need none; trading requires CLOB credentials and the matching wallet/signature path
Rate limits: Endpoint-specific burst and sustained limits
Data format: JSON
Fee: Per-market taker-fee curve; maker platform fee is zero
Best for: Live trading, real-time orderbook data, sports moneylines
from py_clob_client_v2 import ClobClient
client = ClobClient(
host="https://clob.polymarket.com",
chain_id=137,
)
# Read orderbook
book = client.get_order_book("TOKEN_ID_HERE")
bids = book.get("bids", [])
asks = book.get("asks", [])
best_bid = max((float(level["price"]) for level in bids), default=None)
best_ask = min((float(level["price"]) for level in asks), default=None)
if best_bid is None or best_ask is None:
raise ValueError("Two-sided book unavailable")
Public market data does not require credentials. Trading requires CLOB L2 credentials plus a wallet signer, the correct wallet signature type, and the address that holds pUSD. CLOB V2 became production on April 28, 2026; the archived py-clob-client package’s legacy order-signing flow is unsupported in production. New API users should follow Polymarket's deposit-wallet flow with signature type 3 (POLY_1271).
The token-based example assumes a selected CTF outcome token; position-backed markets require the identifier/signing route described by the current SDK.
Market discovery uses the Gamma API (separate from CLOB). This reads one page; continue next_cursor via after_cursor for more:
import requests
# Resolve a league slug to its numeric tag ID; change "nba" as needed.
tag_response = requests.get(
"https://gamma-api.polymarket.com/tags/slug/nba",
timeout=10,
)
tag_response.raise_for_status()
tag = tag_response.json()
page_response = requests.get(
"https://gamma-api.polymarket.com/markets/keyset",
params={"tag_id": tag["id"], "closed": "false", "limit": 50},
timeout=10,
)
page_response.raise_for_status()
page = page_response.json()
markets = page["markets"]
WebSocket streaming skeleton for public market updates; install/pin a compatible websockets release and add reconnect/snapshot reconciliation before using it as a complete feed:
import asyncio
import json
import websockets
async def heartbeat(ws):
while True:
await asyncio.sleep(10)
await ws.send("PING")
async def stream(token_ids):
async with websockets.connect("wss://ws-subscriptions-clob.polymarket.com/ws/market") as ws:
await ws.send(json.dumps({
"assets_ids": token_ids,
"type": "market",
"custom_feature_enabled": True,
}))
ping_task = asyncio.create_task(heartbeat(ws))
try:
async for msg in ws:
if msg != "PONG":
print(json.loads(msg))
finally:
ping_task.cancel()
The market channel expects asset/token IDs in assets_ids; the authenticated user channel instead subscribes by condition ID. Send PING every 10 seconds and resubscribe after reconnecting.
Strengths: Real-time data, full orderbook depth, direct execution Weaknesses: Liquidity varies sharply by market; wallet, collateral, and geographic requirements add operational complexity
Full tutorial: Polymarket API Python Tutorial
2. Kalshi REST API
Kalshi is a CFTC-regulated event contract exchange. They offer sports markets alongside political, economic, and weather events.
Official docs: docs.kalshi.com Authentication: Public market REST reads need none; private requests use API key ID plus RSA-PSS/SHA-256 or Ed25519 according to key type Rate limits: Token-based read/write budgets by account tier Data format: JSON Fee: Variable; check Kalshi's current fee schedule and the selected market
Best for: CFTC-regulated event contracts and US-regulated exchange trading
The code below demonstrates the RSA-key branch of authenticated requests. It is not required for public market/book REST reads and was not executed during this review. Discover a real ticker from the returned listing instead of assuming the dated example game exists.
import base64
import os
import time
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
BASE_URL = "https://external-api.kalshi.com"
with open(os.environ["KALSHI_PRIVATE_KEY_PATH"], "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(), password=None
)
def auth_headers(method, path):
timestamp = str(int(time.time() * 1000))
# Sign the full path from the API root, without query parameters.
message = f"{timestamp}{method.upper()}{path.split('?')[0]}".encode()
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH,
),
hashes.SHA256(),
)
return {
"KALSHI-ACCESS-KEY": os.environ["KALSHI_API_KEY_ID"],
"KALSHI-ACCESS-TIMESTAMP": timestamp,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(signature).decode(),
}
# Get markets
path = "/trade-api/v2/markets"
response = requests.get(
BASE_URL + path,
headers=auth_headers("GET", path),
params={"series_ticker": "KXNBAGAME", "status": "open", "limit": 10},
timeout=10,
)
response.raise_for_status()
markets = response.json()
# Get orderbook
rows = markets.get("markets", [])
if not rows:
raise ValueError("No open markets in this page")
ticker = rows[0]["ticker"]
path = f"/trade-api/v2/markets/{ticker}/orderbook"
book = requests.get(
BASE_URL + path,
headers=auth_headers("GET", path),
timeout=10,
)
book.raise_for_status()
book = book.json()
Kalshi no longer uses the email/password-to-JWT flow shown in older examples. Authenticated requests carry KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, and a signature over timestamp + method + path; RSA keys use the RSA-PSS/SHA-256 branch shown above, while Ed25519 keys use their documented algorithm. Query parameters are excluded from the signed path.
The official limits, checked October 8, list Basic with separate 200-token/second read and 100-token/second write budgets. Most endpoints cost 10 tokens, so the effective request rate depends on both the endpoint cost and account tier rather than one global “10 requests/second” limit.
Strengths: CFTC-regulated venue, direct execution, REST and WebSocket access Weaknesses: Coverage and liquidity vary by market, and RSA signing adds operational work. Kalshi exposes historical market data such as candlesticks, but not a turnkey archive of every past order-book state; exact depth reconstruction requires a live capture archive like our Kalshi historical data.
3. ESPN Website Endpoints
ESPN website endpoints can return scoreboard JSON without an API subscription key. That observed access is not a supported developer service, coverage/freshness promise or data license. Verify permissions for collection, model training and commercial use against the applicable terms.
Docs: None (undocumented) Authentication: None required Rate limits: Undocumented; throttle conservatively Data format: JSON Access: No API subscription key in this example; permitted use is a separate terms question
Possible fields: Scores and game state; win-probability and historical availability vary by endpoint, sport and event
import requests
# Today's NBA games
data = requests.get(
"https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard"
).json()
for event in data["events"]:
game = event["competitions"][0]
teams = {team.get("homeAway"): team for team in game["competitors"]}
home, away = teams.get("home"), teams.get("away")
if home is None or away is None:
continue
print(f"{away['team']['abbreviation']} @ {home['team']['abbreviation']}: "
f"{away['score']}-{home['score']}")
Supported sports:
| Sport | Endpoint |
|-------|----------|
| NBA | basketball/nba |
| NFL | football/nfl |
| NCAAMB | basketball/mens-college-basketball |
| NHL | hockey/nhl |
| MLB | baseball/mlb |
| CFB | football/college-football |
| Soccer | soccer/eng.1 (EPL), soccer/esp.1 (La Liga) |
Observed utility: Scoreboard fields across the example leagues; optional win-probability fields require separate availability checks Weaknesses: Undocumented schema and limits, polling/source delay, no uptime SLA
Full tutorials: ESPN API Python Tutorial and ESPN NBA Summary API.
4. The Odds API (Sportsbook Aggregator)
The Odds API aggregates recent odds from DraftKings, FanDuel, BetMGM, Caesars, and other supported bookmakers. Check its current sport/region/bookmaker lists.
Official docs: the-odds-api.com/liveapi Authentication: API key (free tier: 500 credits/month) Usage: Credit quota by plan; paid plans currently allow up to 30 requests/second Data format: JSON Displayed plans (October 8 check): 500 free credits/month; $30/20K, $59/100K, $119/5M or $249/15M credits/month
Best for: Cross-venue price comparison, devigging sportsbook lines, CLV tracking
import requests
API_KEY = "your_key_here"
resp = requests.get(
"https://api.the-odds-api.com/v4/sports/basketball_nba/odds",
params={
"apiKey": API_KEY,
"regions": "us",
"markets": "h2h",
"oddsFormat": "american",
"bookmakers": "draftkings,fanduel,betmgm",
}
).json()
for event in resp:
print(f"{event['away_team']} @ {event['home_team']}")
for bm in event["bookmakers"]:
for market in bm["markets"]:
for outcome in market["outcomes"]:
print(f" {bm['key']}: {outcome['name']} {outcome['price']}")
Devigging (removing the bookmaker's margin):
def american_to_implied(odds):
if odds > 0:
return 100.0 / (odds + 100.0)
return abs(odds) / (abs(odds) + 100.0)
def devig(home_odds, away_odds):
home_raw = american_to_implied(home_odds)
away_raw = american_to_implied(away_odds)
total = home_raw + away_raw # >1.0 because of vig
return home_raw / total, away_raw / total # proportional no-vig estimates
Strengths: Published sport/bookmaker lists, JSON format and a bounded free credit plan Limits: Read-only; the published featured-market update intervals are about 60 seconds pre-match and 40 seconds in-play. These are upstream refresh intervals, not measured end-to-end response latency. Credits vary with markets/regions.
Full guide: Multi-Venue Edge Detection
Comparison Table
| Feature | Polymarket | Kalshi | ESPN | The Odds API |
|---|---|---|---|---|
| Auth | Public reads none; trading credentials + wallet | Public REST reads none; private signed key requests | None in observed endpoint | API key |
| Execution | Yes | Yes | No | No |
| Real-time | WebSocket | WebSocket + REST | Polling | Polling |
| Free tier | Public reads | Basic account API tier | No paid key | 500 credits/mo |
| Sports | Market-dependent | Market-dependent | Endpoint/event-dependent | Check current supported list |
| Best for | Trading | Regulated trading | Data/scores | Odds comparison |
| Python lib | unified SDK; retained CLOB client 1.2.0 example | requests + cryptography |
requests | requests |
| Delivery / refresh | WS updates; measure source/receipt clocks | Streaming or REST; measure clocks | Source-dependent polling | Published featured-market refresh 40s in-play |
Building a Multi-Source Pipeline
A possible multi-source research design combines these roles:
- A permitted sports-data source for scores and game state; the ESPN endpoint example does not establish reuse rights
- Your ML model trained on data licensed for the intended use to predict fair win probability — if you serve predictions for more than one sport, the response format matters more than you'd expect; we wrote up how we designed one schema for 11 sports — we walk through building one in How to Build a Sports Prediction API with Python
- Polymarket for real-time market prices and execution
- The Odds API for sportsbook prices as confirmation signals
Agreement across quotes can motivate investigation, but shared inputs, timing and rules limit independence. Neither agreement nor disagreement proves executable edge or which estimate is correct.
The linked $7/month VPS report is a dated operational example, not a current capacity, cost or freshness guarantee. It describes source polling and market updates; a polling interval alone does not establish upstream freshness. If you want to turn that ESPN polling into a push-based feed for your own bot, we cover the pattern in streaming live sports data with WebSockets in Python. Our course teaches a data-to-model-to-bot workflow in six notebooks; it does not certify every provider integration or current production behavior.
This guide is maintained by ZenHodl. Documentation checked October 8, 2026. Selected public GET shapes were checked; the code examples were not all executed, and no authenticated orders were tested. See each provider’s primary docs and permissions before deployment.