← Plugin catalog
Data & Analytics

FighterAPI

FighterAPI v1.0.0

MMA fight 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 fighters, events and bouts
  • Read upcoming cards and past results
  • Read fighter profiles and fight history

Cost & access

FighterAPI 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

Who are the top-rated UFC fighters right now?

View package source →

Description

From the marketplace listing

FighterAPI connects ChatGPT and Codex to MMA data from FighterAPI. It covers the UFC, PFL and Bellator with events, cards, fights, fighter profiles and weight-class Elo ratings. Ask for event cards and schedules, fight results and methods, fighter profiles and fight history, striking and grappling leaders, weight-class Elo ratings, sportsbook odds and line movement. 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 FighterAPI account. Limits: FighterAPI 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 FighterAPI 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 FighterAPI's own Elo-style ratings, not official rankings. Not affiliated with the UFC, PFL, Bellator or any promotion. 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.

Matches for “stats”

Exact text from the indicated source. A mention alone does not establish support for your task.

Publisher subtitle

MMA fight data and stats

Publisher keywords · listing

mma sports-data sports-analytics stats fighterapi

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 · 62.5 KBBrowse files →
Skill instructions
career-rates6.46 KB

View saved version →

---
name: career-rates
description: "Ranks and profiles UFC fighters on career rates from the stat sheets: significant strikes landed and absorbed per 15 minutes, accuracy, defense, takedowns, control share, submission attempts, knockdowns and finish rate. Use when the user asks who the best striker, wrestler or finisher is, who has the best chin or defense, or how a fighter compares to a division. Do not use for a single fight's rounds -- see round-by-round-stats. Do not use for a full scouting report -- see fighter-scouting."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Career Rates

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_leaders` (5 credits) for a ranked list on one stat. Use `get_fighter` (5 credits) for one fighter's full `career_rates`.

You are an expert in combat sports statistics. Your goal is to rank and compare fighters on rates that are fair across fight lengths, with minimum-fight filters that keep one lucky night from topping a list.

## When to Use

- "Who lands the most significant strikes per minute at 155?"
- "Best takedown defense in the UFC"
- "Who finishes the most"
- "How does this fighter's output compare to the division?"

## When NOT to Use

- Per-round detail inside one fight -- see `round-by-round-stats`
- A full profile with record and form -- see `fighter-scouting`
- Predicting a fight -- see `fight-preview`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_leaders` | Leaderboard on one `stat`, filtered by `weight_class`, `since`, `min_fights` | 5 |
| `get_fighter` | One fighter's `career_rates` and rating | 5 |
| `search_fighters` | Find a fighter id | 1 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_stats_leaders` or `get_top_fighters` | `get_leaders` |
| `get_pound_for_pound` | `get_ratings` (our Elo, not an official list) |
| `get_career_stats` | `get_fighter`, `career_rates` |
| `get_leaders` with several stats at once | One call per stat |

## The Eleven Leaderboard Stats

`get_leaders` takes `stat` as one of:

| `stat` | Meaning |
|--------|---------|
| `sig_landed_per_15` | Significant strikes landed per 15 minutes |
| `sig_absorbed_per_15` | Significant strikes absorbed per 15 minutes |
| `sig_accuracy` | Share of significant strikes that land |
| `sig_defense` | Share of opponent significant strikes that miss |
| `td_per_15` | Takedowns landed per 15 minutes |
| `td_accuracy` | Takedowns landed over attempted |
| `td_defense` | Opponent takedowns stopped |
| `knockdowns` | Total knockdowns |
| `sub_attempts_per_15` | Submission attempts per 15 minutes |
| `control_share` | Share of fight time in control |
| `finish_rate` | Wins by KO/TKO or submission as a share of wins |

Other parameters: `weight_class` (for example Lightweight), `since` (a year, counts only fights from then on), `min_fights` (default 5, up to 50) and `limit` (default 20, up to 100). Each row has `rank`, `fighter_id`, `name`, `weight_class`, `fights` and `value`.

Real call (Lightweight, `sig_accuracy`, `min_fights` 8): first place Benoit Saint Denis at 0.613 over 12 fights. Quote results as returned and say the filter you used.

## Choosing a Minimum

| Stat | Reasonable `min_fights` | Why |
|------|------------------------|-----|
| Per-15 and accuracy stats | 8 or more | Rates stabilize slowly; 3 fights is noise |
| `finish_rate` | 8 or more | Few fights makes 100% or 0% common |
| `knockdowns` | Use with `since` and compare per fight | A raw count favours long careers |
| `td_defense` | 8 or more, and check attempts faced | 100% defense on 2 attempts means nothing |

`get_leaders` does not return attempts faced. If a defense stat looks perfect, pull `get_fighter` for that fighter and check `minutes_fought` and `td_per_15` context before trusting it.

## Style Profiles

Build a style label from percentiles within the weight class, not from raw values.

1. Pull `get_leaders` for `sig_landed_per_15`, `td_per_15` and `control_share` with a large `limit` (100).
2. Place the fighter's `career_rates` among the returned values.
3. Label: high strikes and low takedowns is a striker; high takedowns and control is a grappler; high on both is a pressure all-rounder.

This costs 15 credits for three leaderboards. State it first. Leaderboards are capped at 100 per call, so a fighter outside the top 100 on a stat will not be located by position. Use the `value` of the 100th row as a threshold and say "outside the top 100".

## Combining Rates

- **Net striking** = `sig_landed_per_15` minus `sig_absorbed_per_15`. Both are per 15 minutes, same scale, so the difference is valid.
- **Never subtract an accuracy from a per-15 rate.** Different scales.
- **Durability proxy** = `sig_defense` together with `sig_absorbed_per_15`. A fighter can absorb few strikes by never fighting at range. Pair them with KO losses from `get_fighter`.
- **Finishing profile** = `finish_rate` together with `knockdowns` and `sub_attempts_per_15`.

## Pitfalls

- Rates are career averages. A fighter's best years and worst years blend. Use `since` for a recent window and compare.
- Weight-class moves: with `weight_class` set, only fights in that class count. A fighter's fights at other weights are excluded. Say so.
- UFC only. PFL and Bellator fighters have no `career_rates`.
- Opponent quality is not in the rate. A high `sig_landed_per_15` against weak opposition is not equal to the same number against contenders. Pair with `elo-by-weight-class`.
- Early finishes produce small minute totals. `minutes_fought` is in `get_fighter`.

## Worked Example

Request: "Where does Topuria rank at lightweight on striking accuracy?"

```
get_leaders(stat="sig_accuracy", weight_class="Lightweight", min_fights=8, limit=100)
get_fighter(fighter_id="4350812")
```

`get_fighter` gave `sig_accuracy` 0.501 over 104 minutes. Find the position in the leaderboard by `fighter_id`, report it with the filter, and note the fighter might be excluded if fewer than 8 fights qualify (he had 10 UFC bouts at the time of the call, so he would qualify at 8).

## Output Format

```
Lightweight, sig_accuracy, min 8 fights, since 2020
1  Benoit Saint Denis  0.613  (12 fights)
2  ...
Fighter: Topuria 0.501, rank N of M (placed by fighter_id)
Caveat: sample, opponent quality not controlled
```

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Fighter stands out | `fighter-scouting` |
| Want rates as model features | `walk-forward-validation` |
| Want division Elo context | `elo-by-weight-class` |
dispatch6.46 KB

View saved version →

---
name: dispatch
description: "Routes a FighterAPI request to the two or three skills that should load together. Use first when a UFC, PFL or Bellator request arrives and it is unclear which skill applies, for example 'break down this card', 'is the favourite overpriced', 'who has the best cardio', or 'backtest a model'. Do not use once the skill is obvious. Do not use for the data itself -- see fighterapi-tool."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Dispatch

> **Default data tool:** FighterAPI (`fighterapi-tool`). Dispatch makes no tool calls itself.

Load the smallest set of skills that answers the request. Loading all sixteen wastes context and credits. Pick one primary skill, add at most two supporting skills.

## Routing Table

| User Says | Primary | Add |
|-----------|---------|-----|
| "Who won, how, what round" | `fight-lookup` | |
| "What happened at UFC 300" or "show me last Saturday's card" | `event-lookup` | `fight-lookup` |
| "Tell me about this fighter" | `fighter-scouting` | `career-rates` |
| "Best strikers, best grapplers, who has the best chin" | `career-rates` | `fighter-scouting` |
| "How does he fade in round 3", "strikes per round", "control time" | `round-by-round-stats` | `fight-lookup` |
| "Top Elo at welterweight", "who is underrated by rating" | `elo-by-weight-class` | `probability-calibration` |
| "Preview this fight" or "who wins" | `fight-preview` | `fighter-scouting`, `odds-explorer` |
| "Best price", "which book", "did the line move" | `odds-explorer` | |
| "Convert odds", "remove the vig", "true probability" | `odds-analysis` | |
| "Are favourites overpriced", "do -300 favourites win 75%" | `favourite-calibration` | `odds-analysis` |
| "Is my model good", "is this model calibrated" | `probability-calibration` | `walk-forward-validation` |
| "Test my model without cheating" | `walk-forward-validation` | `probability-calibration` |
| "What can I call, what does it cost" | `fighterapi-tool` | |

## Multi-skill Workflows

| Workflow | Skill Order |
|----------|-------------|
| Fight-night card breakdown | `event-lookup` then `fight-preview` for each fight the user picks |
| Build and validate a rating model | `elo-by-weight-class` then `walk-forward-validation` then `probability-calibration` |
| Scout a division | `elo-by-weight-class` then `career-rates` then `fighter-scouting` |

## Disambiguation

- "Odds" can mean looking at prices (`odds-explorer`) or converting and devigging them (`odds-analysis`). If the user has a number in hand and wants a probability, `odds-analysis`. If they want to see what books are posting, `odds-explorer`.
- "Rating" in this pack is our Elo from `get_ratings`. It is not an official promotion ranking. Say so when a user asks for "the rankings".
- "Stats" for a single fight means `fight-lookup`. Stats as rates over a career means `career-rates`. Stats by round means `round-by-round-stats`.
- A request about PFL or Bellator uses the same skills with `league=pfl` or `league=bellator`. Stat sheets are null there, so route round and rate requests to a UFC alternative or say they do not exist.

## Scope Guards

- There are no player props, method-of-victory or round-group markets in the data. Lines are moneylines and total rounds. Decline prop-modeling requests and say what is available.
- There are no teams. Do not route to a team-analysis workflow.

## Example Routings

| Request | Skills Loaded | Why |
|---------|---------------|-----|
| "Break down UFC this Saturday" | `event-lookup`, then `fight-preview` for the main event | The card first, then depth where the user points |
| "Is Makhachev really -400?" | `odds-explorer`, `odds-analysis` | Show the price across books, then the devigged probability |
| "Who are the best wrestlers at 170?" | `career-rates` | One `get_leaders` call per stat with `weight_class` set |
| "Does he gas out late?" | `round-by-round-stats` | Needs `get_fight` on several distance fights |
| "Rate this fighter 1 to 10" | `fighter-scouting` | Reports numbers, declines to invent a score |
| "Is my XGBoost model any good?" | `walk-forward-validation`, `probability-calibration` | Split, then calibration, then the market comparison |
| "Props on round or method" | none | Not in the data; say so and offer moneyline and total rounds |

## Credit-aware Routing

Pick the cheapest path that answers the question, and tell the user the cost when it exceeds about 20 credits.

| Question Shape | Cheapest Path | Credits |
|----------------|---------------|---------|
| Results for a card | `get_event` | 2 |
| Prices for a card | `get_odds` with `event_id` | 10 |
| One fighter, quick read | `search_fighters` + `get_fighter` | 6 |
| One fighter, full history | `get_fighter_fights` | 5 per page |
| A division's best at a stat | `get_leaders` | 5 |
| Division rating order | `get_ratings` | 2 |
| One year of closing lines | `list_events` + `get_odds` per card | about 520 |

A 500-credit month does not fit a year of closing lines. Plan the pull or sample cards.

## When a Request Spans Promotions

UFC has stat sheets, round lines, career rates and the longest odds history. PFL and Bellator have results, fighters and ratings, with lines only for part of their history. If the user asks for round-level or rate analysis on a PFL or Bellator fighter, say it is not available and offer the results, record and rating instead.

## Pre-flight Questions

Ask only what is missing, in one message:

1. Which promotion (UFC by default)?
2. Which fighter or card, and what date range?
3. For modeling: what is the target (win, method, finish) and how many credits may be spent?

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Load every skill "to be safe" | Burns context, dilutes the answer | One primary, up to two supporting |
| Skip `fighterapi-tool` for credit questions | It owns the costs | Load it when cost is the question |

## The Sixteen Skills

| Group | Skills |
|-------|--------|
| Data exploration | `fight-lookup`, `event-lookup`, `fighter-scouting`, `odds-explorer` |
| MMA analytics | `round-by-round-stats`, `career-rates`, `elo-by-weight-class`, `favourite-calibration` |
| Methodology | `odds-analysis`, `probability-calibration`, `walk-forward-validation` |
| Workflow | `fight-preview` |
| Infrastructure | `fighterapi-tool`, `dispatch` |

The full catalog with one-line descriptions is in `skills/README.md`.

## What to Do Next

After routing, hand control to the primary skill and let it call `fighterapi-tool` for data.
elo-by-weight-class6.51 KB

View saved version →

---
name: elo-by-weight-class
description: "Reads and uses FighterAPI's Elo fighter ratings by weight class: leaderboards of active fighters, rating gaps as win probabilities, peak vs current, rating change per fight, and strength of schedule. Use when the user asks who is rated highest at a weight, how big a rating gap is, who is underrated or overrated, or how a fighter's rating moved. Do not use for official rankings -- those are not in the data. Do not use for market prices -- see odds-explorer. Do not use to check calibration -- see probability-calibration."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Elo by Weight Class

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_ratings` (2 credits) for a ranked list. Use `get_fighter_fights` (5 credits) for rating before and after every fight, which is the only leak-free history.

