Tutorials · 17 min read

Building a Sports Betting Analytics Dashboard

A complete tutorial on combining odds APIs, historical data, and real-time scores into a betting analytics platform with line movement tracking and live visualization.

Last updated: August 2026

Overview

A betting analytics dashboard sits at the intersection of three data streams: live odds from multiple sportsbooks, historical line movement for context, and real-time scores to mark bets. Building one that stays fast and accurate under live traffic requires careful choices about which odds API to use, how to store time-series price data, and how to push updates to the browser without melting your rate limits.

This tutorial walks through the full architecture: selecting an odds API, designing the historical storage layer, tracking line movement, streaming real-time updates, and rendering the charts. By the end you will have a blueprint for a dashboard that shows live odds, closing line value, and movement trends.

Tip: The Odds API offers a free tier of 500 requests/month, which is enough to prototype the dashboard before you commit to a paid plan. Use the The Odds API profile to confirm market coverage for the sports you care about.

Architecture Overview

A production betting dashboard has five cooperating layers. Each has a distinct responsibility and a different freshness requirement:

LayerResponsibilityUpdate Cadence
Odds ingestorPoll the odds API and write snapshots30-60 seconds
Time-series storeAppend price history per marketOn every snapshot
Score ingestorPull live scores for in-play events10-15 seconds
Real-time pushStream updates to browsersSub-second
VisualizationRender line movement chartsOn data change

Odds API Selection

The odds API is the foundation, and the right choice depends on whether you need pre-match, live, or historical data:

  • The Odds API — the best default for pre-match and live odds across many sportsbooks with a clean REST surface and a generous free tier.
  • Sportradar — for enterprise-grade in-play odds with ultra-low latency, at enterprise pricing.
  • Bets API — an aggregator that returns odds and bet settlements, useful if you also track outcomes.
  • API-Football — a football-specific odds endpoint, good if your dashboard is football-only.

For most teams, The Odds API is the right starting point: it covers the most sportsbooks, normalizes the response shape, and lets you upgrade to a higher plan as traffic grows. The rest of this tutorial assumes The Odds API, but the patterns transfer to any provider once you swap the auth and response parsing.

Historical Data Storage

Line movement tracking requires an append-only time-series of odds snapshots. Each snapshot records the market, sportsbook, price, and timestamp. A relational table works for moderate volume; a dedicated time-series database (TimescaleDB, InfluxDB) is better once you exceed millions of rows. The minimum schema:

