Why this is hard
The problem with NFL defensive data
Offensive box scores are everywhere. Defensive ones are either behind a five-figure enterprise contract or reduced to a tackle count, which is the least informative number a defender produces. A scouting view needs the disruption stats — pressure, tackles for loss, coverage — at the week level, for enough seasons that one good year is visible as one good year.
- Free sources stop at tackles and interceptions, which flatter box-score defenders and hide edge rushers.
- Season totals cannot tell a 17-game grind from a six-game tear; you need the weekly rows.
- Percentiles are the whole point of scouting and almost nobody exposes the population to compute them against.
Path 1 · recommended
Have your AI agent build it
The MCP server can find the player and pull their stats, which covers the single-player half of scouting. It cannot do the population half — ranking a whole position group — because that needs every defender’s rows at once and the tools are per-player. Start here, then drop to REST for the board.
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
find_playersFind canonical player IDs by name for use with player-stat tools.
get_player_statsPer-player career or per-season totals and per-game rates.
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, find Trey Hendrickson and pull his 2024 weekly stats. Total his sacks, QB hits and tackles for loss, then tell me which weeks he was held without a pressure.What the agent cannot reach
The agent can answer for one player at a time. There is no leaders or percentile tool for the NFL, so "who ranks where" is a REST job — the manual path below builds the population and computes the percentiles from it.
Path 2 · hand-coded
Build it yourself
Three calls and a percentile. Roster to get the defenders, weekly stats per player, then rank them. Everything below ran against production on 2026-09-04 and the board is its real output.
- 01
Get a roster and keep the defenders
The roster endpoint returns player_id in the same format the stats route expects, so no id mapping is needed. Note that position here is coarse (DB) while the stats rows are specific (CB, SAF) — filter on depth_chart_position when you need the finer one.
Roster for one teambashcurl -s -H "x-api-key: $BBS_API_KEY" \ "https://api.bigballsdata.com/v1/nfl/rosters?team=CIN&season=2024"Real response, trimmed
jsonjson{ "data": [ { "season": 2024, "team": "CIN", "player_id": "00-0037753", "player_name": "Cam Taylor-Britt", "position": "DB", "depth_chart_position": "CB", "jersey_number": 29, "status": "ACT", "years_exp": 2 } ] } - 02
Pull one defender’s weekly rows
Seventeen rows for a full season, each carrying the nineteen defensive columns. Weekly is the unit that matters: a season total of 17.5 sacks does not tell you they came in eleven of seventeen weeks.
Weekly defensive statsbashcurl -s -H "x-api-key: $BBS_API_KEY" \ "https://api.bigballsdata.com/v1/nfl/players/00-0033935/stats?season=2024"Real response, trimmed
jsonjson{ "data": [ { "season": 2024, "week": 1, "player_name": "T.Hendrickson", "position": "LB", "tackles_solo": 0, "tackle_assists": 1, "tackles_for_loss": 1, "sacks": 0, "qb_hits": 2, "passes_defended": 0, "fumbles_forced": 0, "interceptions_caught": 0, "defensive_tds": 0 } ] } - 03
Build the population, then the percentiles
Percentiles need everybody, not one player — and there is no NFL leaders endpoint, so you assemble the population yourself. Thirty-two roster calls and one stats call per defender is enough for a league-wide board, and it caches well because completed seasons never change.
scout.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(); const SEASON = 2024; const FRONT_SEVEN = new Set(['DE', 'DT', 'LB']); // 1. Every defender on every roster. const teams = ['ARI','ATL','BAL','BUF','CAR','CHI','CIN','CLE','DAL','DEN','DET','GB','HOU','IND', 'JAX','KC','LA','LAC','LV','MIA','MIN','NE','NO','NYG','NYJ','PHI','PIT','SEA','SF', 'TB','TEN','WAS']; const rosters = await Promise.all( teams.map((t) => api(`/v1/nfl/rosters?team=${t}&season=${SEASON}`)), ); // 2. Their weekly rows, then season totals. type Row = { position: string; sacks: number | null; qb_hits: number | null; tackles_for_loss: number | null; passes_defended: number | null; interceptions_caught: number | null }; const totals = new Map<string, { name: string; pos: string; pressure: number; tfl: number; cover: number; weeks: number }>(); for (const r of rosters.flatMap((x) => x.data)) { const { data } = await api(`/v1/nfl/players/${r.player_id}/stats?season=${SEASON}`); const rows = (data as Row[]).filter((w) => FRONT_SEVEN.has(w.position)); if (rows.length < 10) continue; // a season, not a cup of coffee totals.set(r.player_id, { name: r.player_name, pos: rows[0]!.position, // sacks + QB hits is the cheapest honest pressure proxy on this data. pressure: sum(rows, 'sacks') + sum(rows, 'qb_hits'), tfl: sum(rows, 'tackles_for_loss'), cover: sum(rows, 'passes_defended') + sum(rows, 'interceptions_caught'), weeks: rows.length, }); } function sum(rows: Row[], k: keyof Row) { return rows.reduce((n, r) => n + Number(r[k] ?? 0), 0); } // 3. Percentile within the population you just built. const board = [...totals.values()].sort((a, b) => b.pressure - a.pressure); const pct = (i: number) => Math.round((100 * (board.length - 1 - i)) / (board.length - 1)); board.slice(0, 6).forEach((p, i) => console.log(`${p.name.padEnd(16)} ${p.pos.padEnd(3)} pressure ${String(p.pressure).padStart(4)} TFL ${String(p.tfl).padStart(3)} pct ${pct(i)}`), );Real response, trimmed
jsonjsonT.Hendrickson LB pressure 53.5 TFL 19 pct 100 Z.Allen DE pressure 51.5 TFL 17 pct 100 G.Karlaftis DE pressure 48 TFL 13 pct 99 M.Garrett DE pressure 42 TFL 22 pct 99 L.Williams DE pressure 39 TFL 16 pct 99 N.Bonitto LB pressure 38.5 TFL 16 pct 98
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/rosters?team=&season= | player_id, player_name, position, depth_chart_position, jersey_number, status, years_exp | The only way to enumerate a position group. player_id comes back in the format the stats route wants, so there is no id-mapping step. | Free |
| GET | /v1/nfl/players/:id/stats?season= | One row per week with 19 defensive columns alongside the offensive ones | The weekly grain is what makes a scouting view different from a leaderboard. Populated on 112,319 of 112,333 rows across 2020-2025. | Free |
Pricing, honestly
Where the free tier stops
A free key builds the board above for one season. The archive is the paid part, and for scouting it is the part that matters: one season ranks players, six seasons show whether a breakout held.
Free key
One season of weekly defensive rows, 1,000 requests a day.Solo
All six seasons, 2020-2025, and the request budget to walk 32 rosters without pacing.- A league-wide board is ~1,700 calls at one per defender per season — comfortable on a paid key, a full day of a free one.
- Completed seasons never change, so the archive caches permanently. You pay for it once and re-read it forever.
- Six seasons is the difference between "had a good year" and "is good".
More tutorials
Other build guides
Known gaps
What is not here yet
Three things a scouting build would reasonably expect, which this API does not honestly provide today. None of them is coming-soon copy for something that already works.
- There are no NFL percentiles or tiers to call. /v1/players/:id/tier serves percentile and position_percentile for hockey, basketball and soccer; player_tiers holds nothing for american_football. The tutorial computes percentiles from the population it builds, which is why step 3 exists at all.
- nfl_player_stats.interceptions is permanently null. It is in the response, it is 0% populated across all 112,333 rows, and the loader documents why: nflverse has no such column. Use interceptions_caught, which is populated like the rest. A field that is always null is worse than an absent one, so it is named here rather than left to be found.
- There are no snap counts, no coverage grades and no pressure rate denominators. "Pressure" above is sacks plus QB hits, which is a count, not a rate — without snaps you cannot say per-opportunity, and this API does not hold them.
- No college data links to these players. The NFL rows stand alone; there is no draft-year or NCAA production join.
Questions
- How many defensive columns are there really?
- Nineteen, and they were counted per column rather than inherited from a changelog: eighteen arrived together (tackles solo and assists, tackles with assist, tackles for loss and their yards, sacks and sack yards, QB hits, passes defended, forced fumbles, interception yards, defensive touchdowns, safeties, punt/PAT/FG blocks, and the two defensive two-point fields) and interceptions_caught arrived separately. All nineteen are populated on 112,319 of 112,333 rows.
- Why does the code compute percentiles instead of calling an endpoint?
- Because there is no NFL percentile endpoint. The tier route exists and serves percentiles for other sports, but the underlying table has no american_football rows. Computing them from a population you assembled is also more honest for scouting: you can see exactly which players are in the comparison set, which matters when the answer changes depending on whether you included special-teamers.
- Can I scout offensive players with the same code?
- Yes, and it is the same shape — swap the position filter and the columns. The offensive side is what the fantasy football tutorial builds on, including precomputed PPR points, so if that is your target start there instead.
- How far back does it go?
- Six seasons, 2020 through 2025, 112,333 weekly rows. 2026 rows begin landing after the season opens on 2026-09-09.
Related APIs
Other APIs you can build with: