Build a Live Odds Dashboard with Python & Streamlit (Free API)

Odds Dashboard - OddsPapi API Blog
How To Guides March 24, 2026

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_id is a string key in the JSON (not an integer), so pass "101" not 101
  • 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 uses 111

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:

  1. Push your dashboard.py and a requirements.txt to a GitHub repo
  2. Connect the repo at streamlit.io/cloud
  3. Add your API_KEY as a Streamlit secret (st.secrets["API_KEY"])
  4. 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.