← Plugin catalog
Data & Analytics

FastballAPI

FastballAPI v1.0.0

MLB baseball data and stats

Create a free account to save this plugin.

Competitor profile

Developer declarations; not independently tested. Listing last checked Nov 4, 2026. Package observed Nov 3, 2026.

Declared capabilities

  • Look up MLB and college baseball teams and players
  • Read standings, schedules and scores
  • Read batting, pitching and leaderboard stats

Cost & access

FastballAPI 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 2025 MLB standings.

View package source →

Description

From the marketplace listing

FastballAPI connects ChatGPT and Codex to MLB baseball data from FastballAPI. It covers MLB and college baseball with plate appearances, pitch-by-pitch data, pitch mix and spray charts. Ask for standings, schedules and scores, player batting and pitching lines, leaders, injuries, team ratings, plate appearances, pitches, pitch mix, spray charts, sportsbook odds 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 FastballAPI account. Limits: FastballAPI 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 FastballAPI 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 FastballAPI's own Elo-style ratings, not official rankings. Not affiliated with MLB 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.

Keywords

Files & skills

File archives

Plugin package19 files · 102 KBBrowse files →
Skill instructions
dispatch5.47 KB

View saved version →

---
name: dispatch
description: "Routes a baseball analytics request to the right 2 to 3 skills in this pack. Load this FIRST when an MLB or college baseball data or analytics request comes in and you are unsure which skills to activate. Prevents loading all 16 skills when two will do. Do not use when the request already names one skill's job, such as devigging two prices: load odds-analysis directly."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Skill Dispatch Guide

Match the request to a pattern, load those skills, stop. Always pair with `fastballapi-tool` when live data is needed: it holds the tool list, credit costs and the coverage limits.

## First-Use Persona Check

| Signal | Persona | Start with |
|--------|---------|------------|
| "What happened last night?" / exploring | Explorer | `game-lookup` |
| "Who is better, X or Y?" / scouting | Scout | `player-scouting`, `pitch-mix-analysis` |
| "I want to build a model" | Builder | `walk-forward-validation`, `pythagorean-record` |

## Request Patterns

### "What was the score?" / "Who plays tonight?" / "Schedule"
1. `game-lookup`: `get_games`, `get_schedule`, `get_game`.
2. Add `fastballapi-tool` only if you need cost or parameter detail.

### "How are the Dodgers doing?" / "Standings" / "Best team" / "Run differential"
1. `team-analysis`: standings, team stats, Elo.
2. `pythagorean-record` if the question is luck, regression or "are they as good as their record?".

### "Tell me about Aaron Judge" / "Compare two hitters" / "Who leads the league in OPS?"
1. `player-scouting`: search, season lines, leaders, injuries.
2. `run-value-analysis` if the user wants RE24 or clutch splits.
3. `spray-chart-analysis` if the question is where he hits the ball.

### "What does Tarik Skubal throw?" / "Whiff rate by pitch" / "Pitch mix"
1. `pitch-mix-analysis`: `get_pitch_mix`, `get_pitches`.
2. `player-scouting` for the season line behind it.

### "Run value" / "RE24" / "How much did that home run matter?" / "Hitting with runners in scoring position"
1. `run-value-analysis`: `get_plate_appearances`, `get_player`.

### "Where does he hit the ball?" / "Shift" / "Pull rate" / "Spray chart"
1. `spray-chart-analysis`: `get_spray_chart`.
2. `player-scouting` for context.

### "Pythagorean record" / "Who got lucky?" / "Expected wins"
1. `pythagorean-record`: `get_standings`, `get_team_stats`.
2. `team-analysis` for the surrounding picture.

### "What are the odds?" / "Best price" / "Did the line move?"
1. `odds-explorer`: `get_odds`, `get_line_movement`.
2. `odds-analysis` for devigging and implied probability.

### "Preview tonight's Dodgers game" / "Game breakdown"
1. `game-preview`: composes standings, ratings, schedule, injuries and odds.
2. `odds-explorer` only if the user wants the full book-by-book table.

### "Devig these odds" / "No-vig price" / "Implied probability"
1. `odds-analysis`. No data call needed for plain math.

### "Strikeout prop" / "Total bases over" / "Will he go over his hits line?"
1. `prop-modeling`: pitcher strikeouts, hits, total bases.

### "Backtest this" / "Would this have made money?" / "Closing line value"
2. `walk-forward-validation` for the model underneath.

### "Is my model accurate?" / "Cross-validation" / "Train/test split" / "Features"
1. `walk-forward-validation`: temporal splits, leakage, calibration check.

### "College baseball" / "Omaha" / "NCAA"
1. Route as above, then tell the user what college lacks (no pitch rows, no spray charts, no props, no injuries). `fastballapi-tool` has the full list.

## Common Chains

| Goal | Chain |
|------|-------|
| Scout a pitcher end to end | `player-scouting` (season line) then `pitch-mix-analysis` (arsenal) then `prop-modeling` (strikeout line) |
| Understand a hitter's power | `player-scouting` then `spray-chart-analysis` then `run-value-analysis` |
| Find out if a club is overrated | `team-analysis` then `pythagorean-record` |
| Cover tonight's game | `game-preview` then `odds-explorer`, adding `prop-modeling` for a named player |

## Ambiguity Resolver

| The user says | They probably mean | Ask or assume |
|---------------|--------------------|---------------|
| "The odds" | Moneyline for a named game | Assume moneyline, show run line and total too |
| "His stats" | Season batting or pitching line | Assume the latest completed season, say which |
| "Whiffs" | Whiff rate per swing by pitch | `pitch-mix-analysis` |
| "Is he good?" | Rate stats with a sample caveat | `player-scouting` |
| "Over/under" | Game total, unless a player is named | Ask if unclear |
| "Last year" | The most recent completed season | State the year |
| "Tonight" | The next date with games in `get_schedule` | Check the date first |

## Credit-Aware Routing

| Request | Rough cost |
|---------|-----------|
| Scores for a date | 2 |
| Standings and Pythagorean | 2 |
| Pitcher arsenal (search + mix) | 6 |
| Game preview | about 9 to 20 |
| One day of odds | 10 |
| Pitcher strikeout prop history | 11 |
| Season of game-by-game odds | about 12,000: ask first |

If a 500-credit month will not cover the job, say so. Do not trim the method to fit.

## Rules

1. Two or three skills per request. More is noise.
2. Never invent a tool. If a skill's "Commands That Do NOT Exist" table lists it, stay out.
3. Say the data window before the answer when it matters (props start May 2023, win probability 2019, college has no pitch data).

## When NOT to Use This Guide

Single-skill requests do not need routing. "What was the Yankees score last night" is `game-lookup`. "Convert +150 to a probability" is `odds-analysis`.
fastballapi-tool8.46 KB

View saved version →

---
name: fastballapi-tool
description: "Default data source for the FastballAPI skills. Routes every MLB and college baseball data request to the right one of the 20 FastballAPI tools, with credit costs, parameters, coverage limits and the response envelope. Use when any other skill needs games, box scores, standings, player stats, pitches, plate appearances, spray charts, odds or player props."
metadata:
  version: 1.0.0
  author: FastballAPI
  user-invocable: false
---

# FastballAPI 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 in this pack gets its data here. This skill owns the 20 tools, picks the cheapest call that answers the question, and states what the data cannot do.


## Response Envelope

Every REST response is wrapped as `{"data": ...}`. Read `body["data"]` first. Errors come back as `{"error": "..."}` with an HTTP error status. Headers `X-Credits-Used` and `X-Credits-Remaining` report the spend.

```json
{"data": {"players": [{"player_id": "32081", "name": "Gerrit Cole", "position": "SP", "bats": "R", "team": "NYY"}], "count": 1}}
```

Code examples in these skills write `get_x(**args)` for a thin helper that POSTs the tool name and arguments to the REST endpoint and returns the parsed JSON (so `get_x(...)["data"]` is the payload).

## The 20 Tools

| Tool | What it returns | Credits | Skills that use it |
|------|-----------------|---------|--------------------|
| `list_teams` | Team ids, abbreviations, colours for a season | 1 | all (id lookup) |
| `search_players` | Player ids by (partial) name | 1 | player-scouting, prop-modeling |
| `get_schedule` | Next 1 to 30 days of games with the median line | 2 | game-lookup, game-preview |
| `get_standings` | Record, splits, run differential, Pythagorean win pct | 2 | pythagorean-record, team-analysis |
| `get_injuries` | Latest injury report (MLB only) | 2 | game-preview, player-scouting |
| `get_ratings` | Our team Elo ratings, ranked | 2 | team-analysis, walk-forward-validation |
| `get_game` | One game: line score, every batting and pitching line, books, win probability | 5 | game-lookup, game-preview |
| `get_player` | Season-by-season batting and pitching, with RE24 | 5 | player-scouting |
| `get_player_games` | A player's game log (batting and pitching) | 5 | prop-modeling, player-scouting |
| `get_leaders` | Qualified season leaders for 13 stats | 5 | player-scouting |
| `get_team_stats` | Runs for and against, team batting and opponents' batting | 5 | team-analysis, pythagorean-record |
| `get_pitch_mix` | A pitcher's arsenal: usage, velocity, whiff rate, ball rate (MLB only) | 5 | pitch-mix-analysis, prop-modeling |
| `get_plate_appearances` | Play-by-play with base-out state, batted ball, win probability, RE24 | 10 | run-value-analysis |
| `get_pitches` | Pitch-by-pitch: count, type, velocity, location, result (MLB only) | 10 | pitch-mix-analysis |
| `get_spray_chart` | Balls in play by trajectory and field, optional x/y points (MLB only) | 10 | spray-chart-analysis |
| `get_player_props` | Prop lines by book, opening or closing (MLB only) | 10 | prop-modeling |

## Tool Selection

| You need | Use | Not this |
|----------|-----|----------|
| Last night's scores | `get_games` with `date` | `get_schedule` (future only) |
| This week's games and the median line | `get_schedule` | `get_odds` per game |
| One game's box score plus every book's line | `get_game` | `get_games` then `get_odds` |
| A pitcher's arsenal and whiff rates | `get_pitch_mix` (5) | `get_pitches` (10, only for pitch-level detail) |
| Where Judge hits the ball | `get_spray_chart` | `get_plate_appearances` |
| Win probability and run value per plate appearance | `get_plate_appearances` | `get_game` (range only) |
| Closing hits line and the result | `get_prop_history` | `get_player_props` in a loop |
| Standings with Pythagorean record | `get_standings` | computing from `get_games` |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_live_odds`, `get_live_scores` | `get_schedule` (stored snapshots, refreshed daily; not a live feed) |
| `get_boxscore` | `get_game` |
| `get_team` or `get_roster` | `list_teams` for ids; there is no roster endpoint |
| `get_probable_pitchers`, `get_lineups` | Not available. `get_game` shows starters after the game only |
| `get_statcast`, `get_exit_velocity`, `get_spin_rate` | Not available. No exit velocity, launch angle or spin |
| `get_park_factors`, `get_weather` | Not available |
| `get_war` | Not available. `get_player` has RE24; WAR is not computed |
| `get_pitcher_stats` | `get_player` (season line) or `get_leaders` |
| `search_games` | `get_games` with filters |

## Coverage Limits to State Out Loud

- **MLB games and box scores:** 2015 to the current season. Pitch-by-pitch: every pitch since 2018, and about 98 to 99% of each season's pitches for 2015 to 2017. Pitch type and velocity are on 96% or more of pitches (99.8% or more from 2020).
- **Win probability** on plate appearances starts in 2019. Earlier seasons return null.
- **Sportsbook lines:** 4 to 13 books per game from 2015 to 2023; opening and closing lines per book from July 2020 (The Odds API), and from 2024 from ESPN too. Before July 2020, only ESPN's final line. Check https://fastballapi.com/docs/atlas for the live per-season counts before you promise a backtest window.
- **Player props:** from May 2023. Markets: `batter_hits`, `batter_home_runs`, `batter_total_bases`, `batter_rbis`, `pitcher_strikeouts`, `pitcher_outs`. No props for college.
- **College baseball** (`league: "ncaabase"`): the Division I games ESPN carries, not every game. No pitch rows, no spray charts, no injuries, no props. Box scores and play-by-play exist for part of each season (about 800 to 1,550 games a season since 2021). Sportsbook lines from 2023.
- `get_pitches`, `get_pitch_mix`, `get_spray_chart`, `get_injuries`, `get_player_props` and `get_prop_history` take no `league` argument. They are MLB only.

## Identifiers

- **Teams:** abbreviation (`NYY`, `LAD`) or team id. Call `list_teams` once to confirm; the Athletics abbreviate as `ATH` in recent seasons.
- **Players:** the ESPN athlete id as a string. Always get it from `search_players`. Do not guess ids. Judge is `33192`, Skubal `42409`, found that way.
- **Games:** the ESPN game id as a string (`401809265`), from `get_games` or `get_schedule`.
- **Seasons:** a four-digit year. Omit `season` for the latest. MLB regular season runs late March to September, postseason in October, so "this season" in November is the year just finished.

## Cost Planning

| Task | Calls | Credits |
|------|-------|---------|
| Find a player and pull his season line | `search_players` + `get_player` | 6 |
| Pitcher arsenal | `search_players` + `get_pitch_mix` | 6 |
| Pitcher strikeout prop history for a season | `search_players` + `get_prop_history` | 11 |
| One game preview | `get_game` + `get_standings` + `get_ratings` | 9 |
| A day of odds | `get_odds` with `date` | 10 |
| A season of odds, game by game | about 2,430 regular-season games x 5 | about 12,000 |

State the cost before any loop. `get_odds` with `date` (10 credits for the whole slate) beats five or more single-game calls. Props accept `estimate_only: true` to return the price without being charged.

## Anti-patterns

| Mistake | Why it hurts | Do this |
|---------|--------------|---------|
| Guessing a player id from memory | Silent wrong player | `search_players` first (1 credit) |
| Calling `get_pitches` for an arsenal summary | 10 credits and thousands of rows | `get_pitch_mix` (5 credits) |
| Passing `league: "ncaabase"` to a pitch tool | Not offered for college | Say the data does not exist |
| Looping `get_player_props` over many dates | 10 credits per game returned | `get_prop_history` returns up to 200 games for 10 |
| Treating `get_schedule` lines as live | Snapshots refreshed daily | Say when the snapshot was taken |
| Skipping the `{"data": ...}` envelope | Code reads `None` | Unwrap `data` first |

## What to Do Next

| You retrieved | Next | Skill |
|---------------|------|-------|
| Scores or schedule | Look deeper at one game | `game-lookup`, `game-preview` |
| Standings | Check luck vs run differential | `pythagorean-record` |
| Pitcher arsenal | Whiff and usage reading | `pitch-mix-analysis` |
| Plate appearances | Run value, leverage | `run-value-analysis` |
| Odds | Compare books, remove the margin | `odds-explorer`, `odds-analysis` |
game-lookup7.19 KB

View saved version →

---
name: game-lookup
description: "Finds MLB and college baseball games by date, date range, team or season, and pulls one game in full: line score, box score, starters, bullpen usage, each book's line and the win-probability range. Use when the user asks for scores, last night's games, a team's schedule or results, the box score for a game, who pitched, or what the line was. Do not use for team trends over a season: see team-analysis. Do not use for odds comparison across books: see odds-explorer. Do not use for pre-game analysis: see game-preview."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Game Lookup

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_games` (2 credits) for lists, `get_schedule` (2) for the coming days, `get_game` (5) for one game in full.

