Why this is hard
The problem with live NFL data
A live score is table stakes. What makes a game center worth opening mid-game is context: which quarter, whose ball, how the win probability got to where it is — and that needs both a matches feed for the score and a play-by-play feed for the drive underneath it, kept in sync.
- Score and play-by-play usually come from different endpoints with different keys — a match id here, a nflverse-style game id there. This build uses both and shows exactly where they connect.
- Live coverage is real but polled, not pushed: the match response says plainly when a game is being served from the stored tier rather than a live adapter, so a client can show a correct "last updated" instead of implying second-by-second push.
- Win probability is a per-play field, not a separate model call — it is already computed on every row of play-by-play, so a chart is a filter and a line, not a machine-learning project.
Path 1 · recommended
Have your AI agent build it
get_matches covers the score half of this build well. Play-by-play — the drives, the down and distance, the per-play win probability — has no MCP tool, so an agent needs one direct REST call to get the rest of a game center.
1. Connect the MCP server
Add the server in your MCP client and sign in with your Big Balls account. Your client registers itself and handles the token exchange — there is no key to copy.
{
"mcpServers": {
"bigballs-sports-data": {
"url": "https://mcp.bigballsdata.com/mcp"
}
}
}2. Tools your agent gets
get_matchesLive, upcoming or historical matches for a sport or league.
get_coverageMachine-readable map of what we hold and what we do not.
3. Ask for what you want
Paste this at your agent. It is written to make the model check coverage before it designs anything, which is what stops it inventing a field we do not serve.
Build me an NFL live game center for one game.
Use the Big Balls Sports Data MCP server's get_matches to find the game and
its current score and status.
The MCP server has no play-by-play tool, so for drives and win probability
call GET /v1/nfl/games/{game_id}/plays directly with my key — note this uses
the game's nflverse-style id (e.g. "2025_01_DAL_PHI"), not the match UUID
get_matches returned; both point at the same game.
From the plays, build: the current drive (posteam, down, distance, field
position from the latest row) and a win-probability series across the game
(wp field, one point per play). Tell me which fields came from which call.What the agent cannot reach
The two endpoints use two different identifiers for the same game — a match UUID from get_matches, a nflverse game_id ("2025_01_DAL_PHI") for /plays — and nothing joins them automatically today. An agent has to carry both ids rather than assuming one resolves to the other.
Path 2 · hand-coded
Build it yourself
Every call below ran against production on 2026-09-11. All of it is free.
- 01
Find a live or upcoming game
Filter matches by status. A live game center only needs status=live; status=scheduled finds what is coming up.
GET /v1/matchesbashcurl -s "https://api.bigballsdata.com/v1/matches?sport=american_football&league=nfl&status=live" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668", "home": { "name": "Los Angeles Rams", "short_name": "LAR" }, "away": { "name": "San Francisco 49ers", "short_name": "SF" }, "kickoff_utc": "2026-09-11T00:35:00.000Z", "status": "live", "score": { "home": 7, "away": 10 }, "linescore": { "away": [3, 7], "home": [0, 7] }, "broadcast": "Netflix" } ], "meta": { "source": "stored" } } - 02
Poll the match for score changes
GET /v1/matches/:id refreshes score and linescore for one game. Read meta.note before assuming push-frequency: this game was served "from the stored matches table (no live adapter currently lists this match)" at the moment above, which is an honest statement of polling cadence, not a defect.
GET /v1/matches/:idbashcurl -s "https://api.bigballsdata.com/v1/matches/5b2e8a2c-9c4f-4f0f-b035-fa23b0311668" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": { "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668", "status": "live", "score": { "home": 7, "away": 10 } }, "meta": { "source": "stored", "note": "Served from the stored matches table (no live adapter currently lists this match)." } } - 03
Pull the drive from play-by-play
This needs the game_id, not the match UUID — nflverse-style, like "2025_01_DAL_PHI". Every row carries down, distance, field position, the possessing team and a running win-probability value.
GET /v1/nfl/games/:game_id/playsbashcurl -s "https://api.bigballsdata.com/v1/nfl/games/2025_01_DAL_PHI/plays?limit=5" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "play_id": "39", "game_id": "2025_01_DAL_PHI", "posteam": "SEA", "defteam": "SF", "play_type": "kickoff", "yards_gained": 0, "yardline_100": 35, "quarter_seconds_remaining": 900, "score_differential": 0, "epa": 0.562, "wp": 0.546 } ], "pagination": { "total": 166 } } - 04
Build the win-probability chart
wp is already computed per play — just sort by play_id and plot it. No model to run, no EPA-to-probability conversion to write.
Win probability seriespythondef wp_series(plays): return [ {"play_id": p["play_id"], "wp": p["wp"], "clock": p["quarter_seconds_remaining"]} for p in sorted(plays, key=lambda p: int(p["play_id"])) if p["wp"] is not None ] - 05
Show the current drive
The latest play with a non-null posteam and down tells you who has the ball, on what down, and where. Filter out kickoffs and administrative rows (play_type is null on the opening snap in this data) before picking "latest".
Current drive from the last real snappythondef current_drive(plays): live = [p for p in plays if p.get("down") is not None and p.get("posteam")] if not live: return None p = max(live, key=lambda p: int(p["play_id"])) return { "team": p["posteam"], "down": p["down"], "field_position": p["yardline_100"], "epa_last_play": p["epa"], }
Reference
Every endpoint this tutorial uses
All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-11.
| Method | Path | Returns | Why you need it | Plan |
|---|---|---|---|---|
| GET | /v1/matches?sport=american_football&league=nfl&status= | Match id, teams, kickoff, status, score and linescore | The scoreboard half of a game center — find live games, show the running score. | Free |
| GET | /v1/matches/:id | One match at a time: score, linescore, status, broadcast | Poll this to refresh a single open game rather than re-fetching the whole live list.meta.note discloses when a game is served from the stored tier with no live adapter listing it — read it rather than assuming second-by-second push on every game. | Free |
| GET | /v1/nfl/games/:game_id/plays | Every play: down, distance, field position, EPA, win probability, possession | Everything a score feed cannot show — the drive underneath the number, and why the win probability moved. | Free |
Pricing, honestly
Where the free tier stops
Nothing in this build is gated — matches and play-by-play are both free. The paid wedge for a live-scores product on this API is elsewhere: bookmaker odds (see the betting-model tutorial) and the multi-season historical archive some analytics builds want on top of a live feed.
Free key
{ "data": { "status": "live", "score": { "home": 7, "away": 10 } } }Solo
{ "data": { "status": "live", "score": { "home": 7, "away": 10 }, "has_odds": true } }- Score, linescore and full play-by-play with EPA and win probability are all free — there is no reduced shape to unlock here.
- has_odds on a match response tells you whether a betting layer is available for that game; the odds themselves are Edge-plan (see the betting-model tutorial).
More tutorials
Other build guides
Questions
- Is this actually live, or polled?
- Polled, and the API says so rather than implying otherwise. /v1/matches/:id carries a meta.note stating when a game has no live adapter listing it and is being served from the stored tier instead. Build your polling interval around that disclosure rather than assuming push.
- Why do matches and play-by-play use different ids for the same game?
- get_matches (and every /v1/matches route) returns a canonical match UUID; /v1/nfl/games/:game_id/plays is keyed on nflverse’s own game id, a string like "2025_01_DAL_PHI". Nothing joins them today — carry both if your app needs both feeds for one game.
- Do I need to compute win probability myself?
- No. wp arrives precomputed on every play-by-play row, alongside epa. A chart is a sort and a filter, not a model.
- Can an agent build this end to end with no direct HTTP calls?
- Not quite. get_matches covers score and status. Drives, down-and-distance and win probability have no MCP tool yet — an agent has to call /v1/nfl/games/:game_id/plays directly with the same API key.
Related APIs
Other APIs you can build with: