Odds Comparison Dashboard Example - React/Next.js

Create a Next.js dashboard that compares odds across every Australian bookmaker and highlights the best line.

What you'll build

A script that:

  • Displays odds from all bookmakers side by side
  • Highlights the best price for each market
  • Auto-refreshes every 2.5 minutes
  • Links each best price to the bookmaker
  • Keeps the key on the server

Perfect for: Odds comparison sites, betting platforms, research tools

Prerequisites

  • Node.js 18+
  • Basic React/Next.js
  • SockOdds API key (free)

Complete code

Step 1: Create a Next.js project

npx create-next-app@latest odds-dashboard --ts --app --tailwind --eslint
cd odds-dashboard

Step 2: Create the API route (app/api/odds/route.ts)

// app/api/odds/route.ts — proxies the feed so the key never reaches the browser
import { NextResponse } from "next/server";

export async function GET(req: Request) {
  const league = new URL(req.url).searchParams.get("league") ?? "AFL";
  const url = new URL("https://api.sockodds.com/v2/events");
  url.search = new URLSearchParams({ leagueID: league, oddsAvailable: "true", oddID: "points-home-game-ml-home", includeOpposingOdds: "true", limit: "50" }).toString();
  const r = await fetch(url, { headers: { "x-api-key": process.env.SOCKODDS_KEY! }, next: { revalidate: 120 } });
  if (!r.ok) return NextResponse.json({ error: await r.text() }, { status: r.status });
  return NextResponse.json(await r.json());
}

Step 3: Create the dashboard component (app/Dashboard.tsx)

"use client";
import { useEffect, useState } from "react";

type Book = { odds: string; decimal: number; available: boolean };
type Odd = { oddID: string; marketName: string; bookOdds: string; fairOdds: string | null; byBookmaker: Record<string, Book> };
type Event = { eventID: string; teams: { home: { names: { medium: string } }; away: { names: { medium: string } } }; odds: Record<string, Odd>; links: { bookmakers?: Record<string, string> }; info: { stale: boolean } };

export default function Dashboard({ league }: { league: string }) {
  const [events, setEvents] = useState<Event[]>([]); const [updated, setUpdated] = useState("");
  async function load() { const b = await (await fetch(`/api/odds?league=${league}`)).json(); setEvents(b.data ?? []); setUpdated(new Date().toLocaleTimeString()); }
  useEffect(() => { load(); const t = setInterval(load, 150_000); return () => clearInterval(t); }, [league]);
  const books = Array.from(new Set(events.flatMap((e) => Object.values(e.odds).flatMap((o) => Object.keys(o.byBookmaker))))).sort();
  return (
    <div className="p-6 text-sm">
      <p className="text-gray-500 mb-4">{league} head to head · updated {updated}</p>
      <table className="w-full border-collapse">
        <thead><tr><th className="text-left">Market</th><th>Fair</th>{books.map((b) => <th key={b} className="px-2">{b}</th>)}</tr></thead>
        <tbody>{events.flatMap((e) => Object.values(e.odds).map((o) => {
          const best = Math.max(...Object.values(o.byBookmaker).filter((q) => q.available).map((q) => q.decimal));
          return <tr key={o.oddID + e.eventID} className={e.info.stale ? "opacity-50" : ""}>
            <td className="py-1">{e.teams.away.names.medium} @ {e.teams.home.names.medium} — {o.marketName}</td>
            <td className="text-center text-gray-500">{o.fairOdds ?? "—"}</td>
            {books.map((b) => { const q = o.byBookmaker[b]; const isBest = q?.available && q.decimal === best;
              return <td key={b} className={"text-center px-2 " + (isBest ? "bg-green-100 font-bold" : "") + (q && !q.available ? " line-through text-gray-400" : "")}>
                {q ? (isBest && e.links.bookmakers?.[b] ? <a href={e.links.bookmakers[b]} target="_blank" rel="noreferrer">{q.decimal.toFixed(2)}</a> : q.decimal.toFixed(2)) : ""}</td>; })}
          </tr>; }))}</tbody>
      </table>
    </div>
  );
}

Step 4: Update the page (app/page.tsx)

import Dashboard from "./Dashboard";
export default function Page() { return <Dashboard league="AFL" />; }

Step 5: Add the environment variable

echo 'SOCKODDS_KEY=so_live_…' > .env.local

Step 6: Run it

npm run dev
# open http://localhost:3000

Expected output

A table: one row per market, one column per bookmaker, the best available decimal highlighted and linked to the book; stale events dimmed; the fair price in grey.

How it works

1. Proxy API calls through the backend

The route handler holds the key; the browser only ever talks to /api/odds.

2. Process the nested bookmaker structure

Object.values(o.byBookmaker), skipping available: false.

3. Find the best odds

Math.max over decimal.

The best cell links to links.bookmakers[book].

5. Auto-refresh

setInterval at 2.5 minutes, matching the source.

Enhancements

Add totals and lines

oddID: "points-home-game-ml-home,points-all-game-ou-over,points-home-game-sp-home"

Show line movement

// keep the previous body in a ref and compare decimal per book per oddID; render ▲/▼

Add filters

const [league, setLeague] = useState("AFL"); // <select> over /leagues

Export to CSV

const csv = events.flatMap(...).map((r) => r.join(",")).join("\n");

Troubleshooting

"Failed to fetch odds"

Check .env.local exists and restart the dev server.

Odds not updating

The interval is 2.5 minutes; the source refreshes every ~2.

Best odds not highlighted

Only available: true entries compete.

Next steps

Combine with other examples

Learn more

Need help?FAQ · Email · Contact