# Odds Comparison Dashboard Example - React/Next.js
URL: https://sockodds.com/docs/examples/odds-comparison-dashboard/

# Odds Comparison Dashboard Example - React/Next.js [#odds-comparison-dashboard-example---reactnextjs]

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

## What you'll build [#what-youll-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 [#prerequisites]

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

## Complete code [#complete-code]

### Step 1: Create a Next.js project [#step-1-create-a-nextjs-project]

```bash
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) [#step-2-create-the-api-route-appapioddsroutets]

```typescript
// 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) [#step-3-create-the-dashboard-component-appdashboardtsx]

```typescript
"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) [#step-4-update-the-page-apppagetsx]

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

### Step 5: Add the environment variable [#step-5-add-the-environment-variable]

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

### Step 6: Run it [#step-6-run-it]

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

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

### 1. Proxy API calls through the backend [#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 [#2-process-the-nested-bookmaker-structure]

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

### 3. Find the best odds [#3-find-the-best-odds]

`Math.max` over `decimal`.

### 4. Highlight and link [#4-highlight-and-link]

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

### 5. Auto-refresh [#5-auto-refresh]

`setInterval` at 2.5 minutes, matching the source.

## Enhancements [#enhancements]

### Add totals and lines [#add-totals-and-lines]

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

### Show line movement [#show-line-movement]

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

### Add filters [#add-filters]

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

### Export to CSV [#export-to-csv]

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

## Troubleshooting [#troubleshooting]

### "Failed to fetch odds" [#failed-to-fetch-odds]

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

### Odds not updating [#odds-not-updating]

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

### Best odds not highlighted [#best-odds-not-highlighted]

Only `available: true` entries compete.

## Next steps [#next-steps]

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

- [Live odds tracker](https://sockodds.com/docs/examples/live-odds-tracker/)
- [Arbitrage calculator](https://sockodds.com/docs/examples/arbitrage-calculator/)
- [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/)
