Examples · Next.js
Build an Odds Comparison Site With Next.js
An odds comparison table is the core of every betting site: one row per team, one column per sportsbook, and the best price highlighted. This tutorial builds one in a single Next.js page.
Cost: 18 credits per league, at most every 5 minutes. The free plan includes 1,000 credits a month. Full project on GitHub.
1. Fetch odds on the server
The page is a server component, so it calls ml.odds() on the server and your API key never reaches the browser. revalidate = 300 caches each league for 5 minutes, which caps what you spend however many people visit.
2. Build the columns
The page counts how many of the upcoming games each book prices, and orders the columns busiest book first. Each row is one team, with that book's price in each column.
3. Highlight the best price
For each row, the highest American price is the best for the bettor. Every cell with that price gets the best style, so ties are all highlighted.
The code
odds-comparison-table/app/page.tsx, straight from the repository.
import { MoneyLine } from 'moneyline-sports-api'
// The key stays on the server: this page renders on the server and the browser never sees it.
const ml = new MoneyLine() // reads MONEYLINE_API_KEY
export const revalidate = 300 // one odds call (18 credits) per league every 5 minutes at most
const LEAGUES = { nfl: 'NFL', nba: 'NBA', mlb: 'MLB', nhl: 'NHL', ncaa_football: 'College Football' } as const
type League = keyof typeof LEAGUES
type Outcome = { name: string; price: number }
type Book = { bookmakerId: string; bookmakerName: string; markets: { outcomes: Outcome[] }[] }
type Game = { eventId: string; startTime: string; homeTeamName: string; awayTeamName: string; bookmakers: Book[] }
const fmt = (p?: number) => (p == null ? '' : p > 0 ? `+${p}` : String(p))
function priceAt(book: Book, team: string) {
return book.markets.flatMap((m) => m.outcomes).find((o) => o.name === team)?.price
}
export default async function Page({ searchParams }: { searchParams: Promise<{ league?: string }> }) {
const requested = (await searchParams).league
const league: League = requested && requested in LEAGUES ? (requested as League) : 'nfl'
const games = ((await ml.odds({ league, market: 'moneyline', sourceType: 'sportsbook' })) as Game[])
.filter((g) => new Date(g.startTime) > new Date())
.sort((a, b) => a.startTime.localeCompare(b.startTime))
.slice(0, 15)
// One column per sportsbook, busiest books first.
const counts = new Map<string, number>()
for (const g of games) for (const b of g.bookmakers) counts.set(b.bookmakerName, (counts.get(b.bookmakerName) ?? 0) + 1)
const books = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name)
return (
<main>
<h1>{LEAGUES[league]} moneyline odds comparison</h1>
<p>Every sportsbook's price for the next {games.length} games. The best price for each team is highlighted.</p>
<nav>
{Object.entries(LEAGUES).map(([id, name]) => (
<a key={id} href={`/?league=${id}`} aria-current={id === league ? 'page' : undefined}>{name}</a>
))}
</nav>
<div className="scroll">
<table>
<thead>
<tr><th>Team</th>{books.map((b) => <th key={b}>{b}</th>)}</tr>
</thead>
<tbody>
{games.flatMap((g) => [g.awayTeamName, g.homeTeamName].map((team, i) => {
const prices = books.map((name) => {
const book = g.bookmakers.find((b) => b.bookmakerName === name)
return book ? priceAt(book, team) : undefined
})
const best = Math.max(...prices.filter((p): p is number => p != null))
return (
<tr key={`${g.eventId}-${team}`}>
<td>
{team}
{i === 0 && <div className="time">{new Date(g.startTime).toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}</div>}
</td>
{prices.map((p, j) => <td key={books[j]} className={p === best ? 'best' : undefined}>{fmt(p)}</td>)}
</tr>
)
}))}
</tbody>
</table>
</div>
<p className="time">Odds from <a href="https://www.moneylineapp.com">MoneyLine Sports API</a>.</p>
</main>
)
}
Run it
Get a free API key, clone the example, then run:
npm install
export MONEYLINE_API_KEY=your-key
npm run dev