You find the right game fast and report it plainly. Most mistakes here are wrong dates (time zones), wrong ids and forgetting the postseason.

## When to Use

- Scores for a date, a week or a team's recent games
- A team's upcoming schedule with the median line
- A box score: batting lines, pitching lines, line score
- "Who started?", "How many pitches did the starter throw?", "Who closed?"
- Whether the favourite won or the total went over for a finished game
- Finding a `game_id` for another skill

## When NOT to Use

- Season-long team form or rankings: `team-analysis`
- Comparing every book's price or line movement: `odds-explorer`
- A pre-game report with context: `game-preview`
- Play-by-play detail or run value: `run-value-analysis`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_games` | Games by `season`, `season_type`, `team`, `date`, `from`/`to`, `status`; `limit` up to 200, `offset` to page | 2 |
| `get_schedule` | Next `days` (1 to 30, default 7), optional `team`, with median moneyline, run line, total | 2 |
| `get_game` | One game: line score, batting, pitching, each book's open and close, win-probability range | 5 |
| `list_teams` | Team ids and abbreviations | 1 |

All four accept `league` (`mlb` default, `ncaabase`). College postseason games carry a `round` field.

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_boxscore`, `get_scores` | `get_game`, `get_games` |
| `get_live_scores` | Not available. Data is final scores plus daily schedule snapshots |
| `get_probable_pitchers` | Not available. Ask the user or read the team's own announcement |
| `get_head_to_head` | `get_games` with `team`, then filter opponents in code |
| `get_team_schedule` | `get_schedule` with `team` for future, `get_games` with `team` for past |

## Date and Season Rules

- Dates are US Eastern, `YYYY-MM-DD`. A night game that ends after midnight Eastern still belongs to the starting date.
- `starts_at` is UTC. Convert before telling a user a first pitch time.
- Omit `season` for the latest. In January the latest season is last year's.
- `season_type` is `regular` or `postseason`; omit it and you get both. A World Series game and an April game come back from the same call.
- `get_games` returns newest first. For a team's last 10, use `team` plus `limit: 10`.

## How It Works

**Step 1: Resolve the team.** Use the abbreviation (`NYY`). If unsure, `list_teams` (1 credit). Do not pass "Yankees".

**Step 2: Pick the list call.**

- Past games: `get_games` with `date`, or `from` and `to`, or `team` and `season`.
- Future games: `get_schedule` with `days` and optional `team`.
- Only finals: `status: "final"`. Only unplayed: `status: "scheduled"`.

```json
POST /v1/get_games
{"team": "NYY", "season": 2025, "limit": 2}
```

Real response shape (trimmed):

```json
{"data": {"games": [{
  "game_id": "401809266", "season": 2025, "season_type": "postseason",
  "date": "2025-10-08", "status": "final",
  "home": {"abbrev": "NYY", "runs": 2, "hits": 6, "errors": 1},
  "away": {"abbrev": "TOR", "runs": 5, "hits": 12, "errors": 0},
  "innings": 9, "extra_innings": false,
  "venue": "Yankee Stadium", "attendance": 47823
}], "count": 2, "next_offset": 2}}
```

**Step 3: Page when needed.** `next_offset` is the value for the next call's `offset`. A full 162-game team season needs `limit: 200` once.

**Step 4: Go deep on one game.** `get_game` with `game_id` returns `batting`, `pitching`, `odds` and `win_probability`. In `pitching`, a `starter` value of true marks the starter; `outs` and `ip` give the length; `pitches` and `strikes` give the workload. The `odds` array has each book's open and close.

**Step 5: Report the result in baseball terms.** Lead with the score and the swing factor (starter line, big inning, bullpen). Then the table.

## Reading a Finished Game

| Question | Where to look |
|----------|---------------|
| Starter line and bullpen use | `pitching` rows: `starter`, `ip`, `er`, `so` |
| Did the bullpen hold | non-starter `pitching` rows, `r` and `er` |
| Big inning | `get_plate_appearances` with `game_id` (10 credits), `inning` filter |
| Did the favourite win | `get_odds` with `game_id`: `result.favourite_won`, `result.over` against the median close |
| How lopsided was the win probability | `win_probability.home_min` and `home_max` (2019 on) |

## Edge Cases

- **Doubleheaders:** two games the same date for a team. Use `game_id`, never team plus date as a key.
- **Extra innings:** `innings` above 9 and `extra_innings` true. Pitcher workloads run long.
- **Postponed or suspended games:** may show `scheduled` with no runs. Do not report a score.
- **Shortened seasons:** 2020 has 60 games per team. Do not compare counting stats to 162-game seasons.
- **College:** `league: "ncaabase"`. Not every Division I game exists. Some games have scores but no box score or play-by-play. Say so rather than inventing a pitching line.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| A date's scores | 2 |
| A team's season of games (`limit` 200) | 2 |
| This week's schedule with lines | 2 |
| One full game | 5 |
| Look up ids once | 1 |

## Anti-patterns

| Mistake | Why it is wrong | Do this |
|---------|-----------------|---------|
| Searching games by team name | Tool expects abbreviation or id | `list_teams` once |
| Calling `get_game` for every game in a series | 5 credits each | `get_games` first, `get_game` for the one that matters |
| Reading `get_schedule` lines as live | Daily snapshots | State the snapshot is stored |
| Using local time for the date | Late games shift a day | Use US Eastern dates |
| Treating a missing box score as a zero line | College coverage is partial | Report "no box score in the data" |

## Output Format

```
NYY 2, TOR 5 (final, Oct 8 2025, postseason)
Yankee Stadium. 9 innings.
Hits: TOR 12, NYY 6. Errors: NYY 1.
Starters and key relievers: [from get_game pitching rows]
Closing median line: NYY -172, total 8 (from get_odds result block)
Result vs market: favourite lost, total stayed under (7 runs)
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| A game worth understanding | Pre-game or recap context | `game-preview` |
| A team on a streak | Season picture and luck check | `team-analysis`, `pythagorean-record` |
| A big swing inning | Run value per plate appearance | `run-value-analysis` |
| Line moved before first pitch | Book-by-book movement | `odds-explorer` |
game-preview7.05 KB

View saved version →

---
name: game-preview
description: "Builds a pre-game report for one MLB or college baseball matchup: records, run differential, Elo, recent form, injuries, the median and per-book lines, and what the market implies. Use when the user asks for a game preview, a breakdown of Team A vs Team B, tonight's matchup, or a shareable pre-game summary. Do not use for a full slate: loop get_odds by date instead and see odds-explorer. Do not use for odds-only lookups: see odds-explorer. Do not use for building a model: see walk-forward-validation."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Game Preview

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> Typical cost: `get_schedule` (2) + `get_standings` (2) + `get_ratings` (2) + `get_team_stats` (5) + `get_injuries` (2 per team) + `get_odds` (5) = about 20 credits.
> If the user only needs the lines, stop at `get_odds` and use `odds-explorer`.

You compose one tight report that replaces 20 minutes of tab hopping. Starting pitchers drive baseball games and FastballAPI has no probable-pitcher endpoint, so the report states that gap instead of papering over it.

## When to Use

- "Preview tonight's Dodgers game"
- "Brewers vs Dodgers, what should I know?"
- "Break down Game 3"
- A shareable one-page summary of a matchup

## When NOT to Use

- A whole slate: use `get_odds` with `date` (10 credits) and `odds-explorer`
- Only prices or line moves: `odds-explorer`
- Season-long team assessment: `team-analysis`
- A pitcher's full arsenal: `pitch-mix-analysis`

## Commands Available

| Command | Use in a preview | Credits |
|---------|------------------|---------|
| `get_schedule` | Find the game, its id, first pitch, and the median line | 2 |
| `get_standings` | Record, home/away split, last 10, run differential, Pythagorean | 2 |
| `get_ratings` | Elo for both teams | 2 |
| `get_team_stats` | Team offence and run prevention (omit `team` for the league in one call, 5) | 5 |
| `get_injuries` | Injured list for each club (MLB only) | 2 per team |
| `get_odds` | Every book's lines for the `game_id` | 5 |
| `get_games` | The last meetings between the two teams (filter in code) | 2 |
| `get_pitch_mix` | The starter's arsenal, if the user names him | 5 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_game_preview` | Compose from the calls above |
| `get_probable_pitchers`, `get_starting_lineups` | Not available. Ask the user who is starting, or label the section "starter unconfirmed" |
| `get_head_to_head` | `get_games` for one team and filter by opponent |
| `get_weather`, `get_park_factors` | Not available. Do not guess the wind |
| `get_bullpen_usage` | Rebuild from recent `get_game` pitching rows if the user asks (5 credits each) |
| `get_umpire` | Not available |

## How It Works

**Step 1: Find the game.** `get_schedule` with `team` and `days`. Take `game_id`, `starts_at` (UTC; convert), `venue`, `line` (median `home_ml`, `away_ml`, `home_run_line`, `total`, `books`). If `line` is null, the books have not posted. Say so; do not estimate.

**Step 2: Context for both clubs.** One `get_standings` call returns both. One `get_ratings` call returns both. From the standings row take `win_pct`, `pythagorean_win_pct`, home/away record, `last10_wins`, `run_differential`. A 2025 comparison, real values: MIL 97-65, +172, Pythagorean .608; LAD 93-69, +142, Pythagorean .586; Elo 1564.4 vs 1578.3.

**Step 3: Process.** `get_team_stats` (no `team`, 5 credits) gives both clubs in one call. Compare `runs_per_game`, `runs_allowed_per_game`, `batting.ops`, `pitching_against.ops`, `k_pct`, `bb_pct`.

**Step 4: Availability.** `get_injuries` for each `team`. List only players who matter to the game: starters, regulars, closers. `return_date` is the club's estimate.

**Step 5: Starting pitchers.** No endpoint. Options, in order:
1. The user states them. Then pull each starter with `search_players` (1) and `get_pitch_mix` (5), plus `get_player` (5) for the season line.
2. Skip and say "starters not confirmed; this preview is team-level".

A starter change alone can move a baseball line by a wide margin, so a team-level preview without the starters is half a preview. Say that.

**Step 6: The market.** `get_odds` with the `game_id`. Show best price per side, median, and the margin (see `odds-analysis`). Convert the median moneyline into a fair probability with the stated method and put it beside the Elo-implied probability:

```
Elo-implied P(home) = 1 / (1 + 10^(-(elo_home - elo_away + home_adv)/400))
```

The `get_ratings` response notes that a 100-point gap is about a 64% favourite on a neutral field (consistent with a 400-point logistic scale). `home_adv` is not exposed by the API: do not insert a number. Say "neutral-field Elo probability" and note that home field adds a few points.


## Report Layout

```
LAD at MIL, Sun Oct 11 2026, 8:00 PM ET, American Family Field (postseason)
Market (median of 8 books): MIL -111, LAD -105, run line MIL +1.5, total 6.
Fair split (multiplicative devig): about MIL 50.7 / LAD 49.3
Form (2025 reg. season): MIL 97-65 (+172), LAD 93-69 (+142)
Elo (end of 2025): MIL 1564, LAD 1578 (14 points, inside noise)
Offence vs run prevention: from get_team_stats
Injuries: [only players who matter]
Starters: not confirmed in the data. A change can move the line a lot.
Read: market is a coin flip; nothing in team-level data separates them.
```

The numbers in the schedule line above come from a real `get_schedule` response on 2026-10-11 (median line MIL -111 / LAD -105, total 6, 8 books). Verify the date and refresh before using any of it.

## College Baseball

`league: "ncaabase"` works for `get_schedule`, `get_standings`, `get_ratings`, `get_team_stats`, `get_game`, `get_odds`. No injuries, no pitch data. College previews live or die on the starting pitcher and the schedule strength; say both are missing when they are.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| Core preview (schedule, standings, ratings, team stats, odds) | 16 |
| + injuries both clubs | +4 |
| + one starter's arsenal | +6 |
| + last meetings | +2 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Previewing without starters and not saying so | Starter is the biggest single input | Label it |
| Calling a Pythagorean gap a pick | It is a luck estimate over a season | Use it as context |
| Reading regular-season records for a playoff series | Playoff rosters use the top starters and relievers | Note the context shift |
| Quoting the median line as the best line | Median is not purchasable | Show best and median |
| Inventing a park or weather note | No such data | Leave it out |
| Treating Elo difference as a spread | 100 points is about 64% on a neutral field, not a number of runs | Convert to probability only |

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Market and Elo disagree | Check who is starting; price the gap | `odds-analysis` |
| A pitcher with a big arsenal edge | Arsenal and whiff detail | `pitch-mix-analysis` |
| Strikeout or hit props for the game | Prop projections | `prop-modeling` |
odds-analysis8.34 KB

View saved version →

---
name: odds-analysis
description: "Converts baseball betting odds between formats and removes the bookmaker margin to get true implied probabilities for moneylines, run lines, totals and player props. Use when the user asks about devigging, vig, implied probability, no-vig price, multiplicative, power or Shin methods, American vs decimal odds, or what a price really says. Prerequisite for edge detection. Do not use for browsing current lines: see odds-explorer. Do not use for calibrating model output: see walk-forward-validation."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Odds Analysis

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> Mostly pure math. Use `get_odds` (5 credits per game, 10 per date) for closing prices and `get_player_props` or `get_prop_history` (10 credits) for two-way prop prices. User-supplied prices cost nothing.

You turn prices into probabilities honestly. Every price carries a margin; a fair probability needs it removed, and the method you choose matters most on lopsided markets, which baseball has plenty of (pitching mismatches, run lines, props with one juiced side).

## When to Use

- "What is -174 / +142 really saying?"
- Removing the margin from a two-way market (moneyline, over/under, run line)
- Comparing prices across books on an equal footing
- Turning a prop price pair (over -146, under +115) into a fair probability
- Explaining American vs decimal odds

## When NOT to Use

- Browsing current lines: `odds-explorer`
- Judging whether a model's probabilities are well calibrated: `walk-forward-validation`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_odds` | Prices by book for a game (5) or a date (10); finished games include `result` | 5 or 10 |
| `get_player_props` | Over and under prices by book for a game or a player | 10 |
| `get_prop_history` | Closing line and prices for a player-market over time | 10 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_no_vig_line`, `get_fair_odds` | Compute from `get_odds` with the methods below |
| `get_true_probability` | Compute it; the API's `*_close_prob` fields still contain the margin |
| `get_three_way_odds` | Not applicable: baseball has no draw in the moneyline |

## Conversions

```python
def implied(american):
    return -american / (-american + 100) if american < 0 else 100 / (american + 100)

def decimal(american):
    return 1 + (american / 100 if american > 0 else 100 / -american)
```

| American | Decimal | Implied |
|----------|---------|---------|
| -174 | 1.575 | 63.5% |
| +142 | 2.420 | 41.3% |
| -146 | 1.685 | 59.3% |
| +115 | 2.150 | 46.5% |
| -110 | 1.909 | 52.4% |

Payout on a winning stake S is S times (decimal minus 1). A push returns the stake.

## The Margin

For a two-way market, the sum of the implied probabilities exceeds 1. The excess is the margin (vig, overround).

Real example: DraftKings closing moneyline on NYY vs TOR (2025-10-07), NYY -174, TOR +142 from `get_odds`:

```
implied NYY = 174/274 = 0.6350
implied TOR = 100/242 = 0.4132
sum         = 1.0483     margin = 4.8%
```

The `home_close_prob` and `away_close_prob` fields in the API response (0.635, 0.413) are exactly these raw numbers. They are not fair probabilities.

## Devig Methods

All three return probabilities that sum to 1. They differ on how the margin is spread.

**1. Multiplicative (proportional).** Divide each implied probability by the sum.

```
NYY: 0.6350 / 1.0483 = 0.6058     TOR: 0.4132 / 1.0483 = 0.3942
```

Simple, and assumes the margin is spread in proportion to probability. Tends to under-correct favourites.

**2. Power.** Find k such that p_home^k + p_away^k = 1, then use the powered values.

```
k = 1.0758:  NYY 0.6136, TOR 0.3864
```

Takes more margin off the longshot than multiplicative does. Use when you believe the favourite-longshot bias is present.

**3. Shin.** Solves for the share of money from informed bettors, z, and removes margin accordingly.

```
z = 0.048:  NYY 0.6109, TOR 0.3891
```

Between the other two on this example. Preferred in the literature for horse racing and soccer markets; for baseball moneylines the three differ by under 1.5 points near 60/40.

**When the methods disagree.** The gap grows as the market gets lopsided or the margin grows. On a -300/+240 market the favourite is 71.8% (multiplicative), 73.3% (power) or 72.8% (Shin): a 1.5-point spread, which is larger than many claimed edges. Pick one method, state it, and use it everywhere. The default for this pack: **Shin or power for single bets, multiplicative only for quick checks.** Never switch methods to make an edge appear.

```python
from math import sqrt

def devig_multiplicative(p):
    s = sum(p)
    return [x / s for x in p]

def devig_power(p, tol=1e-10):
    lo, hi = 1.0, 3.0
    for _ in range(100):
        k = (lo + hi) / 2
        if sum(x ** k for x in p) > 1:
            lo = k
        else:
            hi = k
    return [x ** k for x in p]

def devig_shin(p):
    s, lo, hi = sum(p), 0.0, 0.4
    for _ in range(100):
        z = (lo + hi) / 2
        q = [(sqrt(z * z + 4 * (1 - z) * x * x / s) - z) / (2 * (1 - z)) for x in p]
        if sum(q) > 1:
            lo = z
        else:
            hi = z
    return q
```

## Devigging a Prop

Real closing line for a pitcher strikeout prop: Tarik Skubal, 2025-09-23, over 7.5 strikeouts at DraftKings, over -146, under +115 (`get_player_props`).

```
implied over  = 146/246 = 0.5935
implied under = 100/215 = 0.4651     sum = 1.0586   margin = 5.9%
fair over: multiplicative 0.5606, power 0.5661, Shin 0.5642
```

The over at -146 breaks even at a true 59.3%; the fair estimate is about 56%. That 3-point gap is the margin you pay. Prop margins usually run higher than moneyline margins, and many apparent prop edges are just margin.

Use the same line at the same book: the over at 7.5 and the under at 7.5. Do not pair an over at 7.5 from one book with an under at 8.5 from another.

## Run Lines and Totals

- Run line at -1.5: pair the two sides at the same book and devig as above. The -1.5 favourite price and the +1.5 underdog price are one two-way market.
- Totals: pair over and under at the same number. If the books post 8 and 8.5, they are different markets. A total of 8 has a push probability; if over and under are both near -110 at a whole number, some of the 52.4% implied on each is compensation for pushes. Do not renormalize it away without saying so.
- Measure the push rate from your own history of final scores against the closing total rather than assuming one. Check how `result.over` treats a push before relying on it.

## Combining Books: The Consensus Line

To get a market probability across books, devig each book first, then take the median of the fair probabilities (or weight sharper books higher). Do not average raw implied probabilities across books: books with larger margins pull the average up. For closing-line work, the median of devigged closes across the books present is a sturdy reference, but the book count changes by era (see `odds-explorer` coverage).

## Credit Usage

| Operation | Credits |
|-----------|---------|
| Convert and devig user-supplied prices | 0 |
| Pull one game's closing prices | 5 |
| Pull a day's closing prices | 10 |
| Pull a prop price pair | 10 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Treating `*_close_prob` as fair | It includes the margin (sum above 1) | Devig first |
| Devigging across two different books | Not one market | Pair sides at one book |
| Averaging raw implied probabilities | Margin leaks in | Devig, then median |
| Switching devig method until an edge shows | Method shopping creates false edges | Fix the method up front |
| Ignoring pushes on a whole-number total | Understates both sides' risk | State the push share |
| Hunting a 2-point edge while methods differ by 1.5 | The method choice is most of the signal | Report the range across methods |

## Output Format

```
Market: NYY -174 / TOR +142 (DraftKings, close)
Implied: 63.5% / 41.3%  (sum 104.8%, margin 4.8%)
Fair NYY: multiplicative 60.6%, power 61.4%, Shin 61.1%
Range across methods: 0.8 points. Method used: Shin.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Prop fair probability | Build the projection to beat it | `prop-modeling` |
| Prices not yet pulled | Fetch and compare books | `odds-explorer` |
odds-explorer7.87 KB

View saved version →

---
name: odds-explorer
description: "Compares MLB and college baseball sportsbook lines across books: moneyline, run line and total, best price, opening vs closing, vig, and pre-game line movement. Use when the user asks what the odds are, which book has the best price, how a line moved, what the closing line was, or whether the favourite won and the total went over. Do not use for devigging or implied probability math: see odds-analysis. Do not use for player prop lines: see prop-modeling."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Odds Explorer

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_odds` with `game_id` (5 credits) or `date` (10 credits for the whole slate). `get_line_movement` (10 credits per game). `get_schedule` (2) for the median line of upcoming games.

You report what the books posted, find the best price, and read movement without over-reading it. FastballAPI stores snapshots of lines, not a live betting feed. Always say when the snapshot was taken.

## When to Use

- "What are the lines on Dodgers at Brewers?"
- Best moneyline, run line or total across books
- Opening vs closing line, per book
- How a line moved before first pitch
- Did the favourite win and did the total go over (finished games)
- Which book is consistently the outlier on a market

## When NOT to Use

- Removing the margin or converting odds to probabilities: `odds-analysis`
- Player props (hits, strikeouts, total bases): `prop-modeling`
- Anything needing a live in-game price: not available

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_odds` | Each book's moneyline, run line, total for a game (`game_id`, 5) or a date (`date`, 10); opening and closing per book | 5 or 10 |
| `get_line_movement` | Snapshots of each book's line before first pitch for one `game_id`; optional `provider` | 10 |
| `get_schedule` | Upcoming games with the median line across books | 2 |
| `get_game` | One game with each book's open and close inside the box score | 5 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_live_odds` | `get_schedule` or `get_odds` (stored snapshots) |
| `get_best_line`, `compare_books` | Compute from the `get_odds` `books` array |
| `get_public_betting` | Not available. Infer nothing about bet splits |
| `get_closing_line` | `get_odds` for a finished game: `home_ml_close`, `total_close` |
| `get_alt_lines`, `get_first_five` | Not available. Only full-game moneyline, run line, total |
| `get_odds_history` | `get_odds` for a past `game_id` or `date` |

## Coverage

- Per-book opening and closing lines from July 2020 (The Odds API), and from 2024 ESPN's feed as well. Before July 2020, only ESPN's final line.
- Books per game: 4 to 13 from 2015 to 2023; 6 to 8 with opening and closing since July 2020. The book list in a response is the truth: do not assume Pinnacle or FanDuel is present for an old game. Live per-season counts: https://fastballapi.com/docs/atlas.
- Pre-game snapshots (`get_line_movement`) are ESPN's once a day and The Odds API's hourly for games in the next two days. That is coarse: a steam move between two snapshots is invisible.
- College lines start in 2023, a median of 5 or 6 books per game, some with one.

## How It Works

**Step 1: Get the game id.** `get_schedule` (upcoming) or `get_games` (past). Do not call `get_odds` with a team name.

**Step 2: Pull the lines.**

```json
POST /v1/get_odds
{"game_id": "401809265"}
```

Real response (NYY 9, TOR 6, 2025-10-07, 8 books, one book shown, trimmed):

```json
{"data": {"games": [{"game_id": "401809265", "date": "2025-10-07",
  "home": {"abbrev": "NYY", "runs": 9}, "away": {"abbrev": "TOR", "runs": 6},
  "books": [{"provider": "DraftKings",
    "home_ml_open": -139, "home_ml_close": -174,
    "away_ml_open": 114,  "away_ml_close": 142,
    "home_close_prob": 0.635, "away_close_prob": 0.413,
    "home_run_line_close": -1.5, "run_line_home_odds": 123, "run_line_away_odds": -149,
    "total_close": 8, "over_odds": -119, "under_odds": -102}],
  "result": {"median_home_ml": -172, "median_total": 8, "favourite_won": true, "over": true}}]}}
```

Notes on the fields: `*_prob` values are raw implied probabilities (0.635 plus 0.413 is 1.048, so the margin is still in them). `result` appears for finished games and is graded against the median close.

**Step 3: Find the best price per side.** For each of home ML, away ML, over, under, run line sides, take the best number across books. Best for a favourite is the least negative; for an underdog the most positive; for a total side, the better price at the same number. A different total (7.5 at one book, 8 at another) is not a price difference; it is a different bet.

**Step 4: Show the margin.** For a two-way market, margin = implied_home + implied_away minus 1. DraftKings above: 0.635 + 0.413 = 1.048, a 4.8% margin. Fine to show; the correct removal method is in `odds-analysis`.

**Step 5: Movement.** Opening to closing per book from `get_odds` is free of extra cost. For the path in between, `get_line_movement`:

```json
POST /v1/get_line_movement
{"game_id": "401809265", "provider": "DraftKings"}
```

The response has a `movement` object keyed by book, each a list of `{captured_at, home_ml, away_ml, home_run_line, total}`. Read it as follows:

| Pattern | Reading |
|---------|---------|
| All books move the same direction | Market-wide news or money; likely real |
| One book moves, others do not | That book's own risk; wait for the others |
| Moneyline moves but total does not | Starter or lineup change is more likely than a weather change |
| Total moves 0.5 and prices shift | Weather, wind or lineup news |
| Home ML -139 to -174 across all books | In the NYY example, the whole market moved toward the home side |

Never claim "sharp money". Without bet splits or timestamps finer than hourly, you cannot know. Say "the market moved toward X".

## The Run Line and Totals

- Run line is almost always the favourite -1.5 or the dog +1.5. The price, not the number, carries the information.
- Totals in baseball are priced off the starters, park and weather. A half-run move is large. Prices on a total can differ by 20 cents between books at the same number: that is where a best-price search pays most.
- Extra innings count in totals, which makes "under" tickets live longer than they look.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| One game's lines | 5 |
| A day's whole slate | 10 |
| One game's line movement | 10 |
| Median lines for the week | 2 |

Prefer the date form: a 15-game slate costs 10 credits as one `get_odds` call vs 75 as 15 calls.

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Quoting a snapshot as live | Stored, refreshed on a schedule | Give the timestamp (`captured_at`) |
| Averaging prices across books | Mixes numbers and margins | Report each book, then best and median |
| Mixing totals | 7.5 at -110 is not 8 at -110 | Group by the number first |
| Reading hourly snapshots as tick data | Misses fast moves | State the resolution |
| Calling the open "the sharp number" | Opens are early and wide | Compare close to close for model work |
| Quoting an old game's book list from memory | Book availability changed | Read the response |

## Output Format

```
NYY vs TOR, 2025-10-07 (final: NYY 9, TOR 6). 8 books.
Moneyline close: NYY best -161 (Pinnacle), TOR best +148 (Pinnacle).
DraftKings: NYY -139 open, -174 close. Raw margin 4.8%.
Total: closed 8 at most books (ESPN BET 7.5). Over -119 / under -102 at DraftKings.
Result vs market: favourite won; over (15 runs vs 8).
Movement: every book moved toward NYY by 20 to 35 cents.
Snapshot resolution: hourly at best.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Prices with the margin in them | Remove it for true probabilities | `odds-analysis` |
| Game context behind a move | Matchup summary | `game-preview` |
pitch-mix-analysis9.42 KB

View saved version →

---
name: pitch-mix-analysis
description: "Analyzes an MLB pitcher's arsenal: pitch types, usage, average and top velocity, whiff rate per swing, ball rate, usage by count, velocity by inning and platoon splits, from FastballAPI pitch-by-pitch data. Use when the user asks what a pitcher throws, which pitch gets whiffs, how his velocity changes through a game or season, or what his pitch mix means for strikeouts. Do not use for season ERA or WHIP lines: see player-scouting. Do not use for pricing a strikeout prop: see prop-modeling. Do not use for college baseball: no pitch data exists there."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Pitch Mix Analysis

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_pitch_mix` (5 credits) gives the arsenal summary. `get_pitches` (10 credits, up to 2,000 rows a call) gives every pitch for custom cuts. `search_players` (1) first.

You read a pitcher's arsenal the way a scout reads a scouting report: what he throws, how hard, how often, and what batters do against it. MLB only. College baseball has no pitch rows.

## When to Use

