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:
| Layer | Responsibility | Update Cadence |
|---|---|---|
| Odds ingestor | Poll the odds API and write snapshots | 30-60 seconds |
| Time-series store | Append price history per market | On every snapshot |
| Score ingestor | Pull live scores for in-play events | 10-15 seconds |
| Real-time push | Stream updates to browsers | Sub-second |
| Visualization | Render line movement charts | On 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}®ions=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
Caching Strategies for Sports API Data
12 min readHow to Build a Live Score App
18 min readHandling API Rate Limits
13 min readFind 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.