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 -y

Step 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 AFL

Expected 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 events

How 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.

Next steps

Combine with other examples

Learn more

Need help?FAQ · Email · Contact