Build a Live Odds Dashboard with Python & Streamlit (Free API)
Stop tabbing between 5 sportsbook sites. Build your own odds comparison dashboard in 20 minutes — one screen, every bookmaker, best prices highlighted automatically.
Manual line shopping is the worst kind of grind. You open Bet365, check the line, switch to DraftKings, check it again, flip to FanDuel — and by the time you’ve finished, the line you wanted has already moved. And that’s with 3-4 books. You’re leaving serious edge on the table because you simply can’t see what’s happening across 300+ bookmakers simultaneously.
This guide fixes that. We’ll build a live odds comparison dashboard using Python, Streamlit, and the OddsPapi API — free to run, free to host on Streamlit Cloud, and genuinely more powerful than most $100/month tools.
Why Manual Line Shopping Doesn’t Work
Here’s what you’re actually up against when you try to shop lines manually:
- Account limits: You physically can’t have accounts at 300+ bookmakers. Most bettors are comparing 3-5 books at best.
- Speed: By the time you’ve tabbed through your accounts, the sharp money has already moved the line at Pinnacle. You’re reacting to yesterday’s price.
- No automated alerts: There’s no way to get notified when an arbitrage window opens or a line diverges significantly across books.
- Paid tools are expensive: OddsJam, OddsBoom, and similar services charge $50–200/month for a curated subset of bookmakers — and you don’t control the logic.
The better path: build it yourself with a real odds aggregator API, take full control of the logic, and pay nothing.
Manual Tab-Switching vs. Paid Tools vs. Building Your Own
| Feature | Manual Tab-Switching | Paid Tools ($99/mo) | Build Your Own (OddsPapi) |
|---|---|---|---|
| Bookmakers covered | 3–5 | 15–40 | 300+ |
| Real-time data | No | Delayed | Yes (REST + WebSocket) |
| Custom alerts | No | Limited presets | Full control |
| Historical data | No | Extra $$ | Free tier included |
| Arbitrage detection | No | Basic alerts | Custom logic |
| Monthly cost | $0 | $99–199/mo | Free |
What You’ll Build
By the end of this tutorial, you’ll have a working Streamlit dashboard with:
- Sport + fixture selector — pick Premier League, NBA, or any supported tournament
- Bookmaker comparison table — all available books side-by-side, with best prices auto-highlighted
- Bar chart visualisation — Plotly chart comparing featured bookmakers (Pinnacle, Bet365, DraftKings, 1xBet) per outcome
- Line shopping edge calculator — shows best available price per outcome and which book offers it
- Arbitrage detector — fires an alert when combined implied probabilities fall below 100%
- Auto-refresh toggle — polls the API every 60 seconds without manual page reloads
The stack: Python + Streamlit + Plotly + Pandas + OddsPapi REST API. No paid subscriptions. No vendor lock-in.
Step-by-Step Tutorial
Step 1: Install Dependencies
Create a new project folder and install the required packages:
pip install streamlit plotly requests pandas
That’s it. No heavy ML frameworks, no complex setup. Streamlit handles the UI, Plotly handles the charts, and requests handles the API calls.
Step 2: Authenticate with the OddsPapi API
Create a file called dashboard.py and start with your credentials and a connection test:
import requests
API_KEY = "YOUR_API_KEY" # Get free at oddspapi.io
BASE = "https://api.oddspapi.io/v4"
# Test connection
r = requests.get(f"{BASE}/sports", params={"apiKey": API_KEY})
print(r.status_code) # Should be 200
print(r.json())
Critical note: apiKey is a query parameter, not an Authorization header. Do not put it in headers={} — it won’t authenticate. Pass it via params={"apiKey": API_KEY} on every request.
Get your free API key at oddspapi.io. The free tier gives you 250 requests per day, access to 59 sports, and free historical odds data — enough to run this dashboard and backtest your logic.
Step 3: Fetch Upcoming Fixtures
OddsPapi uses its own terminology that differs from US sportsbook conventions:
| US Term | OddsPapi Term |
|---|---|
| League | Tournament |
| Game | Fixture |
| Team | Participant |
Now fetch upcoming fixtures for a given sport and tournament:
import time
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()
data = r.json()
now = time.strftime("%Y-%m-%dT%H:%M:%S")
# Filter to upcoming fixtures with odds available
return [f for f in data
if f.get("startTime", "") >= now
and f.get("hasOdds")][:20]
We filter for fixtures where hasOdds is true — no point fetching odds data for fixtures that don’t have any yet. We also cap at 20 to keep API usage manageable on the free tier.
Step 4: Pull Odds from All Bookmakers
This is where OddsPapi’s power becomes obvious. One API call returns odds from every available bookmaker for a given fixture. The JSON is deeply nested — here’s the exact path to a price:
bookmakerOdds[slug] → markets[marketId] → outcomes[outcomeId] → players["0"] → price
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 extract_prices(bookmaker_odds, market_id, outcomes):
# Extract prices from all bookmakers for a given market.
prices = {}
for slug, book_data in bookmaker_odds.items():
market = book_data.get("markets", {}).get(market_id, {})
outs = market.get("outcomes", {})
book_prices = {}
for oid, label in outcomes.items():
price = (outs.get(oid, {})
.get("players", {})
.get("0", {})
.get("price"))
if price and price > 1:
book_prices[label] = price
if book_prices:
prices[slug] = book_prices
return prices
A few things to note:
market_idis a string key in the JSON (not an integer), so pass"101"not101- The
players["0"]key is literal — it’s always the string"0"for standard market outcomes - We filter out prices ≤ 1.0 (invalid or suspended odds appear as 1.0 in some feeds)
- Soccer Full Time Result (1X2) uses market ID
101; Basketball moneyline uses111
Step 5: Line Shopping — Find Best Prices and Detect Arbitrage
Line shopping is straightforward: for each outcome, find the highest decimal odds across all bookmakers. If the combined implied probability of the best prices per outcome is below 100%, you have an arbitrage opportunity.
def find_best_prices(prices, outcome_labels):
# Find the highest price per outcome across all bookmakers.
best = {}
for label in outcome_labels:
best_price = 0
best_book = ""
for slug, p in prices.items():
if p.get(label, 0) > best_price:
best_price = p[label]
best_book = slug
best[label] = {"price": best_price, "book": best_book}
return best
def detect_arb(prices, outcome_labels):
# Check if best prices create an arbitrage opportunity.
# An arb exists when sum(1/best_price_per_outcome) < 1.0
# Margin = (1 - total_implied) * 100 = guaranteed profit %
best = find_best_prices(prices, outcome_labels)
total_implied = sum(
1 / best[lbl]["price"]
for lbl in outcome_labels
if best[lbl]["price"] > 0
)
if total_implied < 1.0:
margin = round((1 - total_implied) * 100, 2)
return {"arb": True, "margin": margin, "best": best}
return {"arb": False,
"overround": round((total_implied - 1) * 100, 2)}
Example: if the best Home price is 2.10 at Pinnacle, best Draw is 3.60 at 1xBet, and best Away is 4.20 at Singbet — implied probabilities are 47.6% + 27.8% + 23.8% = 99.2%. That 0.8% gap is a guaranteed profit window, no matter the outcome.
OddsPapi's 300+ bookmaker coverage dramatically increases the frequency of these windows compared to tools that only show 15-40 books. More data sources = more divergence opportunities.
Step 6: Build the Streamlit Dashboard
Now wire everything together into an interactive dashboard:
import streamlit as st
import plotly.graph_objects as go
import pandas as pd
st.set_page_config(page_title="Odds Dashboard", layout="wide")
st.title("Live Odds Comparison Dashboard")
st.caption("Powered by OddsPapi — 300+ bookmakers")
SPORT_CFG = {
"Soccer — Premier League": {
"sportId": 10, "tournamentId": 17,
"marketId": "101",
"outcomes": {"101": "Home", "102": "Draw", "103": "Away"},
},
"Basketball — NBA": {
"sportId": 11, "tournamentId": 132,
"marketId": "111",
"outcomes": {"111": "Home", "112": "Away"},
},
}
sport = st.selectbox("Sport", list(SPORT_CFG.keys()))
cfg = SPORT_CFG[sport]
auto = st.toggle("Auto-refresh (60s)", value=False)
outcome_labels = list(cfg["outcomes"].values())
fixtures = fetch_fixtures(cfg["sportId"], cfg["tournamentId"])
for fx in fixtures[:5]:
name = f"{fx['participant1Name']} vs {fx['participant2Name']}"
bo = fetch_odds(fx["fixtureId"])
prices = extract_prices(bo, cfg["marketId"], cfg["outcomes"])
best = find_best_prices(prices, outcome_labels)
arb = detect_arb(prices, outcome_labels)
st.markdown(f"### {name}")
st.caption(f"{len(prices)} bookmakers available")
# Best price metrics
cols = st.columns(len(outcome_labels))
for i, lbl in enumerate(outcome_labels):
with cols[i]:
st.metric(f"Best {lbl}", f"{best[lbl]['price']:.2f}",
help=f"at {best[lbl]['book']}")
# Bar chart
featured = ["pinnacle", "bet365", "draftkings",
"fanduel", "bwin", "1xbet"]
fig = go.Figure()
for slug in featured:
if slug not in prices:
continue
vals = [prices[slug].get(lbl, 0) for lbl in outcome_labels]
fig.add_trace(go.Bar(name=slug, x=outcome_labels, y=vals,
text=[f"{v:.2f}" for v in vals],
textposition="outside"))
fig.update_layout(barmode="group", template="plotly_dark",
yaxis_title="Decimal Odds", height=400)
st.plotly_chart(fig, use_container_width=True)
if arb["arb"]:
st.success(f"ARB: {arb['margin']}% guaranteed profit!")
if auto:
import time
time.sleep(60)
st.rerun()
Run it with:
streamlit run dashboard.py
Streamlit opens a browser tab automatically. The dashboard is fully interactive — change the sport, toggle auto-refresh, or expand any fixture to see all bookmakers.
The Complete Script
The full working script combining all functions above is available in the OddsPapi GitHub examples repository. It includes fetch_fixtures, fetch_odds, extract_prices, find_best_prices, detect_arb, and the Streamlit UI in a single ready-to-run file.
To deploy for free on Streamlit Cloud:
- Push your
dashboard.pyand arequirements.txtto a GitHub repo - Connect the repo at streamlit.io/cloud
- Add your
API_KEYas a Streamlit secret (st.secrets["API_KEY"]) - Deploy — your dashboard is live at a public URL, refreshing automatically
What to Build Next
Upgrade to WebSocket for Sub-Second Updates
The REST API approach polls every 60 seconds. For in-play markets, that's too slow. OddsPapi supports real-time WebSocket streaming that pushes odds updates the moment they change. See the WebSocket Odds API guide to upgrade your dashboard to a live feed.
Add Historical Line Movement Tracking
OddsPapi includes free historical odds data on the free tier — something competitors charge separately for. Store odds snapshots in a SQLite database and chart how lines move before kick-off. Pinnacle line movement is one of the most reliable signals in sports betting, and you get it free.
Build Telegram or Discord Alerts
Add a notification layer so you don't need the dashboard open. When detect_arb() returns True, fire a message to a Telegram bot or Discord webhook with the fixture, margin, and best prices.
Add More Markets
This tutorial used 1X2 (market ID 101) for soccer and moneylines for NBA. OddsPapi supports Asian Handicaps (AH 0 = 1072, AH -0.5 = 1068), Both Teams to Score (104), Over/Under 2.5 (1010), and player props. Extend the SPORT_CFG dict to add new markets without touching the core logic.
Frequently Asked Questions
How many bookmakers does OddsPapi cover?
300+ bookmakers, including sharp books (Pinnacle, Singbet, SBOBet), major softs (Bet365, DraftKings, FanDuel), crypto/niche books (1xBet, GG.BET), and regional specialists across Asia, Europe, and Latin America. Competitors typically cover 15–40 books.
Is the OddsPapi API free to use?
Yes. The free tier includes 250 requests per day, access to 59 sports, and free historical odds data — no credit card required. Free historical data is something competitors charge extra for; OddsPapi includes it by default.
What betting markets can I compare?
Moneylines, spreads, totals, Asian handicaps, Both Teams to Score, player props, and more. Each market has a unique numeric ID — soccer 1X2 is 101, Asian Handicap 0 is 1072, AH -0.5 is 1068. Extend the SPORT_CFG dict to add any market without touching the core dashboard logic.
Can I detect arbitrage with this dashboard?
Yes. The detect_arb() function checks whether the combined implied probabilities of the best prices per outcome — drawn from different bookmakers — fall below 100%. When they do, a guaranteed profit margin exists regardless of the result.
What's the difference between sharp and soft bookmakers?
Sharp books (Pinnacle, Singbet) set efficient lines by accepting large sharp wagers. Soft books (DraftKings, FanDuel) cater to recreational bettors with bonuses and lower bet limits. Comparing sharp prices against soft prices is the core methodology for value betting — OddsPapi gives you both in a single API call.
Can I use this for live in-play markets?
Yes. The same /odds endpoint covers in-play fixtures. For live use, enable the auto-refresh toggle or upgrade to WebSocket streaming for sub-second updates. See the WebSocket guide for details.
Stop Paying $99/Month for Someone Else's Dashboard
You just built the same core functionality as OddsJam or OddsBoom — 300+ bookmakers, real-time prices, automatic arb detection, and a clean interactive UI — for free. Now you own the logic, you control the markets you watch, and you can extend it any direction: Telegram alerts, historical tracking, Streamlit Cloud deployment, or a full WebSocket feed.
Get your free API key at oddspapi.io — 250 requests/day, 59 sports, historical data included. No credit card. No trial period. Just the data.