Multi-Sport Betting Analytics Dashboard
Lesson 2 of 2
Building a Live Sports Betting Dashboard
Estimated time: 10 minutes
Building a Live Sports Betting Dashboard
Dashboard Architecture
Three components:
- Data Ingestion: Fetch odds from multiple sources every 30 seconds
- Aggregation: Normalize and store in a database
- Visualization: Real-time UI showing odds side-by-side
Backend: Express + SQLite
const express = require('express');
const sqlite3 = require('sqlite3');
const app = express();
const db = new sqlite3.Database(':memory:');
// Create tables
db.run(`
CREATE TABLE IF NOT EXISTS markets (
id TEXT PRIMARY KEY,
sport TEXT,
title TEXT,
yes_price REAL,
no_price REAL,
updated_at TIMESTAMP
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
market_id TEXT,
yes_price REAL,
no_price REAL,
timestamp TIMESTAMP,
FOREIGN KEY(market_id) REFERENCES markets(id)
)
`);
app.get('/api/markets', (req, res) => {
db.all('SELECT * FROM markets WHERE sport = ?', [req.query.sport || 'NFL'], (err, rows) => {
res.json(rows);
});
});
app.get('/api/markets/:id/history', (req, res) => {
db.all(
'SELECT * FROM price_history WHERE market_id = ? ORDER BY timestamp DESC LIMIT 100',
[req.params.id],
(err, rows) => {
res.json(rows);
}
);
});
app.listen(3000);
Data Ingestion Loop
const fetch = require('node-fetch');
async function pollAllSports() {
const sports = ['NFL', 'NBA', 'NHL', 'FIFA'];
for (const sport of sports) {
const kalshiMarkets = await fetchKalshiMarkets(sport);
const polymarketMarkets = await fetchPolymarketMarkets(sport);
const aggregated = aggregateByTitle(kalshiMarkets, polymarketMarkets);
for (const market of aggregated) {
db.run(
'INSERT OR REPLACE INTO markets (id, sport, title, yes_price, no_price, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[
market.id,
sport,
market.title,
market.yes_price,
market.no_price,
new Date()
]
);
db.run(
'INSERT INTO price_history (market_id, yes_price, no_price, timestamp) VALUES (?, ?, ?, ?)',
[market.id, market.yes_price, market.no_price, new Date()]
);
}
}
}
// Poll every 30 seconds
setInterval(pollAllSports, 30000);
Frontend: React Dashboard
import React, { useState, useEffect } from 'react';
function SportsBoard() {
const [markets, setMarkets] = useState([]);
const [sport, setSport] = useState('NFL');
useEffect(() => {
const interval = setInterval(async () => {
const res = await fetch(`/api/markets?sport=${sport}`);
const data = await res.json();
setMarkets(data);
}, 5000);
return () => clearInterval(interval);
}, [sport]);
return (
<div>
<select onChange={(e) => setSport(e.target.value)}>
<option>NFL</option>
<option>NBA</option>
<option>NHL</option>
<option>FIFA</option>
</select>
<table>
<thead>
<tr>
<th>Event</th>
<th>Yes Price</th>
<th>No Price</th>
<th>Movement</th>
</tr>
</thead>
<tbody>
{markets.map(m => (
<tr key={m.id}>
<td>{m.title}</td>
<td>{m.yes_price.toFixed(2)}</td>
<td>{m.no_price.toFixed(2)}</td>
<td><MovementIndicator marketId={m.id} /></td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function MovementIndicator({ marketId }) {
const [movement, setMovement] = useState(null);
useEffect(() => {
fetch(`/api/markets/${marketId}/history`)
.then(r => r.json())
.then(history => {
if (history.length >= 2) {
const oldest = history[history.length - 1];
const newest = history[0];
const change = ((newest.yes_price - oldest.yes_price) / oldest.yes_price) * 100;
setMovement(change.toFixed(1));
}
});
}, [marketId]);
return movement ? (
<span style={{ color: movement > 0 ? 'green' : 'red' }}>
{movement > 0 ? '↑' : '↓'} {Math.abs(movement)}%
</span>
) : null;
}
export default SportsBoard;
Value Bet Detection
Find discrepancies between market odds and your model:
function findValueBets(markets, myProbabilities) {
return markets
.filter(m => {
const marketProb = 1 / (m.yes_price / 100);
const myProb = myProbabilities[m.id];
const discrepancy = Math.abs(marketProb - myProb);
return discrepancy > 0.05; // 5% edge
})
.map(m => ({
market: m.title,
market_prob: 1 / (m.yes_price / 100),
my_prob: myProbabilities[m.id],
expected_value: m.yes_price * myProbabilities[m.id] - 1
}));
}
Next: Historical Analysis
Analyze win rates, ROI, and Kelly Criterion for bet sizing.