- "What does Tarik Skubal throw?"
- "Which of his pitches gets the most whiffs?"
- Usage vs whiff: does he throw his best pitch enough?
- Velocity trend within a start or across a season
- Changes in pitch mix between seasons
- Inputs for a strikeout model (see `prop-modeling`)

## When NOT to Use

- Season ERA, WHIP, K/9: `player-scouting`
- Hitter-side batted-ball profile: `spray-chart-analysis`
- Run value of outcomes: `run-value-analysis`
- College pitchers: not available

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_pitch_mix` | Per pitch type: `pitches`, `usage`, `avg_velocity`, `max_velocity`, `whiff_rate`, `ball_rate`, `in_play`; takes `pitcher_id`, `season` | 5 |
| `get_pitches` | Every pitch: `at_bat_id`, `pitch_number`, `inning`, `half`, `balls`, `strikes`, `pitch_type`, `pitch_type_name`, `velocity`, `px`, `py`, `result`, `in_play`; filters `game_id`, `pitcher_id`, `batter_id`, `season`, `pitch_type`, `limit` up to 2,000, `offset` | 10 |
| `search_players` | Find `pitcher_id` | 1 |
| `get_player` | Season line to set the context | 5 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_spin_rate`, `get_movement`, `get_extension` | Not available. No spin, break or release data |
| `get_stuff_plus`, `get_pitch_value` | Not available. Build run value from `get_plate_appearances` outcomes |
| `get_pitch_mix` for a game | Not available. Use `get_pitches` with `game_id` and aggregate |
| `get_pitch_arsenal_by_batter_hand` | Use `get_pitches` with `batter_id` filters or batch by handedness in code |
| `get_bullpen_mix` | Same tools, run per reliever |

## Coverage

- Every pitch since 2018. For 2015 to 2017, about 98 to 99% of each season's pitches.
- Pitch type and velocity are on 96% or more of each season's pitches, and 99.8% or more from 2020.
- Automatic balls and strikes (intentional walks, pitch-timer violations) are not pitches.
- `px` and `py` are ESPN's pitch-location frame in pixels, as sent. They are not feet and the strike zone boundaries are not given. Use them for relative location (up/down, in/out) within one pitcher, not for zone-rate claims against an official zone.
- Pitch type labels are ESPN's feed classifications (FF, SI, FC, SL, CH, CU, KC). Some pitchers' four-seam and sinker labels blur.

## How It Works

**Step 1: Resolve the pitcher.** `search_players` with `name`. Check `position` is SP or RP. Real: Tarik Skubal, `player_id` 42409.

**Step 2: Arsenal summary.**

```json
POST /v1/get_pitch_mix
{"pitcher_id": "42409", "season": 2025}
```

Real response (2025, trimmed to the four main pitches):

```json
{"data": {"pitcher": {"id": "42409", "name": "Tarik Skubal"}, "season": 2025, "pitches": 3152,
 "mix": [
  {"pitch_type": "CH", "name": "Changeup",     "pitches": 974, "usage": 0.309, "avg_velocity": 87.6, "max_velocity": 92,  "whiff_rate": 0.488, "ball_rate": 0.30,  "in_play": 161},
  {"pitch_type": "FF", "name": "Four-seam FB", "pitches": 953, "usage": 0.302, "avg_velocity": 97.2, "max_velocity": 102, "whiff_rate": 0.291, "ball_rate": 0.293, "in_play": 141},
  {"pitch_type": "SI", "name": "Sinker",       "pitches": 734, "usage": 0.233, "avg_velocity": 96.9, "max_velocity": 101, "whiff_rate": 0.189, "ball_rate": 0.272, "in_play": 131},
  {"pitch_type": "SL", "name": "Slider",       "pitches": 407, "usage": 0.129, "avg_velocity": 89.8, "max_velocity": 94,  "whiff_rate": 0.262, "ball_rate": 0.324, "in_play": 65}]}}
```

**Step 3: Read it correctly.**

- `whiff_rate` is per swing (swinging strikes divided by swings), not per pitch. A 48.8% whiff rate on the changeup means about half the swings against it missed.
- `usage` is share of all pitches in the season, not by count or batter hand.
- `in_play` is balls in play on that pitch, a sample-size check: 161 changeups in play against 65 sliders.
- `ball_rate` is balls per pitch. High whiff with high ball rate is chase-driven, not zone-driven.

Observations from the real numbers: the changeup is his most-used and highest-whiff pitch (31% usage, .488); the two fastballs together are 53% of his pitches but whiff much less (.291 four-seam, .189 sinker); the sinker, at the same velocity as the four-seam, gets far fewer whiffs, so it is not where his strikeouts come from (what it does to contact needs the plate-appearance data).

**Step 4: Strikeout shape.** A pitcher's strikeout rate is roughly the sum over pitches of usage times swing rate times whiff rate (plus called strikes). `get_pitch_mix` does not give swing rate. If you need the full decomposition, count swings in `get_pitches` using the `result` field. List the distinct `result` values from the first page before filtering; a sample row shows `strike-looking` and `ball`, and swinging strikes appear under their own value. Never guess the label.

**Step 5: Custom cuts with `get_pitches`.** 10 credits per call, so plan:

```json
POST /v1/get_pitches
{"pitcher_id": "42409", "season": 2025, "limit": 2000, "offset": 0}
```

A season of about 3,150 pitches needs two pages (offset 0 and 2000). Real row shape:

```json
{"game_id": "401809272", "at_bat_id": "4018092720101", "pitch_number": 1, "inning": 1, "half": "bottom",
 "pitcher_id": "42409", "batter_id": "36488", "balls": 0, "strikes": 0,
 "pitch_type": "FF", "pitch_type_name": "Four-seam FB", "velocity": 97, "px": 108, "py": 178,
 "result": "strike-looking", "in_play": false}
```

Cuts worth making:

| Cut | How | What it tells you |
|-----|-----|-------------------|
| Usage by count | group by `balls`, `strikes`, `pitch_type` | Does he go to the changeup when ahead? |
| First-pitch mix | `balls == 0 and strikes == 0` | Predictability |
| Velocity by inning | mean `velocity` by `inning`, one game or many | Fatigue; early-inning vs late |
| Velocity by season month | group pitches by game date from `game_id` joins | Season-long drop or injury sign |
| vs batter hand | join `batter_id` to `bats` from `search_players` | Platoon usage |
| Pitch to pitch | order by `at_bat_id`, `pitch_number` | Sequencing |

**Step 6: Velocity trend rules.**

1. Compare the same pitch type only; a mix change moves the pitcher-level average.
2. Use at least 30 pitches of a type per cell.
3. A 1 mph drop in a four-seam over 3 consecutive starts is worth flagging; a single start is not. Weather, mound and radar calibration vary by ballpark; say that and do not diagnose injuries.

## Sample Size

| Metric | Rule of thumb before trusting |
|--------|-------------------------------|
| Usage by pitch | 200+ pitches of the pitcher |
| Whiff rate per pitch type | 100+ swings (about 300 pitches for a changeup) |
| Platoon split usage | 500+ pitches per side |
| Velocity change | 3 starts, 30+ pitches per type per start |

These are practical floors, not API outputs.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| Find pitcher + arsenal | 6 |
| One game of pitches | 10 |
| One season of pitches (two pages) | 20 |
| Pitcher comparison (two arsenals) | 12 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Calling whiff rate "per pitch" | It is per swing | Say per swing |
| Averaging whiff rates weighted by pitch count | Needs swing counts | Recompute from `get_pitches` or avoid |
| Using `px`/`py` as feet | Pixel frame as sent | Relative location only |
| Comparing 2017 velocity to 2020 without caution | Coverage is fuller from 2018, near-complete from 2020 | State coverage |
| Reading a sinker's low whiff as poor stuff | It targets ground balls | Judge by contact outcome |
| Naming spin or movement | Not in the data | Say it is not available |
| Doing this for a college pitcher | No pitch rows | Say so |

## Output Format

```
Tarik Skubal, 2025: 3,152 pitches
Changeup 30.9% | 87.6 avg (92 max) | whiff 48.8% per swing | ball 30.0%
Four-seam 30.2% | 97.2 avg (102 max) | whiff 29.1% | ball 29.3%
Sinker 23.3% | 96.9 avg | whiff 18.9% | ball 27.2%
Slider 12.9% | 89.8 avg | whiff 26.2% | ball 32.4%
Read: changeup carries the strikeouts; fastballs set it up. Sample is a full season.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| High-whiff pitch | Build a strikeout projection | `prop-modeling` |
| Pitcher context needed | Season line and workload | `player-scouting` |
| Which pitches get hit hard | Batted-ball profile of opponents | `spray-chart-analysis` |
| Outcome value by pitch | Run value | `run-value-analysis` |
player-scouting7.66 KB

View saved version →

---
name: player-scouting
description: "Scouts MLB and college baseball players: finds a player id, pulls season-by-season batting (AVG, OBP, SLG, OPS, K%, BB%, RE24) and pitching (ERA, WHIP, K/9, BB/9) lines, game logs, season leaders and injury status, and compares players fairly. Use when the user asks about a named player, wants two players compared, asks who leads the league in a stat, or asks if a player is hurt. Do not use for a pitcher's arsenal detail: see pitch-mix-analysis. Do not use for where a hitter hits the ball: see spray-chart-analysis. Do not use for prop pricing: see prop-modeling."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Player Scouting

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `search_players` (1 credit) always comes first. Then `get_player` (5), `get_player_games` (5), `get_leaders` (5), `get_injuries` (2).

You build an honest picture of a player from rate stats, sample size and role. A hot month is not a skill change; a .380 BABIP is not a breakout.

## When to Use

- "How good is Aaron Judge?" or "How has he trended since 2022?"
- Comparing two hitters or two pitchers
- "Who leads MLB in OPS / K/9 / RE24?"
- A player's recent game log
- Is a player injured, and when is the expected return?
- Building a player list that a model or prop will use

## When NOT to Use

- Pitch types, velocity, whiff rate: `pitch-mix-analysis`
- Hit direction and batted-ball profile: `spray-chart-analysis`
- Run value per plate appearance and RISP splits: `run-value-analysis`
- Pricing a player's over/under: `prop-modeling`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `search_players` | Player id by `name` (partial ok), `limit` 1 to 50 | 1 |
| `get_player` | Every season: batting (PA, AB, H, doubles, triples, HR, BB, SO, HBP, SF, RE24, AVG, OBP, SLG, OPS, K%, BB%) and pitching (G, GS, IP, ERA, WHIP, K/9, BB/9, HR/9) | 5 |
| `get_player_games` | Game log: each game's batting and pitching line, newest first; `season`, `season_type`, `limit` up to 200 | 5 |
| `get_leaders` | Qualified leaders. `stat` one of avg, obp, slg, ops, hr, re24, bb_pct, k_pct, era, whip, so, k_per_9, bb_per_9; `min_pa`, `min_ip`, `limit` up to 100 | 5 |
| `get_injuries` | Status, injury type, expected return for listed players; filter by `team` (MLB only) | 2 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_war`, `get_fwar` | Not available. Use RE24 from `get_player` or `get_leaders` with `stat: "re24"` |
| `get_statcast`, `get_barrel_rate`, `get_exit_velocity` | Not available. No launch data. Use `get_spray_chart` trajectories |
| `get_contract`, `get_salary` | Not available |
| `get_minor_league_stats` | Not available |
| `get_splits` (vs LHP, day/night) | Not available as a call. Pull `get_plate_appearances` for the batter and group in code |
| `get_defense`, `get_oaa` | Not available |

## How It Works

**Step 1: Resolve the id.** `search_players` with `name`. Real response:

```json
{"data": {"players": [{"player_id": "33192", "name": "Aaron Judge", "position": "RF", "bats": "R", "team": "NYY"}], "count": 1}}
```

If several players match (a search for "Cole" returns Cole Hamels, Gerrit Cole, Cole Gillespie), confirm which one, by team and position, before spending 5 credits on stats. Ids are strings.

**Step 2: Season lines.** `get_player` returns `batting` and `pitching` arrays, one object per season, current season first. Real row (Judge, 2025): 679 PA, 53 HR, .331/.457/.688, OPS 1.144, K% .236, BB% .183, RE24 74.3. Real pitching row (Skubal, 2025): 195.1 IP, 2.21 ERA, 0.89 WHIP, 11.1 K/9, 1.5 BB/9. The current in-progress season is included: check `pa` or `ip` before comparing it to full years.

**Step 3: Qualify the sample.** Use these floors before saying anything is "real":

| Stat | Stabilizes roughly at | Rule |
|------|----------------------|------|
| K% (batter) | 60 PA | Trust early |
| BB% (batter) | 120 PA | Trust early-ish |
| HR rate | 170 PA | Needs a half season |
| OBP, SLG | 460 PA, 320 PA | A full season barely stabilizes them |
| BABIP, AVG | 800+ PA | Treat as noise within a season |
| K/9 (pitcher) | 70 BF | Trust early |
| ERA | Never within a season | Use K/9, BB/9, HR/9 for talent, ERA for outcome |

These are widely used approximations from public research, not API outputs. State them as rules of thumb.

**Step 4: Compare fairly.** Two hitters from different seasons: compare rate stats and RE24 per PA, not counting totals. Run `get_leaders` with the same `stat` and `season` to see where each ranks; the `qualifier` field tells you the threshold used (for pitchers, 1 inning per team game: `"162 innings"` in a full season).

```json
POST /v1/get_leaders
{"stat": "k_per_9", "season": 2025, "limit": 3}
```

Real top three: Dylan Cease 11.5 K/9 (215 SO, 168.0 IP), Garrett Crochet 11.2, Tarik Skubal 11.1.

**Step 5: Recent form.** `get_player_games` with `season` and `limit`. Roll 15 to 20 games at most for context. Report the rolling line next to the season line and say how little a 15-game window tells you.

**Step 6: Availability.** `get_injuries` with `team`. Each entry has `status` (10-Day-IL, 15-Day-IL, 60-Day-IL), `type`, `detail`, `return_date`. A `return_date` is the club's estimate, not a promise. Report it that way.

## Hitter Scouting Checklist

1. K% and BB% (plate discipline, stabilizes fast)
2. ISO = SLG minus AVG (power), from `get_player`
3. OPS and RE24 per 600 PA (production)
4. HR per fly ball, from `get_spray_chart` (is the power real?)
5. Age/role change: new team, new lineup slot (visible in `get_player_games` `batting_order`)

## Pitcher Scouting Checklist

1. K/9 minus BB/9 (command)
2. HR/9 (a volatile stat; park and fly-ball rate)
3. IP per start = `ip` divided by `gs` (workload matters for prop work)
4. Arsenal and whiff by pitch: `pitch-mix-analysis`
5. ERA vs the three above (gap = luck or sequencing)

