Live Odds Tracker Example - JavaScript
Complete working example that tracks AFL odds every 2.5 minutes and detects line movement across every Australian bookmaker.
What you'll build
A script that:
- Fetches AFL odds on a schedule
- Tracks line movement across all bookmakers
- Alerts when prices change
- Shows before/after comparisons
Perfect for: Detecting sharp money, identifying steam moves, feeding an alert bot
Prerequisites
- Node.js 18+
- SockOdds API key (free)
- Basic JavaScript
Complete code
Step 1: Setup project
mkdir odds-tracker && cd odds-tracker
npm init -yStep 2: Create tracker.js
// tracker.js — Node 18+ (global fetch)
const API_KEY = process.env.SOCKODDS_KEY;
const BASE = "https://api.sockodds.com/v2";
const LEAGUE = process.argv[2] || "AFL";
const INTERVAL_MS = 150_000; // the source refreshes every ~2 min
const previous = new Map(); // eventID -> event
async function fetchOdds() {
const url = new URL(BASE + "/events");
url.search = new URLSearchParams({ leagueID: LEAGUE, oddsAvailable: "true", oddID: "points-home-game-ml-home,points-all-game-ou-over", includeOpposingOdds: "true", limit: "100" });
const r = await fetch(url, { headers: { "x-api-key": API_KEY } });
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
const body = await r.json();
if (body.notice) console.log("notice:", body.notice);
return body.data;
}
function title(e) { return `${e.teams?.away?.names?.medium ?? "?"} @ ${e.teams?.home?.names?.medium ?? "?"}`; }
function compare(prev, next) {
const moves = [];
for (const [oddID, odd] of Object.entries(next.odds)) {
for (const [book, q] of Object.entries(odd.byBookmaker)) {
const was = prev?.odds?.[oddID]?.byBookmaker?.[book];
if (!was) continue;
if (was.decimal !== q.decimal) moves.push({ oddID, book, from: was.decimal, to: q.decimal, pct: ((q.decimal - was.decimal) / was.decimal * 100).toFixed(1) });
if (was.available !== q.available) moves.push({ oddID, book, available: q.available });
}
}
return moves;
}
async function tick() {
const events = await fetchOdds();
const stamp = new Date().toISOString().slice(11, 19);
for (const e of events) {
if (e.info?.stale) { console.log(`[${stamp}] ${title(e)} — STALE since ${e.info.lastUpdatedAt}`); continue; }
const moves = compare(previous.get(e.eventID), e);
for (const m of moves) console.log(`[${stamp}] ${title(e)} ${m.oddID} ${m.book}: ${"available" in m ? (m.available ? "reopened" : "SUSPENDED") : `${m.from} → ${m.to} (${m.pct}%)`}`);
previous.set(e.eventID, e);
}
console.log(`[${stamp}] tracked ${events.length} ${LEAGUE} events`);
}
tick().then(() => setInterval(() => tick().catch(console.error), INTERVAL_MS)).catch((e) => { console.error(e); process.exit(1); });Step 3: Run it
SOCKODDS_KEY=so_live_… node tracker.js AFLExpected output
[08:30:12] tracked 6 AFL events
[08:32:42] Hawthorn @ Fremantle points-home-game-ml-home sportsbet: 1.91 → 1.87 (-2.1%)
[08:32:42] Hawthorn @ Fremantle points-all-game-ou-over tab: SUSPENDED
[08:32:42] tracked 6 AFL eventsHow it works
1. Fetch odds
One events call per league, trimmed with oddID and includeOpposingOdds so the payload stays small enough for the free plan.
2. Process the nested bookmaker structure
odds[oddID].byBookmaker[book].decimal is the number to compare; available tells you about suspensions.
3. Compare with previous odds
A Map keyed by eventID holds the last event seen.
4. Detect significant movement
Filter moves by Math.abs(pct) > 3 for steam.
5. Respect stale events
info.stale means the source stopped writing; don't alert on it.
Enhancements
Track multiple leagues
for (const league of ["AFL", "NRL", "EPL"]) await tickFor(league);Add Discord notifications
await fetch(process.env.DISCORD_WEBHOOK, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: `${title(e)} ${m.book} ${m.from}→${m.to} ${e.links?.bookmakers?.[m.book] ?? ""}` }) });Store history
import { appendFileSync } from "node:fs";
appendFileSync("moves.jsonl", JSON.stringify({ t: Date.now(), eventID: e.eventID, ...m }) + "\n");Troubleshooting
"No AFL events found"
Out of season, or your plan doesn't include the league — check notice and /leagues.
Rate limit errors (429)
The free plan is 10/min; one league per tick is fine, don't poll faster than the source.
Missing odds data
Some events carry oddsPresent: true but oddsAvailable: false — every book has suspended.