Prediction Market Dashboard: Build a Live Tracker with Python & Streamlit
Polymarket shows one price. DraftKings shows another. Pinnacle — the sharpest book in the world — shows a third. Which one is right?
If you’re trading prediction markets or sharp betting, you already know the answer: all of them matter. The edge lives in the gap between what a prediction market thinks and what the sharp sportsbooks think. The problem is seeing that gap in real time without tabbing between five different sites.
In this tutorial, you’ll build a live prediction market dashboard using Python and Streamlit that pulls odds from Polymarket, Kalshi, Pinnacle, Bet365, and DraftKings — all from a single API call.
Why a Unified Dashboard Matters
Prediction markets like Polymarket are crowd-driven. Sportsbooks like Pinnacle are set by professional traders with skin in the game. When these two sources disagree on the probability of an outcome, one of them is wrong — and that’s where the opportunity lives.
But right now, getting a unified view requires:
- Polymarket: REST API + blockchain wallet + CLOB client
- Kalshi: Separate REST API, separate auth, US-only
- Pinnacle: Closed API, enterprise-only access
- Bet365 / DraftKings: No public API at all
That’s 4 different integrations with 4 different auth methods. Or you can use one API key from OddsPapi and get all of them in a single JSON response.
Prediction Market Data: Old Way vs OddsPapi
| Feature | DIY (3+ APIs) | OddsPapi (1 API) |
|---|---|---|
| Polymarket odds | py-clob-client + wallet setup | ✅ Included (slug: polymarket) |
| Kalshi odds | Separate API, US KYC required | ✅ Included (slug: kalshi) |
| Pinnacle odds | ❌ Closed to public | ✅ Included (slug: pinnacle) |
| Bet365 / DraftKings | ❌ No public API | ✅ Included (350+ books) |
| Authentication | 4 different methods | 1 API key, query parameter |
| Historical data | Polymarket on-chain (slow), others paid | ✅ Free on all tiers |
| Format | Different JSON schemas per source | Unified JSON, consistent structure |
What We’re Building
A Streamlit dashboard that:
- Fetches upcoming fixtures (soccer, basketball) from OddsPapi
- Pulls odds from Polymarket, Pinnacle, Bet365, and DraftKings in one call
- Converts decimal odds to implied probabilities
- Displays grouped bar charts comparing all sources
- Flags edges where Polymarket diverges from Pinnacle’s sharp line
- Auto-refreshes every 60 seconds
Step 1: Install Dependencies
pip install streamlit plotly requests
That’s it. No blockchain wallet, no CLOB client, no OAuth flows.
Step 2: Fetch Prediction Market Fixtures
OddsPapi doesn’t separate prediction markets into their own sport — Polymarket and Kalshi are integrated as bookmakers alongside traditional sportsbooks. This means you fetch fixtures the same way you would for any sport, and the odds response includes prediction market prices right next to Pinnacle and Bet365.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
# Fetch upcoming Champions League fixtures
params = {
"apiKey": API_KEY,
"sportId": 10, # Soccer
"tournamentId": 7, # Champions League
"limit": 20
}
fixtures = requests.get(f"{BASE}/fixtures", params=params).json()
for fx in fixtures[:5]:
print(f"{fx['participant1Name']} vs {fx['participant2Name']} — {fx['startTime'][:10]}")
Output:
Galatasaray vs Liverpool — 2025-03-11
Atletico Madrid vs Tottenham — 2025-03-11
Newcastle vs Barcelona — 2025-03-12
Atalanta vs Bayern Munich — 2025-03-12
Real Madrid vs Manchester City — 2025-03-12
Step 3: Pull Odds from All Sources
One API call returns odds from every bookmaker that covers the fixture — including prediction markets.
# Get odds for a fixture
fixture_id = fixtures[0]["fixtureId"]
odds = requests.get(f"{BASE}/odds", params={"apiKey": API_KEY, "fixtureId": fixture_id}).json()
bookmaker_odds = odds["bookmakerOdds"]
print(f"Bookmakers covering this fixture: {len(bookmaker_odds)}")
# Check which prediction markets are present
for slug in ["polymarket", "kalshi", "pinnacle", "bet365", "draftkings"]:
if slug in bookmaker_odds:
print(f" ✅ {slug}")
else:
print(f" ❌ {slug}")
A typical Champions League fixture returns 100+ bookmakers including both prediction markets and traditional sportsbooks.
Step 4: Extract and Compare Implied Probabilities
The key insight: decimal odds convert to implied probabilities with 1 / price * 100. When Polymarket says 2.00 on an outcome, that’s a 50% implied probability. When Pinnacle says 1.80, that’s 55.6%. The 5.6 percentage point gap is your edge signal.
# Market 101 = Full Time Result (1X2) for soccer
# Outcomes: 101 = Home, 102 = Draw, 103 = Away
MARKET_ID = "101"
OUTCOMES = {"101": "Home", "102": "Draw", "103": "Away"}
def get_implied_probs(bookmaker_odds, slug, market_id, outcomes):
"""Extract implied probabilities for a bookmaker."""
if slug not in bookmaker_odds:
return {}
market = bookmaker_odds[slug].get("markets", {}).get(market_id, {})
probs = {}
for oid, label in outcomes.items():
price = (market.get("outcomes", {}).get(oid, {})
.get("players", {}).get("0", {}).get("price"))
if price and price > 0:
probs[label] = round(1 / price * 100, 1)
return probs
# Compare across sources
for slug in ["polymarket", "pinnacle", "draftkings"]:
probs = get_implied_probs(bookmaker_odds, slug, MARKET_ID, OUTCOMES)
print(f"\n{slug.title()}:")
for label, prob in probs.items():
print(f" {label}: {prob}%")
Example output for Galatasaray vs Liverpool:
Polymarket:
Home: 31.5%
Draw: 19.5%
Away: 56.0%
Pinnacle:
Home: 21.7%
Draw: 24.0%
Away: 57.6%
Draftkings:
Home: 22.2%
Draw: 24.4%
Away: 58.8%
Notice Polymarket has Galatasaray at 31.5% while Pinnacle’s sharp line says 21.7%. That’s a 9.8 percentage point divergence — exactly the kind of signal a trader wants to see.
Step 5: Build the Streamlit Dashboard
Here’s the complete dashboard. Save this as dashboard.py:
import time, requests, streamlit as st, plotly.graph_objects as go
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
BOOKS = ["polymarket", "pinnacle", "bet365", "draftkings"]
COLORS = {"polymarket": "#6366f1", "pinnacle": "#f59e0b",
"bet365": "#10b981", "draftkings": "#ef4444"}
EDGE_THRESHOLD = 5.0 # percentage-point divergence to flag
SPORT_CFG = {
"Soccer — Champions League": {
"sportId": 10, "tournamentId": 7,
"marketId": "101",
"outcomes": {"101": "Home", "102": "Draw", "103": "Away"}
},
"Basketball — NBA": {
"sportId": 11, "tournamentId": 132,
"marketId": "111",
"outcomes": {"111": "Home", "112": "Away"}
},
}
@st.cache_data(ttl=120)
def fetch_fixtures(sport_id, tournament_id):
params = {"apiKey": API_KEY, "sportId": sport_id,
"tournamentId": tournament_id, "limit": 300}
r = requests.get(f"{BASE}/fixtures", params=params, timeout=15)
r.raise_for_status()
now = time.strftime("%Y-%m-%dT%H:%M:%S")
return [f for f in r.json() if f.get("startTime", "") >= now][:20]
@st.cache_data(ttl=60)
def fetch_odds(fixture_id):
r = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY, "fixtureId": fixture_id},
timeout=15)
r.raise_for_status()
return r.json().get("bookmakerOdds", {})
def implied(price):
return round(1 / price * 100, 1) if price and price > 0 else None
def extract_probs(bo, market_id, outcomes):
rows = {}
for slug in BOOKS:
if slug not in bo:
continue
market = bo[slug].get("markets", {}).get(market_id, {})
outs = market.get("outcomes", {})
probs = {}
for oid, label in outcomes.items():
price = (outs.get(oid, {}).get("players", {})
.get("0", {}).get("price"))
p = implied(price)
if p:
probs[label] = p
if probs:
rows[slug] = probs
return rows
def detect_edges(probs):
poly = probs.get("polymarket", {})
pin = probs.get("pinnacle", {})
return {lbl: poly[lbl] - pin[lbl]
for lbl in poly if lbl in pin
and abs(poly[lbl] - pin[lbl]) >= EDGE_THRESHOLD}
# ── UI ──────────────────────────────────────────
st.set_page_config(page_title="Prediction Market Dashboard", layout="wide")
st.title("Prediction Market vs Sportsbook Odds")
st.caption("Powered by OddsPapi API")
sport = st.selectbox("Sport", list(SPORT_CFG.keys()))
cfg = SPORT_CFG[sport]
auto = st.toggle("Auto-refresh (60s)", value=False)
fixtures = fetch_fixtures(cfg["sportId"], cfg["tournamentId"])
if not fixtures:
st.warning("No upcoming fixtures found.")
st.stop()
for fx in fixtures[:8]:
bo = fetch_odds(fx["fixtureId"])
if "polymarket" not in bo:
continue
probs = extract_probs(bo, cfg["marketId"], cfg["outcomes"])
if len(probs) < 2:
continue
edges = detect_edges(probs)
labels = list(cfg["outcomes"].values())
name = f"{fx['participant1Name']} vs {fx['participant2Name']}"
fig = go.Figure()
for slug in BOOKS:
if slug not in probs:
continue
vals = [probs[slug].get(l, 0) for l in labels]
fig.add_trace(go.Bar(
name=slug.title(), x=labels, y=vals,
marker_color=COLORS[slug],
text=[f"{v}%" for v in vals], textposition="outside"))
for lbl, diff in edges.items():
tag = "POLY HIGHER" if diff > 0 else "POLY LOWER"
fig.add_annotation(
x=lbl,
y=max(probs["polymarket"].get(lbl, 0),
probs["pinnacle"].get(lbl, 0)) + 6,
text=f"{abs(diff):.1f}pp {tag}", showarrow=False,
font=dict(color="#ef4444", size=12))
fig.update_layout(
barmode="group", title=f"{name} ({fx['startTime'][:16]})",
yaxis_title="Implied Probability (%)",
template="plotly_dark", height=420)
st.plotly_chart(fig, use_container_width=True)
for lbl, diff in edges.items():
side = "overestimates" if diff > 0 else "underestimates"
st.info(f"Edge: Polymarket {side} {lbl} by "
f"{abs(diff):.1f}pp vs Pinnacle sharp line.")
if auto:
time.sleep(60)
st.rerun()
Step 6: Run the Dashboard
streamlit run dashboard.py
Your browser opens to a live dashboard comparing Polymarket against Pinnacle, Bet365, and DraftKings. Select Soccer or Basketball, and the charts update with implied probabilities from every source. Red annotations flag any outcome where Polymarket diverges from Pinnacle’s sharp line by more than 5 percentage points.
How to Read the Edge Signals
| Signal | What It Means | Possible Action |
|---|---|---|
| POLY HIGHER (+5pp+) | Polymarket overprices this outcome vs sharp books | Fade on Polymarket, or back the opposite on sportsbook |
| POLY LOWER (-5pp+) | Polymarket underprices this outcome vs sharp books | Buy on Polymarket, or bet the same side on sportsbook |
| No flag | Consensus — sources agree within 5pp | No clear edge, wait for movement |
The threshold is configurable. Set EDGE_THRESHOLD = 3.0 for more signals, or 10.0 for only the most extreme divergences.
Why Prediction Markets Diverge from Sportsbooks
You’ll notice Polymarket prices often differ from Pinnacle by 5-15 percentage points. This isn’t noise — prediction markets and sportsbooks have fundamentally different pricing mechanisms:
- Sportsbooks (Pinnacle): Prices set by professional traders managing risk. The vig is baked in. Lines move on sharp action.
- Prediction markets (Polymarket): Prices set by crowd order flow. Thinner liquidity means bigger swings. Retail sentiment drives prices more than fundamentals.
This structural difference is why the dashboard works. It’s not comparing the same thing twice — it’s comparing professional pricing against crowd pricing. When they disagree, someone has better information.
Extending the Dashboard
This is a starting point. Some ideas for upgrades:
- Add Kalshi: Already in the API response (slug:
kalshi). Just add it to theBOOKSlist. - More markets: Switch from Full Time Result (market 101) to Over/Under 2.5 (market 1010) or Both Teams to Score (market 104). Polymarket covers 8+ markets on soccer.
- Historical tracking: OddsPapi includes free historical data on all tiers. Fetch closing prices to backtest whether Polymarket or Pinnacle was more accurate (see our Polymarket API guide for more).
- WebSocket upgrade: Replace polling with OddsPapi’s WebSocket API for sub-second updates.
Why OddsPapi for Prediction Market Data
Building this dashboard without OddsPapi means integrating Polymarket’s CLOB, Kalshi’s REST API, and somehow getting Pinnacle data (their API is closed to the public). That’s three different auth systems, three different JSON schemas, and zero access to the sharpest bookmaker on earth.
With OddsPapi, it’s one API key and one JSON response containing 350+ bookmakers — including every prediction market exchange and every sharp book. The free tier includes historical data for backtesting, and you can upgrade to WebSockets when you need real-time feeds.
Stop Tabbing Between Polymarket and DraftKings
You just built a prediction market tracker in under 80 lines of Python. Every divergence between Polymarket and Pinnacle is now visible on one screen — no blockchain wallet, no enterprise API access, no scraping.
Get your free API key and run this dashboard yourself. The code works out of the box — just replace YOUR_API_KEY and run streamlit run dashboard.py.