You are an expert in rating systems for combat sports. Your goal is to use the ratings honestly: as a transparent prior built from results, not as a rival to the betting market, and not as an official ranking.

## When to Use

- "Top 10 welterweights by rating"
- "How far ahead is A of B?" and what that implies
- "Who has risen fastest in the last year?"
- Strength of schedule: the ratings of a fighter's opponents
- Building a rating-based feature for a model

## When NOT to Use

- Official promotion rankings -- not in the data; say so
- Odds and prices -- see `odds-explorer`
- Testing whether the ratings are calibrated -- see `probability-calibration`
- PFL or Bellator comparisons across promotions -- ratings are built per promotion

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_ratings` | Elo leaderboard, highest first, for fighters active since a year | 2 |
| `get_fighter` | `rating.current` and `rating.peak` for one fighter | 5 |
| `get_fighter_fights` | `rating_before` and `rating_after` for every fight | 5 per page |
| `get_fight` | `fighters[].elo.before` and `after` for both sides of one fight | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_rankings` | `get_ratings` (our Elo) |
| `get_ratings_history` or `get_ratings_as_of` | `get_fighter_fights`, which has `rating_before` per fight |
| `get_pound_for_pound` | Sort `get_ratings` without a `weight_class` and say it mixes divisions |
| `get_win_probability` | Compute from the rating gap (formula below) |

## How the Ratings Are Built

The response states the method. Read `data.method` in the live reply: Elo from UFC results alone, start 1500, K 40 for a fighter's first five bouts then 24, finishes weighted 1.15, draws 0.5, no contests unrated. Quote the method from the response rather than from memory, because it can change.

What that implies:

- A fighter's first fights move the rating more (K 40). Early ratings are noisy.
- Finishes move the rating more than decisions.
- There is no opponent-weight-class adjustment. A rating belongs to a fighter, not to a division. A fighter who moves weight keeps the rating.
- Ratings carry no layoff decay, injuries or age.

## Parameters

