DunkAPI
DunkAPI v1.0.0
NBA basketball data and stats
Competitor profile
Developer declarations; not independently tested. Listing last checked Nov 4, 2026. Package observed Nov 3, 2026.
Declared capabilities
- Look up NBA, WNBA and college teams and players
- Read standings, schedules and scores
- Read box scores, leaders and team stats
Cost & access
DunkAPI does not sell anything through ChatGPT. The plugin is read-only: it has no checkout, purchase, subscription or credit top-up flow.
Declared limitations
- Place a wager: wagering is not supported
- Out of coverage: another sport
- Account and billing change: not supported
Onboarding example
Show the 2024-25 NBA standings.
Description
From the marketplace listing
DunkAPI connects ChatGPT and Codex to NBA basketball data from DunkAPI. It covers the NBA since 2004 with every shot located, the WNBA since 2006, and Division I men's and women's college basketball. Ask for standings, schedules and scores, box scores, player averages and game logs, leaders, team ratings, play-by-play, shot charts, sportsbook odds, line movement and player prop lines. The plugin looks the answer up with read-only tools and reports what the data says, including the season, date and source of each number. The first time you use it, you sign in with your DunkAPI account. Limits: DunkAPI is read-only. It cannot place wagers, accept or track bets, or edit your account. Odds and prop lines are shown as information about the market, not as advice. Data comes from public sources and the DunkAPI pipeline and is not guaranteed to be error-free or current to the minute, so do not rely on it as the sole basis for financial decisions. Ratings shown are DunkAPI's own Elo-style ratings, not official rankings. Not affiliated with the NBA or any team. Bundled skills teach the model how to choose the right tool for a question and how to explain results.
Language: English · Automatically detected from descriptions.
Changes
No changes recorded yet. Follow this plugin to keep track of future changes.
First seen Oct 30, 2026. Last seen Nov 4, 2026.
Similar plugins
Find competitors to follow as you develop your plugin.
-
EndzoneAPI brings NFL football data into ChatGPT and Codex with read-only tools and workflow skills: standings, schedules and results, pl...
Shared terms: stat, accept, advice, basis
-
FastballAPI brings MLB baseball data into ChatGPT and Codex with read-only tools and workflow skills: standings, schedules and scores, pl...
Shared terms: stat, accept, advice, basis
-
FighterAPI brings MMA data into ChatGPT and Codex with read-only tools and workflow skills: event cards and schedules, fight results and ...
Shared terms: stat, accept, advice, basis
-
AcesAPI brings tennis data into ChatGPT and Codex with read-only tools and workflow skills: tournaments and draws, schedules and results,...
Shared terms: stat, accept, advice, basis
-
StrikerAPI brings soccer data into ChatGPT and Codex with read-only tools and workflow skills: league tables, schedules and results, line...
Shared terms: stat, accept, advice, basis
Keywords
Files & skills
File archives
Skill instructions
dispatch4.54 KB
---
name: dispatch
description: "Routes a basketball analytics request to the right 2-3 skills. Load this FIRST when an NBA, WNBA or college basketball request comes in and you are unsure which skills to activate. Prevents loading all 16 skills when only 2-3 are needed. Do not use for a single obvious request such as devigging two prices, see odds-analysis directly."
metadata:
version: 1.0.0
author: DunkAPI
---
# Skill Dispatch Guide
Match the request to a pattern below, load those skills, and stop. Always load `dunkapi-tool` alongside any skill that needs data.
> **Default data tool:** DunkAPI (`dunkapi-tool`). A typical monthly allowance is 500 credits.
## When to Use
- A basketball request arrives and more than one skill could apply
- The user is new and does not know what to ask for
- A request spans data, modeling and market lines (for example "is Brunson's points line beatable?")
## When NOT to Use
- The request names one skill's job exactly ("devig these prices" goes straight to `odds-analysis`)
- The user wants a tool's parameters or cost, see `dunkapi-tool`
## Commands Available
None. This skill routes; `dunkapi-tool` makes the calls.
## First-Use Persona Detection
| Signal | Persona | Start with |
|--------|---------|-----------|
| "Who plays tonight?" "How did the Knicks do?" | Explorer | `game-lookup` + `team-analysis` |
| "Break down this player" "Compare these two" | Scout | `player-scouting` + `player-form` |
| "Where does he shoot from?" | Shot analyst | `shot-zone-analysis` |
| "What's the best price?" "How did the line move?" | Line shopper | `odds-explorer` |
| "Help me build a model" | Builder | `feature-engineering` + `walk-forward-validation` |
| "Does he go over his points line?" | Odds reader | `prop-modeling` + `player-form` |
## Request Patterns to Skills
### "Scores from last night" / "Who plays tonight" / "Find the Celtics-Heat game"
1. `game-lookup`
2. `dunkapi-tool`: `list_games`, `get_schedule`, `get_game`
### "Standings" / "Best team" / "Who plays fastest" / "Net rating"
1. `team-analysis`: standings, Elo, pace, four factors
### "Tell me about Jalen Brunson" / "Compare Tatum and Brown" / "Who led the league in true shooting"
1. `player-scouting`
2. If the question is about recent form: add `player-form`
### "Is he hot?" / "Last 10 games" / "Rolling average" / "Minutes trend"
1. `player-form`
2. `dunkapi-tool`: `get_player_games`
### "Where does he shoot?" / "Expected points" / "Best defence at the rim" / "Clutch shots"
1. `shot-zone-analysis`
2. `dunkapi-tool`: `get_shot_chart`, `get_plays`
### "What are the odds?" / "Best price" / "Line moved?" / "Sharp money?"
1. `odds-explorer`
2. For probability math: add `odds-analysis`
### "Preview tonight's Knicks game"
1. `game-preview` (it calls schedule, ratings, team stats, injuries and odds itself)
2. Add `team-analysis` if the user wants the efficiency detail
### "Devig these" / "What probability is -200 / +165?"
1. `odds-analysis`
### "Build a model" / "What features should I use?" / "Is my CV okay?"
1. `feature-engineering`
2. `walk-forward-validation`
3. `probability-calibration` once there are probabilities
### "Player props" / "Over/under on 24.5 points" / "Does he hit his line"
1. `prop-modeling`
2. `player-form` for the projection inputs
3. `dunkapi-tool`: `get_prop_history`, `get_player_props`
### "Women's college" / "WNBA" / "March Madness"
Same skills, pass `league`. Check `dunkapi-tool` for which tools support which league. Props exist for `nba`, `wnba`, `ncaam` only; `get_shot_chart` and `get_injuries` are NBA and WNBA only.
## Season Resolution
- `season` is the year the season ends. October 2026 to June 2027 is `2027`.
- The 2026-27 NBA regular season opens Oct 20, 2026. Before that, games in `get_schedule` are `preseason`, and `get_team_stats`, `get_ratings` and `get_standings` for the new season hold little or nothing. Preview the opener from `season: 2026` data plus the preseason schedule, and say so.
- WNBA: the calendar year. College men and women: the year the season ends.
## Multi-skill Chains
| Goal | Chain |
|------|-------|
| Defence audit | `team-analysis` > `shot-zone-analysis` |
## Anti-patterns
| Rationalization | Why it is wrong | Do this instead |
|-----------------|-----------------|-----------------|
| "Load every skill to be safe" | Burns context and mixes methods | Load the 2-3 that match |
| "Skip `dunkapi-tool`, I remember the params" | Params and costs change; wrong params fail | Check the router |
## What to Do Next
After the first answer, ask which of the chains above the user is on and load the next skill in it.
dunkapi-tool6.96 KB
---
name: dunkapi-tool
description: "Default data source for the DunkAPI skills. Routes every call to the 18 DunkAPI tools for NBA, WNBA, men's and women's college basketball: games, box scores, play-by-play, shot charts, team ratings, odds, line movement and player props. Use when any other skill needs basketball data, or when the user asks which tool answers a question or what a call costs."
metadata:
version: 1.0.0
author: DunkAPI
user-invocable: false
---
# DunkAPI Tool
> Connection: this plugin reaches the data through its MCP server, which the user connects with a normal OAuth sign-in. Never ask the user for an API key, token or password, and never put one in a tool argument.
Every other skill delegates data retrieval here. This skill owns the 18 tools, picks the cheapest call that answers the question, tracks credits, and explains the response envelope.
Full per-tool reference: https://dunkapi.com/docs. Live coverage by season: https://dunkapi.com/docs/atlas.
## Connecting
| Path | How |
|------|-----|
| Tool catalogue | `GET https://mcp.dunkapi.com/v1/tools` (JSON Schema for every tool) |
Over REST, every response is wrapped: `{"data": {...}}`. Read `response["data"]["standings"]`, not `response["standings"]`. REST responses carry `X-Credits-Used` and `X-Credits-Remaining` headers.
## Conventions every tool shares
- `league`: `nba` (default), `wnba`, `ncaam` (men's college), `ncaaw` (women's college). Not every tool accepts every league (table below).
- `season` is the year the season ends: `2026` = 2025-26, `2027` = 2026-27. WNBA seasons are the calendar year.
- `season_type`: `regular`, `postseason`, `play_in`, `preseason`.
- Dates are `YYYY-MM-DD` in US Eastern time.
- Game ids are ESPN game ids (strings like `"401859967"`). Player ids are ESPN athlete ids (like `"3934672"`). Teams take an id or an abbreviation (`NY`, `BOS`, `GS`, `SA`).
- Paged tools return `next_offset`; pass it back as `offset`.
## The 18 tools
| Tool | Credits | Leagues | Returns | Used by |
|------|---------|---------|---------|---------|
| `list_teams` | 1 | all | Team ids, abbreviations, conference | game-lookup, team-analysis |
| `search_players` | 1 | all | Player id from a name | player-scouting, player-form, prop-modeling |
| `get_schedule` | 2 | all | Next 1-30 days with the median line | game-preview, odds-explorer |
| `get_standings` | 2 | all | Standings computed from final games | team-analysis |
| `get_injuries` | 2 | nba, wnba | Status, injury type, expected return | game-preview, prop-modeling |
| `get_ratings` | 2 | all | DunkAPI Elo ratings, ranked | team-analysis, game-preview |
| `get_game` | 5 | all | Both box scores, every player line, each book's open and close, win probability range | game-lookup, game-preview |
| `get_player` | 5 | all | Profile, season averages and splits, last 10 games | player-scouting |
| `get_player_games` | 5 | all | A player's game log | player-form, prop-modeling |
| `get_leaders` | 5 | all | Season leaders by stat | player-scouting |
| `get_team_stats` | 5 | all | Pace, ratings, four factors, three-point rate | team-analysis, feature-engineering |
| `get_line_movement` | 10 | all | Snapshots of each book's line before tip-off | odds-explorer |
| `get_plays` | 10 | all | Play-by-play with shot x/y, zone, expected points, win probability | shot-zone-analysis |
| `get_shot_chart` | 10 | nba, wnba | Zone profile with expected points per shot | shot-zone-analysis |
| `get_player_props` | 10 (per game, see below) | nba, wnba, ncaam | Prop lines by book, opening or closing | prop-modeling |
`get_player_props` costs 10 credits for a whole game, or 10 credits per game returned when you pass `player_id` with `from_date` (up to 100 games). Pass `estimate_only: true` first to see the price without being charged.
## Tool selection guide
| You need | Use | Not |
|----------|-----|-----|
| Last night's scores | `list_games` with `date` | `get_game` per game |
| Tonight's games and spread | `get_schedule` | `list_games` (past and scheduled mixed, no line) |
| One game's box score and every book's line | `get_game` | `get_odds` + `get_plays` |
| A day's lines for 3+ games | `get_odds` with `date` (10 flat) | `get_odds` per game (5 each) |
| Player id | `search_players` | guessing from a name |
| Last 10 games for a player | `get_player` (already included) | `get_player_games` (5 more credits) |
| Rolling form over a season | `get_player_games` | `get_plays` |
| Where a player shoots | `get_shot_chart` | `get_plays` and aggregating by hand |
| Individual shots with game context | `get_plays` with `shots_only` | `get_shot_chart` |
| Does a player beat his points line | `get_prop_history` | `get_player_props` per game |
| Today's prop prices across books | `get_player_props` with `game_id` | `get_prop_history` |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_box_score` | `get_game` |
| `get_player_stats` | `get_player` (profile, averages) or `get_player_games` (log) |
| `get_live_odds`, `get_live_scores` | None. Data is historical and next-day; `get_schedule` shows posted pre-game lines |
| `get_lineups`, `get_on_off` | None. Lineup and on/off data are not served |
| `get_player_tracking` | None. No speed, touches or defender-distance data |
| `get_advanced_stats` | `get_team_stats` for team ratings; compute player rates from `get_player_games` |
| `get_head_to_head` | `list_games` with `team`, filter the opponent yourself |
## Credit discipline
- Look up ids once (1 credit each) and reuse them.
- Prefer the date form of `get_odds` over per-game calls once you need 3 or more games.
- `get_plays` and `get_shot_chart` are 10 credits each. Ask for a zone, a period, `clutch` or `shots_only` instead of pulling everything.
- Never loop `get_line_movement` over a slate without telling the user the cost (10 per game).
- A backtest over a full season of lines is thousands of credits. Sample first, then scale.
## Error handling
- Required fields are `game_id` (`get_game`, `get_line_movement`), `player_id` (`get_player`, `get_player_games`, `get_prop_history`), `name` (`search_players`), `stat` (`get_leaders`) and `market` (`get_prop_history`). Fix a rejected call, do not retry it blindly.
- An empty list usually means the season, league or date has no data, not that the call failed. Check `season` (ends-in-year convention) and `league` first.
- In `get_odds`, `count` is the number of book rows returned, not the number of games. Count `len(data["games"])`.
- Opening and closing fields can be `null` for one book while others have values.
## Bring your own data
If the user supplies a CSV or JSON, skip the API and work with the file. State which columns you need (`date`, `home`, `away`, `home_score`, `away_score` at minimum for team work) and verify dates parse before any modeling.
## What to Do Next
| Situation | Skill |
|-----------|-------|
| Don't know which skill to load | `dispatch` |
| Want games or scores | `game-lookup` |
| Want team strength | `team-analysis` |
| Want lines and prices | `odds-explorer` |
feature-engineering9.39 KB
---
name: feature-engineering
description: "Builds leak-free features for basketball prediction models from DunkAPI data: rolling team margin and efficiency, Elo rebuilt game by game, rest days and back-to-backs, home court, opponent-adjusted form, market features and player minutes. Use when the user wants to build a model of game outcomes, margins, totals or player stats, or asks what features to use or whether a feature leaks. Do not use for choosing a validation scheme, see walk-forward-validation. Do not use for calibrating output probabilities, see probability-calibration. Do not use for descriptive team or player summaries, see team-analysis or player-form."
metadata:
version: 1.0.0
author: DunkAPI
---
# Feature Engineering
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> Cheapest feature source: `list_games` (2 credits per page of up to 50 games). Box-score features need `get_game` (5 credits per game) or `get_player_games` (5 per player-season).
A basketball model fails in one of two ways: the features contain the future, or they contain nothing the market does not already know. This skill is about the first problem and honest about the second.
## When to Use
- "What features should I use to predict NBA margins?"
- "Build me rolling team ratings that don't leak"
- "Is using `get_team_stats` for a backtest okay?" (usually no, see below)
- Setting up a training table for game, total or player-stat models
## When NOT to Use
- Splitting data for validation, see `walk-forward-validation`
- Making probabilities trustworthy, see `probability-calibration`
- Describing a team or player without building a model, see `team-analysis`, `player-form`
## Commands Available
| Command | What it gives a feature table | Credits |
|---------|-------------------------------|---------|
| `list_games` | Date, teams, scores, overtime, season type, neutral site: enough for margin, rest, Elo | 2 per page |
| `get_game` | Team box scores (possessions, four factors) and each book's open and close | 5 per game |
| `get_player_games` | Player-level minutes and lines for availability and role features | 5 per player-season |
| `get_odds` | Opening and closing lines per book; `date` returns a whole day for 10 | 5 / 10 |
| `get_injuries` | Current availability only (not a historical archive by default) | 2 |
| `get_team_stats`, `get_ratings`, `get_standings` | Season-level summaries: do not use as pre-game features for past games | 5 / 2 / 2 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_features`, `get_training_data` | Build from `list_games` and `get_game` |
| `get_historical_ratings` (as of a past date) | Rebuild Elo from `list_games` (below) |
| `get_historical_injuries` by game | `get_injuries` accepts a `date`, one report per call; archive it going forward |
| `get_lineups`, `get_tracking` | Not served |
## The Rule: As-Of Features Only
A feature for game N may use only information available before tip-off of game N.
| Source | Safe for past games? | Why |
|--------|---------------------|-----|
| `list_games` rolled with `shift(1)` | Yes | Built from earlier games only |
| `get_team_stats {"season": 2025}` for a 2024-25 game | No | Whole-season average includes the game and every game after it |
| `get_ratings` end-of-season Elo | No | Includes later games; also regresses at season boundaries |
| `get_standings` | No | Final standings |
| `get_player` season averages | No | Same as team stats |
| Closing line | At tip only | Use opening for earlier decisions |
| Opening line (`*_open`) | Yes if you act at open | Check `captured_at` |
If the model scores well with season-level inputs and badly with as-of ones, the first score was leakage.
## How It Works
**Step 1: Build the game table.** Page `list_games` for the seasons you want (oldest first after sorting). Keep `date`, home and away ids, scores, `overtime_periods`, `season_type`, `neutral_site`. Drop preseason; decide on play-in and postseason separately.
**Step 2: Target.** Pick one and keep it fixed:
| Target | Definition | Use |
|--------|-----------|-----|
| Margin | `home_score - away_score` | Spread model; regress, then convert to a cover probability |
| Home win | `margin > 0` | Moneyline model |
| Total | `home_score + away_score` | Totals model |
| Cover | `margin + home_spread_close > 0` against the median close (`home_covered` on `get_odds`) | Betting-specific |
**Step 3: Rest and schedule (cheap, strong).**
```python
g = g.sort_values("date")
for side in ("home", "away"):
# long format: one row per team-game, then merge back
...
t["rest_days"] = t.groupby("team").date.diff().dt.days.clip(upper=7) # NaN for first game
t["back_to_back"] = (t.rest_days == 1)
t["games_last_7"] = t.groupby("team").rolling("7D", on="date").date.count().values - 1
```
**Step 4: Rolling form with `shift(1)`.**
```python
t["margin"] = t.pts_for - t.pts_against
t["roll_margin_10"] = t.groupby("team").margin.transform(lambda s: s.shift(1).rolling(10, min_periods=5).mean())
t["roll_margin_30"] = t.groupby("team").margin.transform(lambda s: s.shift(1).rolling(30, min_periods=10).mean())
t["ewm_margin"] = t.groupby("team").margin.transform(lambda s: s.shift(1).ewm(halflife=8).mean())
```
Windows: short for form, long for talent. Carry a season-boundary decision (reset, or blend toward the prior season).
**Step 5: Elo, rebuilt as-of.** The API publishes one Elo per team per date range; for a backtest, rebuild it game by game so each pre-game rating is clean:
```python
K, HFA = 20, 70 # starting values, tune on a training window
def expected(rh, ra): return 1 / (1 + 10 ** (-(rh - ra + HFA) / 400))
def mov_mult(margin, dr): return ((abs(margin) + 3) ** 0.8) / (7.5 + 0.006 * dr)
ratings = defaultdict(lambda: 1500.0)
for g in games_sorted:
pre_h, pre_a = ratings[g.home], ratings[g.away]
g.pre_elo_diff = pre_h - pre_a # the feature
e = expected(pre_h, pre_a); s = 1.0 if g.margin > 0 else 0.0
winner_dr = (pre_h - pre_a) if g.margin > 0 else (pre_a - pre_h) # winner's rating edge
delta = K * mov_mult(g.margin, winner_dr) * (s - e)
ratings[g.home] += delta; ratings[g.away] -= delta
# at each season start: ratings[t] = 1500 + 0.75 * (ratings[t] - 1500)
```
The API's note describes its Elo as including home advantage, a margin-of-victory multiplier and a quarter regression to the mean each season, so this mirrors it. Your constants will not match its exactly; compare your rebuilt end-of-season values with `get_ratings` for a sanity check (rank correlation, not equality).
**Step 6: Efficiency features.** If you can afford `get_game` per game (5 credits each, about 1,230 games per regular season), compute possessions `fga - oreb + tov + 0.44 * fta`, rolling offensive and defensive rating, `efg_pct`, and rebound and turnover rates with `shift(1)` rolling. A cheaper partial route is rolling points for and against from `list_games` alone.
**Step 7: Availability and role.** Roster certainty is the biggest missing signal. Historical injury state per game is not in a ready table; for a live model, archive `get_injuries` daily. For a past season, proxy availability by the share of a team's top-5-minute players who logged minutes in the game (needs `get_game`).
**Step 8: Market features.** Opening spread, total and moneyline probability are strong features and are only "free" if your model's job is not simply to rediscover them. Model the residual (`margin - (-closing_spread)`) to see whether anything beyond the market is left. If a model without market inputs cannot beat the market and one with them is just the market, the honest result is that you have no edge yet.
**Step 9: Player-level features** (props, see `prop-modeling`): rolling minutes, per-36 rates, usage proxy, opponent allowed rate, rest, home/road, all `shift(1)`.
## Leakage Checklist
1. Every rolling feature has `shift(1)` before `rolling`.
2. No season-level API summary for a game inside that season.
3. Train/validation split is chronological (see `walk-forward-validation`).
4. Opening lines only for decisions made before the close.
5. Standardization fitted on the training window only.
6. Opponent features are as-of too; a team's rolling defensive rating comes from the same shifted pipeline.
7. After feature building, correlate each feature with the target; a feature that is suspiciously strong (|r| near 0.5+ on margin from a single column) deserves a leak hunt.
## Sample Size
A regular season is about 1,230 NBA games; the WNBA and women's college have a few hundred to a few thousand games, men's Division I a few thousand. Over 60 features on one NBA season will overfit. Start with 5 to 10 (Elo difference, rest difference, back-to-back flags, rolling margin 10 and 30, home) and add only what improves out-of-sample log loss.
## Credit Usage
| Operation | Credits |
|-----------|---------|
| One NBA season of game rows (`list_games`, ~25 pages of 50) | ~50 |
| Box scores for every game in a season | ~6,150 |
| Opening and closing lines for a season by date | ~1,700 (about 165 game days at 10 credits) |
| A 500-credit free month | One season of game rows, then a sampled slice of box scores |
The date counts are estimates; count game days from your own `list_games` pull.
## What to Do Next
| Built | Next | Skill |
|-------|------|-------|
| Feature table | Validate chronologically | `walk-forward-validation` |
| Probabilities from the model | Check calibration | `probability-calibration` |
game-lookup6.55 KB
---
name: game-lookup
description: "Finds basketball games and reads results: scores by date, team, range or season, upcoming schedule, and a single game's full box score. Use when the user asks who played, what the score was, who plays tonight, a team's recent results, or wants a game id for another call. Covers NBA, WNBA, men's and women's college. Do not use for odds comparison, see odds-explorer. Do not use for team strength, see team-analysis. Do not use for a pre-game breakdown, see game-preview."
metadata:
version: 1.0.0
author: DunkAPI
---
# Game Lookup
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `list_games` (2 credits) for results, `get_schedule` (2) for upcoming games, `get_game` (5) for one game in full.
You find the right game fast and return it cleanly. Most other skills need a `game_id` first; this one produces it.
## When to Use
- "What was the score of the Knicks game?" / "Last night's NBA results"
- "Who plays tonight?" / "Celtics schedule this week"
- "Show me the box score for that game"
- "Every Lakers road game this season"
- Any request that needs a `game_id` before another skill can run
## When NOT to Use
- Comparing sportsbooks or reading line moves, see `odds-explorer`
- Ranking teams or reading efficiency, see `team-analysis`
- A full matchup breakdown with injuries and ratings, see `game-preview`
- Shot-level detail from a game, see `shot-zone-analysis`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `list_teams` | Team ids and abbreviations for a season | 1 |
| `list_games` | Games with scores by season, date, range or team, newest first | 2 |
| `get_schedule` | Next 1-30 days with the median spread, total and moneylines | 2 |
| `get_game` | One game: team box scores, every player line, each book's open and close, win-probability range | 5 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_box_score` | `get_game` |
| `get_scoreboard` | `list_games` with `date` (finals) or `get_schedule` (upcoming) |
| `get_live_scores` | None. Data is historical and next-day, not live |
| `get_head_to_head` | `list_games` with `team`, then filter the opponent in code |
## Season Resolution
- `season` is the year the season ends: 2025-26 is `2026`, 2026-27 is `2027`.
- Oct 20, 2026 is the NBA opener. Before it, `get_schedule` returns `preseason` games tagged `season: 2027`.
- WNBA is the calendar year. `season_type` values: `regular`, `postseason`, `play_in`, `preseason`.
- Dates are US Eastern. A 10:30 pm ET tip can have a UTC `starts_at` on the next day; filter by the `date` field, not `starts_at`.
## How It Works
**Step 1: Resolve the team.** If the user says "the Knicks" and you do not have the abbreviation, call `list_teams` (1 credit). The API accepts an id or an abbreviation (`NY`, `BOS`, `GS`, `SA`). Do not guess; `GS` and `NY` are the codes here, not `GSW` or `NYK`.
**Step 2: Pick the listing call.**
| Question | Call |
|----------|------|
| Last night's scores | `list_games` with `date` set to yesterday |
| A team's recent games | `list_games` with `team` and `limit` |
| A playoff series | `list_games` with `team`, `season`, `season_type: "postseason"` |
| A date range | `list_games` with `from` and `to` |
| Tonight and the week | `get_schedule` with `days` and optionally `team` |
**Step 3: Get the box score only when needed.** `list_games` already has the score, venue and overtime count. Call `get_game` (5 credits) for player lines, team box scores, books' lines or the win-probability range.
**Step 4: Present it.** Lead with the answer (score, winner, margin), then the detail.
### Live example
`list_games {"team": "NY", "season": 2026, "limit": 3}` returned the latest postseason games (NY vs SA, June 2026), newest first. Trimmed:
```json
{"data": {"games": [
{"game_id": "401859967", "season": 2026, "season_type": "postseason",
"date": "2026-06-13", "status": "final",
"home": {"abbrev": "SA", "score": 90}, "away": {"abbrev": "NY", "score": 94},
"periods": 4, "overtime_periods": 0, "venue": "Frost Bank Center"}
], "count": 3, "next_offset": 3}}
```
Pass `next_offset` back as `offset` for the next page. Paging past 50 games (the default `limit`) costs 2 credits per page.
### What `get_game` returns
- `game`: score, venue, attendance, periods
- `team_stats`: per team `pts fgm fga tpm tpa ftm fta oreb dreb reb ast stl blk tov pf fast_break_pts paint_pts points_off_turnovers largest_lead`
- `players`: per player `minutes pts fgm fga tpm tpa ftm fta oreb dreb reb ast stl blk tov pf plus_minus`, plus `starter` and `did_not_play`
- `odds`: per book opening and closing moneyline, spread and total
- `win_probability`: `home_min`, `home_max`, `plays_with_probability`
## Reading a box score
- **Possessions** ~ `fga - oreb + tov + 0.44 * fta` (same formula the API uses for ratings). Points per possession is a better efficiency read than raw points.
- **True shooting** = `pts / (2 * (fga + 0.44 * fta))`.
- **Effective FG%** = `(fgm + 0.5 * tpm) / fga`.
- Compare `paint_pts`, `fast_break_pts` and `points_off_turnovers` to see how a team scored, not just how much.
- `plus_minus` in a single game is noisy. Do not rank players on it.
- A very wide `home_min` to `home_max` win-probability range means the lead changed hands; a narrow band means a wire-to-wire game.
## Gotchas
| Gotcha | Handling |
|--------|----------|
| `list_games` is newest first | Reverse before computing rolling stats |
| Postseason, play-in and preseason are separate | Filter `season_type` or you mix preseason with real games |
| Scheduled games have `score: null` | Do not treat null as zero |
| Overtime games | Check `overtime_periods`; per-game totals and pace need adjusting |
| A game with no line | `line.books` can be 1 or 0 on preseason games; do not call it "the market" |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Look up a team abbreviation | 1 |
| One page of results (up to 50 games) | 2 |
| Tonight's slate | 2 |
| One full box score | 5 |
| Every box score for a 6-game series | 30 |
## Output Format
```
New York Knicks 94, San Antonio Spurs 90 (Final, Jun 13 2026)
Frost Bank Center | Postseason | game_id 401859967
Top lines: [player] [pts], [fgm]-[fga] FG, [tpm]-[tpa] 3P, [ftm]-[fta] FT
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| A game to analyze before tip | Full preview | `game-preview` |
| A game with lines | Compare books | `odds-explorer` |
| Shot detail | Zone profile for the game | `shot-zone-analysis` |
| A player's big night | Context against his average | `player-form` |
game-preview7.28 KB
---
name: game-preview
description: "Builds a full pre-game basketball preview: teams' Elo and efficiency, recent form, rest and back-to-backs, injuries, the market line across books, and a simple projection with its uncertainty. Use when the user asks to preview or break down tonight's game or the season opener, or wants everything needed before tip in one place. Works for the new season before it has games (falls back to last season's numbers and says so). Covers NBA, WNBA, men's and women's college. Do not use for results after the fact, see game-lookup. Do not use for devigging, see odds-analysis."
metadata:
version: 1.0.0
author: DunkAPI
---
# Game Preview
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> A full preview costs about 20 credits: `get_schedule` (2), `get_ratings` (2), `get_team_stats` (5), `get_injuries` (2), `get_odds` (5), and two `list_games` calls (2 each) for form and rest.
A compound skill: it calls several tools in a fixed order and assembles one page. The point is a number you can defend and the list of things that would change it.
## When to Use
- "Preview tonight's Knicks game" / "Break down the opener"
- "What do I need to know before Celtics-Heat?"
## When NOT to Use
- Final scores and box scores, see `game-lookup`
- Only prices and movement, see `odds-explorer`
- Only team strength, see `team-analysis`
## Commands Available
| Command | Use in a preview | Credits |
|---------|------------------|---------|
| `get_schedule` | Find the `game_id`, tip time, venue and the median line | 2 |
| `get_ratings` | Elo for both teams | 2 |
| `get_team_stats` | Pace, ratings and four factors (all teams in one call, or one with `team`) | 5 |
| `get_injuries` | Who is out or day-to-day (NBA and WNBA only) | 2 |
| `list_games` | Each team's last 10 results and the dates for rest | 2 each |
| `get_odds` | Every book's line for the game | 5 |
| `get_line_movement` | Optional: how the line has moved | 10 |
| `get_player` | Optional: a key player's last 10 games | 5 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_game_preview`, `get_matchup` | This skill assembles it |
| `get_lineups`, `get_starting_five` | Not served. Infer likely starters from the last box score via `get_game` |
| `get_referee_assignments` | Not served |
| `get_head_to_head` | `list_games` with `team`, filter the opponent |
## Season Resolution (important for the opener)
The 2026-27 NBA regular season opens Oct 20, 2026. Before that:
- `get_schedule` shows preseason games with `season: 2027` and often only one book's spread and total.
- `get_ratings` and `get_team_stats` for `season: 2027` have little or nothing. Use `season: 2026` and label every number "2025-26". Elo regresses a quarter of the way toward 1500 between seasons per the API's method note, so last season's end-of-year rating overstates how far apart teams are.
- Preseason box scores are a poor guide to rotations: starters play limited minutes. Do not fold preseason results into form.
- After about 15 games per team, switch to `season: 2027` and drop the carryover caveat.
## How It Works
**Step 1: Find the game.** `get_schedule {"days": 3, "team": "NY"}`. Take `game_id`, `home.abbrev`, `away.abbrev`, `starts_at` and `line` (`home_spread`, `total`, `home_ml`, `books`).
**Step 2: Strength.** `get_ratings {"season": 2026}` gives Elo for every team. The rating gap `d = rHome - rAway + home_edge`. Home edge is not published as a parameter; use about 60 to 100 points and say it is an assumption. Win probability `= 1 / (1 + 10^(-d/400))`. The API's own anchor: a 100-point gap is about a 64% favourite on a neutral floor.
**Step 3: Efficiency.** `get_team_stats {"season": 2026}` (all teams, 5 credits). Pull `pace`, `off_rating`, `def_rating`, `net_rating`, `efg_pct`, `tov_pct`, `oreb_pct`, `ft_rate`, `three_rate` for both teams. Projected margin (home minus away, neutral):
```
home_pts = (home.off_rating + away.def_rating) / 2 * pace_game / 100
away_pts = (away.off_rating + home.def_rating) / 2 * pace_game / 100
pace_game = (home.pace + away.pace) / 2
```
Add the home edge in points (about 2 to 3). This is a prior from last season's averages; it knows nothing about roster changes.
**Step 4: Form and rest.** `list_games {"team": "NY", "limit": 10}` for each side. Compute from the `date` field: days since the previous game, whether this is the second night of a back-to-back, games in the last 7 days, and the last-10 margin. Exclude preseason from form (filter `season_type`).
**Step 5: Availability.** `get_injuries {"team": "NY"}` returns `name position status type detail return_date`. `Day-To-Day` is not `Out`. Quote the `detail` text when it names a decision (rest, coach's decision). If a top-three rotation player is out, the efficiency numbers in step 3 overstate the team; say by how much you cannot know, and widen the range rather than invent an adjustment.
**Step 6: Market.** `get_odds {"game_id": "..."}`. Take the median spread and total, the range across books, and the best prices (see `odds-explorer`). Convert the market to a probability with `odds-analysis`.
**Step 7: Compare and conclude.** Your projected margin versus the median spread, and your Elo-based win probability versus the devigged market probability. A gap of 1 to 2 points on the spread is inside the error of this method; do not call it an edge.
## Uncertainty
A single NBA game has a standard deviation of roughly 12 points around the projected margin; a 3-point projected edge is therefore only about a 60% proposition even if the projection were exact. Say "lean", not "pick", and show the numbers.
## Output Format
```
[Away] @ [Home], [date, time ET] | [venue]
Market: [Home] [spread], total [x] ([n] books, range [lo] to [hi]) | ML [h]/[a]
Strength (2025-26 carryover): Elo [home] [r] vs [away] [r] -> home [p]%
Efficiency: [Home] off [x] def [x] pace [x] | [Away] off [x] def [x] pace [x]
Projection: [Home] by [m] (range +/- 12), total [t] vs market [x]
Form: [Home] [W-L last 10], [rest]; [Away] [W-L last 10], [rest]
Availability: [Out/Day-To-Day with detail]
What would change this: [injury news, rest decisions, line move > 1 point]
Verdict: [lean or no lean, one sentence]
```
## Worked Example (shape only)
On Oct 11, 2026 the real schedule showed `get_schedule {"days": 7}` returning 34 games, all `preseason`, each with `line.books: 1`, for example CLE (home) vs ORL with `home_spread: -5.5, total: 226.5` and `home_ml: null`. A preview of that game would say: single-book line, no moneyline, preseason; rely on 2025-26 Elo (CLE 1602.9 against ORL 1564.8, a 38-point gap before home edge) and efficiency, and treat the market as indicative only. At 38 points plus an assumed 70 home edge, `d = 108`, which is about a 65% home win probability by the formula above, consistent with a 5.5-point favourite. Preseason rest decisions dominate; check `get_injuries` for "Coach's Decision" entries before saying more.
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Core preview (steps 1-6) | ~20 |
| Add line movement | +10 |
| Add one player profile | +5 |
| Preview a 10-game slate | ~120 (share the all-teams calls across games) |
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| A player's role is in question | His recent minutes | `player-form` |
| Wants a prop angle | Prop history | `prop-modeling` |
odds-analysis6.73 KB
---
name: odds-analysis
description: "Odds math for basketball betting markets: American, decimal and implied probability conversion, vig, and four devigging methods (multiplicative, additive/Shin, power) applied to moneylines, spreads, totals and player props, plus building a consensus no-vig line across sportsbooks. Use when the user asks what a price implies, how to remove the vig, what the true probability is, or what a fair line is. Do not use for finding the best price or line movement, see odds-explorer."
metadata:
version: 1.0.0
author: DunkAPI
---
# Odds Analysis
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_odds` (5 credits per game) for moneylines, spreads and totals with prices. `get_player_props` (10) for prop prices. The math itself is local.
Prices are not probabilities. A book's two sides add to more than 100%; the excess is its margin. Removing it gives the market's own view, which is the benchmark every edge claim has to beat.
## When to Use
- "What probability does -195 imply?"
- "Devig this moneyline: -200 / +165"
- "What is the fair total if over is -108 and under is -112?"
- "What's the no-vig line across all books?"
## When NOT to Use
- Hunting the best number or reading line moves, see `odds-explorer`
## Commands Available
| Command | Role | Credits |
|---------|------|---------|
| `get_odds` | Per-book prices and closing implied probabilities | 5 per game, 10 per date |
| `get_player_props` | Over and under prices for props | 10 |
| `get_schedule` | Median line and `home_implied_prob` on upcoming games | 2 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_no_vig_odds`, `get_fair_odds` | Compute locally with the methods below |
| `get_true_probability` | Same |
## Conversions
```
American to implied probability:
odds > 0: p = 100 / (odds + 100)
odds < 0: p = -odds / (-odds + 100)
Decimal odds = 1 / p (American +165 = 2.65; -195 = 1.513)
American from p:
p < 0.5: odds = +100 * (1 - p) / p
p >= 0.5: odds = -100 * p / (1 - p)
Payout per 1 unit staked at American odds o:
o > 0: o/100 o < 0: 100/(-o)
```
The API returns `home_close_prob` and `away_close_prob` on `get_odds` rows: those are the raw implied probabilities of the closing moneyline (they sum above 1). Treat them as inputs to devigging, not as the answer.
## Vig
`overround = pA + pB - 1`. A -110/-110 spread is 4.76%. Sharper books run lower. In the live row set below, raw sums ranged from 1.019 (ESPN BET) to 1.044 (BetMGM) on one game's moneyline.
## Devigging Methods
Given two raw probabilities `a` and `b` with `a + b = S > 1`:
| Method | Formula | Behaviour |
|--------|---------|-----------|
| Multiplicative (proportional) | `a/S`, `b/S` | Simple default. Spreads the margin proportionally |
| Additive | `a - (S-1)/2`, `b - (S-1)/2` | Equal cut from each side. Can push a longshot too low, so check it stays positive |
| Shin | Solves for an insider-trading fraction `z` | Shifts more of the margin onto longshots. For a two-way market it equals the additive result |
| Power | Find `k` so `a^k + b^k = 1`; use `a^k`, `b^k` | Takes relatively more off the longshot, which matches the favourite-longshot bias often seen in betting markets |
### Worked example with live numbers
`get_odds {"game_id": "401859967"}` returned Pinnacle's closing moneyline as SA (home) -195, NY (away) +173. Raw: `0.6610 + 0.3663 = 1.0273` (2.73% margin).
| Method | Home (SA) | Away (NY) |
|--------|----------:|----------:|
| Raw | 0.6610 | 0.3663 |
| Multiplicative | 0.6434 | 0.3566 |
| Additive / Shin | 0.6474 | 0.3526 |
| Power (k = 1.043) | 0.6493 | 0.3507 |
The methods differ by less than 1 point on a favourite priced near 65%, so for NBA sides the choice rarely changes a decision. The spread widens as the favourite gets shorter (-600 or more), where power and Shin cut the longshot harder. Pick one and keep it for the whole analysis; mixing methods inside one backtest adds noise that looks like signal.
Spread and total pairs are devigged the same way. DraftKings' total in that game was over -108 / under -112: raw `0.5192 + 0.5283 = 1.0475`, multiplicative fair over probability `0.4957`.
## Consensus No-Vig Line
For a game with many books:
1. Devig each book's own two-way market (same method).
2. Take the median across books. The median ignores an outlier book's stale price.
3. Optionally weight a sharp reference (Pinnacle appears in the live rows) heavier than a retail book such as a recreational app.
4. Report the range as well: the width of disagreement across books is part of the answer.
For the eight books on that game, multiplicative home probabilities ran from 0.631 to 0.651. Pinnacle at 0.643 sat in the middle of that range; the highest was Bovada at 0.651 and the lowest DraftKings at 0.631.
## Converting Spread to Win Probability
When a game has no moneyline (common in preseason, where one book may post only a spread and total), use the spread. An NBA spread of `s` points corresponds roughly to `Phi(-s / sigma)` for the favourite, with `sigma` near 12. A 5.5-point favourite: `Phi(5.5/12) = Phi(0.458) = 0.677`, a rough guide; the moneyline is better when you have it. When both a spread and a moneyline exist, compare the two; a large gap means a stale line or the wrong sigma.
## Props
A prop has a line and two prices. Devig the pair: with over -125 and under -105 (BetMGM's closing points prop in a call for Jalen Brunson on 2025-10-22), raw `0.5556 + 0.5122 = 1.0678`, fair over probability `0.5203`. Note the line also differs by book (23.5 or 24.5 for the same player that night), so compare prices only at the same line. See `prop-modeling`.
## Pitfalls
| Pitfall | Handling |
|---------|----------|
| Treating raw implied probability as true | Always devig |
| Mixing opening and closing prices | Keep one snapshot type per analysis |
| Using one retail book | Use the median or a sharp reference |
| Different lines treated as the same market | Compare only identical lines (or adjust for the line) |
| Three-way markets | Basketball has none; do not apply soccer logic |
| Tiny overrounds | A raw sum under 1 means a stale or mistaken row; discard |
| Null prices | `home_spread_open` and similar fields can be `null`; skip those rows |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| One game, all books | 5 |
| A day, all games | 10 |
| One game's player props | 10 |
| The conversions themselves | 0 |
## Output Format
```
[Game] closing moneyline, [method]
[Book]: [home odds]/[away odds] -> raw [x]+[y] = [S] -> fair [h]% / [a]%
Consensus (median of [n] books): home [x]% (range [lo]% to [hi]%)
Fair price: home [odds], away [odds]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Need the best number to bet | Line shop | `odds-explorer` |
odds-explorer8.21 KB
---
name: odds-explorer
description: "Multi-book basketball odds comparison: best moneyline, spread and total across sportsbooks, vig per book, opening versus closing lines, and pre-tip line movement. Use when the user asks for current or historical odds, the best price, which book is off the market, how a line moved, or whether the home side covered. Covers NBA, WNBA, men's and women's college. Do not use for devigging or probability math, see odds-analysis. Do not use for player props, see prop-modeling."
metadata:
version: 1.0.0
author: DunkAPI
---
# Odds Explorer
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_schedule` (2 credits) for the median line on upcoming games, `get_odds` (5 per game, 10 per date) for every book, `get_line_movement` (10 per game) for snapshots before tip-off.
You find the best available price, show what each book charges, and read line movement without overstating what snapshots can prove.
## When to Use
- "Best spread on Knicks-Spurs?" / "Which book has the highest total?"
- "What were the closing lines for last night's slate?"
- "Did the home team cover?" / "Did it go over?"
- "How did the line move before tip?"
- Reading the market before a game preview or a bet
## When NOT to Use
- Converting prices to true probabilities, see `odds-analysis`
- Player prop lines, see `prop-modeling`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `get_schedule` | Upcoming games with the median spread, total and moneylines across posted books | 2 |
| `get_odds` | Every book's moneyline, spread and total, opening and closing, with results against the median close. `game_id` for one game, `date` for a day | 5 per game, 10 per date |
| `get_line_movement` | Snapshots of each book's moneyline, spread and total before tip-off. Optional `provider` | 10 |
| `get_game` | Box score plus each book's open and close (use when you want both) | 5 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_live_odds` | None. Lines are pre-game; `get_schedule` shows what is posted |
| `get_best_book` | Compute from `get_odds` rows |
| `get_public_betting` | Not served. Do not infer public percentages |
| `get_closing_line` | Closing fields (`*_close`) on `get_odds` rows |
| `get_alt_lines` | Not served; one spread and one total per book |
## Coverage and What a Line Is
- Per-book moneyline, spread and total with opening and closing values. Opening and closing per book come from The Odds API from July 2020 and from ESPN from 2023-24; before that, only ESPN's final line. Check https://dunkapi.com/docs/atlas for coverage by season and league before promising history.
- Women's college lines start November 2024, with fewer books.
- Pre-game snapshots: ESPN's once a day, The Odds API's every hour for games in the next two days. That sets the resolution of `get_line_movement`: you can see a half-point move across hours or days, not a 90-second steam move.
- Books appear under their display names in `get_odds` and `get_line_movement` (`DraftKings`, `FanDuel`, `ESPN BET`, `Pinnacle`, `BetMGM`, `Bovada`, `BetOnline`, `MyBookie` in the live call below). Prop calls use lowercase keys (`draftkings`); do not mix them.
- Preseason games can have one book and no moneyline. Say so; one book is not a market.
## Season Resolution
For games before the Oct 20, 2026 opener, `get_schedule` returns preseason games with `season: 2027`. Use `get_schedule` for lines on future games and `get_odds` for lines on games already played or about to be.
## How It Works
**Step 1: Identify the game.** Use `get_schedule {"days": 3, "team": "NY"}` or `list_games` (see `game-lookup`). Do not pull odds for a guessed id.
**Step 2: Pull every book.** One game: `get_odds {"game_id": "401859967"}`. A whole day, 10 flat: `get_odds {"date": "2026-06-13"}`. For three or more games the date form is cheaper.
**Step 3: Read the columns.** Each `books` row has `provider`, `home_ml_open`, `home_ml_close`, `away_ml_open`, `away_ml_close`, `home_close_prob`, `away_close_prob`, `home_spread_open`, `home_spread_close`, `spread_home_odds`, `spread_away_odds`, `total_open`, `total_close`, `over_odds`, `under_odds`, `captured_at`. The `result` object holds `median_home_spread`, `median_total`, `home_covered` and `over` for finished games.
Note: `home_close_prob` and `away_close_prob` are raw implied probabilities from the closing moneyline, not devigged. They add to more than 1; the excess is the vig.
**Step 4: Best price.** For each side independently, the best price is the highest number (+175 beats +160; -185 beats -210). The best home price and best away price usually sit at different books.
**Step 5: Vig.** For a two-way market with decimal implied probabilities `pH` and `pA`: `vig = pH + pA - 1`. For American odds: `p = 100/(odds+100)` if odds are positive, `p = -odds/(-odds+100)` if negative. Spread and total prices of -110/-110 carry a vig of 4.76% (2 * 0.5238 - 1).
**Step 6: Movement, if asked.** `get_line_movement {"game_id": "401585601", "provider": "DraftKings"}` returns `movement[book]` as a list of `{captured_at, home_ml, away_ml, home_spread, total}`. Compare the first and last snapshot.
### Live example
`get_odds {"game_id": "401859967"}` (NY at SA, 2026-06-13). Closing values, trimmed to the columns that matter:
| Book | Home ML | Away ML | Raw sum | Spread | Total |
|------|--------:|--------:|--------:|-------:|------:|
| BetMGM | -200 | +165 | 1.044 | SA -5.5 | 215.5 |
| BetOnline | -187 | +163 | 1.032 | SA -5.5 | 215.5 |
| Bovada | -210 | +175 | 1.041 | SA -5.5 | 215 |
| DraftKings | -192 | +160 | 1.043 | SA -5.5 | 216.5 |
| ESPN BET | -185 | +170 | 1.019 | SA -5.5 | 216.5 |
| FanDuel | -198 | +166 | 1.040 | SA -5.5 | 215.5 |
| MyBookie | -204 | +171 | 1.040 | SA -5.5 | 215.5 |
| Pinnacle | -195 | +173 | 1.027 | SA -5.5 | 215 |
Best home price -185 (ESPN BET), best away price +175 (Bovada). Eight books agreed on the spread and spread within 1.5 points on the total. `result` came back `home_covered: false, over: false` (NY won 94-90 as a 5.5-point underdog; 184 combined points against a 215.5 total). The lowest raw sum, ESPN BET at 1.019, is the cheapest to bet; the highest, BetMGM at 1.044, the most expensive.
## Reading Movement
| Observation | Cautious read |
|-------------|---------------|
| Spread moves 1 point or more across all books | Real information or a large position taken; look for an injury in `get_injuries` |
| One book out of line, rest agree | Stale line or a book managing its own exposure; the others are the market |
| Moneyline moves, spread unchanged | Price adjustment around a key number |
| Total drops 2+ points | Pace or availability news; check injuries and rest |
| Line returns to open | Noise; no signal |
Without bet-count or handle data, "sharp money" and "reverse line movement" cannot be proven from lines alone. Label them hypotheses.
## Anti-patterns
| Rationalization | Why it is wrong | Do this instead |
|-----------------|-----------------|-----------------|
| "Line moved toward the favorite, so favorites are right" | Direction alone says nothing about who is correct | Report size, timing and what else changed |
| "Use the first book that answers" | Differences of 5-10 cents add up over a season | Show the range across books |
| "Pinnacle is always right" | A reference, not an oracle; check availability and limits | Use it as a benchmark line |
| "Open to close tells the whole story" | Snapshots miss intra-day swings | State the snapshot frequency |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Tonight's median lines | 2 |
| One game, every book | 5 |
| A whole day of lines | 10 |
| One game's movement | 10 |
| Movement for a 10-game slate | 100; warn the user first |
## Output Format
```
[Away] @ [Home], [date] (n books)
Moneyline: [Away] [best] @ [book] | [Home] [best] @ [book]
Spread: [Home] [x] (range [lo] to [hi]) | Total [x] (range [lo] to [hi])
Cheapest to bet: [book] ([vig]%) | Most expensive: [book] ([vig]%)
Result (if final): home covered [yes/no], over [yes/no]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Best prices identified | Remove vig to get true probability | `odds-analysis` |
| Large move on one game | Check availability and form | `game-preview` |
player-form7.92 KB
---
name: player-form
description: "Reads a basketball player's recent form from his game log: rolling averages, minutes trend, per-minute rates, rest effects, home and road splits and opponent context, with proper shrinkage so a hot week is not mistaken for a new talent level. Use when the user asks if a player is hot, how his minutes have changed, what his last 10 games say, or needs rolling inputs for a projection. Covers NBA, WNBA, men's and women's college. Do not use for season profile and comparisons, see player-scouting. Do not use for shot location, see shot-zone-analysis. Do not use for prop lines, see prop-modeling."
metadata:
version: 1.0.0
author: DunkAPI
---
# Player Form
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_player_games` (5 credits for a season's log, paged by `limit` and `offset`). `get_player` (5) already contains the last 10 games if that is all you need.
Recent form is mostly minutes and role, a little shooting luck, and very little "heat". This skill separates them.
## When to Use
- "Is Tatum hot?" / "What has his minutes done since the trade?"
- "Give me his last 10 and last 20"
- Rolling inputs for a points, rebounds or assists projection
- Checking whether a big night was role-driven or shooting-driven
- Home/road, rest and opponent splits
## When NOT to Use
- Season averages and cross-player comparisons, see `player-scouting`
- Where the shots came from, see `shot-zone-analysis`
- Turning a projection into a bet against a line, see `prop-modeling`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `search_players` | Player id from a name | 1 |
| `get_player_games` | Full game log, newest first: opponent, home, result, minutes, box score line. Filters `season`, `season_type`, `limit`, `offset` | 5 |
| `get_player` | Profile, season rows, last 10 games | 5 |
| `list_games` | Team schedule dates, for rest and team context | 2 |
| `get_game` | One game's full lines (when you need teammates' minutes) | 5 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_player_stats` | `get_player_games` or `get_player` |
| `get_hot_players`, `get_trending` | Compute from the game log |
| `get_player_splits` | Group the log yourself |
| `get_minutes_projection`, `get_lineups` | Not served; model minutes from the log |
| `get_usage_rate` | Not served; use `fga + 0.44 * fta + tov` per minute as a usage proxy |
## Season Resolution
`season` is the year the season ends (2025-26 is `2026`). The default is the latest season. In October of a new season the log holds only preseason games; filter on `season_type` before reading anything into them. In game-log rows `season_type` is returned as a number (3 appeared on postseason games); in `list_games` it is a string. Request the type you want with the string parameter and do not rely on the numeric code for anything else.
## How It Works
**Step 1: Pull the log.** `get_player_games {"player_id": "3934672", "season": 2026, "limit": 100}` returns newest first. Each row: `game_id date season season_type opponent home result starter did_not_play minutes pts fgm fga tpm tpa ftm fta oreb dreb reb ast stl blk tov pf plus_minus`.
```json
{"date": "2026-06-13", "opponent": "SA", "home": false, "result": "W",
"starter": true, "minutes": 41, "pts": 45, "fgm": 14, "fga": 27,
"tpm": 4, "tpa": 7, "ftm": 13, "fta": 15, "reb": 3, "ast": 3, "tov": 3}
```
**Step 2: Clean.** Reverse to oldest-first. Drop `did_not_play` rows from rate stats but keep them as a count (availability is itself information). Mark blowouts if you have the score (from `list_games`): minutes in a 25-point game are not minutes in a close one.
**Step 3: Minutes first.** Compute a rolling mean of `minutes` over 5 and 10 games, and the share of games under 30. Most "hot" and "cold" stretches are minutes moving.
**Step 4: Rates.** Per-36 and per-minute rates are steadier than counts:
```python
import pandas as pd
df = pd.DataFrame(rows).sort_values("date") # oldest first
df = df[~df.did_not_play]
df["pts_per_min"] = df.pts / df.minutes
roll = df[["minutes","pts","reb","ast","tpm"]].rolling(10, min_periods=5).mean()
# a feature for a model must exclude the current game:
feat = df[["minutes","pts"]].shift(1).rolling(10, min_periods=5).mean()
```
`shift(1)` before rolling is the leakage guard: a prediction for game N may only use games before N.
**Step 5: Shrink.** A 10-game points average has a standard error of about `sd / sqrt(10)`. With a per-game sd near 7 for a star scorer, that is about 2.2 points. Blend toward the longer baseline:
```
projection ~ w * rolling10 + (1 - w) * season_mean, w ~ 0.3 to 0.5
```
Start at 0.3 for scoring and 0.4 for minutes (minutes are stickier). These are starting values; fit `w` on a walk-forward split (see `walk-forward-validation`) instead of trusting them.
**Step 6: Context splits.** Group by `home`, by rest days (difference in `date` between rows), by `opponent`. Splits need 15+ games per bucket to say anything; below that, report the number and call it unproven.
**Step 7: Hot-hand check.** Compare shooting percentage in the last 10 to the season, with attempts. A jump of 8 points of FG% on 130 attempts is within normal variation. A jump in `fga` or `minutes` is real; a jump only in `fg_pct` mostly is not.
### Live example
`get_player_games {"player_id": "3934672", "season": 2026, "limit": 3}` returned his last three games of 2025-26 (all postseason): 45, 36 and 32 points in 41, 44 and 35 minutes. Per minute that is 1.10, 0.82 and 0.91 points. Three games say little; the minute loads (35 to 44) are what stand out, and they are a postseason pattern. A regular-season projection from these rows would overstate his minutes. Filter `season_type: "regular"` for a regular-season baseline and keep playoff rows separate.
For a quick scoring view against a market number, `get_prop_history` (10 credits) returns the same player-game results beside the closing line; its first six rows for the same player, from 2025-10-22, had actual points of 23, 31, 37, 36, 29 and 31 (average 31.2) against an average line of 25.8.
## Interpreting the Output
| Pattern | Likely cause | Action |
|---------|--------------|--------|
| Minutes up 4+ for 5 games | Role change, injury to a teammate | Check `get_injuries` for teammates; raise projection |
| Points up, minutes flat, FGA up | Usage up | Believable |
| Points up, minutes and FGA flat | Shooting luck | Shrink hard |
| Minutes down in blowouts | Garbage time | Control for margin |
| Sudden DNP/low minutes after a game | Injury, rest or discipline | Read the injury report before projecting |
| Two short games then a normal one | Early exit then return | Do not average the short games into the rate |
## Gotchas
| Gotcha | Handling |
|--------|----------|
| Log is newest first | Reverse before rolling |
| Preseason games in the early season | Filter by type |
| Traded players | Team context changes; re-baseline after the move |
| Playoff minutes vs regular season | Separate series |
| `limit` default is 100 | Page with `offset` for longer careers |
| College logs | Fewer games; use a smaller window (5) |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Id lookup | 1 |
| One season log (up to 100 rows) | 5 |
| Two seasons | 10 |
| Last 10 only | 5 via `get_player` (also returns season rows) |
## Output Format
```
[Player], [team], last 10 vs season
Minutes: [r10] vs [season] ([trend])
Points: [r10] vs [season] | per-36 [x] vs [x]
FGA: [r10] vs [season] | FG% [r10] vs [season] on [n] attempts
Context: [rest/home/road split if 15+ games per bucket]
Verdict: [role change / shooting variance / injury effect], shrunk projection [x]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| A projection to compare with a line | Hit-rate and calibration | `prop-modeling` |
| Rolling features for a model | Full feature set | `feature-engineering` |
| Efficiency question | Zone profile | `shot-zone-analysis` |
player-scouting6.94 KB
---
name: player-scouting
description: "Looks up and profiles basketball players: finds the player id, reads per-season averages and shooting splits, compares two or more players on a per-minute basis, and pulls season leaderboards. Use when the user asks about a player, who leads a stat, how two players compare, or whether a scoring line is efficient or lucky. Covers NBA, WNBA, men's and women's college. Do not use for rolling or recent form across a season, see player-form. Do not use for shot location, see shot-zone-analysis. Do not use for prop lines, see prop-modeling."
metadata:
version: 1.0.0
author: DunkAPI
---
# Player Scouting
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `search_players` (1 credit) to get the id, `get_player` (5) for profile and averages, `get_leaders` (5) for leaderboards.
You turn a name into a defensible read on a player: usage-adjusted, efficiency-checked, with the sample size stated.
## When to Use
- "Tell me about Jalen Brunson" / "Is he actually efficient?"
- "Compare Tatum and Brown" / "Who is the better rebounder per minute?"
- "Who led the league in true shooting?" / "Top 10 in assists"
- "How did he do in the playoffs vs the regular season?"
- Finding a `player_id` for another skill
## When NOT to Use
- Game-by-game rolling averages or minutes trends, see `player-form`
- Where a player shoots from, see `shot-zone-analysis`
- Whether he will beat a points line, see `prop-modeling`
- A team-level question, see `team-analysis`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `search_players` | Player id from a full or partial name, most recent players first | 1 |
| `get_player` | Profile, per-season averages and shooting splits (regular season and postseason as separate rows), last 10 games | 5 |
| `get_leaders` | Season leaders by `pts reb ast stl blk tpm min plus_minus fg_pct tp_pct ft_pct ts_pct total_pts` | 5 |
| `get_player_games` | Full game log (for form, see `player-form`) | 5 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_player_stats` | `get_player` |
| `get_usage_rate`, `get_per`, `get_bpm`, `get_vorp` | Not served. Compute per-minute rates and true shooting from `get_player` fields |
| `get_player_tracking` | Not served; no speed, touches or contested-shot data |
| `compare_players` | Call `get_player` for each, compare in code |
| `get_draft_prospects`, `get_contract` | Not served |
## Season Resolution
`season` is the year the season ends (2025-26 is `2026`). The WNBA uses the calendar year. `search_players` returns `last_season`; a stale value means the player is retired or has not played recently.
## How It Works
**Step 1: Find the id.** `search_players {"name": "brunson", "limit": 3}` returned two people:
```json
{"data": {"players": [
{"player_id": "3934672", "name": "Jalen Brunson", "position": "G", "team": "NY", "last_season": 2027},
{"player_id": "106", "name": "Rick Brunson", "position": "PG", "team": "PHI", "last_season": 2007}
]}}
```
Match on `last_season` and `team` before spending 5 credits on the wrong profile. Pass `league` for WNBA or college; the default is the NBA.
**Step 2: Pull the profile.** `get_player` returns `player`, `seasons` (one row per season per `season_type`, with `games starts min pts reb ast stl blk tov oreb dreb pf plus_minus fgm fga tpm tpa ftm fta fg_pct tp_pct ft_pct efg_pct ts_pct`) and `recent_games` (last 10).
**Step 3: Normalize.** Per-game numbers hide minutes. Convert before comparing:
```
per36(stat) = stat / min * 36
fga_per36 = fga/games / min * 36
ts_pct = pts / (2 * (fga + 0.44 * fta)) [totals, not per game]
three_rate = tpa / fga
ft_rate = fta / fga
ast_to_tov = ast / tov
```
`total_pts`, `fgm`, `fga` and the other counting fields on a season row are season totals; `pts`, `reb`, `ast` and `min` are per-game averages. Do not divide a total by a per-game number.
**Step 4: Check the sample.** Under about 20 games, shooting percentages are noise. Under 100 three-point attempts, `tp_pct` has a standard error near 5 points (`sqrt(p*(1-p)/n)` with p = 0.35, n = 100 gives 0.048). State the attempts next to every percentage.
**Step 5: Split by season type.** The API returns postseason rows separately. Playoff rows are fewer games against better defences; compare them to the player's own regular season, not to other players' regular season.
**Step 6: Leaderboards.** `get_leaders {"stat": "ts_pct", "season": 2024, "limit": 3}` returns `leaders` (`player_id name team games value`) with a `min_games` floor (default: a quarter of the season). Percentage leaders are dominated by low-usage centers. Read `games` and pair a percentage leader with a volume stat before calling anyone elite.
## Comparing Two Players
1. Same season, same `season_type`, same league.
2. Normalize to per-36 minutes and show the minutes gap.
3. Show shooting as `ts_pct` plus the attempts that produced it.
4. Show role: `starts / games`, `min`, `three_rate`, `ft_rate`.
5. Say what the data cannot see: defence, off-ball value, lineup effects (no tracking or on/off data here).
6. One-line verdict that matches the numbers shown.
## Interpreting Efficiency
| Reading | Meaning |
|---------|---------|
| `ts_pct` above ~0.60 on 15+ FGA per game | Elite efficient volume scorer |
| `efg_pct` well above `fg_pct` | Threes drive the efficiency |
| `ts_pct` far above `efg_pct` | Free throws drive it |
| `fg_pct` high, `fga` very low | Role player finishing, not creating |
| `plus_minus` per game | Team context; do not rank players on it |
Thresholds are rules of thumb from league-wide norms. Compare with `get_leaders` for the same season rather than trusting a fixed number.
## Gotchas
| Gotcha | Handling |
|--------|----------|
| Traded players | `teams` can read like `DAL/WSH` on a season row; the line is combined |
| Two rows for one season | Regular season and postseason; filter by `season_type` |
| College players | Rows are by season; players change teams and roles; sample sizes are 25-35 games |
| Same name, different players | Check `last_season` and `team` |
| A rookie with few games | Show games played first and widen the caveat |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Id lookup | 1 |
| Profile | 5 |
| Two-player comparison | 12 (2 lookups + 2 profiles) |
| Leaderboard | 5 |
## Output Format
```
[Player], [team], [pos]: [season] regular season ([games] GP, [min] mpg)
Per game: [pts]/[reb]/[ast] on [fg%]/[3p%]/[ft%], TS [ts%]
Per 36: [pts]/[reb]/[ast]
Sample: [tpa] 3PA, [fta] FTA
Playoffs: [games] GP, [pts]/[reb]/[ast], TS [ts%]
Read: [one sentence that matches the numbers above]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Efficiency looks off | Where the shots come from | `shot-zone-analysis` |
| Recent change in output | Rolling form | `player-form` |
| Wants to bet his line | Prop history | `prop-modeling` |
| Wants the team context | Team efficiency | `team-analysis` |
probability-calibration7.17 KB
---
name: probability-calibration
description: "Checks and fixes whether basketball model probabilities mean what they say: reliability tables, Brier score and log loss, Platt scaling and isotonic regression fitted out of sample, converting predicted margins to win and cover probabilities, and shrinking toward the market. Use when a model outputs win, cover or over probabilities, asks if 65% really wins 65% of the time, or needs probabilities from a margin model. Do not use for building the model, see feature-engineering. Do not use for choosing splits, see walk-forward-validation."
metadata:
version: 1.0.0
author: DunkAPI
---
# Probability Calibration
> **Default data tool:** DunkAPI (`dunkapi-tool`) only for outcomes and the market benchmark: `list_games` (2 credits per page), `get_odds` (`result.home_covered`, `result.over`). Calibration itself is local.
Every number built on a probability inherits its error. An uncalibrated 70% that is really 60% turns a small edge into a negative one.
## When to Use
- "My model says 68% for the Knicks. Can I trust that?"
- "Is Platt scaling or isotonic better here?"
- "How do I turn a predicted margin into a win probability?"
## When NOT to Use
- Fitting the model itself, see `feature-engineering`
- Time-ordered evaluation design, see `walk-forward-validation`
## Commands Available
| Command | Role | Credits |
|---------|------|---------|
| `list_games` | Final scores to label outcomes | 2 per page |
| `get_odds` | Market probability to benchmark (and `home_covered`, `over` to label cover and total outcomes) | 5 per game, 10 per date |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_calibration`, `get_model_probabilities` | Compute locally from your own predictions |
## What Calibrated Means
Among all games where the model said about 70%, the home side should win about 70% of them. Two separate qualities:
- **Calibration:** do stated probabilities match frequencies?
- **Discrimination (sharpness):** does the model separate likely from unlikely outcomes?
A model that always says 58% for the home team in a league where home teams win 58% is calibrated and useless. You need both.
## How It Works
**Step 1: Use out-of-sample predictions only.** Generate predictions with `walk-forward-validation`. Calibrating on training predictions measures overfit, not reliability.
**Step 2: Reliability table.** Bin predictions (deciles, or equal-count bins), and compare the mean prediction to the observed rate.
```python
import numpy as np, pandas as pd
df["bin"] = pd.qcut(df.p, 10, duplicates="drop")
rel = df.groupby("bin").agg(pred=("p","mean"), obs=("y","mean"), n=("y","size"))
rel["se"] = np.sqrt(rel.obs * (1 - rel.obs) / rel.n)
```
Read it with the standard error: a bin of 120 games has a standard error near 4.5 points, so a 3-point miss is noise. Plot `obs` against `pred` with error bars; the diagonal is perfect.
**Step 3: Scores.**
| Metric | Formula | Notes |
|--------|---------|-------|
| Log loss | `-mean(y*log(p) + (1-y)*log(1-p))` | Coin flip 0.693; punishes overconfidence |
| Brier | `mean((p - y)^2)` | Coin flip 0.25 |
| Calibration slope | Logistic regression of `y` on `logit(p)` | 1.0 ideal; below 1 means overconfident |
| Calibration intercept | Same regression | 0 ideal |
Brier decomposes into reliability (bad) minus resolution (good) plus uncertainty (fixed). Improve reliability by calibrating; improve resolution only with better features.
**Step 4: Choose a correction.**
| Method | Use when | Risk |
|--------|----------|------|
| Platt (logistic on the score) | Under about 3,000 calibration games | Assumes a sigmoid shape |
| Isotonic regression | 3,000+ calibration games | Steps; overfits small samples |
| Shrink toward the market: `p' = w*p_model + (1-w)*p_market` | Model has some edge but is overconfident | Needs a devigged market probability (see `odds-analysis`) |
NBA seasons are small. One season is about 1,230 games, so Platt or shrinkage is usually right; isotonic needs several seasons pooled.
```python
from sklearn.linear_model import LogisticRegression
platt = LogisticRegression(C=1e6).fit(logit(p_cal).reshape(-1, 1), y_cal) # fit on a calibration block
p_adj = platt.predict_proba(logit(p_test).reshape(-1, 1))[:, 1]
```
Calibration data must be earlier than the data it is applied to. Fit on seasons `s-2..s-1`, apply to `s`.
**Step 5: Margin models to probabilities.** If the model predicts a home margin `m`, the win probability is `Phi(m / sigma)`, where `sigma` is the residual standard deviation of margin around the prediction (about 12 points for NBA games; estimate it from your own out-of-sample residuals, because it changes with the era and the model). For covers against a spread `s` (home spread, negative when the home team is favoured): `P(cover) = Phi((m + s) / sigma)`. For totals use the same with the total prediction and its own sigma. Then calibrate those probabilities like any others.
**Step 6: Re-check after correction.** Rebuild the reliability table on a later block. If the slope moved toward 1 and log loss dropped, keep it.
### Worked Example (market benchmark)
The closing moneylines for game 401859967 from `get_odds` (SA home, NY away) devig to a home probability between 0.631 (DraftKings) and 0.651 (Bovada); Pinnacle's devigged value is 0.643. SA lost. One result says nothing about calibration: even a perfectly calibrated 64% favourite loses 36% of the time. That is why calibration is judged over hundreds of games, with bin standard errors, never from a single outcome.
## Common Failures
| Symptom | Cause | Fix |
|---------|-------|-----|
| Slope well below 1 | Overfit model; extreme probabilities | Shrink toward the market or a prior |
| Over-prediction of home wins | Home advantage double counted | Check intercept; refit |
| Good overall, bad on underdogs | Regression to the mean in tails | Calibrate by bin; isotonic with enough data |
| Calibrated in-sample only | Calibration fitted on training predictions | Refit on held-out data |
| Probabilities above 0.9 for NBA games | Model too sure | Cap; NBA favourites rarely exceed 90% |
| Different periods calibrate differently | Drift | Calibrate on a rolling recent window |
## Sample Size Warning
Reliability needs data. With 1,000 games in 10 bins, each bin has 100 games and a standard error near 5 points. Do not slice by team, month and conference at once; every slice you add multiplies the false alarms.
## Credit Usage
No calibration calls. Outcome labels come free from the `list_games` scores you already pulled. If you want cover and over labels, use `get_odds` by date (10 credits per day of games) and read `result.home_covered` and `result.over`.
## Output Format
```
Out-of-sample games: [n], seasons [..]
Log loss [x] (market [x]) | Brier [x] | slope [x], intercept [x]
Reliability (deciles): [pred -> obs +/- se] ...
Correction: [Platt/isotonic/shrink w=..], fitted on [window]
After: log loss [x], slope [x]
Usable range: [pred interval where bins are within 1.5 se]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Calibrated, still behind the market | Look for better features | `feature-engineering` |
| Need a market probability | Remove the vig | `odds-analysis` |
prop-modeling9.01 KB
---
name: prop-modeling
description: "Builds basketball player prop models for points, rebounds, assists, threes, points + rebounds + assists, blocks and steals: projects minutes and per-minute rates from the game log, turns a projection into an over/under probability with a suitable distribution, compares it with devigged prop prices across books, and checks a player's history against his lines. Use when the user asks if a player will go over a number, how often he has hit his line, what a fair prop price is, or how to project a stat line. Props exist for NBA, WNBA and men's college only. Do not use for team-side or total models, see feature-engineering. Do not use for recent-form description alone, see player-form."
metadata:
version: 1.0.0
author: DunkAPI
---
# Prop Modeling
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_prop_history` (10 credits for up to 200 games) shows each game's closing line against what happened. `get_player_props` (10 credits for a whole game, or 10 per game for one player across dates) gives every book's price. `get_player_games` (5) feeds the projection.
A prop is a single player-game outcome with a wide spread. The line already contains minutes, matchup and injuries. The only way to add value is to model one of those better than the market, and to know when you have not.
## When to Use
- "Will Brunson go over 24.5 points?"
- "How often has he hit his points line this season?"
- "Which book has the best price on his rebounds over?"
- "Project his points, rebounds and assists for tonight"
- Checking whether a player's line has been set too high or low historically
## When NOT to Use
- Team sides and totals, see `feature-engineering`, `odds-analysis`
- Descriptive form with no line involved, see `player-form`
- Women's college basketball: no player props are served for `ncaaw`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `get_prop_history` | One market for one player: per game the closing line (median across books or one `bookmaker`), opening line, prices, `actual`, `result`, `margin`, and a `summary` with `over_rate`. Returns up to 200 games | 10 |
| `get_player_props` | Props by book for a game (`game_id`), or one player across dates (`player_id` + `from_date`, optional `to_date`). `market`, `bookmaker`, `snapshot_type` (`closing` default, or `opening`). `estimate_only` returns the cost without charging | 10 per game |
| `search_players` | Player id | 1 |
| `get_player_games` | Game log for minutes and rates | 5 |
| `get_injuries` | Availability of the player and his teammates | 2 |
| `get_odds` | Game spread and total (blowout risk, pace) | 5 |
Markets: `player_points`, `player_rebounds`, `player_assists`, `player_threes`, `player_points_rebounds_assists`, `player_blocks`, `player_steals`. Leagues: `nba`, `wnba`, `ncaam`.
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_prop_projections`, `get_dfs_projections` | Build locally |
| `get_alt_lines` | Not served; one line per book per snapshot |
| `get_live_props` | Not served; closing or opening snapshots only |
| `get_lineups`, `get_minutes_projection` | Model minutes from the log |
| Props for `ncaaw` | Not available |
## Coverage
Prop lines run from May 2023 for the NBA and WNBA and from the 2023-24 season for men's college. Prices come from several books (DraftKings, FanDuel, BetMGM, ESPN BET, Pinnacle and others). `get_player_props` returns lowercase bookmaker keys (`draftkings`, `betmgm`, `espnbet`); `get_odds` returns display names. Use the right form per tool.
## How It Works
**Step 1: Establish the base rate from history.** `get_prop_history {"player_id": "3934672", "market": "player_points", "from_date": "2025-10-20", "limit": 6}` returned six games. Trimmed:
```json
{"data": {"games": [
{"game_date": "2025-10-22", "line": 24.5, "open_line": 24.5, "over_odds": -106, "under_odds": -120, "books": 7, "actual": 23, "result": "under", "margin": -1.5},
{"game_date": "2025-10-24", "line": 25.5, "actual": 31, "result": "over", "margin": 5.5}
],
"summary": {"games": 6, "graded": 6, "overs": 5, "unders": 1, "pushes": 0, "over_rate": 0.833, "avg_line": 25.8, "avg_actual": 31.2},
"truncated": true, "next_from_date": "2025-11-03"}}
```
Read it carefully: 5 overs in 6 games is an 83% over rate, but with 6 games the 95% interval runs from about 44% to 97%. It is a story, not evidence. The line moved from 24.5 to 27.5 over those games, so the market was already chasing his scoring. Use the full season (up to 200 games per call) before saying anything.
**Step 2: Project minutes.** Minutes are the biggest driver. From `get_player_games`, take a shrunk rolling mean (see `player-form`), then adjust:
| Factor | Direction |
|--------|-----------|
| Game spread large (blowout risk) | Minutes down for starters; the absolute spread above about 10 is a flag |
| Back-to-back or heavy recent load | Possible rest, check `get_injuries` |
| Teammate out (starter or primary creator) | Minutes and usage up for the next player |
| Foul trouble history | Wider minutes distribution |
**Step 3: Rate per minute.** `rate = stat / minutes` over a window with shrinkage toward a season value. Project `mean = proj_minutes * rate`.
**Step 4: Distribution.** Convert the mean to `P(over line)`.
| Market | Distribution | Notes |
|--------|--------------|-------|
| Points | Normal around the mean, or the empirical distribution of the player's recent games scaled to the projection | Per-game standard deviation of a star scorer is often near 7; estimate it from his own log |
| Rebounds, assists | Negative binomial or normal | Overdispersed relative to Poisson |
| Threes, blocks, steals | Poisson or negative binomial | Low counts; a line of 2.5 means over needs 3 or more |
| Points + rebounds + assists | Sum of components with covariance | Do not add variances as if independent; minutes link them |
Half-point lines remove pushes. Whole-number lines can push; if the line is 25, `P(over) = P(X >= 26)` and `P(push) = P(X = 25)`.
```python
from scipy.stats import norm
p_over = 1 - norm.cdf(line + 0.5 - mean, scale=sd) # continuity correction for integer outcomes
```
**Step 5: Compare with the market.** Pull prices: `get_player_props {"player_id": "3934672", "from_date": "2025-10-22", "to_date": "2025-10-22", "market": "player_points"}` returned a row per book. The lines differed by book that night (23.5 at Bovada and ESPN BET, 24.5 at BetMGM, BetOnline and DraftKings), with BetMGM at over -125 / under -105. Devig each pair (see `odds-analysis`): BetMGM's fair over probability was 0.520. Only compare prices at the same line, or adjust the probability for the half-point.
**Step 7: Decide.** Required: a gap over the vig plus model error. Single-prop edges under 3 points do not survive that. Correlated props (the same game's points and PRA, or a player's over and his team total over) must not be sized independently.
## Cost Control
- `get_player_props` with `estimate_only: true` returns the cost without running it (`{"estimate_only": true, "tool": "get_player_props", "credits": 10}` for one game).
- One player across 40 games with `player_id` and `from_date` is 400 credits; `get_prop_history` is 10 credits for up to 200 games of lines and results, so use it for history and reserve `get_player_props` for current book-by-book prices.
- Pull `bookmaker` filters when you only need one book.
- `snapshot_type: "opening"` against `"closing"` shows how the line moved: closing line value for props is measured against the closing price of the same book.
## Props-specific Pitfalls
| Pitfall | Handling |
|---------|----------|
| Small history windows | Under 30 games, a hit rate is noise |
| Line differs across books | Edge may be a line difference, not a probability difference |
| The posted line is stale | Check `captured_at`; confirm the live price at the book |
| Player ruled out | A void or missing game is not an under; exclude games with `did_not_play` |
| Regular season vs playoffs | Separate samples |
| Overs in a blowout | Starters sit; model with the spread |
| Selection bias | A player who gets props is already a role player with a stable role |
| Treating hit rate as probability | It is a base rate; the line was set knowing the same history |
This skill estimates probabilities. It does not produce picks or promise returns.
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Id lookup | 1 |
| Full season of one market's history | 10 |
| Game log for projection | 5 |
| Every book's prop prices for one game | 10 |
| Four markets for one player's history | 40 |
## Output Format
```
[Player], [market], line [x] ([book]), [date]
History: [n] games, over rate [x]% (95% CI [lo]% to [hi]%), avg line [x] vs avg actual [x]
Projection: minutes [x], rate [x]/min, mean [x], sd [x]
P(over) model [x]% | market fair [x]% | shrunk [x]%
Best price: over [odds] @ [book] / under [odds] @ [book]
Caveats: [injury, blowout risk, small sample]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Minutes uncertainty | Role and trend | `player-form` |
shot-zone-analysis8.32 KB
---
name: shot-zone-analysis
description: "Analyzes basketball shot profiles by zone using DunkAPI shot data: attempts, makes and points per shot in restricted area, paint, midrange, corner three and above-the-break three, compared with expected points per shot to separate shot quality from shot making. Also covers team defence by zone, clutch shots and drawing a shot chart from x/y. Use when the user asks where a player or team shoots, whether a hot shooter is lucky, which defence gives up the worst shots, or wants expected points. Do not use for season box-score averages, see player-scouting. Do not use for team pace and ratings, see team-analysis."
metadata:
version: 1.0.0
author: DunkAPI
---
# Shot Zone Analysis
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_shot_chart` (10 credits) for zone summaries, `get_plays` (10) for individual shots with game context.
The question is rarely "what is his FG%". It is: where do the shots come from, what is each shot worth on average, and is the result above or below what those shots usually produce.
## When to Use
- "Where does Brunson score from?" / "Does he live in the midrange?"
- "Is his shooting sustainable?" (shot quality versus shot making)
- "Which team allows the most corner threes?" (defence by zone)
- "Show his clutch shots" / "Every shot he took in the 4th"
- "Draw me a shot chart"
## When NOT to Use
- Season averages and per-36 rates, see `player-scouting`
- Team pace, ratings and four factors, see `team-analysis`
- Game-by-game minutes and scoring trend, see `player-form`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `get_shot_chart` | Zone table (`attempts made pct points_per_shot expected_points_per_shot`) and a `share` of attempts for a player, a team, a team's opponents (`opponent: true`), or one game. `include_shots: true` adds each shot's x, y, result (up to 3,000). NBA and WNBA only | 10 |
| `get_plays` | Play-by-play with `shot` objects: `made points_attempted x y distance_ft zone expected_points`, plus score and home win probability. Filter by `zone`, `period`, `made`, `clutch`, `shots_only`. All four leagues | 10 |
| `search_players` | Find the player id | 1 |
| `list_teams` | Team abbreviations | 1 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_shot_quality`, `get_xpts` | Fields on `get_shot_chart` and `get_plays` |
| `get_defender_distance`, `get_shot_tracking` | Not served; no contest or tracking data |
| `get_league_shot_averages` | Not served; build a baseline by calling `get_shot_chart` per team |
| College `get_shot_chart` | Not available for `ncaam` or `ncaaw`; use `get_plays` and check the `shot` fields are populated before relying on zones |
## Zones
`restricted_area`, `paint` (non-restricted), `midrange`, `corner_3`, `above_break_3`, `free_throw`. The free throw row has `expected_points_per_shot: null` and no x/y; `share` and `field_goal_attempts` cover field goals only.
Play-by-play covers NBA games back to 2003-04, and the `get_plays` tool description says win probability after each play is available from 2018. Check https://dunkapi.com/docs/atlas for the seasons that carry shot locations before building a multi-year study.
## How It Works
**Step 1: Pull the profile.** `get_shot_chart {"player_id": "3934672", "season": 2026}`.
**Step 2: Read quality and result separately.**
- `expected_points_per_shot` (xPPS) is what the shot is worth on average given where and how it was taken.
- `points_per_shot` (PPS) is what the player actually scored: `pct * 2` or `pct * 3`.
- Shot making = `PPS - xPPS` per zone.
- Points above expected = `sum(attempts * (PPS - xPPS))` over field-goal zones.
The same zone has different xPPS for different players (restricted area read 1.27 for Jayson Tatum in 2024 and 1.132 for Brunson in 2026), so xPPS reflects shot location and type within the zone, not a single zone constant. Compare a player to himself across seasons before comparing across players.
**Step 3: Look at the mix.** `share` sums to 1 over field-goal zones. A profile heavy in the restricted area, corner three and free throws is efficient by construction; one heavy in midrange and long twos needs above-average making to break even.
**Step 4: Weigh the sample.** A zone with under 100 attempts has a large standard error; for a 35% three-point shooter on 90 attempts it is about 5 points of percentage. Shot making is noisy and regresses; shot quality is more stable.
### Live example
`get_shot_chart {"player_id": "3934672", "season": 2026}` (Jalen Brunson, 2025-26), trimmed:
| Zone | Attempts | FG% | PPS | xPPS | Above expected per shot |
|------|---------:|----:|----:|-----:|-----------------------:|
| restricted_area | 196 | 0.653 | 1.306 | 1.132 | +0.174 |
| paint | 470 | 0.474 | 0.949 | 0.846 | +0.103 |
| midrange | 302 | 0.507 | 1.013 | 0.808 | +0.205 |
| corner_3 | 90 | 0.411 | 1.233 | 1.147 | +0.086 |
| above_break_3 | 456 | 0.349 | 1.046 | 1.031 | +0.015 |
`field_goal_attempts` 1,514. Summing `attempts * (PPS - xPPS)` gives about +159 points over expected, or +0.105 per shot. Shot mix: `paint` 31.0%, `above_break_3` 30.1%, `midrange` 19.9%, `restricted_area` 12.9%, `corner_3` 5.9%.
Read: his shot quality is ordinary (mean xPPS about 0.95, driven by a low restricted-area share), and the outcome beats it in every zone, most in the midrange (+0.205 per shot on 302 attempts, about +62 points). That is the part least likely to repeat at that size. A forecast should keep the shot mix and shrink the making toward zero, not extrapolate +0.105.
## Defence by Zone
`get_shot_chart {"team": "BOS", "opponent": true, "season": 2026}` returns what opponents took and made against that team. Compare its `share` and xPPS with other teams (30 calls = 300 credits for the league; budget it). Two things to separate:
1. Shot selection allowed (what zones opponents take): a defence, scheme and personnel signal.
2. Shot making allowed (opponent PPS minus xPPS): partly luck, and regresses.
## Clutch and Single-game Shots
`get_plays {"player_id": "3934672", "season": 2026, "clutch": true, "shots_only": true, "limit": 2}` returned a 16-foot step-back midrange make with 164 seconds left in the 4th (xPPS 0.82, home win probability after the play 0.609) and a missed free throw. `clutch` is the last 5 minutes of the 4th quarter (second half for college men) or overtime with the score within 5.
Use `home_win_prob` before and after a play to measure its leverage: `delta = home_win_prob_after - home_win_prob_before`, sign-flipped for the away team. Sum it over a player's shots for a "win probability added from shooting" figure, with a small sample warning: clutch samples are tens of shots per season.
## Drawing a Chart
`include_shots: true` adds every located shot's x, y and result (cap 3,000). x and y are court-grid coordinates (one corner three above came back x=48, y=1). Plot a scatter, color by `made`, and check orientation against a known corner three before trusting the axes. Bin into hexagons for large samples and size by attempts, not by percentage.
## Pitfalls
| Pitfall | Handling |
|---------|----------|
| Treating PPS - xPPS as skill | Small samples; shrink toward zero |
| Mixing regular season and postseason | Pass `season_type` |
| Counting free throws in `share` | They are excluded; add them separately |
| Comparing players' xPPS directly | It varies within a zone; compare to the player's own history and role |
| Two-point zones use `points_attempted` 2, threes 3 | Keep it when computing PPS by hand |
| Large `include_shots` payloads | Ask only when drawing |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| One player or team profile | 10 |
| League defence by zone (30 teams) | 300 |
| 200 individual plays | 10 |
## Output Format
```
[Player/Team], [season], [n] FGA
Zone FGA FG% PPS xPPS +/- per shot
restricted_area [n] [x] [x] [x] [x]
...
Mix: [top 2 zones and share]
Above expected: [points] over [n] shots ([x] per shot)
Read: [what is shot quality, what is shot making, what is likely to repeat]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Player shooting far above expected | Is the minutes/role stable | `player-form` |
| Defence gives up corner threes | Total or spread angle | `odds-explorer` |
| Want quality as a model feature | Leak-free rolling version | `feature-engineering` |
team-analysis7.96 KB
---
name: team-analysis
description: "Evaluates basketball teams: standings, DunkAPI Elo power ratings, pace, offensive and defensive rating, net rating and the four factors (eFG%, turnover rate, offensive rebound rate, free throw rate). Use when the user asks how a team is doing, who the best or fastest team is, why a team's record misleads, or wants two teams compared. Covers NBA, WNBA, men's and women's college. Do not use for a single game, see game-lookup or game-preview. Do not use for a player, see player-scouting. Do not use for where a team shoots from, see shot-zone-analysis."
metadata:
version: 1.0.0
author: DunkAPI
---
# Team Analysis
> **Default data tool:** DunkAPI (`dunkapi-tool`).
> `get_standings` (2 credits), `get_ratings` (2), `get_team_stats` (5). A full three-call team profile is 9 credits.
You read a team the way an analyst does: results first, then the efficiency that explains them, then whether the record is likely to hold.
## When to Use
- "How good are the Knicks?" / "Rank the East"
- "Who plays fastest?" / "Best defence in the league?"
- "Is Boston's record real or lucky?"
- Comparing two teams before a game or a series
- Producing team-level features for a model (see `feature-engineering`)
## When NOT to Use
- One game's box score or result, see `game-lookup`
- A full pre-game breakdown with injuries and odds, see `game-preview`
- Shot location and expected points by zone, see `shot-zone-analysis`
- Individual players, see `player-scouting`
## Commands Available
| Command | What it does | Credits |
|---------|-------------|---------|
| `get_standings` | Regular-season standings computed from every final game: W-L, home and away, points for and against, margin, last 10 | 2 |
| `get_ratings` | DunkAPI Elo ratings, ranked, as of the end of a season or today | 2 |
| `get_team_stats` | Pace, offensive, defensive and net rating per 100 possessions, four factors, three-point rate | 5 |
| `list_teams` | Team ids and abbreviations | 1 |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_advanced_team_stats` | `get_team_stats` |
| `get_power_rankings` | `get_ratings` (our Elo) or rank `get_team_stats` by `net_rating` |
| `get_lineup_stats`, `get_on_off` | None; lineup data is not served |
| `get_strength_of_schedule` | Derive from `list_games` plus `get_ratings` (see below) |
## Season Resolution
`season` is the year the season ends (2025-26 is `2026`). The 2026-27 NBA season opens Oct 20, 2026. Until a few weeks of games exist, `season: 2027` ratings and efficiency rest on almost nothing; use `season: 2026` and say it is last season. `get_team_stats` accepts `season_type`, so you can separate playoff efficiency from the regular season.
## How It Works
**Step 1: Results.** `get_standings` returns per team `wins losses home_wins home_losses away_wins away_losses points_for points_against avg_margin last10_wins win_pct`. The `conf_wins` and `conf_losses` fields read 0 in the responses seen for NBA, so do not rank on conference record from this tool.
**Step 2: Efficiency.** `get_team_stats` returns, per team:
| Field | Meaning |
|-------|---------|
| `pace` | Possessions per game |
| `off_rating`, `def_rating`, `net_rating` | Points scored, allowed and the difference, per 100 possessions |
| `efg_pct` | Effective FG%: `(fgm + 0.5 * tpm) / fga` |
| `tov_pct` | Turnover rate (fraction of possessions ending in a turnover) |
| `oreb_pct` | Offensive rebound rate |
| `ft_rate` | Free throw attempts per field goal attempt |
| `three_rate` | Share of shots that are threes |
| `opp_efg_pct` | Opponent effective FG% (the defensive half of factor one) |
Possessions are estimated as `FGA - OREB + TOV + 0.44 * FTA`, averaged over both teams (the API states this in `method`). Ratings are points per 100 possessions.
**Step 3: Strength.** `get_ratings` gives the Elo rating. 1500 is average; a 100-point gap is about a 64% favourite on a neutral floor. In general `P(A beats B) = 1 / (1 + 10^(-(rA - rB + home_edge) / 400))`. The API says its ratings include home advantage and a margin-of-victory multiplier, and regress a quarter of the way to the mean between seasons, so an early-season rating is mostly last year's.
**Step 4: Cross-check.** A team's Elo, net rating and record should roughly agree. When they do not, that is the finding:
| Pattern | Likely read |
|---------|-------------|
| Record well above what `net_rating` implies | Close-game luck; expect regression |
| `net_rating` strong, record average | Unlucky or a slow starter; check `last10_wins` |
| Elo far above `net_rating` rank | Elo carries last season or playoff results |
| High `off_rating`, low `efg_pct` | Scoring from free throws, turnovers and rebounding, not shooting |
A quick luck check: Pythagorean wins `= games * pts^14 / (pts^14 + opp_pts^14)`, using `pts` and `opp_pts` from `get_team_stats`. The exponent 14 is the usual NBA value; a gap of 3 or more wins between this and actual wins is worth flagging.
### Live example
`get_team_stats {"season": 2026, "team": "NY"}`, trimmed:
```json
{"data": {"season": 2026, "teams": [{
"abbrev": "NY", "games": 83, "wins": 54, "losses": 29,
"pts": 116.5, "opp_pts": 110.2, "pace": 99.8,
"off_rating": 116.8, "def_rating": 110.4, "net_rating": 6.4,
"efg_pct": 0.557, "tov_pct": 0.137, "oreb_pct": 0.296,
"ft_rate": 0.237, "three_rate": 0.427, "opp_efg_pct": 0.54}]}}
```
Read: a +6.4 net rating over 83 games is a strong team but not a dominant one; its offensive rebounding (`oreb_pct` 0.296) is doing real work. `get_ratings {"season": 2026}` ranked the same team first in Elo at 1808.1 as of its last game (2026-06-14), ahead of SA at 1767.9. Elo gives credit for the playoff run, which `net_rating` for the regular season does not.
## Strength of schedule
No tool returns it. Build it: `list_games` for the team (2 credits per page), then average the opponents' `get_ratings` values. Use opponent ratings from the same season, and say the ratings are end-of-season so the result is a description, not a pre-game input.
## Comparing two teams
1. Pull `get_team_stats` for each (a call with no `team` returned all 30 NBA teams for 5 credits, cheaper than two single-team calls).
2. Build the matchup lens: team A `off_rating` against team B `def_rating`, and the reverse; add `pace` (the faster team tends to set it, but the average is a fair projection).
3. Projected margin ~ `(A.off_rating + B.def_rating)/2 - (B.off_rating + A.def_rating)/2`, scaled by `pace / 100`, plus a home-court edge. Treat this as a prior, then check `get_injuries`.
## Pitfalls
| Pitfall | Why it matters |
|---------|---------------|
| Season aggregates used for past games | `get_team_stats` and `get_ratings` summarize the whole season. Using them to "predict" an earlier game leaks the future. For modeling, rebuild as-of values from `list_games` |
| Small samples | Under 15 games, net rating moves 3+ points on a single blowout |
| Garbage time and rest | Net rating includes both; a team resting starters understates talent |
| Mixing season types | Pass `season_type` to separate playoffs from regular season |
| College comparisons | Conferences are weakly connected; a high rating inside a weak conference is not comparable to one outside it |
## Credit Usage
| Operation | Credits |
|-----------|---------|
| Standings only | 2 |
| Elo only | 2 |
| All teams' efficiency | 5 |
| Full profile (standings + Elo + efficiency) | 9 |
## Output Format
```
[Team], [season]: [W]-[L], net rating [x] ([rank] of [n]), Elo [x] ([rank])
Pace [x] | Off [x] | Def [x]
Four factors: eFG [x], TOV [x], ORB [x], FT rate [x]; opponent eFG [x]
Read: [one sentence on whether the record is supported by the efficiency]
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Two teams meeting soon | Full matchup | `game-preview` |
| Shooting is driving the gap | Zone breakdown | `shot-zone-analysis` |
| Want team features for a model | Rolling, leak-free | `feature-engineering` |
| Rating gap vs the market | Compare to the line | `odds-explorer` |
walk-forward-validation7.39 KB
---
name: walk-forward-validation
description: "Validates basketball prediction models with chronological walk-forward splits instead of random k-fold: expanding or rolling windows, season-block folds, purge gaps, proper benchmarks (home-team baseline and the closing line), paired significance tests and the sample sizes needed to detect a small edge. Use when the user has a model and asks if it is any good, asks whether k-fold or a random split is okay (it is not), or wants to tune hyperparameters on time-ordered game data. Do not use for building the features, see feature-engineering. Do not use for probability reliability, see probability-calibration."
metadata:
version: 1.0.0
author: DunkAPI
---
# Walk-Forward Validation
> **Default data tool:** DunkAPI (`dunkapi-tool`) for game rows (`list_games`, 2 credits per page) and closing lines (`get_odds`). Validation itself needs no API calls.
Games are ordered in time and teams change slowly. A random split puts a team's April games in training and its March games in test; the model memorizes that team's season. That inflates every metric.
## When to Use
- "Is my NBA model any good?"
- "Can I use 5-fold cross-validation?" (no)
- "How many seasons should I train on?"
- Choosing between two models or two feature sets
- Deciding whether a result is signal or noise
## When NOT to Use
- Constructing features, see `feature-engineering`
- Judging whether 62% means 62%, see `probability-calibration`
## Commands Available
| Command | Role | Credits |
|---------|------|---------|
| `list_games` | The game table: dates, scores, season type | 2 per page |
| `get_odds` | Closing lines as the benchmark; `date` form is 10 credits for a day | 5 per game, 10 per date |
## Commands That Do NOT Exist
| Not available | Use instead |
|---------------|-------------|
| `get_train_test_split`, `get_cv_folds` | Split the table by date in code |
| `get_model_predictions` | Predictions come from your model |
## Why k-fold Fails Here
| Problem | Effect |
|---------|--------|
| Future games in training | The model sees a team's later form, injuries and trade effects |
| Neighbouring games are correlated | Test games look "easy" because adjacent games are in training |
| Season-level inputs (see `feature-engineering`) | Final-season summaries leak into every fold |
| Hyperparameters tuned on the test fold | Optimistic by construction |
## How It Works
**Step 1: Order by date, never shuffle.** Sort by `date`, then `game_id`. Games on the same night share a date; keep them together in a fold.
**Step 2: Choose a scheme.**
| Scheme | Use when | Layout |
|--------|----------|--------|
| Expanding window | Little data, stable relationships | Train seasons 1..k, test k+1 |
| Rolling window | Rules drift (pace, three-point rate) | Train the last W seasons, test the next |
| Within-season monthly | Need many test points, model refits cheaply | Train through month m, test month m+1 |
For NBA game models: train through season `s`, test season `s+1`, advance one season at a time. With 5 seasons that gives 4 tests of about 1,230 games each.
**Step 3: Gap.** If a feature uses a rolling window of W games, leave a gap of at least W games (or a few weeks) between train end and test start when features overlap the boundary. A feature built with `shift(1)` rolling windows needs no gap for the target, but any model fitted on a window ending at date d should not have been selected using later data.
**Step 4: Nested tuning.** Tune hyperparameters inside the training window (an inner walk-forward), freeze them, then score the outer test block. Never report the best of 50 tries on the same test block.
```python
def walk_forward(df, first_test_season, fit, predict):
out = []
for s in sorted(df.season.unique()):
if s < first_test_season: continue
train, test = df[df.season < s], df[df.season == s]
model = fit(train) # tuning inside train only
out.append(test.assign(p=predict(model, test)))
return pd.concat(out)
```
**Step 5: Benchmarks. A model must beat these first.**
| Benchmark | What it tells you |
|-----------|------------------|
| Always pick the home team | The floor for win classification |
| Pre-game Elo (rebuilt as-of) | The "free" signal |
| Closing line probability (devigged, see `odds-analysis`) | The market, the real bar |
Score the model and the benchmarks on the same games. A model that beats Elo and loses to the closing line has found real signal the market already had.
**Step 6: Metrics.**
| Task | Primary | Also |
|------|---------|------|
| Win probability | Log loss | Brier score, accuracy (diagnostic only) |
| Margin | MAE and RMSE vs the closing spread | Mean error by predicted-margin bucket |
| Total | MAE vs the closing total | |
| Cover | Log loss on `home_covered` | Hit rate vs the break-even rate |
Log loss rewards honest probabilities; accuracy hides miscalibration. Lower is better; a coin flip is 0.693.
**Step 7: Significance.** Compare two models on the same games with the per-game loss difference `d_i = loss_A_i - loss_B_i`. Report the mean, a paired bootstrap interval (resample games or, better, game-days) and the fraction of folds where A wins. If the interval includes 0, you do not have a winner. For a bet-level claim, see the sample table below.
**Step 8: Sample sizes for betting claims.** At -110 a Odds reader needs 52.38% to break even. To tell a true 54% from the 52.38% break-even with 80% power at 5% one-sided significance you need about 5,900 bets (`n ~ (1.645 + 0.84)^2 * p(1-p) / (0.54 - 0.5238)^2`). A 1,230-game season cannot show that; do not let a good single season stand in for evidence.
| Bets | Standard error on win rate | What a 55% result means |
|------|---------------------------:|-------------------------|
| 100 | 5.0 points | Indistinguishable from 50% |
| 500 | 2.2 points | Suggestive |
| 1,500 | 1.3 points | Meaningful |
| 5,000 | 0.7 points | Strong evidence |
**Step 9: Report by fold.** Show each season's metric, not only the pooled number. A model with one great fold and three weak ones is a coincidence, not a model.
## Basketball-specific Traps
| Trap | Handling |
|------|----------|
| Season boundary roster changes | Features reset or blend priors at season start; evaluate early-season games separately |
| Early season: little data | Report metrics for games 1-15 and 16+ separately |
| COVID-shortened and bubble seasons | Flag or exclude 2019-20 and 2020-21 when comparing |
| Rule or style drift | Prefer rolling windows; compare to expanding |
| Play-in, playoffs, preseason | Separate evaluation; playoff games have different rotations |
| Same-night games in train and test | Keep a date entirely on one side |
| Using the final-season Elo from `get_ratings` as a feature | Leaks the future; rebuild as-of |
## Credit Usage
## Output Format
```
Scheme: [expanding/rolling], tests [seasons], gap [n]
Model vs benchmarks (log loss, lower is better):
Home-team baseline [x] | Elo [x] | Closing line [x] | Model [x]
Per-fold: [season] [x] ... (model beats Elo in [k] of [n])
Paired difference vs closing line: [mean] (95% interval [lo], [hi])
Verdict: [signal / no evidence / worse than market], with n games
```
## What to Do Next
| Found | Next | Skill |
|-------|------|-------|
| Model beats Elo, loses to the line | Check calibration, then look for what the line misses | `probability-calibration` |
| Unstable folds | Fewer features, more regularization | `feature-engineering` |
Release notes
Initial release. Read-only NBA basketball data tools with OAuth sign-in and 14 workflow skills.
Declared in the saved package. Remote tools may change independently.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package license
- MIT
- Package author
- DunkAPI
- Keywords
- See keywords
- Commerce declaration
- Does not support commerceThis does not establish whether access is free or paid.
- Review scenarios
- 5 positive · 3 negativeScenario counts, not test results.
Declared capabilities
- Look up NBA, WNBA and college teams and players
- Read standings, schedules and scores
- Read box scores, leaders and team stats
- Read play-by-play and shot charts
- Read sportsbook odds and prop lines (information only)
Package observed Nov 3, 2026.
Technical details
- First seen
- Oct 30, 2026 · 22:33 UTC
- Last seen
- Nov 4, 2026 · 06:00 UTC
- Collection status
- Collected
plugin_asdk_app_6ad146326fa48191a98ccaee7059af55
Download plugin data (JSON)