## College Baseball

`league: "ncaabase"` works for `search_players`, `get_player`, `get_player_games`, `get_leaders`. Box scores exist for only part of each season, so a college player's line can cover fewer games than he played. Say so. No injuries, no pitch data.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| One player, season lines | 1 + 5 = 6 |
| Two-player comparison | 2 + 10 = 12 |
| Game log | +5 |
| Leaders table | 5 |
| Injury list for a team | 2 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Comparing a partial current season to full years | Different sample | Show PA/IP beside every line |
| Calling a 20-game hot streak a change | Noise at that n | Report the interval, wait for sample |
| Ranking by ERA | Mostly defense, luck and sequencing | Add K/9, BB/9, HR/9 |
| Treating RE24 as a skill stat | It is context-dependent counting | Pair with a rate stat |
| Trusting `return_date` | It is an estimate | Say "club estimate" |

## Output Format

```
Aaron Judge (RF, NYY), id 33192
2025: 679 PA, .331/.457/.688, 53 HR, K% 23.6, BB% 18.3, RE24 74.3
2024: 704 PA, .322/.458/.701, 58 HR, RE24 98.7
Trend: same profile both years (walks above 18%, strikeouts near 24%); RE24 gap is mostly 25 fewer PA and 5 fewer HR.
Sample note: full seasons both years. Current season partial (285 PA at last pull).
Injury report (2026-10-11): 10-Day-IL, calf strain, club estimate Oct 12.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| A pitcher worth a closer look | Arsenal and whiff by pitch | `pitch-mix-analysis` |
| A hitter with unusual power | Batted-ball profile | `spray-chart-analysis` |
| A player with a prop line | Model the number | `prop-modeling` |
| Context-heavy production | Run value splits | `run-value-analysis` |
prop-modeling12.7 KB

View saved version →

---
name: prop-modeling
description: "Builds player prop projections for MLB: pitcher strikeouts and outs, batter hits, total bases, home runs and RBI. Pulls prop lines and results from FastballAPI, projects workload and rates, adjusts for the opponent, converts to over and under probabilities and compares them with the devigged market. Use when the user asks about a strikeout prop, a hits or total bases line, how often a player goes over, or how to model a prop. Do not use for team-level game prediction: see walk-forward-validation. Do not use to browse lines: see odds-explorer."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Prop Modeling

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_prop_history` (10 credits, up to 200 games) for line vs result history. `get_player_props` (10) for tonight's lines by book. `get_player_games` (5), `get_player` (5), `get_team_stats` (5), `get_pitch_mix` (5) for inputs. `estimate_only: true` on `get_player_props` prices a call without charging.

You project a number of events for one player in one game, turn it into a probability, and compare with the market's devigged price. Props are one-game, high-variance bets with margins near 5% to 6% on each side. Honest conclusion most of the time: the market is about right.

MLB only. College baseball has no props.

## When to Use

- "What is the projection for Skubal's strikeouts tonight?"
- "How often does Judge go over 1.5 total bases?"
- Pitcher strikeouts (`pitcher_strikeouts`), pitcher outs (`pitcher_outs`)
- Batter hits (`batter_hits`), total bases (`batter_total_bases`), home runs (`batter_home_runs`), RBI (`batter_rbis`)
- Evaluating whether a prop model beats the closing line over a sample

## When NOT to Use

- Browsing prop lines: `odds-explorer` covers game lines; use `get_player_props` here
- Game-level win probability: `walk-forward-validation`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_player_props` | Lines by book. `game_id`: every player's props in that game. `player_id` plus `from_date` (and `to_date`): one player across games. `market`, `bookmaker`, `snapshot_type` (`closing` default, `opening`), `estimate_only` | 10 (per game returned on the player route, up to 100) |
| `get_prop_history` | One row per game for a player and `market`: closing `line`, `open_line`, `over_odds`, `under_odds`, `books`, `actual`, `result`, `margin`, plus a summary with `over_rate`. `from_date`, `to_date`, `bookmaker`, `limit` up to 200 | 10 |
| `get_player_games` | Game log: pitching rows have `starter`, `outs`, `h`, `r`, `er`, `bb`, `so`, `hr`, `pitches`, `strikes`; batting rows have `ab`, `h`, `hr`, `bb`, `so`, `batting_order` | 5 |
| `get_player` | Season line including doubles, triples, HR (for total bases) | 5 |
| `get_team_stats` | Opponent K%, BB%, OPS (omit `team` for all 30 clubs) | 5 |
| `get_pitch_mix` | Pitcher arsenal and whiff rates | 5 |
| `search_players` | Player ids | 1 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_probable_pitchers`, `get_lineups` | Not available. Props posted for a pitcher imply he is starting; confirm from the club's own announcement |
| `get_umpire`, `get_weather`, `get_park_factors` | Not available. Leave these out and say so |
| `get_prop_projection`, `get_dfs_projection` | Not available. Build with the method below |
| `get_alt_lines` | Not available. One posted line per book per market |
| `get_live_props` | Not available. Closing or opening snapshots only |
| `get_player_splits` | Not available as a call. Use `get_plate_appearances` for a batter and group by pitcher in code |

## Coverage

- Prop lines start in May 2023 (DraftKings, FanDuel, BetMGM, ESPN BET, Pinnacle and others); markets: hits, home runs, total bases, RBI, pitcher strikeouts, pitcher outs.
- Closing is the last line before first pitch. Opening is the first captured. Both per book.
- Check counts with https://fastballapi.com/data/player-props before promising a sample.

## Real Data Shape

`get_player_props` for Tarik Skubal, 2025-09-23, closing (trimmed):

```json
{"data": {"snapshot_type": "closing", "games": 1, "props": [
 {"game_id": "401697253", "game_date": "2025-09-23", "player_name": "Tarik Skubal", "market": "pitcher_strikeouts",
  "bookmaker": "draftkings", "line": 7.5, "over_odds": -146, "under_odds": 115, "source": "the_odds_api"},
 {"bookmaker": "betmgm", "line": 7.5, "over_odds": -150, "under_odds": 115},
 {"bookmaker": "fanduel", "line": 7.5, "over_odds": -132, "under_odds": 100}]}}
```

`get_prop_history` for the same pitcher, April 2025, four games:

```json
{"data": {"market": "pitcher_strikeouts", "games": [
 {"game_date": "2025-04-02", "line": 7.5, "over_odds": -110, "under_odds": -125, "books": 6, "actual": 8, "result": "over",  "margin": 0.5},
 {"game_date": "2025-04-08", "line": 7.5, "over_odds": 118,  "under_odds": -160, "books": 7, "actual": 6, "result": "under", "margin": -1.5},
 {"game_date": "2025-04-14", "line": 6.5, "over_odds": -164, "under_odds": 127,  "books": 7, "actual": 9, "result": "over",  "margin": 2.5},
 {"game_date": "2025-04-20", "line": 6.5, "over_odds": -112, "under_odds": -118, "books": 7, "actual": 4, "result": "under", "margin": -2.5}],
 "summary": {"games": 4, "overs": 2, "unders": 2, "over_rate": 0.5, "avg_line": 7, "avg_actual": 6.8}}}
```

Four games prove nothing; the summary exists to show the shape. Pull up to 200 and treat 100 as a minimum before quoting an over rate.

## Method: Pitcher Strikeouts

Strikeouts = batters faced (BF) times strikeout probability per batter. Workload (BF) is usually the larger source of uncertainty.

**Step 1: Workload.** From `get_player_games` pitching rows over the last 8 to 12 starts, take `outs` and `pitches` for rows where `starter` is true. Estimate BF per start from `outs`, hits and walks: BF is about `outs + h + bb` (misses hit-by-pitch and errors, so it understates slightly). Look at the distribution, not just the mean; skip starts cut short by injury. Check news the model cannot see: skipped starts, a new pitch limit, a recent injury.

**Step 2: Baseline K%.** From the pitching log and `get_player`, K per BF over the last full season plus the current one. Shrink toward the league mean with weight `n / (n + 70)` (K% stabilizes around 70 batters faced; rule of thumb). Skubal 2025 from real calls: 241 strikeouts, `outs` 586, 141 hits, 33 walks, so BF is about 760, K% about 31.7%.

**Step 3: Opponent adjustment (odds-ratio / log5).** Opponent K% from `get_team_stats` (`batting.k_pct`). League K% is the PA-weighted mean across the 30 clubs: 22.2% for 2025.

```python
def odds(p): return p / (1 - p)

def log5(pitcher, batter, league):
    o = odds(pitcher) * odds(batter) / odds(league)
    return o / (1 + o)
```

Real inputs: pitcher 0.317, opponent SEA batting K% 0.233, league 0.2219 → 0.331. Against TOR (0.178, the lowest club K%) the same pitcher drops well below that. This ignores park, handedness mix and lineup (see Limits).

**Step 4: Distribution.** Strikeouts out of BF batters are close to binomial. A binomial is tighter than a Poisson, which overstates tail probabilities.

```python
from math import comb

def p_at_least(n, p, k):
    return sum(comb(n, i) * p**i * (1 - p)**(n - i) for i in range(k, n + 1))

# over 7.5 means K >= 8
```

Workload dominates the answer. Same p = 0.331, only BF changes (real arithmetic):

| BF | Expected K | P(K >= 8) |
|----|------------|-----------|
| 22 | 7.3 | 45.0% |
| 24 | 7.9 | 56.6% |
| 25 | 8.3 | 62.1% |
| 27 | 8.9 | 71.6% |

So a two-batter swing in workload moves the over probability by more than 10 points. Mix over your BF distribution (weights from his recent starts) rather than picking one number.

**Step 5: Compare.** Devig the market (`odds-analysis`): DraftKings 7.5 at -146/+115 gives fair over about 56%. Your model number must beat that by more than your calibration error before it is an edge, and a single-book price is not the market: devig several books.

## Method: Pitcher Outs

Same workload model; the prop is outs recorded, which drops the strikeout rate and depends on pitches per out. Use `outs` from the pitching log, a pitch-count cap, and the hook tendency. Outs and strikeouts for the same pitcher are positively correlated: never price both as independent in a parlay.

## Method: Batter Hits

Hits ~ Binomial(AB, p), with p the per-AB hit probability, not the season average alone.

1. Expected AB: from `batting_order` in `get_player_games` (leadoff gets more PA than eighth; lineup spot is known only close to game time).
2. p: season hits per AB, shrunk to the league average with weight `AB / (AB + 400)` (average stabilizes slowly; rule of thumb). Then adjust for the opposing starter's quality with the same odds-ratio trick, using the starter's hits allowed per batter faced from his own pitching log.
3. `P(over 0.5) = 1 - (1 - p)^AB` for 0.5; sum the binomial for 1.5.

For a hitter with p = 0.25 and 4 AB: `1 - 0.75^4 = 68.4%` for at least one hit. Lines at 0.5 hits are priced near that, which is why edges are thin.

## Method: Total Bases

TB depends on hit type. `get_player_games` has only `h` and `hr`, so you cannot rebuild total bases from the log. Two options:

- Season level: `get_player` has `h`, `doubles`, `triples`, `hr`, giving TB = H + doubles + 2 x triples + 3 x HR.
- Event level: `get_plate_appearances` for the batter (10 credits per 1,000 PAs) with `result` values such as `single`, `double`, `home-run`, `walk`, `strikeout`. Group them for a per-PA outcome distribution.

Then simulate: draw PA count (3.5 to 5, by lineup slot), for each PA draw an outcome from the multinomial, sum bases. A line of 1.5 means P(TB >= 2). This distribution has a heavy right tail (one home run is 4), which is why a normal approximation misprices it. Simulate; do not use a normal curve.

## Evaluating a Prop Model

1. Pull `get_prop_history` (up to 200 games per player-market). Each row has the closing line, the prices and the `actual`. 
2. For each game, produce your probability using only data before that date (see `walk-forward-validation`; the shift-before-roll rule applies).
3. Score against the devigged closing probability: Brier score and log loss of the market vs yours. The market is the bar. 
4. Report `over_rate` against `avg_line` and your calibration by bucket. See the calibration section of `walk-forward-validation`.
5. Treat 100 graded games for one player as little; pool across players for any strong statement, with several hundred graded props per market.

## Limits to State

- No confirmed lineup, pitching change, weather, park or umpire data. Say so. 
- Props posted for a pitcher are not a guarantee he starts. A scratch voids or reprices a bet.
- Lines move; the closing line is the best information. A projection made at the opening line is not directly comparable to the closing result.
- Margins: two-way prop margins near 5% to 6% mean an estimated edge of a couple of points is inside the error of the estimate itself.

## Credit Usage

| Workflow | Credits |
|----------|---------|
| Pitcher K projection: search (1) + game log (5) + season (5) + team stats (5) + tonight's props (10) | 26 |
| + history to test (`get_prop_history`) | +10 |
| Batter total bases: search + `get_player` + one PA page + props | 26 |
| Price check before pulling | `estimate_only` costs 0 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Using season K/9 as the projection | Ignores workload and opponent | Project BF and K% separately |
| Poisson for strikeouts | Overstates tails | Binomial on BF, mixed over BF |
| Normal curve for total bases | Heavy right tail, discrete | Simulate PA outcomes |
| Pricing the over at one book only | Margins differ | Devig several books |
| Judging by 10-game over rate | Noise | 100+ graded games, pooled |
| Treating a sub-3-point edge as a bet | Inside error | Pass |
| Parlaying correlated props as independent | Correlated | Price jointly or not at all |
| Quoting a "model" with 4 starts of data | n too small | Shrink toward the league mean |

## Output Format

```
Tarik Skubal, strikeouts, vs SEA (illustration, not a pick)
Workload: 24.5 BF mean over 31 starts (BF approximated as outs + h + bb); use the spread of his recent starts
K%: 31.7% season, odds-ratio vs SEA 23.3% (league 22.2%) = 33.1%
Mean K: 8.1. P(K >= 8): 57% at 24 BF, 45% at 22 BF, 72% at 27 BF
Market: DK 7.5 at -146/+115, fair over 56% (multiplicative)
Read: model is inside the market once workload uncertainty is respected. No edge claim.
Missing: confirmed starter, weather, umpire, park.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Need the price without margin | Fair probability | `odds-analysis` |
| Pitcher arsenal question | Whiff by pitch | `pitch-mix-analysis` |
pythagorean-record9.18 KB

View saved version →

---
name: pythagorean-record
description: "Computes Pythagorean and Pythagenpat expected records from runs scored and allowed, measures how far a team's actual record is from it (luck), and projects regression for MLB and college baseball. Use when the user asks about Pythagorean record, expected wins, run differential, who got lucky or unlucky, one-run-game luck, or whether a team is as good as its record. Do not use for general standings and rankings: see team-analysis. Do not use for single-game prediction: see game-preview. Do not use to validate a model: see walk-forward-validation."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Pythagorean Record

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_standings` (2 credits) already returns `runs_scored`, `runs_allowed`, `run_differential` and `pythagorean_win_pct` for every team. `get_team_stats` (5) adds the process behind the runs.

You separate what a team did from what its scoring says it should have done. The gap is mostly luck in one-run games and sequencing, and it regresses.

## When to Use

- "Who was lucky in 2025?"
- "Is Toronto really a 94-win team?"
- "What does run differential say about the Brewers?"
- Expected wins at midseason, and what to expect the rest of the way
- Fitting a Pythagorean exponent for college baseball or another run environment

## When NOT to Use

- Standings, rankings, form: `team-analysis`
- A single game's price: `game-preview`, `odds-explorer`
- Testing a prediction model: `walk-forward-validation`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_standings` | W-L, home/away, last 10, `runs_scored`, `runs_allowed`, `run_differential`, `win_pct`, `pythagorean_win_pct` | 2 |
| `get_team_stats` | Runs per game for and against, team batting and opponents' batting | 5 |
| `get_games` | Final scores for one-run-game splits | 2 |
| `get_ratings` | Elo, a cross-check that accounts for opponent strength | 2 |

All accept `league` (`mlb`, `ncaabase`) and `season`.

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_expected_record`, `get_pythag` | Read `pythagorean_win_pct` from `get_standings`, or compute below |
| `get_base_runs`, `get_xwins` | Not available. Compute BaseRuns yourself from `get_team_stats` if needed |
| `get_one_run_record` | `get_games` for the team, then count games decided by one run |
| `get_clutch` | Not available. See `run-value-analysis` for RISP splits |

## The Formulas

**Classic Pythagorean** (James): expected win pct = RS^x / (RS^x + RA^x).

**Exponent.** The classic value is 2. A better fit for modern MLB is about 1.83. FastballAPI's own `pythagorean_win_pct` matches 1.83: checked against four 2025 teams (MIL .608, NYY .597, TOR .546, PHI .583), the exponent 1.83 reproduces the API value to three decimals each time, and the exponent 2 does not. Use 1.83 for MLB unless you fit your own.

**Pythagenpat** (Smyth/Patriot): exponent x = ((RS + RA) / G)^0.287, so the exponent rises with run environment. For 2025 MIL, x = 1.87 and the result is within a fraction of a win of the 1.83 version. It matters more for college and other high-scoring environments.

```python
def pythag(rs, ra, exp=1.83):
    return rs**exp / (rs**exp + ra**exp)

def pythagenpat(rs, ra, g):
    x = ((rs + ra) / g) ** 0.287
    return rs**x / (rs**x + ra**x)
```

**Rule of thumb:** about 10 runs of differential is about 1 win in MLB. Check it against your own data rather than quoting it blindly.

## How It Works

**Step 1: Pull standings.** `get_standings` with `season`.

```json
POST /v1/get_standings
{"season": 2025}
```

Real rows (2025, trimmed):

```json
{"data": {"season": 2025, "standings": [
 {"abbrev": "MIL", "wins": 97, "losses": 65, "runs_scored": 806, "runs_allowed": 634, "run_differential": 172, "win_pct": 0.599, "pythagorean_win_pct": 0.608},
 {"abbrev": "TOR", "wins": 94, "losses": 68, "runs_scored": 798, "runs_allowed": 721, "run_differential": 77,  "win_pct": 0.58,  "pythagorean_win_pct": 0.546},
 {"abbrev": "NYY", "wins": 94, "losses": 68, "runs_scored": 849, "runs_allowed": 685, "run_differential": 164, "win_pct": 0.58,  "pythagorean_win_pct": 0.597}]}}
```

**Step 2: Expected wins and luck.**

| Team | Wins | Expected (G x Pythag) | Luck (wins minus expected) |
|------|------|------------------------|----------------------------|
| TOR | 94 | 88.5 | +5.5 |
| PHI | 96 | 94.4 | +1.6 |
| MIL | 97 | 98.5 | -1.5 |
| NYY | 94 | 96.7 | -2.7 |

Toronto and New York finished with the same 94-68 record on very different scoring: +77 vs +164. A team that outperforms its run differential by 5 wins is a candidate for regression, with caveats below. Sort the whole league by `luck` to find the outliers.

```python
rows = get_standings(season=2025)["data"]["standings"]
for r in rows:
    g = r["wins"] + r["losses"]
    r["expected_wins"] = g * r["pythagorean_win_pct"]
    r["luck"] = r["wins"] - r["expected_wins"]
rows.sort(key=lambda r: r["luck"])
```

**Step 3: Explain the gap before predicting regression.** Over-performance of Pythagorean is not all luck. Ranked by how much of it is repeatable:

| Source of gap | Repeatable? |
|---------------|-------------|
| One-run-game record | Mostly luck (hold near .500 across teams) |
| Sequencing: hits clustered with runners on | Mostly luck |
| Elite closer plus bullpen used to win close games | Partly skill |
| Blowout wins and losses (runs wasted) | Neutral on average |
| Mid-season trades changing talent | Not luck; a timing issue (use rolling windows) |

Check the one-run record with `get_games`: count finals where the absolute run difference is 1 and split by wins. A team that went 30-15 in one-run games has a story; a team at 22-23 does not.

**Step 4: Regression.** Do not publish a forecast from rules of thumb alone. Test it:

1. Take `win_pct` and `pythagorean_win_pct` for season N and the actual `win_pct` for season N+1, for every team in 10 or more seasons (`get_standings`, 2 credits per season).
2. Fit next-year win pct on both predictors, with teams as the unit.
3. If `pythagorean_win_pct` carries more weight than `win_pct`, run differential is the better predictor and the luck term does regress. Report the coefficients and the sample size. This is a walk-forward question: fit on seasons up to N-1, test on N. See `walk-forward-validation`.

Rosters change between seasons, so this test mixes luck regression with talent turnover. Do not call all of the gap "luck".

**Step 5: Fit an exponent for another environment** (college baseball, or another era):

```python
import numpy as np
x = np.log([r["runs_scored"] / r["runs_allowed"] for r in rows])
y = np.log([r["wins"] / max(r["losses"], 1) for r in rows])
exponent = (x @ y) / (x @ x)    # least squares through the origin
```

Use only teams with a meaningful number of games.

## College Baseball

`league: "ncaabase"`. The standings list every team ESPN carries, in win-pct order, including teams with a few games. A 3-0 team with 31 runs for and 10 against has a Pythagorean of .888: meaningless at that sample. Filter on games played (for example at least 30) before sorting. ESPN does not carry every Division I game, so runs and records can be partial. Fit the exponent on the filtered set, since college run environments are higher than MLB.

## Sample Size Reality

A 162-game record has a standard error near 6 wins from luck alone (sqrt(162 x 0.25) = 6.4). Pythagorean cuts the noise but not to zero. A one-win gap is nothing. A five-win gap in a full season is notable, not decisive. Before the All-Star break, 90 games means a standard error near 4.7 wins.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| One season's league table | 2 |
| Ten seasons for a regression | 20 |
| One-run-game split for one team | 2 |
| Process stats for all teams | 5 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Calling every gap "luck" | Bullpens and trades are real | Split by source |
| Using exponent 2 and comparing to the API | Values will differ by a few thousandths | Use 1.83 or state yours |
| Projecting a team's next win total as its Pythagorean | Rosters change | Fit and test, then say the error |
| Ranking college teams on a few games | Tiny samples | Filter on games |
| Using full-season Pythagorean for a playoff series | Playoff opponents, rotations differ | Use as context only |
| Ignoring postseason in run totals | Standings are regular-season; `get_games` mixes both | Filter `season_type` |

## Output Format

```
2025 regular season, luck vs run differential
Most over expected record: TOR 94-68 vs 88.5 expected (+5.5), RD +77
Most under: NYY 94-68 vs 96.7 expected (-2.7), RD +164
Exponent: 1.83 (matches pythagorean_win_pct)
Read: Toronto's record is ahead of its scoring; New York's is behind it.
Caveat: one season; luck is not a forecast until tested (see walk-forward-validation).
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Large gap | Check Elo and process stats | `team-analysis` |
| Want to test regression | Temporal validation | `walk-forward-validation` |
| Gap explained by clutch hitting | RISP run value | `run-value-analysis` |
| Want to price a team's win total | Fair odds first | `odds-analysis` |
run-value-analysis10.1 KB

View saved version →

---
name: run-value-analysis
description: "Measures what happened in baseball in runs and win probability: RE24 run value per plate appearance, base-out state, runners in scoring position splits, win probability added and leverage-style context for MLB and college baseball. Use when the user asks about run value, RE24, run expectancy, clutch or RISP hitting, how much a home run mattered, or which plate appearances decided a game. Do not use for season rate stats: see player-scouting. Do not use for hit direction: see spray-chart-analysis. Do not use for pitcher arsenals: see pitch-mix-analysis."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Run Value Analysis

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_plate_appearances` (10 credits, up to 1,000 rows a call) has the base-out state, result, run value and win probability for every plate appearance. `get_player` (5) and `get_leaders` with `stat: "re24"` (5) give season RE24 totals.

You value outcomes by the runs and wins they add, not by the box-score label. A home run with the bases empty and two outs is worth exactly one run. A double with the bases loaded is worth far more than a double with the bases empty.

## When to Use

- "How much was that home run worth?"
- Player RE24 totals and per-PA values
- RISP (runners in scoring position) hitting, honestly framed
- Win probability swings in one game: which plate appearances decided it
- Comparing two hitters in run terms
- Building a context-neutral input for a model

## When NOT to Use

- Season rate stats (AVG, OBP, K%): `player-scouting`
- Hit direction and batted-ball type: `spray-chart-analysis`
- Pitch-type detail: `pitch-mix-analysis`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_plate_appearances` | One row per plate appearance: `before` (outs, on_first, on_second, on_third), `result`, `batted_ball` (trajectory, x, y), `runs_scored`, `outs_after`, `score_after`, `home_win_prob` after, `run_value`. Filters: `game_id`, `batter_id`, `pitcher_id`, `team`, `season`, `season_type`, `result`, `inning`, `risp`, `limit` (max 1,000), `offset` | 10 |
| `get_player` | Season RE24 beside the rate stats | 5 |
| `get_leaders` | RE24 leaders (`stat: "re24"`), qualified | 5 |
| `get_game` | Win-probability range for one game | 5 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_run_expectancy_table` | Not available as a call. Rebuild it from `get_plate_appearances` or use `run_value` directly |
| `get_leverage_index`, `get_wpa` | Not available. Compute WPA from consecutive `home_win_prob` as shown below |
| `get_war`, `get_wrc_plus` | Not available |
| `get_splits` | Filter `get_plate_appearances` (`risp`, `inning`, `result`) |
| `get_clutch_stats` | Not available. Build a RISP split and report the sample |

## What `run_value` Is

RE24 for a plate appearance:

```
run_value = runs_scored + RE(after) - RE(before)
```

where RE(state) is the average number of runs scored from that base-out state to the end of the inning, taken from each season's own run expectancy table (the API states it uses each season's own table). There are 24 states: 8 base configurations times 3 outs.

Real rows from two Judge home runs (2025):

```
Home run, bases empty, 2 outs, 1 run scored:   run_value 1.0
Home run, runner on first, 1 out, 2 runs:      run_value 1.749
```

The first is exactly 1 because the base-out state is the same before and after (bases empty, 2 outs) and one run scored. The second is above 2 minus the loss from clearing the runner: 2 runs plus RE(bases empty, 1 out) minus RE(runner on first, 1 out). The state before is more valuable than the state after, so the value lands under 2, near 1.75.

Rules this gives you: a walk with the bases empty is worth roughly the gain in expectation for adding a runner; an out with a runner on third and fewer than two outs is costly unless a run scores.

## How It Works

**Step 1: Pull the plate appearances.**

```json
POST /v1/get_plate_appearances
{"batter_id": "33192", "season": 2025, "result": "home-run", "limit": 2}
```

Real row shape (trimmed):

```json
{"game_id": "401697308", "inning": 1, "half": "bottom", "batting_team": "NYY",
 "batter": {"id": "33192", "name": "Aaron Judge"}, "pitcher": {"name": "Tomoyuki Sugano"},
 "before": {"outs": 2, "on_first": false, "on_second": false, "on_third": false},
 "result": "home-run", "description": "Judge homered to left center (383 feet).",
 "pitches": 2, "final_count": "0-1",
 "batted_ball": {"trajectory": "F", "x": 51, "y": 63},
 "runs_scored": 1, "outs_after": 2, "score_after": {"home": 1, "away": 0},
 "home_win_prob": 0.75, "run_value": 1}
```

`result` strings are lower-case hyphenated (`home-run`, `strikeout`, `walk`, `single`, `double`, `ground-out`). List the distinct values from a first page before filtering on one you have not seen.

**Step 2: Total and rate.** Sum `run_value` for a batter's season and divide by plate appearances. Judge's 2025 line from `get_player`: RE24 74.3 over 679 PA, about 0.109 runs per PA, about 65.7 runs per 600 PA. Compare per-PA values, not totals, between hitters with different playing time.

**Step 3: Break it down.** Group `run_value` by:

| Group | Why |
|-------|-----|
| `result` | Which outcomes carried the value (HR, BB, singles) |
| `before` base state | How much came from having runners on |
| `risp` filter | A hitter's value with runners in scoring position |
| `inning` | Late-inning value |
| `pitcher` team | Value against particular opponents |

**Step 4: RISP honestly.** Use `risp: true` to pull only plate appearances with a runner on second or third. Report the sample (PA count) and the rate stats next to RE24. RISP performance is mostly noise year to year: a hitter's RISP rate over one season has a wide interval, and the correlation between one year's RISP average and the next is weak. Report the RISP split, show how it compares to the same hitter's other PAs, and call a gap "clutch" only after several seasons of consistency, not one.

```python
risp = get_plate_appearances(batter_id=bid, season=2025, risp=True, limit=1000)["data"]["plate_appearances"]
allpa = get_plate_appearances(batter_id=bid, season=2025, limit=1000)["data"]["plate_appearances"]
risp_ids = {(p["game_id"], p["inning"], p["half"], p["description"]) for p in risp}
other = [p for p in allpa if (p["game_id"], p["inning"], p["half"], p["description"]) not in risp_ids]
```

Each call is 10 credits. Match rows on a stable key before splitting; the two calls return overlapping plate appearances. Check that both calls returned fewer rows than `limit`, or page with `offset`.

**Step 5: Win probability added.** `home_win_prob` is the home team's win probability after each plate appearance, available from 2019. Walk a game in order (`game_id`, ascending):

```python
prev = None
for pa in pas:                              # a whole game, in order
    wp = pa["home_win_prob"]
    if prev is not None and wp is not None:
        delta = wp - prev                   # home perspective
        wpa = delta if pa["half"] == "bottom" else -delta   # batter perspective
    prev = wp
```

The home team bats in the bottom half, so a rise in home win probability is good for the batter in the bottom and bad for the batter in the top. The first plate appearance has no previous value; the API does not give a pre-game win probability, so skip it or take the pre-game line from `get_odds`. Win probability is ESPN's model, not ours. Large swings in the 8th and 9th innings are normal, not noise.

**Step 6: Decide what to say.** Run value rewards both skill and opportunity. A leadoff hitter with the same ability gets fewer runners on than a number-four hitter. For skill, use rate stats and discipline; for what happened, use RE24. Say which you are reporting.

## College Baseball

`league: "ncaabase"` works for `get_plate_appearances`, but only part of each season's games have play-by-play (about 800 to 1,550 a season since 2021; 324 in 2020). On 94% or more of the games that have it since 2023, the play-by-play adds up to the final score. Do not compute a player's season RE24 from it and call it complete.

## Sample Size

| Statement | Needs |
|-----------|-------|
| "Hitter's RE24 per PA is X" | 300+ PA to be a rough read |
| "RISP split differs" | Several hundred RISP PAs, several seasons |
| "One PA decided the game" | A single game's WPA is descriptive, not predictive |
| "Team RE24 by lineup slot" | A full season of PAs |

## Credit Usage

| Operation | Credits |
|-----------|---------|
| One game of play-by-play (about 76 PAs) | 10 |
| A batter's season (about 650 PAs, one page of 1,000) | 10 |
| RISP-only filter for one batter-season | 10 |
| Season RE24 totals via `get_player` | 5 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Calling a hitter "clutch" off one season | RISP rates are noisy | Show sample, call it unproven |
| Comparing total RE24 across different playing time | Totals reward volume | Use per-PA or per-600 |
| Treating RE24 as a skill stat | It includes opportunity and context | Pair with rate stats |
| Using pre-2019 win probability | Not in the data | Say it begins in 2019 |
| Reusing last year's run expectancy for this year's values | The API uses each season's own table | Do not rebuild unless needed |
| Summing `run_value` and calling it WAR | WAR needs position, defense, replacement | Call it RE24 |

## Output Format

```
Aaron Judge, 2025 (regular season)
679 PA, RE24 74.3 (0.109 per PA, 65.7 per 600)
Home runs: 53; value 1.0 (solo, 2 outs) to 1.75 (runner on first, 1 out) each in the two examples pulled
RISP: [N] PA, [rate stats]; compare to non-RISP before saying anything.
Read: run value follows his on-base and slugging; the RISP split is not a separate skill claim.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| A hitter with high RE24 | Season rate profile | `player-scouting` |
| A power profile | Batted-ball direction | `spray-chart-analysis` |
| A game-deciding at-bat | The pitches that made it | `pitch-mix-analysis` |
| A team RE24 gap | Luck vs scoring | `pythagorean-record` |
spray-chart-analysis9.36 KB

View saved version →

---
name: spray-chart-analysis
description: "Reads where an MLB batter or team hits the ball: balls in play by trajectory (ground ball, line drive, fly ball, pop-up) and by field (left, center, right), hit rates, home runs per fly ball, pull and opposite-field tendencies, and ball-by-ball x/y points for drawing a spray chart. Use when the user asks where a hitter hits the ball, whether he pulls, how many of his fly balls leave the park, or wants a spray chart. Do not use for season rate stats: see player-scouting. Do not use for run value: see run-value-analysis. Do not use for pitch data: see pitch-mix-analysis."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Spray Chart Analysis

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_spray_chart` (10 credits). Add `include_balls: true` for each ball's x/y (up to 3,000 balls). `search_players` (1) first. MLB only.

You describe a hitter's batted-ball profile from the data FastballAPI holds: trajectory, field and location. There is no exit velocity or launch angle, so you describe where and what kind, not how hard.

## When to Use

- "Where does Aaron Judge hit the ball?"
- Pull, center and opposite-field shares
- Home runs per fly ball; line-drive rate
- Draw a spray chart (scatter of x/y)
- Compare a team's batted-ball tendencies with another team's
- Context for a hitter's BABIP or home run total

## When NOT to Use

- Season slash lines and plate discipline: `player-scouting`
- Run value of outcomes: `run-value-analysis`
- Pitcher arsenals: `pitch-mix-analysis`
- Exit velocity, barrels, launch angle: not available in this data

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_spray_chart` | Balls in play by trajectory and by field with hit rates. `batter_id` or `team`, `season`, `include_balls` | 10 |
| `get_plate_appearances` | Each batted ball with `trajectory`, `x`, `y`, result and run value, if you need per-event detail (filter by `batter_id`, `result`) | 10 |
| `search_players` | Find `batter_id` | 1 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_exit_velocity`, `get_launch_angle`, `get_barrels`, `get_xwoba` | Not available |
| `get_hit_distance` | The `description` text on a `get_plate_appearances` home run includes distance in feet (for example "383 feet"); parse it, and do not assume other hits have it |
| `get_shift_data` | Not available |
| `get_hard_hit_rate` | Not available. Line-drive rate is the nearest proxy |
| `get_spray_chart` for college | Not available |

## Trajectory and Field Codes

| Code | Meaning |
|------|---------|
| `G` | Ground ball |
| `L` | Line drive |
| `F` | Fly ball |
| `P` | Pop-up |
| `unknown` | Not classified (a handful per season) |

Fields are `left`, `center`, `right`, `unknown`. For a right-handed hitter, left field is the pull side; for a left-handed hitter, right field is. Get the handedness from `search_players` (`bats`: R, L, or null for some players) before labeling pull or opposite.

## How It Works

**Step 1: Pull the chart.**

```json
POST /v1/get_spray_chart
{"batter_id": "33192", "season": 2025}
```

Real response (Aaron Judge, 2025):

```json
{"data": {"season": 2025, "balls_in_play": 410,
 "by_trajectory": [
  {"trajectory": "F", "balls": 154, "hits": 70, "hr": 52, "hit_rate": 0.455},
  {"trajectory": "G", "balls": 138, "hits": 50, "hr": 0,  "hit_rate": 0.362},
  {"trajectory": "L", "balls": 95,  "hits": 72, "hr": 2,  "hit_rate": 0.758},
  {"trajectory": "P", "balls": 22,  "hits": 0,  "hr": 0,  "hit_rate": 0}],
 "by_field": [
  {"field": "left",   "balls": 168, "hits": 99, "hr": 24, "hit_rate": 0.589},
  {"field": "center", "balls": 122, "hits": 34, "hr": 6,  "hit_rate": 0.279},
  {"field": "right",  "balls": 119, "hits": 59, "hr": 24, "hit_rate": 0.496}]}}
```

Two things to notice. First, `balls_in_play` here includes home runs (52 of the 154 fly balls are home runs), so it is a count of batted balls, not the usual "in play excluding HR". Second, `hits` includes home runs, and `hit_rate` is hits divided by balls.

**Step 2: Derive the numbers that matter.**

| Metric | Formula | Judge 2025 (from the response) |
|--------|---------|--------------------------------|
| Pull share (RHH) | left balls / total balls | 168 / 410 = 41.0% |
| Center share | center / total | 122 / 410 = 29.8% |
| Opposite share | right / total | 119 / 410 = 29.0% |
| Fly-ball share | F / total | 154 / 410 = 37.6% |
| HR per fly ball | F.hr / F.balls | 52 / 154 = 33.8% |
| Line-drive share | L / total | 95 / 410 = 23.2% |
| Ground-ball share | G / total | 138 / 410 = 33.7% |
| BABIP-like (excluding HR) | (hits - hr) / (balls - hr) | (192 - 54) / (410 - 54) = 38.8% |

The BABIP-like number excludes strikeouts and sacrifices differently than official BABIP (strikeouts are not batted balls, and sacrifice flies are in or out depending on the source), so call it "batted-ball hit rate without home runs". Do not compare it to a published BABIP to the third decimal.

**Step 3: Reconcile with the season line.** In this live call the chart showed 54 home runs and 192 hits, while `get_player` showed 53 home runs and 179 hits for the 2025 regular season. The chart's season appears to include postseason balls in play. Before publishing a count, reconcile the two sources, or label the chart "all games in the season, regular and postseason". Do not mix numerators from one source with denominators from another.

**Step 4: Read it.**

- A high `HR per fly ball` is a mix of skill and sample. Compare across two or more seasons, and against the league with a `team` call, before calling it repeatable.
- Trajectory comes from the feed's scoring, so line-drive and fly-ball labels may not match another source's. Compare within this data, across several seasons, before concluding.
- Pull share with high fly-ball share is the profile of a pull-side power hitter. Pop-ups are almost always outs (0 hits in 22 for Judge).
- A hitter's hit rate by field tells you where his hits come from, not where he should be defended. There is no shift data in this API.

**Step 5: Draw it.** With `include_balls: true`, each ball has an `x`, `y`, trajectory and result. Plot as a scatter.

```python
import matplotlib.pyplot as plt
balls = get_spray_chart(batter_id="33192", season=2025, include_balls=True)["data"]["balls"]
colors = {"F": "tab:blue", "G": "tab:green", "L": "tab:orange", "P": "tab:gray"}
plt.scatter([b["x"] for b in balls], [b["y"] for b in balls],
            c=[colors.get(b["trajectory"], "k") for b in balls], s=14)
plt.gca().invert_yaxis()
```

Each entry in `balls` has `x`, `y`, `trajectory` and `result` (real examples: `{"x": 103, "y": 80, "trajectory": "L", "result": "line-out"}`, `{"x": 15, "y": 43, "trajectory": "F", "result": "home-run"}`). In the live call, 409 of 410 balls had coordinates. The response's `balls_note` gives the frame: ESPN's spray image is 250 x 250 pixels, home plate near (125, 205), with y decreasing toward center field. That is an image frame, so inverting the y axis in the plot (as above) puts home plate at the bottom. These are pixels, not feet: do not convert to distances. The only distances in the data are in the `description` text of home runs in `get_plate_appearances`.

**Step 6: Team spray.** `team` instead of `batter_id` returns the same aggregates for a lineup. Use it to compare how two clubs hit: fly-ball share and pull share explain a lot of the home run difference between clubs, along with parks (not in the data).

## Sample Size

| Statement | Needs |
|-----------|-------|
| Pull / center / opposite shares | 150+ batted balls for a rough read (about a third of a season) |
| Ground-ball and fly-ball shares | 100+ batted balls |
| HR per fly ball | 300+ fly balls to be stable; a season is not enough, so combine seasons |
| Line-drive hit rate | Noisy; treat as descriptive |

## Credit Usage

| Operation | Credits |
|-----------|---------|
| One batter-season chart | 10 |
| Chart with each ball (`include_balls`) | 10 |
| Find batter id | 1 |
| Two hitters compared | 22 |

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Calling a ball "hard hit" | No exit velocity | Say line drive or fly ball |
| Treating `balls_in_play` as excluding HR | It includes them | Subtract `hr` |
| Treating `x`, `y` as feet | ESPN's frame | Plot relatively |
| Labeling pull for a switch-hitter without splitting | Side varies by pitcher | Split by batter side first (play-by-play) |
| Quoting HR/FB as a talent level after one season | High variance | Combine seasons |
| Applying this to a college hitter | No batted-ball locations for college | Say it is not available |
| Mixing chart totals with `get_player` totals | The chart can include postseason | Reconcile or label |

## Output Format

```
Aaron Judge (RHH), 2025: 410 batted balls (includes HR)
Trajectory: fly 37.6%, ground 33.7%, line 23.2%, pop 5.4%
Field: left (pull) 41.0%, center 29.8%, right 29.0%
HR/FB 33.8% (52 of 154). Batted-ball hit rate without HR 38.8%.
Note: chart total of 54 HR includes postseason; regular-season line shows 53.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| A power profile | Season line and run value | `player-scouting`, `run-value-analysis` |
| A hitter's weak spots | Pitcher arsenals that attack them | `pitch-mix-analysis` |
| A hit-total prop | Projection | `prop-modeling` |
| Two clubs' contrasts | Team view | `team-analysis` |
team-analysis7.56 KB

View saved version →

---
name: team-analysis
description: "Analyzes MLB and college baseball teams: standings, home and away splits, runs scored and allowed, team batting and run prevention, Elo power ratings and strength of schedule. Use when the user asks how a team is doing, who the best team is, which offense or pitching staff is better, or wants a power ranking. Do not use for single-player questions: see player-scouting. Do not use for Pythagorean luck analysis on its own: see pythagorean-record. Do not use for a single upcoming game: see game-preview."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Team Analysis

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_standings` (2 credits), `get_ratings` (2), `get_team_stats` (5), `get_games` (2) for splits and schedule strength.

You describe how good a team is and why, using results (standings), process (rates from every plate appearance) and our Elo. Records alone mislead in baseball: 162 games is a short sample for team quality.

## When to Use

- "How are the Brewers doing?"
- "Who is the best team in baseball?" (power ranking)
- "Which offense is best? Which staff prevents runs?"
- Comparing two teams before a series
- A division race or wild-card picture from the standings
- College baseball team ratings and tournament seeding context

## When NOT to Use

- One player's performance: `player-scouting`
- Whether a record is lucky: `pythagorean-record` (this skill points there)
- A specific game and its price: `game-preview`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_standings` | Record, home/away, last 10, runs scored and allowed, run differential, Pythagorean win pct | 2 |
| `get_ratings` | Elo for every team, ranked, as of season end or today | 2 |
| `get_team_stats` | Runs per game for and against; team batting and opponents' batting (AVG, OBP, SLG, OPS, K%, BB%, HR) | 5 |
| `get_games` | A team's games, for splits and schedule strength | 2 |
| `get_injuries` | Current injured list (MLB only) | 2 |
| `list_teams` | Team ids and abbreviations | 1 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_team_roster`, `get_depth_chart` | Not available. Use `get_leaders` and `get_player` for named players |
| `get_bullpen_stats` | Not available. Derive from `get_game` pitching rows where `starter` is false |
| `get_team_defense`, `get_drs` | Not available. Use opponents' batting from `get_team_stats` as a rough proxy |
| `get_strength_of_schedule` | Compute from `get_games` and `get_ratings` as shown below |
| `get_playoff_odds` | Not available. Simulate with Elo if the user wants a number |

## How It Works

