Examples · React and Vite
Build a Live Sports Scoreboard With React
This tutorial builds a scoreboard with today's games for each league. It refreshes every minute while a game is in progress and every 10 minutes otherwise, so it stays current without wasting calls.
Cost: 2 credits per refresh. The free plan includes 1,000 credits a month. Full project on GitHub.
1. Keep the key off the browser
Anything in browser code is public. The Vite config forwards /api requests to MoneyLine and adds your key on the dev server. In production, do the same from your own backend or an edge function.
2. Load today's games
/v1/events/today returns each game's status, period, clock and score. The useScores hook loads it for the chosen league and sorts games by start time.
3. Refresh faster while games are live
After each load, the hook checks whether any game is in progress. If so it refreshes in a minute, and if not in 10 minutes. Each refresh costs 2 credits.
The code
live-scoreboard/src/App.jsx, straight from the repository.
import { useEffect, useState } from 'react'
const LEAGUES = { nfl: 'NFL', mlb: 'MLB', nba: 'NBA', nhl: 'NHL', ncaa_football: 'College Football', soccer_mls: 'MLS' }
// Each refresh costs 2 credits: check every minute while a game is live, every 10 minutes otherwise.
const LIVE_MS = 60 * 1000
const IDLE_MS = 10 * 60 * 1000
async function todaysGames(league) {
const res = await fetch(`/api/v1/events/today?league=${league}`)
const body = await res.json()
if (!body.success) throw new Error(body.error?.message || `HTTP ${res.status}`)
return body.data.sort((a, b) => a.startTime.localeCompare(b.startTime))
}
function useScores(league) {
const [state, setState] = useState({ games: [], error: null, updatedAt: null })
useEffect(() => {
let timer
let cancelled = false
const load = async () => {
try {
const games = await todaysGames(league)
if (cancelled) return
setState({ games, error: null, updatedAt: new Date() })
timer = setTimeout(load, games.some((g) => g.status === 'in_progress') ? LIVE_MS : IDLE_MS)
} catch (err) {
if (cancelled) return
setState((s) => ({ ...s, error: err.message }))
timer = setTimeout(load, IDLE_MS)
}
}
load()
return () => { cancelled = true; clearTimeout(timer) }
}, [league])
return state
}
function status(game) {
if (game.status === 'in_progress') return [game.period, game.clock].filter(Boolean).join(' · ') || 'Live'
if (game.status === 'final') return 'Final'
return new Date(game.startTime).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}
function Game({ game }) {
const started = game.status !== 'scheduled'
const row = (team, score, won) => (
<div className={`team${won ? ' won' : ''}`}>
<span>{team}</span>
<span className="score">{started ? score : ''}</span>
</div>
)
const { home, away } = game.scores ?? {}
return (
<div className={`game ${game.status}`}>
<div className="status">{status(game)}</div>
{row(game.awayTeamName, away, game.status === 'final' && away > home)}
{row(game.homeTeamName, home, game.status === 'final' && home > away)}
</div>
)
}
export default function App() {
const [league, setLeague] = useState('mlb')
const { games, error, updatedAt } = useScores(league)
return (
<main>
<h1>Today's scores</h1>
<nav>
{Object.entries(LEAGUES).map(([id, name]) => (
<button key={id} onClick={() => setLeague(id)} aria-pressed={id === league}>{name}</button>
))}
</nav>
{error && <p className="error">Couldn't load scores: {error}</p>}
{!error && updatedAt && games.length === 0 && <p>No {LEAGUES[league]} games today.</p>}
<div className="grid">{games.map((g) => <Game key={g.eventId} game={g} />)}</div>
{updatedAt && <p className="muted">Updated {updatedAt.toLocaleTimeString()} · Scores from MoneyLine Sports API</p>}
</main>
)
}
Run it
Get a free API key, clone the example, then run:
npm install
echo "MONEYLINE_API_KEY=your-key" > .env.local
npm run dev