Why this is hard
The problem with conditions data
Everyone believes weather moves NFL totals and almost nobody measures it, because the measurement needs two things joined: the conditions a game was played in, and the score it produced. Conditions live in weather APIs keyed by latitude and time; scores live in sports APIs keyed by game. Joining them is most of the work, and the join is where the errors get in.
- A weather API gives you a forecast for a stadium, not a result for a game.
- Roof state is the largest and cleanest conditions effect, and it is the one most often left out.
- A pooled multi-season average can hide a trend that reverses the practical answer.
Path 1 · recommended
Have your AI agent build it
The conditions fields are not exposed through an MCP tool — the tools cover matches, standings, player stats, Elo and coverage, none of which carries roof. An agent can scaffold the season and scores; the roof join is a REST call.
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.
Using the Big Balls Sports MCP server, list the NFL games for the 2025 season with their final scores, then tell me the average combined points.What the agent cannot reach
The agent gets scores but not roof, so it cannot split them by stadium type. That split is the whole tool, and it needs the REST endpoint below.
Path 2 · hand-coded
Build it yourself
One endpoint, one grouping. The entire analysis is a single call per season plus an average — the difficulty is in the bucketing, not the code.
- 01
Pull a season with roof state attached
Every game row carries stadium, roof and surface alongside the scores, so there is no second call and no geocoding. Rest days come along too, which is the other conditions-adjacent variable worth having.
One season of gamesbashcurl -s -H "x-api-key: $BBS_API_KEY" \ "https://api.bigballsdata.com/v1/nfl/games?season=2025"Real response, trimmed
jsonjson{ "data": [ { "game_id": "2025_01_DAL_PHI", "season": 2025, "week": 1, "game_date": "2025-09-04", "game_type": "REG", "home_team": "PHI", "away_team": "DAL", "home_score": 24, "away_score": 20, "stadium": "Lincoln Financial Field", "roof": "outdoors", "surface": "grass", "home_rest": 7, "away_rest": 7 } ] } - 02
Bucket the roof values — this is the step that decides your answer
There are four values and only two buckets. `dome` and `closed` are both roof-shut; `outdoors` and `open` are both roof-open, `open` being a retractable stadium with the roof back. Putting `open` with `dome` because both sound like stadiums with roofs is the mistake that changes the number.
The bucketingtstype Roof = 'dome' | 'closed' | 'outdoors' | 'open'; // closed = retractable roof SHUT -> plays like a dome // open = retractable roof BACK -> plays like outdoors const indoor = (roof: Roof) => roof === 'dome' || roof === 'closed'; - 03
Split the scoring, then look at it per season
The pooled gap is +3.87 over six seasons. Print it per season as well: the trend is the finding, and a single pooled number would have hidden it.
conditions.tstsconst KEY = process.env.BBS_API_KEY!; const api = async (p: string) => (await fetch(`https://api.bigballsdata.com${p}`, { headers: { 'x-api-key': KEY } })).json(); type Game = { season: number; roof: string | null; home_score: number | null; away_score: number | null }; const indoor = (r: string | null) => r === 'dome' || r === 'closed'; const seasons = [2020, 2021, 2022, 2023, 2024, 2025]; const all: Game[] = ( await Promise.all(seasons.map((y) => api(`/v1/nfl/games?season=${y}`))) ).flatMap((r) => r.data as Game[]); // A null score is an unplayed game; a null roof is a stadium we have no value // for. Dropping them is not the same as counting them as zero. const played = all.filter((g) => g.home_score !== null && g.away_score !== null && g.roof !== null); const total = (g: Game) => g.home_score! + g.away_score!; const mean = (xs: number[]) => xs.reduce((a, b) => a + b, 0) / xs.length; for (const y of seasons) { const s = played.filter((g) => g.season === y); const i = s.filter((g) => indoor(g.roof)).map(total); const o = s.filter((g) => !indoor(g.roof)).map(total); console.log(`${y} indoor ${i.length.toString().padStart(3)} @ ${mean(i).toFixed(1)} outdoor ${o.length.toString().padStart(4)} @ ${mean(o).toFixed(1)} gap ${(mean(i) - mean(o)).toFixed(2)}`); } const i = played.filter((g) => indoor(g.roof)).map(total); const o = played.filter((g) => !indoor(g.roof)).map(total); console.log(`\nPOOLED indoor ${i.length} @ ${mean(i).toFixed(2)} outdoor ${o.length} @ ${mean(o).toFixed(2)} gap ${(mean(i) - mean(o)).toFixed(2)}`);Real response, trimmed
jsonjson2020 indoor 93 @ 53.5 outdoor 176 @ 47.4 gap 6.12 2021 indoor 83 @ 48.7 outdoor 202 @ 45.0 gap 3.76 2022 indoor 86 @ 47.7 outdoor 198 @ 42.5 gap 5.21 2023 indoor 86 @ 46.6 outdoor 199 @ 42.6 gap 4.05 2024 indoor 98 @ 47.4 outdoor 187 @ 45.3 gap 2.18 2025 indoor 92 @ 46.9 outdoor 193 @ 45.5 gap 1.39 POOLED indoor 538 @ 48.50 outdoor 1155 @ 44.63 gap 3.87
Reference
Every endpoint this tutorial uses
All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-04.
| Method | Path | Returns | Why you need it | Plan |
|---|---|---|---|---|
| GET | /v1/nfl/games?season= | game_id, season, week, game_date, game_type, teams, scores, stadium, roof, surface, home_rest, away_rest | Conditions and result on the same row, which is the join this analysis would otherwise have to make itself. Roof is populated on 1,693 completed games across 2020-2025. | Free |
Pricing, honestly
Where the free tier stops
Six seasons of games is a small number of calls, so this build is cheap either way. The paid tier is about the archive rather than the volume: a one-season split has an n of roughly 285 and a confidence interval wide enough to swallow the effect.
Free key
The most recent completed season, 1,000 requests a day.Solo
All six seasons — the 1,693-game sample the pooled figure is computed on.- 2025 alone gives a +1.39 gap on 285 games. Six seasons give +3.87 on 1,693.
- The per-season trend is only visible with the archive; one season is a point, not a direction.
- Six calls, one per season, and the result caches forever because completed games do not change.
More tutorials
Other build guides
Known gaps
What is not here yet
This is a conditions tool with one conditions variable. The others are not degraded or partial — they are absent, and the difference matters when you are deciding whether to build on this.
- There is no wind and no temperature. The match_conditions table exists and holds zero rows; the fields are pending a weather licence, not pending an ingest. /v1/matches/:id/weather is a live forecast route that needs a geocoded venue, and NFL matches carry no venue_id, so it does not answer for the NFL either.
- Roof is a proxy for weather, not a measurement of it. An indoor game is climate-controlled; an outdoor game in a dome-less stadium in September and the same stadium in December are one bucket here.
- The gap is shrinking and this page does not explain why. 6.12 in 2020 to 1.39 in 2025 is a real trend in the data, and attributing it — rule changes, kicking, scheme, which stadiums opened — is beyond what these rows can support.
- No surface analysis is offered even though the surface field is right there. Turf-versus-grass needs injury data joined to it to be interesting, and the injury side is not usable for the NFL today.
Questions
- Is +3.87 points a real effect or a small-sample artefact?
- It is computed on 1,693 completed games, 538 indoor and 1,155 outdoor, which is a large sample for a league that plays about 285 games a season. The more useful caveat is not sample size but drift: the per-season gap runs 6.12, 3.76, 5.21, 4.05, 2.18, 1.39, so the pooled figure averages a declining effect. If you are modelling next week, 2025’s +1.39 is the more relevant number.
- Why does the bucketing matter so much?
- Because two of the four roof values are retractable stadiums in opposite states. `closed` is a roof-shut game and behaves like a dome; `open` is the same stadium with the roof back and behaves like outdoors. Grouping by "has a roof" rather than "was covered" moves games between buckets and changes the answer. The endpoint gives you the state, not the stadium type, which is the useful thing.
- Can I get wind speed or temperature for a game?
- No. That is the honest answer rather than a partial one: the conditions table is empty and the fields are held pending a weather licence. There is a forecast endpoint for other sports, but it needs a geocoded venue and NFL matches do not carry one.
- Does this cover college football?
- Not through this endpoint. /v1/nfl/games is NFL only. NCAAF games are in the unified /v1/matches surface, which does not carry roof.
Related APIs
Other APIs you can build with: