# Live Odds Tracker Example - JavaScript
URL: https://sockodds.com/docs/examples/live-odds-tracker/

# Live Odds Tracker Example - JavaScript [#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 [#what-youll-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 [#prerequisites]

- Node.js 18+
- SockOdds API key ([free](https://sockodds.com/signup/))
- Basic JavaScript

## Complete code [#complete-code]

### Step 1: Setup project [#step-1-setup-project]

```bash
mkdir odds-tracker && cd odds-tracker
npm init -y
```

### Step 2: Create tracker.js [#step-2-create-trackerjs]

```javascript
// 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 [#step-3-run-it]

```bash
SOCKODDS_KEY=so_live_… node tracker.js AFL
```

## Expected output [#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 [#how-it-works]

### 1. Fetch odds [#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 [#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 [#3-compare-with-previous-odds]

A `Map` keyed by `eventID` holds the last event seen.

### 4. Detect significant movement [#4-detect-significant-movement]

Filter `moves` by `Math.abs(pct) > 3` for steam.

### 5. Respect stale events [#5-respect-stale-events]

`info.stale` means the source stopped writing; don't alert on it.

## Enhancements [#enhancements]

### Track multiple leagues [#track-multiple-leagues]

```javascript
for (const league of ["AFL", "NRL", "EPL"]) await tickFor(league);
```

### Add Discord notifications [#add-discord-notifications]

```javascript
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 [#store-history]

```javascript
import { appendFileSync } from "node:fs";
appendFileSync("moves.jsonl", JSON.stringify({ t: Date.now(), eventID: e.eventID, ...m }) + "\n");
```

## Troubleshooting [#troubleshooting]

### "No AFL events found" [#no-afl-events-found]

Out of season, or your plan doesn't include the league — check `notice` and `/leagues`.

### Rate limit errors (429) [#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 [#missing-odds-data]

Some events carry `oddsPresent: true` but `oddsAvailable: false` — every book has suspended.

## Next steps [#next-steps]

### Combine with other examples [#combine-with-other-examples]

- [Arbitrage calculator](https://sockodds.com/docs/examples/arbitrage-calculator/)
- [Odds comparison dashboard](https://sockodds.com/docs/examples/odds-comparison-dashboard/)
- [Player props analyzer](https://sockodds.com/docs/examples/player-props-analyzer/)
- [Parlay builder](https://sockodds.com/docs/examples/parlay-builder/)

### Learn more [#learn-more]

- [The oddID grammar](https://sockodds.com/docs/data-types/odds/)
- [Best practices](https://sockodds.com/docs/info/best-practices/)
- [API reference](https://sockodds.com/docs/reference/)

> Need help?[FAQ](https://sockodds.com/docs/faq/) · [Email](mailto:api@sockodds.com) · [Contact](https://sockodds.com/contact-us/)