**Step 1: Standings snapshot.** `get_standings` with `season`. Each row has `wins`, `losses`, `home_wins`, `home_losses`, `away_wins`, `away_losses`, `last10_wins`, `runs_scored`, `runs_allowed`, `run_differential`, `win_pct`, `pythagorean_win_pct`. Real row (2025 Brewers): 97-65, 806 runs scored, 634 allowed, +172, `pythagorean_win_pct` 0.608.

**Step 2: Process stats.** `get_team_stats` with `team` and `season`. Real shape (2025 Yankees, trimmed):

```json
{"data": {"teams": [{"abbrev": "NYY", "games": 162,
  "runs_per_game": 5.24, "runs_allowed_per_game": 4.23, "run_differential": 164,
  "batting": {"hr": 274, "avg": 0.251, "obp": 0.332, "slg": 0.455, "ops": 0.787, "k_pct": 0.235, "bb_pct": 0.102},
  "pitching_against": {"hr": 175, "avg": 0.229, "obp": 0.305, "slg": 0.379, "ops": 0.684, "k_pct": 0.236, "bb_pct": 0.092}
}]}}
```

Reading it: offense is `batting`; run prevention is `pitching_against` (opponents' line against this team, so it mixes pitching and defense). A team with a .787 OPS and a .684 OPS against is a plus team on both sides.

**Step 3: Power rating.** `get_ratings` returns Elo, 1500 average. The method note in the response says a 100-point gap is about a 64% favourite on a neutral field, and ratings regress a third of the way to the mean between seasons. Real values, 2025 end: LAD 1578.3, MIL 1564.4, PHI 1559.5, NYY 1557.2. A 14-point gap between the top two is noise; do not rank on it.

**Step 4: Reconcile the three.** Compare:

| Signal | Source | Question it answers |
|--------|--------|---------------------|
| Record | `win_pct` | What happened |
| Run differential / Pythagorean | `run_differential` | What the scoring says should have happened |
| Elo | `get_ratings` | Who beat whom, weighted for opponents and margin |

When they disagree, say why. Example from the 2025 standings: Toronto won 94 games but its run differential (+77) implies about .546, roughly 88 to 89 wins. That is a record the underlying scoring did not fully earn. See `pythagorean-record`.

**Step 5: Splits and form.** `last10_wins` is a 10-game window: treat as color, not signal. Home/away splits over 81 games each carry roughly a 4.5-win standard error per side; do not call a team "a road team" off 81 games.

**Step 6: Strength of schedule (optional).**

```python
games = get_games(team="MIL", season=2025, status="final", limit=200)["data"]["games"]
elo = {r["abbrev"]: r["rating"] for r in get_ratings(season=2025)["data"]["ratings"]}
opp = [g["away"]["abbrev"] if g["home"]["abbrev"] == "MIL" else g["home"]["abbrev"] for g in games]
sos = sum(elo[o] for o in opp) / len(opp)
```

End-of-season Elo is a mild look-ahead for early-season games; say so. Postseason games are in the list unless you filter `season_type: "regular"`.

## College Baseball

Pass `league: "ncaabase"`. `get_standings`, `get_ratings` and `get_team_stats` work. There are no conference-adjusted ratings beyond our Elo, schedules differ enormously in strength, and ESPN does not carry every Division I game, so records may be incomplete. Always show games played next to the record. The college standings list every team ESPN carries, sorted by win percentage, so a team with 3 games and a 3-0 record can top the list. Filter on games played (wins plus losses) before ranking anything.

## Credit Usage

| Operation | Credits |
|-----------|---------|
| Standings + ratings (league view) | 4 |
| One team's process stats | 5 |
| All 30 teams' process stats (`get_team_stats` with no `team`) | 5 |
| Team schedule strength | 2 + 2 |

`get_team_stats` without `team` returns the whole league in one 5-credit call. Prefer that to 30 calls.

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Ranking by win-loss alone | One-run luck swings records by several wins | Show run differential and Elo next to it |
| Calling opponent OPS "pitching" | It includes defense and park | Label it "run prevention" |
| Using last 10 as a trend | n = 10 | Report it, do not conclude from it |
| Comparing 2020 (60 games) to other years | Different sample | Scale rates, not counts |
| Treating a 20-point Elo gap as meaningful | Elo error is larger | Group teams in tiers |
| Quoting a record without a date | Standings move daily | Say "through [date]" |

## Output Format

```
Milwaukee Brewers, 2025 (regular season)
Record: 97-65 (.599). Home 52-29, away 45-36. Last 10: 4-6.
Runs: 806 scored, 634 allowed, +172. Pythagorean .608 (98.5 wins).
Elo: 1564.4 (2nd). Gap to 1st (LAD, 1578.3): inside noise.
Read: a real top-two team; record matches its scoring.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Record far from Pythagorean | Quantify luck and expected regression | `pythagorean-record` |
| An offense or staff that stands out | Find the players driving it | `player-scouting` |
| Two teams about to meet | Matchup report | `game-preview` |
| Want a win probability model | Validate it properly | `walk-forward-validation` |
walk-forward-validation10 KB

View saved version →

---
name: walk-forward-validation
description: "Evaluates baseball prediction models honestly with time-ordered walk-forward splits: leakage-safe rolling features, expanding vs sliding windows, baselines (home field, Elo, closing line), log loss, Brier score and calibration checks. Use when the user asks how to validate a model, whether cross-validation is okay, how to engineer features without leakage, or whether a model's accuracy is real. Refuses random k-fold on games. Do not use for odds math: see odds-analysis. Do not use for prop pricing: see prop-modeling."
metadata:
  version: 1.0.0
  author: FastballAPI
---

# Walk-Forward Validation

> **Default data tool:** FastballAPI (`fastballapi-tool`).
> `get_games` (2 credits per call, up to 200 games) for results, `get_standings` (2) and `get_ratings` (2) for as-of-season context, `get_odds` (10 per date) for a market baseline. This skill is mostly method.

You make a model's score honest. Baseball is the hardest major sport to predict from team data: even the best clubs win about 60% of games, so the gap between a real model and noise is a couple of percentage points. Leakage or a bad split can create that much by itself.

## When to Use

- "Is my win-probability model any good?"
- "Can I use k-fold cross-validation on games?" (no)
- Choosing features and making sure they use only past information
- Comparing a model with Elo, home field and the closing line
- Checking whether predicted probabilities are calibrated
- Setting up a train/test structure for props or totals

## When NOT to Use

- Computing fair probabilities from prices: `odds-analysis`
- Pitcher or hitter prop math: `prop-modeling`
- Team record luck: `pythagorean-record`

## Commands Available

| Command | What it does | Credits |
|---------|--------------|---------|
| `get_games` | Final scores by season, date range or team; `limit` up to 200, `offset` to page | 2 per call |
| `get_standings` | Season standings (use only as-of-prior-season for features) | 2 |
| `get_ratings` | Elo, "as of the end of a season or today" | 2 |
| `get_odds` | Closing prices for a date (10) or a game (5), with `result` for finished games | 5 or 10 |
| `get_team_stats` | Season team process stats | 5 |

## Commands That Do NOT Exist

| Not available | Use instead |
|---------------|-------------|
| `get_train_test_split`, `evaluate_model` | Implement the splits and metrics below |
| `get_rolling_stats`, `get_team_form` | Build from `get_games` with a lag, as shown |
| `get_elo_history` | `get_ratings` is end-of-season or today only. Build your own game-by-game Elo from `get_games` for as-of features |
| `get_probable_pitchers` | Not available. Pitcher identity cannot be a pre-game feature from this API |
| `get_lineups`, `get_weather`, `get_park_factors` | Not available |

A hard limit to say up front: `get_ratings` returns one rating per team per call, as of a season's end or now. Using `get_ratings` for season N as a feature for games in season N is leakage. Use the previous season's end rating, or build a rolling Elo yourself.

## Why Random K-Fold Fails

Random folds put games from the same week in train and test. A team's April and May games share its roster, its Elo and its streaks. The model sees the future of the team's season and the test score rises. Measure it: run both a random k-fold and a walk-forward split on the same data and report the gap. If the random score is higher, that gap is the leak.

Walk-forward means every prediction is made with only information that existed before first pitch.

## Splits

**Expanding window (default).** Train seasons 2015 to N-1, test season N, for N from 2019 to the latest complete season. The train set grows each fold.

**Sliding window.** Train on the last K seasons only, if the older data hurts (rule changes, the 2020 shortened season, pitch-clock effects). Test this with data, not by assumption.

**Within a season (for in-season models).** Train through date D, predict D+1 to D+7, step forward weekly. Same rule: nothing after D enters the features.

Always report per-fold results, not just an average. Seasons differ.

## Leakage-Safe Features

Every feature is built as of the start of the game.

```python
import pandas as pd

games = games.sort_values(["date", "game_id"])
# long format: one row per team per game
g["runs_for_roll10"] = (g.groupby("team")["runs_for"]
                         .transform(lambda s: s.shift(1).rolling(10, min_periods=5).mean()))
```

The `shift(1)` is the whole point: a rolling mean that includes the current game contains the answer. Rules:

1. Shift before rolling or expanding.
2. Season-level features (win pct, run differential) come from games before the date, not the final standings. Do not pull `get_standings` for the season being predicted.
3. Reset or damp features at season boundaries (rosters change over the winter); carry over with shrinkage, not at full strength.
4. Early-season rows with few prior games are noisy. Use `min_periods` and shrink toward the league mean: `(n * team_value + k * league_value) / (n + k)` with k picked by validation.
5. Doubleheaders and same-day games: order by `starts_at`, and exclude a same-day game's result from the other game's features.
6. Starting pitchers are usually the biggest input in baseball and this API has no probable-pitcher data. Team-only models are therefore structurally capped. Say that when reporting accuracy.

Candidate features that are available and leakage-safe: rolling run differential, prior-season Elo, home/away flag, rest days (from schedule dates), rolling team batting OPS from `get_games`-derived runs and hits, bullpen usage from recent box scores.

## Baselines the Model Must Beat

| Baseline | Accuracy / log loss reference |
|----------|-------------------------------|
| Always pick home | Home win rate over your sample |
| Prior-season Elo | Strong simple baseline |
| Closing moneyline, devigged | The market. The hardest baseline |

For the market baseline, `get_odds` with `date` returns closing prices; devig per `odds-analysis`. Results are in `result.favourite_won` for finished games. If your model does not beat the closing line on log loss, it is not adding information the market lacks.

Report accuracy alongside a bootstrap interval: with about 2,430 regular-season games, a standard error on accuracy is about 1 percentage point (sqrt(0.25 / 2430) = 0.0101). A 52% model against a 54% market is not close to settled.

## Metrics

```python
import numpy as np
def log_loss(y, p): p = np.clip(p, 1e-6, 1 - 1e-6); return -np.mean(y*np.log(p) + (1-y)*np.log(1-p))
def brier(y, p): return np.mean((p - y) ** 2)
```

- **Log loss** punishes confident misses. Primary metric.
- **Brier** is less sensitive to extremes. Report both.
- **Accuracy** only as a secondary number; it hides probability quality.
- Compare log loss to the market's (devigged) log loss on the same games.

## Calibration

A probability of 60% should win about 60% of the time.

1. Bin predictions (deciles, or fewer if the sample is small) in the test folds only.
2. For each bin: mean predicted, observed win rate, count.
3. Plot or tabulate. Bins with under 100 games are noise.
4. If it is off, fit Platt scaling (logistic on the logit of the score) or isotonic regression on a separate calibration fold (the fold before the test fold), never on the test fold.
5. Re-check on the test fold after calibrating.

Baseball outcomes have low signal: most calibrated predictions will sit between 40% and 65%. A model that outputs 80% on a regular-season game is almost certainly overfit.

## Worked Layout

```
Folds (expanding):  train 2015-2018 -> test 2019
                    train 2015-2019 -> test 2021   (2020 treated separately: 60 games)
                    train 2015-2021 -> test 2022
                    ...
Per fold report:    games, accuracy, log loss, Brier, market log loss, difference
Pooled:             mean difference and a bootstrap 95% interval over games
Calibration:        decile table on pooled test predictions
```

Odds exist per book from July 2020 and as ESPN's final line earlier (see `fastballapi-tool`), so the market baseline has a shorter history than results. Say which seasons the comparison covers.

## Credit Planning

| Task | Credits |
|------|---------|
| One season of games (about 2,430 games, 13 calls at 200) | about 26 |
| Ten seasons of games | about 260 |
| Closing odds for 162 days of one season (`get_odds` by date) | about 1,620 |
| Closing odds for ten seasons | about 16,000: plan the budget first |

Pull results once, store them, and add features locally.

## Anti-patterns

| Mistake | Why it is wrong | Do This |
|---------|-----------------|---------|
| Random k-fold on games | Leaks the future | Walk-forward |
| Rolling mean without `shift(1)` | The current game is in the feature | Shift first |
| `get_ratings` for season N as a feature in season N | End-of-season value includes future games | Previous season's end rating |
| Final standings as features | Leakage | Games-before-date aggregates |
| Tuning on the test fold | Optimistic | Tune on an inner earlier fold |
| Reporting accuracy only | Hides probability quality | Log loss and calibration |
| Calling a 1-point gain real | Inside the standard error | Interval over games |
| Including 2020 as normal | 60-game season, different conditions | Handle separately |
| Skipping the market baseline | Can't tell if you add information | Compare with devigged closes |

## Output Format

```
Walk-forward, expanding window, test seasons 2019, 2021-2025 (2020 excluded)
Games: [N]. Model log loss 0.6xx vs prior-season Elo 0.6xx vs devigged close 0.6xx
Accuracy: x% (bootstrap 95% interval a to b); home baseline y%
Calibration (deciles, pooled): within z points in every bin with 100+ games
Leakage checks: shift(1) before rolling; no same-season ratings; no final standings
Read: does or does not beat the closing line; sample covers [seasons].
Limits: no starting pitcher, lineup or weather data in the API.
```

## What to Do Next

| You found | Next | Skill |
|-----------|------|-------|
| Need market probabilities | Devig | `odds-analysis` |
| A team-level luck question | Expected record | `pythagorean-record` |

Release notes

Initial release. Read-only MLB baseball 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
FastballAPI
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 MLB and college baseball teams and players
  • Read standings, schedules and scores
  • Read batting, pitching and leaderboard stats
  • Read pitches, pitch mix and spray charts
  • Read sportsbook odds and prop lines (information only)

Package observed Nov 3, 2026.

Technical details
First seen
Oct 30, 2026 · 00:00 UTC
Last seen
Nov 4, 2026 · 06:00 UTC
Collection status
Collected

plugin_asdk_app_6ad14649a5748191bff0e95ed590320c

Download plugin data (JSON)