-- schema.sql
CREATE TABLE odds_snapshots (
    id          BIGSERIAL PRIMARY KEY,
    sport       TEXT NOT NULL,
    event_id    TEXT NOT NULL,
    market      TEXT NOT NULL,        -- h2h, spreads, totals
    bookmaker   TEXT NOT NULL,        -- draftkings, fanduel, ...
    outcome     TEXT NOT NULL,        -- team name or over/under
    price       NUMERIC(10,2),        -- American or decimal odds
    point       NUMERIC(10,2),        -- spread or total point
    recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Index for fast line-movement queries per event
CREATE INDEX idx_odds_event_book
    ON odds_snapshots (event_id, bookmaker, market, recorded_at DESC);

-- Partition by week for easier retention (TimescaleDB)
-- SELECT create_hypertable('odds_snapshots', 'recorded_at');

Line Movement Tracking

Line movement is the delta between the current odds and the opening odds. Tracking it means persisting every snapshot and computing the difference to the first record per market. The ingestor that polls The Odds API and writes a snapshot:

// ingestor.js — polls The Odds API and stores snapshots
import pg from "pg";

const { Pool } = pg;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const ODDS_KEY = process.env.THE_ODDS_API_KEY;
const BASE = "https://api.the-odds-api.com/v4";

export async function ingestOdds(sport = "soccer_epl") {
  // 1. Fetch normalized odds for the sport
  const url = `${BASE}/sports/${sport}/odds/?apiKey=${ODDS_KEY}&regions=us&oddsFormat=american`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Odds API error: ${res.status}`);
  const events = await res.json();

  // 2. Flatten into per-book, per-market rows
  const rows = [];
  for (const ev of events) {
    for (const book of ev.bookmakers) {
      for (const market of book.markets) {
        for (const outcome of market.outcomes) {
          rows.push([
            sport, ev.id, market.key, book.key,
            outcome.name, outcome.price, outcome.point ?? null,
          ]);
        }
      }
    }
  }

  // 3. Bulk-insert the snapshot (append-only)
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    for (const r of rows) {
      await client.query(
        `INSERT INTO odds_snapshots
           (sport, event_id, market, bookmaker, outcome, price, point)
         VALUES ($1,$2,$3,$4,$5,$6,$7)`,
        r
      );
    }
    await client.query("COMMIT");
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
  }

  return { ingested: rows.length };
}

Fetching and Displaying Live Odds

The dashboard API serves the latest snapshot plus the opening line for each market, so the chart can render movement from open to now. A route handler that returns the data the frontend needs:

// app/api/odds/[eventId]/route.js
import { NextResponse } from "next/server";
import { pool } from "@/lib/db";

export async function GET(request, { params }) {
  const { eventId } = params;
  const client = await pool.connect();
  try {
    // Latest snapshot per book/market/outcome
    const latest = await client.query(
      `SELECT DISTINCT ON (bookmaker, market, outcome)
              bookmaker, market, outcome, price, point, recorded_at
       FROM odds_snapshots
       WHERE event_id = $1
       ORDER BY bookmaker, market, outcome, recorded_at DESC`,
      [eventId]
    );

    // Opening line per book/market/outcome
    const opening = await client.query(
      `SELECT DISTINCT ON (bookmaker, market, outcome)
              bookmaker, market, outcome, price AS open_price, recorded_at AS open_at
       FROM odds_snapshots
       WHERE event_id = $1
       ORDER BY bookmaker, market, outcome, recorded_at ASC`,
      [eventId]
    );

    // Merge latest + opening for the chart
    const openMap = new Map(
      opening.rows.map((r) => [r.bookmaker + r.market + r.outcome, r])
    );
    const series = latest.rows.map((r) => {
      const o = openMap.get(r.bookmaker + r.market + r.outcome);
      return { ...r, open_price: o?.open_price ?? r.price };
    });

    return NextResponse.json({ eventId, series });
  } finally {
    client.release();
  }
}

Real-Time Updates

Polling the odds API from every browser is both expensive and slow. Instead, a single server-side ingestor polls on a 30-60 second cadence and pushes diffs to connected clients over WebSockets or Server-Sent Events. The browser subscribes to a per-event channel and re-renders only the affected market rows:

  • Single poller — one background job polls the odds API and writes snapshots, regardless of how many users are watching. This keeps your API quota flat.
  • Diff-based push — after each poll, compare the new snapshot to the last and emit only changed rows over the socket. This keeps the wire payload tiny.
  • Client reconciliation — the browser applies diffs to its local store and re-renders the line movement chart incrementally, avoiding full reloads.

Best Practices

Poll once, push many

Never poll the odds API per connected user. A single server-side poller feeds a fan-out layer that pushes diffs to all subscribers. This keeps request volume constant as your user base grows.

Store every snapshot, never overwrite

Line movement is a time series. Append every snapshot with its timestamp and never update existing rows. This preserves the full movement history and lets you backtest strategies later.

Normalize book and market keys

Different providers use different keys for the same sportsbook or market (h2h vs moneyline). Normalize to a canonical set at ingest time so the dashboard never has to handle variant keys.

Cap chart history on the client

A live chart that accumulates every tick will leak memory. Cap the client-side data window (for example, the last 500 points or the last 24 hours) and roll off older entries so the chart stays smooth and the page stays responsive.

Related Guides

Find the right odds API for your dashboard

Get a shortlist of odds and in-play APIs tailored to your sports and budget, then estimate your monthly cost.