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-dashboardStep 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.localStep 6: Run it
npm run dev
# open http://localhost:3000Expected 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.
4. Highlight and link
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 /leaguesExport 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.