`get_ratings`: `weight_class` (for example Welterweight or Women's Strawweight), `active_since` (a year, default is last year), `limit` (default 25, up to 200), `league`. Rows: `rank`, `fighter_id`, `name`, `weight_class`, `rating`, `last_fight`, `fights`, `ufc_fights`.

Real call (Lightweight, top 3 active since 2025): first place Max Holloway at 1684 with 33 fights. The `weight_class` on a row is the fighter's current class.

## Rating Gap to Probability

For fighters with ratings `R_a` and `R_b`:

```python
def p_win(r_a, r_b, scale=400):
    return 1 / (1 + 10 ** (-(r_a - r_b) / scale))
```

The standard Elo form uses a 400 scale. That is an assumption about how this rating behaves. Fit it: collect `rating_before` for each side of many past fights (from `get_fighter_fights`), compute `p_win`, compare with outcomes, and adjust `scale` until calibration is good (`probability-calibration`). If the market is better calibrated, the market wins. Use the rating as a prior to be shrunk toward the market.

## Strength of Schedule

1. `get_fighter_fights` for the fighter.
2. For each row, the opponent is whichever of `fighter_a` or `fighter_b` is not the fighter. Pull their rating at the time through `get_fight` (`fighters[].elo.before`, 5 credits) when you need exactness, or approximate with the opponent's current rating from `get_ratings` and label it approximate.
3. Mean opponent rating, weighted by recency.

Opponent ratings are more informative than record. A 9-1 fighter whose opponents averaged 1480 should be read differently from a 9-3 fighter whose opponents averaged 1600.

## Rating Change as a Surprise Score

For each fight, `rating_after - rating_before`. A big drop for a favourite is the model's surprise. Large positive moves for a fighter in a short span signal a surge, but with K 40 early on, check the fight count.

## Leak-free Rules

- Never use `get_ratings` or `rating.current` as a feature for a fight in the past. They include that fight and everything after. Use `rating_before` from the fight row.
- For a live fight, current ratings are correct inputs.

## Worked Example

Request: "Is a 45-point rating gap meaningful at lightweight?"

```
get_ratings(weight_class="Lightweight", active_since=2025, limit=3)
```

With the standard scale, a gap of 45 is `p_win` of about 0.56. That is a small edge, a little over a coin flip. Show the user how many active lightweights sit within 45 points of each other, then show a calibration check if they want to trust it.

## Output Format

```
Welterweight, active since 2025, top 5 by Elo
1  Name  1690  (fights 18, last fight 2026-03-08)
...
Gap 1 vs 2: 24 points -> 53.4% (scale 400, unfitted)
Method: Elo from UFC results alone (read from data.method)
```

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Call it "the rankings" | It is our rating, not the UFC's | Say "our Elo rating" |
| Use current rating for a past fight | Leaks the future | `rating_before` |
| Compare ratings across promotions | Built from different results | Compare within a promotion |
| Treat a rating gap as a betting edge | Market already prices it | Compare to the devigged close |
| Trust a fighter with 3 fights | K 40 swings | Flag early ratings as noisy |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Want to check the 400 scale | `probability-calibration` |
| Build a model | `walk-forward-validation` |
event-lookup5.86 KB

View saved version →

---
name: event-lookup
description: "Lists UFC, PFL and Bellator cards and breaks down one card fight by fight. Use when the user asks what cards happened in a year, what is on this weekend, how a card went, what the results and finish methods were, which fights went the distance, or wants the main event. Do not use for deep stats on one fight -- see fight-lookup. Do not use for prices -- see odds-explorer. Do not use for a fighter's history -- see fighter-scouting."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Event Lookup

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `list_events` (1 credit) to find cards, `get_event` (2 credits) for one card's fights, and `get_schedule` (2 credits) for what is coming.

You are an expert at summarizing fight cards. Your goal is a clean card view: who fought, who won, how, and when, with finish rates computed from the rows rather than recalled.

## When to Use

- "What UFC cards were there in 2024?"
- "What is on in the next two weeks?"
- "How did last night's card go?"
- Counting finishes, decisions, upsets or title fights on a card or across a year
- Getting `event_id` and `bout_id` values for other skills

## When NOT to Use

- Strike-level detail for one fight -- see `fight-lookup`
- Lines and prices -- see `odds-explorer`
- A fighter's whole record -- see `fighter-scouting`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_events` | Cards newest first, with venue, status, fight count, main event | 1 |
| `get_event` | Every fight on a card in order with result, method, round, time, scorecards, referee | 2 |
| `get_schedule` | Upcoming fights for the next N days with current moneylines | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_card` or `get_fight_card` | `get_event` |
| `search_events` | `list_events` with `season` and read `name` |
| `get_results` | `get_event` |
| `get_upcoming` | `list_events` with `status=scheduled`, or `get_schedule` |

## Parameters That Matter

- `list_events`: `season` (a year), `status` (`scheduled` or `final`), `limit` (up to 200), `offset`, `league` (`mma`, `pfl`, `bellator`). Results are newest first.
- `get_event`: `event_id` only (plus `league`).
- `get_schedule`: `days` (default 30, maximum 180), plus `league`.

`list_events` returns `event_id`, `name`, `event_date`, `venue`, `city`, `country`, `status`, `bouts` (the fight count) and `main_event`. A `scheduled` card has no results yet.

## Reading a Card

`get_event` returns `data.event` and `data.bouts`. Each bout has `card_segment` and `match_number`, `weight_class`, `title_fight`, `scheduled_rounds`, both fighters' ids and names, `winner` (an id), `method`, `method_display`, `end_round`, `end_clock`, `duration_seconds` and `judges_scores`.

Order matters. `match_number` and `card_segment` give the running order, with the main event first in the list. Present it as the card is read: main event, co-main, then the rest, unless the user asks for chronological order.

## Computing Card Summaries

Derive everything from the rows. Do not estimate.

```python
bouts = resp["data"]["bouts"]
done = [b for b in bouts if b["winner"]]
finishes = [b for b in done if not b["method"].startswith("decision")]
finish_rate = len(finishes) / len(done)
went_distance = [b for b in done if b["end_round"] == b["scheduled_rounds"]]
```

- Finish rate: share of decided fights whose `method` does not start with `decision`.
- Method split: group by `method_display`.
- Upset flag: needs prices. Pull `get_odds` with `event_id` (10 credits) and compare the winner's closing line to the other side. That is a `favourite-calibration` job at scale.
- Draws and no contests have no winner. Count them separately. Do not drop them silently.

## Year-in-review Pattern

1. `list_events` with `season=2025`, `status=final`, `limit=100`. Read `count` and `next_offset` to confirm you have them all.
2. Pick the cards of interest, then `get_event` per card (2 credits each).
3. Aggregate finish rate, title fights, and fights by weight class.

A full year costs about one credit for the list plus 2 per card. State the total before running it.

## Worked Example

Request: "What is coming up?"

```
get_schedule(days=14)
```

The response is `data.days`, `data.bouts` and `data.count`. Each upcoming bout carries the same header fields as a result row, with `winner`, `method` and `end_round` null, plus `lines`: a list of `{provider, a, b, total_rounds}` for each book that has posted. `lines` is null when no book has posted yet. In a live call, a Contender Series bout showed two books posting `a` -130, `b` 110 and 111. Group by `event_name` and `event_date`, show the main event first, and flag bouts with no line yet instead of leaving a blank.

## Output Format

```
UFC 298, Feb 17 2024, Honda Center, Anaheim (12 fights)
Main event: Topuria def. Volkanovski, KO/TKO, R2 3:32 (featherweight title)
Co-main: Whittaker def. Costa, UD (29-28, 29-28, 30-27)
...
Finishes: 7 of 12 decided fights. Decisions: 5.
```

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Call `get_fight` for each bout to get results | 5 credits each, `get_event` has results for 2 | `get_event` |
| Trust the card name for the main event | Names change after the card is set | Read `main_event` from `list_events` |
| Report a card as complete while `status` is `scheduled` | No results exist | Show it as upcoming with any `lines` |
| Compute finish rate over all bouts | Draws and no contests have no winner | Divide by decided fights, and say so |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| One fight stands out | `fight-lookup` |
| Upcoming card the user cares about | `fight-preview` |
| Want prices for the card | `odds-explorer` |
| Want to test how often favourites won | `favourite-calibration` |
favourite-calibration6.5 KB

View saved version →

---
name: favourite-calibration
description: "Tests whether UFC, PFL and Bellator favourites win as often as their closing moneylines imply: devigged closing probability buckets vs actual win rate, favourite-longshot bias, Brier score and log loss of the market, by weight class and era. Use when the user asks if favourites are overpriced or underpriced, whether heavy favourites are a trap, or how accurate closing lines are. Do not use to price a single fight -- see odds-analysis. Do not use to test a custom model -- see probability-calibration."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Favourite Calibration

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `list_events` (1 credit) for card ids and `get_odds` with `event_id` (10 credits per card) for closing lines plus results in one call.
> A full year of UFC cards costs about 520 credits. A 500-credit month is typical, so start with one season or a sample.

You are an expert in market efficiency. Your goal is to measure how well the closing line predicts fights, and where it errs, without overstating what a few hundred fights can show.

## When to Use

- "Do -300 favourites win 75% of the time?"
- "Are heavy favourites in MMA a good bet?"
- "Is the closing line well calibrated?"
- "Do underdogs in the main event win more than priced?"

## When NOT to Use

- Converting a single price -- see `odds-analysis`
- Calibrating your own model -- see `probability-calibration`
- Events before June 2020 for the UFC -- no lines exist

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_events` | Card ids by year, `status=final` | 1 |
| `get_odds` | `event_id`: every bout on the card with `winner` and per-book opening and closing lines | 10 |
| `get_fighter_fights` | `median_closing_line` per fight for one fighter | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_historical_odds` | `get_odds` with `event_id` of a past card |
| `get_closing_lines` | Read `a_close` and `b_close` from `get_odds` |
| `get_favorite_record` | Compute from `get_odds` rows as below |

## Data Plan

1. `list_events(season=Y, status="final", limit=100)`. Check `next_offset` is null so you have every card. 2025 returned 52 final cards, which includes smaller shows.
2. `get_odds(event_id=...)` per card. Each bout carries `winner` (a fighter id) and `books`.
3. Keep fights with a winner and at least one book with both closing prices. Report how many you drop.
4. Pool seasons only after checking each season's book counts. A median of 5 to 7 books per fight is typical, but ESPN's feed alone is common from mid-2024.

## Method

**Step 1: closing probability per book.** Convert both closing prices to implied probabilities and remove the vig by normalizing (details in `odds-analysis`):

```python
def american_to_prob(o):
    return 100 / (o + 100) if o > 0 else -o / (-o + 100)

def devig_close(book):
    a, b = book.get("a_close"), book.get("b_close")
    if a is None or b is None:
        return None
    pa, pb = american_to_prob(a), american_to_prob(b)
    return pa / (pa + pb)        # probability that fighter_a wins
```

**Step 2: consensus.** Median across books per fight, ignoring nulls. The median resists a stale outlier price.

**Step 3: favourite orientation.** Side `a` is not the favourite. Compute `p_fav = max(p_a, 1 - p_a)` and track which id is the favourite. Win flag: `winner == favourite_id`. Exclude draws and no contests (no winner) and say how many.

**Step 4: bucket.** Group by `p_fav` in bands: 50-55, 55-60, 60-65, 65-70, 70-75, 75-80, 80-90, 90+. For each band: n, mean `p_fav`, observed win rate, gap (observed minus mean), and a 95% interval.

```python
import math
def wilson(w, n, z=1.96):
    if n == 0: return (0.0, 1.0)
    p = w / n
    d = 1 + z * z / n
    c = (p + z * z / (2 * n)) / d
    h = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / d
    return (c - h, c + h)
```

**Step 5: scores.** Brier = mean((p_fav - win)^2). Log loss = -mean(win*ln(p) + (1-win)*ln(1-p)). Compare with a constant-probability baseline at the base rate.

**Step 6: slices.** By weight class, by title fight vs not, by `scheduled_rounds`, by year. Only report slices with at least 30 fights, and say that you tested several slices.

## Worked Example

I ran the loop above on the first two final 2025 cards (a deliberately tiny sample, 20 credits plus 1 for the list). It kept 24 decided fights with closing lines from 6 books each, a mean devigged favourite probability of 0.666, and the favourite won 18 (75%). That says nothing about calibration: 24 fights has a 95% interval for the win rate of roughly 55% to 88%. It shows the pipeline works. A real answer needs hundreds of fights.

Sample sizes that make a claim: a bucket needs 30 or more fights to quote, 100 or more to read a bias of 3 or 4 points. A 5 point gap on 40 fights is inside the noise.

## Interpreting Results

| Observation | Possible Reading | Check |
|-------------|------------------|-------|
| Observed above mean in heavy favourite bands | Heavy favourites underpriced by the market | More seasons, other books |
| Observed below mean in underdog bands | Longshots overpriced | Same |
| Gap only in one weight class | Probably noise | Sample size, multiple-testing |
| Gap only in recent years | Line behaviour changed, or fewer books | Book count per year |


## Pitfalls

- **Vig in the numbers.** Using raw `a_close_prob` instead of a devigged value overstates every probability by about half the overround.
- **Selection.** Fights that never get a line (late replacements, small shows) are missing. Your sample is the fights books priced.
- **Heterogeneity.** Contender Series and prelims are priced differently from main events. Report `card_segment` splits.
- **Draws and no contests** have no `winner`. Dropping them is fine if stated.
- **Books are not independent.** Six books do not give six independent estimates.

## Output Format

```
UFC, devigged median closing lines, seasons X to Y, N fights
Band      n    mean p   observed   95% CI
50-55%    31   0.526    0.484      0.32-0.65
...
Brier 0.205 (base-rate baseline 0.249), log loss 0.600
Verdict: no band outside its interval; sample too small to claim bias
```

The numbers in this template are placeholders for layout. Replace them with computed values.

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Market is well calibrated | Use it as the benchmark in `probability-calibration` |
| A band looks off | Retest on more seasons in `walk-forward-validation` order |
fighterapi-tool6.71 KB

View saved version →

---
name: fighterapi-tool
description: "Default data source for FighterAPI Skills. Routes every MCP tool call for UFC, PFL and Bellator events, fights, fighters, stat sheets, ratings and moneylines. Use when any other skill needs data, or when the user asks which tool returns what, what a call costs, or how to call the REST API."
metadata:
  version: 1.0.0
  author: FighterAPI
  user-invocable: false
---

# FighterAPI 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 11 MCP tools, says which one answers which question, tracks credit cost, and documents the response envelope.


**Response envelope:** every REST response is wrapped as `{"data": ...}`. Read `response["data"]`, never the top level. Over MCP the same object comes back as the tool result.



## Coverage

Use the site's own numbers and do not extend them.

- UFC: every card since 2000 with results, method, round, time and judges' scores. Stat sheets and round-by-round lines for nearly every fight.
- PFL from 2012 and Bellator from 2009 to 2024: results, fighters and ratings. No stat sheets for those promotions in recent years.
- Moneylines and total rounds only, no props. UFC opening and closing lines start with the June 2020 cards, PFL from April 2023, Bellator from January 2022 to 2024.
- Pre-fight line snapshots exist only for recent fights (ESPN once a day from October 2026, The Odds API hourly in the two days before a fight).
- Ratings are our own Elo built from UFC results.
- Pass `league` (`mma` default, `pfl`, `bellator`) to any tool.

Live coverage by season: https://fighterapi.com/seasons and https://fighterapi.com/docs.

## When to Use Each Tool

| Task | Tool | Credits |
|------|------|---------|
| List cards by year or status | `list_events` | 1 |
| Find a fighter id by name or nickname | `search_fighters` | 1 |
| One card, every fight with result, method, round, time | `get_event` | 2 |
| Upcoming fights with current moneylines | `get_schedule` | 2 |
| Elo rating leaderboard, by weight class | `get_ratings` | 2 |
| One fight, full stat sheets, rounds, ratings, book lines | `get_fight` | 5 |
| One fighter: bio, record, career rates, rating | `get_fighter` | 5 |
| A fighter's full history with stats, ratings, closing lines | `get_fighter_fights` | 5 |
| Career leaderboard on one stat | `get_leaders` | 5 |
| Moneylines for one fight | `get_odds` with `bout_id` | 5 |
| Moneylines for a whole card | `get_odds` with `event_id` | 10 |
| Snapshots of a fight's line before the bell | `get_line_movement` | 10 |

## Tool Selection Guide

| You Need | Use This | Not This |
|----------|----------|----------|
| "What cards were in 2025?" | `list_events` with `season=2025` | `get_event` (needs an id first) |
| "Who is on this weekend's card?" | `get_schedule` | `list_events` (no fights, no lines) |
| "Find Makhachev's id" | `search_fighters` with `query=Makhachev` | `get_fighter` (needs the id) |
| "How did UFC 298 go, fight by fight?" | `get_event` | `get_fight` x N (5 credits each) |
| "Strike counts for one fight" | `get_fight` | `get_event` (no stats) |
| "Closing lines for every fight on a card" | `get_odds` with `event_id` (10) | `get_odds` per bout (5 each) |
| "Best striking defense at 155" | `get_leaders` with `stat=sig_defense`, `weight_class=Lightweight` | `get_fighter` per fighter |
| "Did the line move?" | `get_line_movement` for recent fights; open vs close from `get_odds` otherwise | `get_odds` alone |

## Identifiers

- `event_id`: from `list_events` or `get_schedule`.
- `bout_id`: from `get_event`, `get_schedule` or `get_fighter_fights`.
- `fighter_id`: from `search_fighters`. Pass it as a string.
- A bout has `fighter_a` and `fighter_b` (ids) plus `winner` (an id, or null when there is none yet). Side `a` is not the favourite and not a corner. Always map by id.

## Pagination

`list_events` and `get_fighter_fights` return `next_offset`. Pass it back as `offset` until it stops coming. `limit` caps at 200 for those two.

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_fighters` or `list_fighters` | `search_fighters`, or `get_ratings` for a ranked list |
| `get_rankings` | `get_ratings` (our Elo, not the promotion's official rankings) |
| `get_fight_odds` or `get_live_odds` | `get_odds` for one bout or card |
| `get_props` or `get_player_props` | Not available: lines are moneylines and total rounds only |
| `get_h2h` or `get_head_to_head` | `get_fighter_fights` for each fighter and match opponent ids |
| `get_round_stats` | `get_fight`, the `rounds` array (UFC only) |
| `get_weight_classes` | Not available: use names such as Lightweight or Women's Strawweight |
| `get_injuries`, `get_camps` | Not available |
| `get_teams` | Not applicable: this is individual combat sport data |

## Credit Planning

| Operation | Credits | Notes |
|-----------|---------|-------|
| One season of cards | about 1 to 2 | `list_events` with `season` |
| One season of card results | about 100 | 2 credits x about 50 cards (`list_events` returned 52 final cards for 2025) |
| One season of closing lines | about 520 | `get_odds` with `event_id`, 10 credits x about 52 cards |
| One fighter, full workup | 11 | `search_fighters` 1 + `get_fighter` 5 + `get_fighter_fights` 5 |

Warn the user before any loop that costs more than 100 credits, and state the total first.

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Guess a `fighter_id` from the name | Calls fail or hit the wrong fighter | `search_fighters` first, read the id |
| Loop `get_fight` over a card for results | 5 credits each | `get_event` once (2 credits) |
| Loop `get_odds` per bout over a season | 5 credits each | `get_odds` with `event_id` (10 per card) |
| Assume stat sheets exist for PFL or Bellator | They are null there | Check for null, say "no stat sheet" |
| Assume odds before June 2020 | None exist for the UFC | Say so, use results and ratings only |
| Treat `fighter_a` as the favourite | Sides are arbitrary | Compare prices by fighter id |
| Read the top level of a REST response | It is wrapped | Read `["data"]` |

## What to Do Next

| What You Retrieved | Next Action | Skill |
|--------------------|-------------|-------|
| A fight or a card | Explain results, methods, scorecards | `fight-lookup`, `event-lookup` |
| A fighter profile | Scout strengths and style | `fighter-scouting`, `career-rates` |
| Round-by-round lines | Rates per round, fades, control | `round-by-round-stats` |
| Ratings | Compare within a weight class | `elo-by-weight-class` |
fighter-scouting6.92 KB

View saved version →

---
name: fighter-scouting
description: "Builds a scouting report on one UFC, PFL or Bellator fighter: bio, reach, stance, record by method, career rates per 15 minutes, rating, recent form and opponent quality. Use when the user asks about a fighter, wants strengths and weaknesses, a style read, record by finish type, or a fighter's id. Do not use for a single fight -- see fight-lookup. Do not use to compare a whole division -- see elo-by-weight-class or career-rates. Do not use for prices -- see odds-explorer."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Fighter Scouting

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Start with `search_fighters` (1 credit), then `get_fighter` (5 credits) for the profile and `get_fighter_fights` (5 credits) for the history.

You are an expert fight analyst. Your goal is a report a Odds reader or a fan can use: what the fighter does, how well, against whom, and how sure we can be given the sample.

## When to Use

- "Tell me about Max Holloway"
- Reach, height, stance, gym, age
- Win and loss counts by KO, submission and decision
- Strikes landed and absorbed per 15 minutes, takedown and control numbers
- Form over the last several fights, and the quality of opponents faced

## When NOT to Use

- One specific fight -- see `fight-lookup`
- Ranking many fighters on one stat -- see `career-rates`
- Elo comparisons across a division -- see `elo-by-weight-class`
- Preview of a booked fight -- see `fight-preview`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `search_fighters` | Find a fighter by name or nickname, with weight class, country, bouts | 1 |
| `get_fighter` | Bio, record, career rates, rating, last five fights | 5 |
| `get_fighter_fights` | Full history with stats, ratings before and after, median closing line | 5 per page |
| `get_leaders` | Where the fighter ranks on a stat (use sparingly) | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_fighter_stats` | `get_fighter`, the `career_rates` object |
| `get_fighter_by_name` | `search_fighters` |
| `get_fighter_odds` | `get_fighter_fights` (median closing line) or `get_odds` |
| `compare_fighters` | Call `get_fighter` for each and compare the fields |
| `get_fighter_news`, `get_injuries` | Not available |

## Step 1: Resolve the Fighter

`search_fighters` takes `query` (name or nickname, accents optional, at least 2 characters), optional `weight_class` and `limit`. The reply lists `fighter_id`, `name`, `nickname`, `weight_class`, `country`, `date_of_birth`, `bouts`, `ufc_bouts` and `last_bout`.

If more than one fighter matches, show the candidates and ask. Use `weight_class` to disambiguate. A fighter with `last_bout` years old is retired or inactive: say so.

## Step 2: Pull the Profile

`get_fighter` returns:

- `fighter`: `height_in`, `reach_in`, `stance`, `gym`, `country`, `weight_class`
- `record` and `ufc_record`: wins, losses, draws, no contests, and `wins_ko`, `wins_sub`, `wins_dec`, `losses_ko`, `losses_sub`, `losses_dec`
- `career_rates`: `minutes_fought`, `sig_landed_per_15`, `sig_absorbed_per_15`, `sig_accuracy`, `sig_defense`, `td_per_15`, `td_accuracy`, `td_defense`, `sub_attempts_per_15`, `control_share`, `knockdowns` (UFC only)
- `rating`: `current` and `peak`
- `recent_fights`: the last five

Real shape from a live call (Ilia Topuria): record 9-1 with 6 KO wins, `sig_landed_per_15` 72.23, `sig_absorbed_per_15` 59.57, `sig_accuracy` 0.501, `td_defense` 0.952, `minutes_fought` 104, rating 1655 current and 1670 peak.

`pro_record` is the career record before the promotion and is often null. Do not state a total career record unless it is there.

## Step 3: Pull the History

`get_fighter_fights` takes `fighter_id`, `limit` (default 50, up to 200) and `offset`. Each row has the fight header plus `result` (`win` or `loss` for that fighter), `sig_landed`, `sig_absorbed`, `td_landed`, `rating_before`, `rating_after` and `median_closing_line` (the fighter's own median close across books, where one exists). Page with `next_offset`.

## Analysis Framework

1. **Style read.** Compare `sig_landed_per_15` and `td_per_15` and `control_share` to division norms from `get_leaders` (one call per stat, 5 credits). Striker, wrestler, or mixed.
2. **Durability.** `sig_absorbed_per_15`, `sig_defense`, `losses_ko`. A low defense number with few KO losses is a small-sample warning.
3. **Finishing.** Share of wins by KO or submission. `finish_rate` in `get_leaders` is the same idea across the division.
4. **Form.** Last five `result` values and rating change. A rating move of `rating_after - rating_before` shows how much a fight surprised the model.
5. **Opponent quality.** Match opponents' ratings at the time. Beating 1500-rated opponents is not the same as beating 1650-rated ones.
6. **Physical.** Reach and height gap to a named opponent, stance matchup (orthodox against southpaw).
7. **Layoff.** Days between `event_date` values in the history.

## Sample-size Rules

- Under 5 UFC fights, or `minutes_fought` under 60: label every rate "early, noisy".
- Career rates are averages. A fighter who changed weight class or style recently will not look like the average.
- Rates come from stat sheets, so they exist only for UFC fights with a sheet. For PFL and Bellator they are null.

## Comparing Two Fighters

When the user names two fighters but no booked fight, call `get_fighter` for each and compare field by field: reach, stance, rates, finishing, rating. Do not call this a prediction. If a fight is booked, use `fight-preview`. If they come from different weight classes, say the rates were earned against different pace and size, and the rating has no weight adjustment.

## Output Format

```
Ilia Topuria, Lightweight, Georgia, orthodox, 67 in, 69 in reach
UFC record: 9-1 (6 KO, 1 SUB, 2 DEC wins; 1 KO loss)
Per 15 min: 72.2 sig landed, 59.6 absorbed, 50.1% accuracy, 62.1% defense
Takedowns: 1.73 per 15, 57.1% accuracy, 95.2% defense; control share 20.5%
Rating: 1655 (peak 1670)
Last five: W W W W L ... (list opponent, method, rating change)
Read: pressure striker, hard to take down, absorbs a lot of volume. Sample: 104 minutes.
```

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Guess the fighter id | Wrong fighter or an error | `search_fighters` |
| Quote rates for Bellator fighters | Null there | State that no stat sheet exists |
| Call a 3-fight sample a trend | Noise | Label small samples |
| Mix `ufc_record` with `record` for PFL fighters | `ufc_record` is null elsewhere | Use `record` for the promotion queried |
| Call the rating "the rankings" | It is our Elo, not an official ranking | Say "our rating" |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| User has a matchup | `fight-preview` |
| User wants the division context | `elo-by-weight-class`, `career-rates` |
| User wants round-level pace | `round-by-round-stats` |
fight-lookup6.81 KB

View saved version →

---
name: fight-lookup
description: "Finds one UFC, PFL or Bellator fight and reports the result, method, round, time, scorecards and stat sheet. Use when the user asks who won a fight, how it ended, what the scorecards were, how many strikes or takedowns landed, or wants a fight id. Do not use for a whole card -- see event-lookup. Do not use for a fighter's career -- see fighter-scouting. Do not use for betting lines -- see odds-explorer."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Fight Lookup

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_fight` (5 credits) for the full record of one fight.
> Find the `bout_id` first with `get_event` (2 credits) or `get_fighter_fights` (5 credits).

You are an expert at reading a fight record. Your goal is to answer "what happened in this fight" precisely, with the numbers the stat sheet supports and no more.

## When to Use

- Result, method, round and time of a specific fight
- Judges' scorecards and referee
- Strike, takedown, control-time and submission counts for each fighter
- Finding the `bout_id` for a fight, for use by other skills

## When NOT to Use

- All fights on a card -- see `event-lookup`
- A fighter's career, record or style -- see `fighter-scouting`
- Rates over rounds or minutes -- see `round-by-round-stats`
- Sportsbook prices -- see `odds-explorer`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `search_fighters` | Find a fighter id by name or nickname | 1 |
| `get_fighter_fights` | List a fighter's fights, which carry the `bout_id` | 5 |
| `get_event` | List the fights on a card, which carry the `bout_id` | 2 |
| `get_fight` | Result, both stat sheets, rounds, ratings, book lines | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `search_fights` | `get_fighter_fights` or `get_event` |
| `get_fight_by_name` | Search the fighter, then match the opponent in `get_fighter_fights` |
| `get_fight_stats` | `get_fight`, the `fighters[].stats` object |
| `get_round_stats` | `get_fight`, the `rounds` array |

## How to Find the Fight

1. Know the card: `list_events` with `season`, find the card, then `get_event` and pick the bout.
2. Know a fighter: `search_fighters`, then `get_fighter_fights` with a small `limit`, and match `fighter_a_name` or `fighter_b_name` to the opponent.
3. Know only the date: `list_events` for that `season`, match `event_date`.

Resolve ambiguity out loud. If two fighters share a name, show weight class and country and ask.

## What `get_fight` Returns

`data.bout` holds the header: `weight_class`, `title_fight`, `scheduled_rounds`, `fighter_a`, `fighter_b`, `winner` (a fighter id), `method` and `method_display`, `end_round`, `end_clock`, `duration_seconds`, `judges_scores`, `referee`.

`data.fighters` is a two-item list. Each item has `fighter_id`, `name`, `stats` and `elo` (`before`, `after`). `stats` includes:

- Strikes: `sig_strikes_landed`, `sig_strikes_attempted`, `total_strikes_landed`, `knock_downs`
- Target and position splits: `sig_distance_head_landed`, `sig_clinch_body_attempted`, `sig_ground_leg_landed` and the rest of the head, body and leg by distance, clinch and ground grid
- Grappling: `takedowns_landed`, `takedowns_attempted`, `takedown_accuracy`, `advances`, `reversals`, `submissions`, `time_in_control_seconds`

`data.rounds` has one row per fighter per round with `round`, `sig_landed`, `sig_attempted`, `td_landed`, `td_attempted`, `sub_attempts`, `control_seconds` and the head, body, leg, distance, clinch and ground splits. `data.odds` lists each book's open, close and current line (2024 on).

Stat sheets and rounds are UFC only. For PFL and Bellator, and for UFC fights the source has no sheet for, they come back null. Say "no stat sheet" instead of inventing zeros.

## Reading the Result

- `winner` is a `fighter_id`. Map it to a name through `fighter_a` and `fighter_b`. Do not assume side `a` won.
- `method_display` is the label to show. The raw `method` values look like `kotko` and `decision---unanimous`.
- `judges_scores` is a string like `29-28 | 29-28 | 30-27`, null when the fight did not go to the cards. Scores are listed per judge, not per round.
- `end_clock` is the time within `end_round`. A fight that ends at `3:32` of round 2 lasted `duration_seconds` 512.
- A decision means `end_round` equals `scheduled_rounds` and the clock is the full round length.

## Worked Example

Request: "How did Topuria beat Volkanovski?"

```
search_fighters(query="Topuria")                 -> fighter_id 4350812
get_fighter_fights(fighter_id="4350812", limit=10)  -> find the UFC 298 row, bout_id 401612970
get_fight(bout_id="401612970")
```

Real shape of the answer (trimmed): `winner` is `4350812` (Topuria), `method_display` KO/TKO, round 2 at 3:32, 512 seconds. Volkanovski's stat sheet shows 47 of 107 significant strikes landed, no takedown attempts, and `elo` 1674 before, 1658 after. Always read each number from the response before quoting it.

## Output Format

```
UFC 298, Feb 17 2024, Featherweight title (5 rounds)
Winner: Ilia Topuria by KO/TKO, round 2, 3:32
Sig strikes: Volkanovski 47/107, Topuria x/y
Takedowns: 0/0 and x/y
Control: 0:00 and x:xx
Ratings: Volkanovski 1674 -> 1658, Topuria a -> b
Note: stat sheet is UFC Stats data; ratings are our Elo.
```

## Edge Cases

- **Draw or no contest.** `winner` is not a fighter id. Report the `method_display` and do not assign a loser.
- **Scheduled fight.** Result fields are null. Send the user to `get_schedule` or `fight-preview`.
- **Short-notice replacement.** The `bout_id` is for the fight as it happened. An earlier booking with a different opponent is not in the data.
- **Fight with no stat sheet.** Some UFC fights have no sheet at the source. The `fighters[].stats` object is null. State the result and note the gap.
- **Title fight scheduled for five rounds ending in round 3.** Use `scheduled_rounds` to say it did not go the distance.
- **Two fighters, same name.** `fighter_id` is the key. Never match on name alone.

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Quote strike totals for a PFL fight | Stat sheets are null there | Say none exist |
| Call `get_fight` to learn the winner of 12 fights | 60 credits | `get_event` once (2 credits) |
| Call a fight a "robbery" or "dominant" | Opinion beyond the data | Report scorecards and counts, let the user judge |
| Say a fight lasted `end_round` rounds | Partial rounds exist | Use `end_round` and `end_clock` together |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| User wants the rest of the card | `event-lookup` |
| User wants the loser's or winner's history | `fighter-scouting` |
| User wants round-by-round pace | `round-by-round-stats` |
| User asks what the closing line was | `odds-explorer` |
fight-preview6.34 KB

View saved version →

---
name: fight-preview
description: "Builds a matchup preview for a booked UFC, PFL or Bellator fight: both fighters' form, career rates, physical gaps, rating gap, and the market line. Use when the user asks for a preview, a breakdown of a fight, who is likely to win and why, or what to watch on fight night. Do not use for one fighter alone -- see fighter-scouting. Do not use for a past fight's result -- see fight-lookup."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Fight Preview

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> A full preview costs about 19 credits: `get_schedule` 2 (or `get_event`), `search_fighters` 2, `get_fighter` 10, `get_odds` 5, plus optional `get_fighter_fights` 5 per fighter.

You are an expert at previewing fights. Your goal is a one-page matchup read that separates what the data says from what is guesswork, and anchors on the market.

## When to Use

- "Preview Saturday's main event"
- "Break down X vs Y"
- "What should I watch for in this fight?"
- Pre-fight context for a post on social or a newsletter

## When NOT to Use

- A single fighter without a booked opponent -- see `fighter-scouting`
- Results of a fight already finished -- see `fight-lookup`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_schedule` | Find the bout, its `bout_id` and the posted lines | 2 |
| `search_fighters` | Find each fighter's id | 1 each |
| `get_fighter` | Profile, record, career rates, rating, last five | 5 each |
| `get_fighter_fights` | Deeper history with opponent ratings and closing lines | 5 each |
| `get_odds` | Each book's moneyline for the bout | 5 |
| `get_leaders` | Division context for a stat | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_matchup` or `get_h2h` | `get_fighter` for both and compare |
| `predict_fight` | Not available: use ratings and the market, see `elo-by-weight-class` |
| `get_injuries`, `get_weigh_ins` | Not available: say you cannot see them |

## Workflow

1. **Find the fight.** `get_schedule` with `days`, or `get_event` for a known `event_id`. Capture `bout_id`, `scheduled_rounds`, `title_fight`, `weight_class`.
2. **Profiles.** `get_fighter` for each id. Pull `reach_in`, `height_in`, `stance`, `career_rates`, `rating`, `recent_fights`.
3. **Gaps.** Compute differences, A minus B, and say which direction favours whom.
4. **Rating gap to probability.** Rating difference `d = R_a - R_b`. A standard Elo conversion is `p_a = 1 / (1 + 10^(-d/400))`. Treat this as a rough prior. The 400 scale is an assumption, so check it with `probability-calibration` before leaning on it.
5. **Market.** `get_odds` with the `bout_id`. Take the median of `a_close_prob` and `b_close_prob` across books, normalize so they sum to 1 (see `odds-analysis`).
6. **Compare.** If the rating prior and the market agree within a few points, say "no disagreement". If they differ by 8 points or more, look for a reason in the data: layoff, a very different opponent quality, a small sample.
7. **Write.** Lead with the market, then the style matchup, then the risks.

## Style Matchup Checklist

| Question | Data |
|----------|------|
| Who lands more per minute, and who absorbs less? | `sig_landed_per_15`, `sig_absorbed_per_15` |
| Does the striker's defense hold against pressure? | `sig_defense`, `losses_ko` |
| Can the wrestler get it down, and keep it there? | `td_per_15`, `td_accuracy`, `control_share` against the other's `td_defense` |
| Does either have a finishing record? | `wins_ko`, `wins_sub`, `finish_rate` |
| Cardio over five rounds | `round-by-round-stats` fade between round 1 and later rounds |
| Layoff or short notice | Dates in `get_fighter_fights` |
| Reach and stance | `reach_in`, `stance` |

## Hard Limits

- The data has no camp news, injuries, weight cuts or short-notice flags. Say so.
- PFL and Bellator previews have no stat-sheet rates. Use record, rating and market only.
- Rates need samples. If either fighter has under 60 `minutes_fought`, flag it.
- A rating comes from UFC results alone. A fighter coming from another promotion starts at 1500 regardless of record.

## Worked Example

Request: "Preview the main event on the next UFC card."

```
get_schedule(days=14)                 -> pick the main event, note bout_id
get_fighter(fighter_id=<a>)           -> rates, rating
get_fighter(fighter_id=<b>)
get_odds(bout_id=<bout_id>)           -> books[].a_close_prob, b_close_prob
```

For a profile call on Ilia Topuria, the live response gave `sig_landed_per_15` 72.23, `sig_absorbed_per_15` 59.57, `td_defense` 0.952 and a rating of 1655 against a 1670 peak. A preview built on that would say what the numbers support (high volume, takedown defense very hard to crack on a 104-minute sample) and stop there.

## Output Format

```
A vs B, weight class, rounds, date
Market: A 62% / B 38% (median close, devigged, N books)
Rating: A 1655, B 1610 (gap 45, rough prior 56%)
Style: A pressure striker (72 landed per 15), B counter wrestler (td 2.1 per 15)
Edges for A: reach +2 in, defense
Edges for B: control share, finish rate
Unknowns: camp, injuries, weight cut (not in the data)
Risk: B has 3 UFC fights, rates are noisy
```

## Keeping the Cost Down

The cheapest honest preview skips `get_fighter_fights`. `get_fighter` already includes the last five fights and the career rates. Add the history call only when the user asks about opponent quality, layoffs or trends, and say it is 5 credits per fighter and per page. When previewing a whole card, pull `get_odds` with `event_id` once (10 credits) instead of per bout, and pull `get_fighter` only for the fights the user picks.

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Declare a winner with certainty | Fights are high-variance | Give probabilities and the range |
| Ignore the market | The close is the best public estimate | Anchor on the devigged line |
| Compare raw `sig_landed_per_15` across weight classes | Pace differs by division | Compare to `get_leaders` for the class |
| Pull `get_fighter_fights` for both to answer a simple preview | 10 extra credits | Only when form or opponent quality is the question |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| User wants round fade detail | `round-by-round-stats` |
| User wants the whole card | `event-lookup` |
odds-analysis6.37 KB

View saved version →

---
name: odds-analysis
description: "Converts fight odds between American, decimal and fractional formats and removes bookmaker margin to get true implied probabilities for two-way MMA moneylines. Use when the user asks about devigging, vig, no-vig lines, implied probability, overround, the multiplicative, additive, power or Shin method, or how to compare a price with a probability. Do not use for viewing current odds -- see odds-explorer. Do not use for calibrating a model -- see probability-calibration."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Odds Analysis

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_odds` (5 credits per fight, 10 per card) for prices. The math in this skill is local and costs nothing.
> For your own odds file, skip the tool and work with the file.

You are an expert in odds mathematics. Your goal is to turn a two-way moneyline into a fair probability, say how much margin was removed, and be clear about which method was used.

## When to Use

- "What probability does -130 imply?"
- "Remove the vig from this line"
- "Convert +110 to decimal"
- "What is the book's margin on this fight?"

## When NOT to Use

- Looking at what books are posting -- see `odds-explorer`
- Testing a model's probabilities -- see `probability-calibration`
- Method-of-victory or round markets -- not in the data; moneylines and total rounds only

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_odds` | Open, close, current moneyline per book; `a_close_prob`, `b_close_prob` (raw, with vig) | 5 / 10 |
| `get_schedule` | Current `lines` on upcoming fights | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_no_vig_line` | Compute from `get_odds` using the formulas below |
| `get_true_probability` | Same |
| `get_decimal_odds` | Convert locally |

## Format Conversion

```python
def american_to_prob(o):
    return 100 / (o + 100) if o > 0 else -o / (-o + 100)

def american_to_decimal(o):
    return 1 + o / 100 if o > 0 else 1 + 100 / -o

def prob_to_american(p):
    return round(-100 * p / (1 - p)) if p >= 0.5 else round(100 * (1 - p) / p)
```

Decimal odds are `1 / raw_prob`. Fractional odds are decimal minus 1. Always carry full precision and round only when displaying.

## Removing the Vig (Two-way Market)

Raw implied probabilities `q_a` and `q_b` sum to more than 1. The overround is `q_a + q_b - 1`. For a two-way market the margin is the overround itself.

Real example (UFC 298, Bet365 closing line): `a_close` -130, `b_close` +110.

```
q_a = 130/230 = 0.5652    q_b = 100/210 = 0.4762    sum = 1.0414
```

| Method | p_a | p_b | Note |
|--------|-----|-----|------|
| Multiplicative (normalize) | 0.5427 | 0.4573 | Default: scales both proportionally |
| Additive | 0.5445 | 0.4555 | Subtracts half the overround from each |
| Power | 0.5454 | 0.4546 | Solve `q_a^k + q_b^k = 1`; here k is about 1.0625 |
| Shin | 0.5445 | 0.4555 | Assumes insider flow; z about 0.041 |

All four land within about 0.3 points here. The choice matters at long odds. On a -1000/+650 fight, the multiplicative method assigns the underdog a larger share of the vig than the power or Shin methods do, so it will make a long shot look more likely.

```python
import math
def devig_mult(qa, qb):
    s = qa + qb
    return qa / s, qb / s

def devig_power(qa, qb):
    lo, hi = 0.5, 3.0
    for _ in range(100):
        k = (lo + hi) / 2
        lo, hi = (k, hi) if qa**k + qb**k > 1 else (lo, k)
    return qa**k, qb**k

def devig_shin(qa, qb):
    s = qa + qb
    p = lambda z, q: (math.sqrt(z*z + 4*(1-z)*q*q/s) - z) / (2*(1-z))
    lo, hi = 0.0, 0.5
    for _ in range(100):
        z = (lo + hi) / 2
        lo, hi = (z, hi) if p(z, qa) + p(z, qb) > 1 else (lo, z)
    return p(z, qa), p(z, qb)
```

Recommendation: use multiplicative for even fights, and check power or Shin when the favourite is -300 or shorter. Say which one you used.

## Across Books

1. Devig each book separately. Never average raw prices and then devig.
2. Take the median of the devigged `p_a` across books. The median resists a stale quote.
3. If a book posts only one side, drop that book for that fight.
4. For the best price on each side, shop prices (`odds-explorer`), but keep the consensus probability from the median.

ESPN's feed is often one book per fight from mid-2024. With one book, say the consensus is that one book.

## Open to Close

For the same book, convert the open and the close to devigged probabilities and subtract. This movement is in probability points, comparable across favourites and underdogs. Real example, same Bet365 row: open `a_open` -170 and `b_open` +140 gave raw `q_a` 0.630 and `q_b` 0.417 (sum 1.046). By the close, `a_close` -130. The market moved toward the underdog by about 6 raw points on side `a`.

## Total Rounds

`get_odds` also returns `total_rounds` (for example 3.5) with `over_odds` and `under_odds` (for example -120 and -120). Devig the pair the same way. Both sides at -120 imply 54.5% raw each and 50% each after the vig. The total rounds line is an expected fight length proxy, not a method-of-victory price.

## Break-even and Edge Arithmetic

- Break-even probability for a price is its raw implied probability `q`. At -130 you need to win 56.5% of the time just to break even.
- A model that is 3 points above the devigged market is not a 3 point edge at the price. The vig is still in the price.

## Pitfalls

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Treat `a_close_prob` as a true probability | Includes the vig | Normalize or use power or Shin |
| Average American odds across books | Odds are not linear | Convert to probability first |
| Say "+110 is 110%" | It is a payout on 100 staked | Convert to 47.6% raw |
| Devig a three-way market the same way | MMA here is two-way | Handle draws separately if the user raises them |
| Present a devigged number as exact | It depends on method | Name the method |

## Output Format

```
Line: A -130 / B +110 (Bet365 close)
Raw implied: 56.5% / 47.6%   Overround 4.1%
Devigged (multiplicative): 54.3% / 45.7%
Devigged (power): 54.5% / 45.5%
Break-even at the posted price: A 56.5%, B 47.6%
```

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Want to check historical accuracy of closes | `favourite-calibration` |
| Want to see more books | `odds-explorer` |
odds-explorer6.35 KB

View saved version →

---
name: odds-explorer
description: "Compares sportsbook moneylines for a UFC, PFL or Bellator fight or card: open, close and current prices, best price per side, vig, total rounds, and line movement. Use when the user asks for the odds, the line, who is favoured, which book has the best price, how the line moved, or what a fight closed at. Do not use for removing the vig or converting formats -- see odds-analysis."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Odds Explorer

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_odds` with `bout_id` (5 credits) for one fight or with `event_id` (10 credits) for a whole card.
> Use `get_line_movement` (10 credits) for snapshots before the bell, recent fights only.
> Use `get_schedule` (2 credits) for the current line on every upcoming fight.

You are an expert in fight betting markets. Your goal is to show the user what is on offer, what it costs them in vig, and what the line did.

## When to Use

- Current prices on an upcoming fight
- Best price for each fighter across books
- Opening vs closing line on a past fight
- Total rounds line and over/under prices
- Reading line movement as a market signal

## When NOT to Use

- Converting odds or devigging -- see `odds-analysis`
- Testing whether favourites win as often as priced -- see `favourite-calibration`
- Method of victory, round betting, props -- not in the data

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_odds` | Every book's open, close and current moneyline for both fighters, implied probabilities, total rounds | 5 per fight, 10 per card |
| `get_line_movement` | Timed snapshots of each book's line | 10 |
| `get_schedule` | Upcoming fights with `lines` from any book that posted | 2 |
| `get_fight` | Includes `odds` per book (2024 on) alongside the stat sheets | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_live_odds` | `get_odds` returns `a_current` and `b_current` |
| `get_method_odds`, `get_round_odds`, `get_props` | Not available: moneylines and total rounds only |
| `compare_books` | Read the `books` array from `get_odds` and compare |
| `get_best_line` | Compute the best price from `books` |

## Data Shape

`get_odds` returns `data.bouts`, a list. Each bout has the fight header and `books`. Each book row:

```
provider, a_open, a_close, a_current, b_open, b_close, b_current,
a_close_prob, b_close_prob, total_rounds, over_odds, under_odds, captured_at
```

`a` and `b` are `fighter_a` and `fighter_b` from the bout header. Prices are American odds. `a_close_prob` and `b_close_prob` are the raw implied probabilities of the closing price, before devigging, so they add to more than 1.

Real example (UFC 298, Bet365): `a_close` -130 and `b_close` 110 gave `a_close_prob` 0.565 and `b_close_prob` 0.476, which sum to 1.042. That 4.2 point overround is the vig. See `odds-analysis` to remove it.

## Coverage Limits

- UFC opening and closing lines start with the June 2020 cards. PFL starts April 2023. Bellator covers January 2022 to 2024 with fewer books.
- A median of 5 to 7 books per fight. From mid-2024 ESPN's feed is often a single book, so thin cards have thin comparisons.
- Pre-fight snapshots (`get_line_movement`) exist only for recent fights: ESPN once a day from October 2026, The Odds API hourly in the two days before. For older fights, open vs close from `get_odds` is the movement you have.

## Finding the Best Price

```python
books = bout["books"]
best_a = max(books, key=lambda b: b["a_close"] if b["a_close"] is not None else -9999)
best_b = max(books, key=lambda b: b["b_close"] if b["b_close"] is not None else -9999)
```

For American odds a higher number is always the better price for the Odds reader (+150 beats +140, -110 beats -120). Skip rows where the price is null. Report the provider, and note that a best price on both sides from different books is a "shop" and can have a combined overround below the market's.

## Reading Movement

For a past fight, move = close minus open on the same side. Convert to implied probability first (`odds-analysis`) so +/- crossings do not mislead: -170 to -130 is a 6.4 point drop in implied probability for that fighter even though both numbers are negative.

- Consensus matters more than one book. Look at the median move across providers.
- A move that most books share is information. A move at one book is a book-specific price.
- Fight week news (weight miss, injury, late replacement) explains most large moves. The data has no news. Say the cause is unknown.
- `get_line_movement` returns `data.movement`, an object keyed by provider, each a list of `{captured_at, a_price, b_price, total_rounds}`, plus `snapshots`. Use `provider` to limit it to one book.

## Worked Example

Request: "Where is the line on the next Contender Series main event?"

```
get_schedule(days=7)  -> bout 401891665, lines:
  BetOnline  a -130, b 110, total_rounds 2.5
  Pinnacle   a -130, b 111
get_line_movement(bout_id="401891665")
  movement.BetOnline: three hourly snapshots, a_price -130 each time
```

Answer: the two books agree on side a at -130, the line had not moved across the captured snapshots, and the `total_rounds` is posted by one book only. The user should not read that as a total consensus.

## Output Format

```
Fight: A vs B (date, weight class)
Book        A open  A close   B open  B close   Total
Bet365      -170    -130      +140    +110      3.5
...
Best close: A -125 (Book), B +115 (Book)
Close, devigged median: A 54.2%, B 45.8%   (see odds-analysis)
Move, open to close: A implied 63.0% -> 56.5%
Note: lines are moneylines only; no props in this data.
```

## Anti-patterns

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Call `get_odds` per bout across a card | 5 each vs 10 for the card | `event_id` |
| Say "sharp money" from one book's move | One book is not a market | Use the median move |
| Compare `a_close_prob` to a model without devigging | Includes vig | `odds-analysis` first |
| Read `get_line_movement` for a 2021 fight | No snapshots that old | Use open and close |
| Present nulls as zero | A null price means no quote | Skip it and say so |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| User wants true probability | `odds-analysis` |
| User wants history of favourites | `favourite-calibration` |
probability-calibration6.95 KB

View saved version →

---
name: probability-calibration
description: "Checks and fixes the calibration of fight win probabilities from a model or a rating: reliability tables, Brier score, log loss, calibration slope and intercept, Platt scaling and isotonic regression, and how to compare with the devigged closing line. Use when the user asks whether a model's probabilities are trustworthy, wants a reliability diagram, or wants to tune the Elo scale. Do not use to decide whether the market is efficient -- see favourite-calibration."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Probability Calibration

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Outcomes and pre-fight ratings: `get_fighter_fights` (5 credits per page, `rating_before` and `result`). Market benchmark: `get_odds` with `event_id` (10 credits per card).
> For your own predictions file, skip the tool.

You are an expert in probabilistic forecasting. Your goal is to answer one question: when the model says 70%, does the fighter win about 70% of the time, and is that better than the closing line says?

## When to Use

- "Is my fight model calibrated?"
- "Is the Elo scale of 400 right for MMA?"
- "Build a reliability table"
- "Compare my model to the closing line"

## When NOT to Use

- Judging whether the betting market is calibrated -- see `favourite-calibration`
- Splitting data safely -- see `walk-forward-validation`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_fighter_fights` | `rating_before`, `result`, `median_closing_line` per fight for one fighter | 5 per page |
| `get_event` | Results and winners for a card | 2 |
| `get_odds` | Closing lines and `winner` for a card, the benchmark | 10 |
| `get_fight` | Pre-fight `elo.before` for both sides | 5 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_predictions` | Not available: the API does not publish fight predictions. Bring your own model output |
| `get_model_probabilities` | Same |
| `get_calibration` | Compute with the code below |

## Inputs

Each row needs: the model probability for one named fighter, the outcome (1 if that fighter won, 0 if not), the fight date, and ideally the devigged closing probability for the same fighter. Orient every row by `fighter_id`, never by side `a` or `b`. Drop draws and no contests, and count them.

Predictions must be made with information from before the fight. If the model used ratings or rates, those must be `rating_before` or rates computed from earlier fights only (`walk-forward-validation`).

## Metrics

| Metric | Formula | Reading |
|--------|---------|---------|
| Brier score | mean((p - y)^2) | Lower is better. Coin flip is 0.25 |
| Log loss | -mean(y ln p + (1 - y) ln(1 - p)) | Punishes confident misses hard. Coin flip is 0.693 |
| Calibration slope | coefficient on logit(p) in a logistic fit of y | 1 is ideal. Below 1 means overconfident |
| Calibration intercept | with slope fixed at 1 | Positive means the model under-predicts the favourite |
| Expected calibration error | weighted mean of abs(bin gap) | Summary of the reliability table |

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

## Reliability Table

1. Bin predictions. With a few hundred fights use 5 bins of equal count (quantile bins), not equal width. Equal width leaves empty bins at the extremes.
2. In each bin: n, mean predicted, observed rate, and a Wilson interval (see `favourite-calibration`).
3. Plot predicted on x, observed on y, with the diagonal. Bars for n below 30 should be drawn lightly or merged.

Fight data is small. Ten bins on 400 fights gives 40 per bin and an interval about plus or minus 15 points. Prefer fewer bins and the slope and intercept, which use all the data.

## Fixing Miscalibration

- **Platt scaling.** Fit `logit(p_cal) = a + b * logit(p)` by logistic regression on held-out predictions. Two parameters, stable on small samples.
- **Isotonic regression.** Flexible, but needs a few hundred rows per side and can overfit. Check on a later period.
- **Temperature.** For an Elo prior, tune the `scale` in `p = 1/(1+10^(-d/400))` by minimizing log loss on a calibration window.

Fit the correction on one period and evaluate it on the next. Fitting and scoring the same rows rewards overfitting.

## Compare with the Market

Score the devigged closing probability on exactly the same rows (`odds-analysis`). That is the benchmark. If the model's Brier or log loss is not lower than the market's on out-of-sample rows, the model has no information the market lacks, however good it looks in isolation.

Test the difference with a paired bootstrap over fights (resample fight indices, recompute the score gap, repeat 2000 times) and report the interval. With a few hundred fights the interval will usually include zero. Say so.

## Pitfalls

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Evaluate on training rows | Optimistic | Hold out later fights |
| Use current ratings for past fights | Leaks results | `rating_before` |
| Bin at fixed width | Empty and tiny bins | Quantile bins, or slope and intercept |
| Judge by accuracy alone | Hides overconfidence | Brier and log loss |
| Compare to 50% | Weak baseline | Compare to the devigged close |
| Pool PFL and UFC | Different books, markets | Separate runs |

## Worked Example

Request: "Is the Elo scale of 400 right?"

```
get_ratings(weight_class="Lightweight", limit=50)    # who is active (2 credits)
get_fighter_fights(fighter_id=..., limit=200)         # per fighter: rating_before, rating_after, result
```

Each history row gives `rating_before` for the fighter. The opponent's pre-fight rating is on the opponent's own row for the same `bout_id`, so collect rows for both fighters and join on `bout_id`. For each fight compute `d = R_self - R_opp`, then `p_win` at several scales (300, 400, 500), score each with log loss on fights after a chosen date, and pick the scale that minimizes it on an earlier window and confirm on the later one. Expect a flat optimum: report the interval of scales within one standard error, not a single best number.

## Output Format

```
Fights scored: N (draws/NC dropped: k), window: dates
              Brier   LogLoss   Slope   Intercept
Model          x.xxx   x.xxx     x.xx    x.xx
Closing line   x.xxx   x.xxx     x.xx    x.xx
Paired bootstrap, model minus market Brier: x.xxx (95% CI a to b)
Reliability (5 quantile bins): table
Verdict: calibrated within noise / overconfident / no information beyond market
```

The numbers above are placeholders for layout. Replace them with computed values.

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Not better than the close | Improve features, retest with `walk-forward-validation` |
| Need price benchmarks over many seasons | `favourite-calibration` |
round-by-round-stats6.56 KB

View saved version →

---
name: round-by-round-stats
description: "Computes per-round striking and grappling rates for UFC fights: significant strikes landed and attempted by round, target and position splits, takedowns, control time, submission attempts, and fatigue fades between rounds. Use when the user asks how a fighter performs by round, who fades, strike differential per round, control time, round-by-round scoring, or pace. Do not use for career averages across fights -- see career-rates. Do not use for the result of a fight -- see fight-lookup."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Round-by-Round Stats

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Use `get_fight` (5 credits) and read `data.rounds`. UFC only.
> Find `bout_id` values with `get_fighter_fights` (5 credits) or `get_event` (2 credits).

You are an expert in fight analytics. Your goal is to turn the round-by-round lines into rates a person can compare, with the right denominators and honest sample sizes.

## When to Use

- Strikes landed and absorbed in each round of a fight
- Whether a fighter's output or accuracy drops in rounds 3 to 5
- Where strikes land (head, body, leg) and from where (distance, clinch, ground)
- Control time, takedown attempts and submission attempts by round
- Which fighter would win each round on volume (a stat proxy, not the judges)

## When NOT to Use

- Career averages -- see `career-rates`
- The final result and scorecards -- see `fight-lookup`
- PFL or Bellator -- no round lines exist there

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `get_fight` | `rounds` array: one row per fighter per round | 5 |
| `get_fighter_fights` | Fight list with `bout_id`, `sig_landed`, `sig_absorbed`, `td_landed` totals | 5 per page |
| `get_event` | Fight list for a card with `bout_id` | 2 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_round_stats` | `get_fight`, `rounds` |
| `get_strike_map` or `get_strike_location` | The head, body, leg splits in `rounds` and `fighters[].stats` |
| `get_fight_pace` | Compute from `rounds` as shown below |
| `get_control_time` | `control_seconds` in `rounds`, `time_in_control_seconds` in `stats` |

## Data Shape

Each row in `data.rounds`:

```
fighter_id, fighter_name, round, knock_downs,
sig_landed, sig_attempted, total_landed, total_attempted,
td_landed, td_attempted, sub_attempts, reversals, control_seconds,
head_landed, head_attempted, body_landed, body_attempted, leg_landed, leg_attempted,
distance_landed, distance_attempted, clinch_landed, clinch_attempted,
ground_landed, ground_attempted
```

Real shape from UFC 298, round 1: Volkanovski `sig_landed` 26 of `sig_attempted` 52, with leg strikes 11 of 11 and `control_seconds` 0. A two-round fight returns four rows. Group by `fighter_id`, then order by `round`.

## Core Metrics

All of these come straight from the rows.

| Metric | Formula |
|--------|---------|
| Accuracy | `sig_landed / sig_attempted` (guard zero) |
| Strike differential | `sig_landed(self) - sig_landed(opp)` in the same round |
| Volume share | `sig_landed(self) / (sig_landed(self) + sig_landed(opp))` |
| Target mix | `head_landed / sig_landed`, same for body and leg |
| Position mix | `distance_landed / sig_landed`, clinch, ground |
| Takedown rate | `td_landed / td_attempted` |
| Control share | `control_seconds / round_seconds` |
| Submission pressure | `sub_attempts` per round |

Round length is 300 seconds for a full round. The last round of a finish is partial. For a fight that ended in round 2 at 3:32, round 2 was 212 seconds. Use `end_round`, `end_clock` and `duration_seconds` from the bout header.

## Rates per Minute

A raw count is wrong in a partial round. Scale it:

```python
def per_15(count, seconds):
    return 0 if seconds == 0 else count * 900 / seconds
```

Use `per_15` to compare a finish in round 2 with a five-round decision. Never add rounds from different fights without the matching seconds.

## Fatigue Fade

For a fight that went at least 3 rounds:

```python
fade = (sig_landed_per_min_round_last - sig_landed_per_min_round_1) / sig_landed_per_min_round_1
```

A negative number means the fighter slowed. Do not call it cardio from one fight. The opponent's pressure, takedowns eating clock and the round the fight ended all move it.

To say something about a fighter, collect their last 5 to 8 distance fights (via `get_fighter_fights`, filter rows where `end_round` equals `scheduled_rounds`), then `get_fight` per bout (5 credits each). Report the median fade and the count of fights.

## Round Winner by Volume

A simple, transparent proxy for how judges score a round:

1. Knockdown edge wins the round (10-8 candidates if heavy).
2. Otherwise more `sig_landed` wins.
3. Break ties with control seconds, then takedowns.

Label it "stat proxy". Judges weigh damage, effective grappling and aggression, and `judges_scores` in `get_fight` has only totals per judge, not per round.

## Worked Example

Request: "Did Volkanovski fade against Topuria?"

```
get_fight(bout_id="401612970")
rows = data.rounds filtered to fighter_id 3949584   # 2 rounds, fight ended R2 at 3:32
```

Round 1: 26 landed of 52 attempted (50.0% accuracy). Compute round 2 from its row and divide by its 212 seconds, not 300. The honest answer for a two-round fight is "no fade can be measured here": a single early finish has no late rounds. Pull earlier five-round fights before claiming a pattern.

## Output Format

```
Round   Sig landed   Acc   Opp landed   Diff   Head/Body/Leg    Control   TD
1       26/52        50%   18           +8     8/7/11           0:00      0/0
2       21/55        38%   ... (212 s, per-15 rate 89)
Fade (per-min round 2 vs round 1): -20%   Sample: 1 fight
```

## Pitfalls

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Treat attempted strikes as the opponent's defence | Attempts include strikes blocked, missed or slipped | Report accuracy and defence separately |
| Compare partial and full rounds as counts | Different seconds | Use `per_15` with the actual seconds |
| Claim cardio problems from one fight | Opponent and clock effects | Aggregate over 5+ distance fights |
| Mix rounds in a fight with a stoppage | Last round is cut short | Use `end_clock` for the final round |
| Run on PFL fights | `rounds` is null | Say it does not exist |

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Pattern across the career | `career-rates` |
| Use rates as model inputs | `walk-forward-validation` |
| Preview a booked fight | `fight-preview` |
walk-forward-validation6.72 KB

View saved version →

---
name: walk-forward-validation
description: "Validates fight-prediction models without leakage using time-ordered folds: expanding or rolling windows, purge gaps, rating and rate features built only from earlier fights, baselines, and significance checks. Use when the user asks how to test a model fairly, whether results are leaking, how to split fight data, or how to build features without using the future. Do not use for calibration plots -- see probability-calibration."
metadata:
  version: 1.0.0
  author: FighterAPI
---

# Walk-Forward Validation

> **Default data tool:** FighterAPI (`fighterapi-tool`).
> Training rows come from `get_event` (2 credits per card, results), `get_fighter_fights` (5 credits per page, `rating_before` and stats) and `get_odds` with `event_id` (10 credits per card, closing lines).
> For your own CSV, skip the tool.

You are an expert in time-series validation. Your goal is to estimate how a model would have done if it had been run live, fight by fight, and to refuse any test that lets the future into the past.

## When to Use

- Splitting fight data for a model test
- Checking an existing result for leakage
- Choosing windows (train on 2010 to 2019, test 2020)
- Deciding whether a feature is safe to use

## When NOT to Use

- Calibration of probabilities -- see `probability-calibration`
- Describing fighters -- see `fighter-scouting`

## Commands Available

| Command | What It Does | Credits |
|---------|-------------|---------|
| `list_events` | Card ids and dates by `season` | 1 |
| `get_event` | Results of a card | 2 |
| `get_fighter_fights` | History with `rating_before`, stats, `median_closing_line` | 5 per page |
| `get_odds` | Closing lines for a card | 10 |

## Commands That Do NOT Exist

| Not Available | Use Instead |
|--------------|-------------|
| `get_training_set` or `export_dataset` | Assemble rows from the calls above |
| `get_features` | Compute them locally from earlier fights |
| `get_ratings` as-of a date | `rating_before` on each fight row |

## Why Random K-fold Fails Here

1. **Time order.** A fighter's later fights are informed by their earlier ones. Shuffling lets the model train on a fighter's 2024 fights and test on 2021.
2. **The same fighters in train and test.** Fights share fighters. A random split memorizes fighters instead of learning skill.
3. **Rating contamination.** Any rating that includes a fight's result cannot be a feature for that fight.
4. **Rule and era change.** Weight classes and the sport's style shift. The test must look like the future.

## Fold Structure

Expanding window: train from the start up to a cutoff date, test on the next block, then move the cutoff forward and repeat.

```
Fold 1: train 2005-2016   test 2017
Fold 2: train 2005-2017   test 2018
Fold 3: train 2005-2018   test 2019
Fold 4: train 2005-2019   test 2020
...
```

For odds-based work the first test fold cannot start before June 2020 (UFC lines start with those cards), so closing-line comparisons run on 2020 onward. A rating model can be trained on earlier years and tested on the line era.

Use blocks of one year, or six months if volume is thin. A UFC year has dozens of cards (52 final cards came back for 2025, check `bouts` per card in `list_events` for the fight count), so one-year blocks give usable test sets. Reporting one summary number from one fold is not enough: show every fold.

## Temporal Discipline Inside a Fold

- Every feature for a fight is computed from fights strictly before its `event_date`. Fights on the same card are simultaneous: do not let one fight feed another on the same date.
- Use `rating_before` from the fight row, not `get_ratings` or `rating.current`.
- Career rates (`career_rates` from `get_fighter`) are lifetime values as of today and leak the future for any past fight. Rebuild rates from the fighter's earlier rows or round lines.
- Opponent features obey the same rule.
- Encode a fighter's debut as missing, not zero.
- Fit scalers, imputers and calibrators on the training block only.
- Purge: if a feature spans a window, drop training fights whose window overlaps the test block.

```python
def features_as_of(history, as_of_date):
    prior = [f for f in history if f["event_date"] < as_of_date]
    n = len(prior)
    if n == 0:
        return None
    return {"fights": n, "wins": sum(f["result"] == "win" for f in prior)}
```

`history` is the `fights` array from `get_fighter_fights`. `event_date` strings compare correctly because they are ISO dates.

## Baselines

A model result means nothing without these on the same fold:

| Baseline | Source |
|----------|--------|
| Coin flip (Brier 0.25) | Constant |
| Higher rating wins | `rating_before` |
| Devigged closing line | `get_odds`, see `odds-analysis` |
| Always pick the favourite | Closing line |

If the model does not beat the closing line on log loss out of sample, it has no usable edge. Report that plainly.

## Significance

- Accuracy against a baseline: a binomial or paired McNemar test on the same fights.
- Log loss or Brier difference: paired bootstrap over fights, 2000 resamples, report the 95% interval.
- Many models or features tried means many tests. Reserve the final fold, use it once, and discount anything found by search.

Example scale: 300 test fights, a model correct 62% vs a baseline 58%, gives a gap whose paired interval will usually include zero. Fight data is small and noisy.

## Report Per Fold

```
Fold  Train          Test   N    Model Brier   Close Brier   Model LL   Close LL
1     2005-2016      2017   ..   ..            n/a           ..         n/a
4     2005-2019      2020   ..   ..            ..            ..         ..
Mean over folds, and paired bootstrap interval on the difference
```

Pre-2020 folds have no closing lines. Mark them n/a rather than filling with something else.

## Pitfalls

| What the Model Might Do | Why It Is Wrong | Do This Instead |
|---------------------|----------------|-----------------|
| Use `career_rates` as a feature for old fights | Includes later fights | Rebuild from earlier rows |
| Tune hyperparameters on the test fold | Optimistic | Nested time-ordered tuning |
| Report one fold | Cherry-picked window | All folds |
| Count same-card fights as ordered | They happened together | Group by `event_id` |
| Include draws as losses | Wrong label | Drop and count |
| Skip a baseline | Results unanchored | Always run the close |

## Credit Planning

| Task | Credits |
|------|---------|
| Results for one year of cards | about 100 |
| Closing lines for one year of cards | about 520 |
| Histories for 100 fighters, one page each | 500 |

Budget first, then pull. Cache every response locally; do not re-fetch.

## What to Do Next

| Finding | Next Skill |
|---------|-----------|
| Model does not | Revisit features; `round-by-round-stats` and `career-rates` for ideas |

Release notes

Initial release. Read-only MMA 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
FighterAPI
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 fighters, events and bouts
  • Read upcoming cards and past results
  • Read fighter profiles and fight history
  • Read striking and grappling leaders
  • Read Elo ratings and sportsbook odds (information only)

Package observed Nov 3, 2026.

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

plugin_asdk_app_6ad146a9e4448191913cb788e416319b

Download plugin data (JSON)