# Using Sports Data

> The Wayfinder sports data catalog — every canonical resource, its params, and which sports support it; betting (odds/props/futures) coverage per league; Lab (backtesting) specifics; id/season/category conventions.

- Skill: `wayfinderfoundation/using-sports-data` (Agent Skill)
- Install (CLI): `npx skillmds add wayfinderfoundation/using-sports-data`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wayfinderfoundation/using-sports-data/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: WayfinderFoundation (https://skillmd.com/u/wayfinderfoundation)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/wayfinderfoundation/using-sports-data

---


## What you need to know (TL;DR)

All sports data flows through three MCP tools backed by the backend gateway (the provider
key never leaves the backend; the surface is provider-agnostic):

- `wayfinder_sports_snapshot` — bounded live reads (scoreboard/odds/props/injuries/lookups/stats).
- `wayfinder_sports_provider` — the full façade: `action="catalog"` lists every callable
  `endpoint_id` **with `supported_leagues` per data endpoint — that catalog is the runtime
  source of truth for "which sports support what"**; `action="call"` invokes one.
- `wayfinder_sports_backtest_state` — canonical run/job monitoring for Lab backtests.

An unsupported (resource, sport) combo returns `resource_unavailable_for_league` **with the
leagues that DO support it** — never guess availability, read the error or the catalog.
Within one task, treat that error as a task-local unavailable-resource guard: do not retry
the same `(endpoint_id, sport)` combo for every game/match.

Leagues: `nba nfl mlb nhl wnba ncaaf ncaab ncaaw cbb epl laliga seriea bundesliga ligue1 ucl
mls worldcup mma f1 atp wta pga cs2 lol dota2`. Lab (backtesting) = **nba/nfl/nhl/mlb only**.
Common wrong guesses → right slug: `fifa`/`fiba` → `worldcup`; `soccer` → a league slug
(`epl`/`laliga`/`ucl`/...); `football` → `nfl`; `tennis` → `atp`/`wta`; `golf` → `pga`.
An invalid slug returns a 400 listing the valid choices — read it, don't keep guessing.

## Canonical resources (what each returns, key params)

Resource ids are generic and resolve per-league (e.g. `competitors` = players/fighters/drivers;
`events` = games/matches/MMA events/F1 sessions/golf tournaments). Call shape:
`sports_provider(action="call", endpoint_id="data.<resource>.<list|get>", sport=..., query={...}, path_params={...})`.

| endpoint_id | Returns | Key params |
|---|---|---|
| `data.events.list` / `data.event.get` | schedule/fixtures + results | `query`: `dates[]`, `seasons[]`, `per_page`; get: `path_params.id` |
| `data.teams.list` / `data.team.get` | teams / clubs / constructors | `query.per_page` |
| `data.competitors.list` / `.get` | players / fighters / drivers | `query`: `search`, `player_ids` (array), `per_page` |
| `data.competitors_active.list` | current rosters only | as above |
| `data.standings.list` | standings / rankings / driver standings | `query.season` (most team sports require it) |
| `data.team_standings.list` | constructor/team standings (F1) | `query.season` |
| `data.player_stats.list` | per-game logs | `query`: `player_ids` (array), `seasons[]`, `dates[]`, `per_page`, `postseason` |
| `data.player_season_stats.list` | player season totals | `query`: `season`, `player_ids` where supported; NHL is per-player: `path_params.player_id` |
| `data.season_averages.list` | per-player season averages (NBA-family) | `query`: `season`, `player_id`; `path_params.category` + `query.type` for categorized |
| `data.team_season_averages.list` | team season averages | `query`: `season`, `season_type` (`regular`/`playoffs`), `type` (`base`/`advanced`); `path_params.category` |
| `data.team_stats.list` | team per-game stats | `query`: `game_id`/`season` |
| `data.team_season_stats.list` | team season stats | `query.season`; NHL per-team: `path_params.team_id` |
| `data.player_advanced_stats.list` | advanced metrics | `query`: `seasons[]`, `player_ids`; NFL: `path_params.category` = `rushing`/`passing`/`receiving` |
| `data.leaders.list` | stat leaders | `query`: `season`, `stat_type` |
| `data.injuries.list` | injury/availability report | `query.per_page` |
| `data.box.list` / `data.box.live` | box scores (historical / live) | `query.date` / none |
| `data.lineups.list`, `data.plays.list` | lineups, play-by-play | `query.game_id` (game-scoped) |
| `data.matchups.list` | tennis head-to-head; MLB batter-vs-pitcher | tennis: `query` player ids; MLB versus: batter/pitcher ids |
| `data.career_stats.list` | career stats (tennis) | `query.player_id` |
| `data.shots.list` | soccer shot maps with **xG** | `query.match_id`/`game_id` depending on league |
| `data.match_events.list` | soccer goals/cards/subs | `query.match_id`/`game_id` depending on league |
| `data.momentum.list`, `data.pregame_forms.list` | soccer momentum / recent form where catalog-supported; not guaranteed for every soccer-family league | `query.match_id`/`game_id` depending on league |
| `data.rosters.list` | rosters; NFL depth charts | soccer: `query`; NFL: `path_params.team_id` |
| `data.results.list` | F1 session results / PGA tournament results / MMA fight results | `query`: season/event/fight/tournament filters |
| `data.qualifying.list`, `data.pit_stops.list`, `data.laps.list` | F1 detail (plan-gated upstream) | `query` session/event ids |
| `data.venues.list` | circuits / stadiums / courses | — |
| `data.round_stats.list` | PGA strokes-gained round stats | `query` tournament/player |
| `data.splits.list`, `data.plate_appearances.list` | MLB splits / plate appearances | `query` player/season |
| `data.pitcher_pitch_stats.list`, `data.hitter_pitch_stats.list` | MLB pitch-type breakdowns | `query` player/season |
| `data.conferences.list`, `data.bracket.list` | college conferences / March Madness bracket | `query.season` |
| `data.player_contracts.list`, `data.team_contracts.list` | NBA salaries/payroll (plan-gated upstream) | `query` |
| `data.shot_locations.list` | WNBA shooting zones | `query` player/season |
| `data.odds.list` | odds context | snapshot: `event_id`/`game_id`/`fight_id`/`tournament_id` OR `date`; façade: provider-specific ids |
| `data.player_props.list` | player prop lines + over/under odds | snapshot: `event_id`; façade: `game_id`, `match_id`, or `tournament_id` |
| `data.futures.list` | outright/futures odds | snapshot: `season`, optional `event_id`/`market_type`; façade: provider-specific filters |

## Which sports have what (highlights — catalog is authoritative)

- **nba** — richest: full stats family (game logs, season averages + categories
  `general/clutch/shooting/playtype/tracking/hustle/defense/shotdashboard` with `type` sub-param,
  team averages incl. `pace`/`def_rating` under `type=advanced`), advanced stats, box/live box,
  lineups, plays, leaders, injuries, contracts (plan-gated), odds + player props (the only league
  on the absolute v2 betting surface — handled transparently).
- **nfl** — game logs, season stats, team stats + team season stats, advanced
  rushing/passing/receiving (via `category`), per-team depth-chart rosters
  (`path_params.team_id`), plays, injuries, standings, odds + props.
- **mlb** — game logs, season stats, **batter-vs-pitcher matchups**, splits, plate appearances,
  pitch-type stats (pitcher + hitter), lineups, plays, injuries, odds + props.
- **nhl** — box scores, plays, injuries, standings; season stats are **per-player/per-team
  id-scoped** (no flat game-log endpoint); player/team leaders; odds + props.
- **wnba** — NBA-style stats + advanced + **shot_locations**; odds + props.
- **soccer leagues** (epl/laliga/seriea/bundesliga/ligue1/ucl/mls) — matches, rosters,
  injuries, standings, player/team match stats, **xG shots**, match events, momentum and
  pregame forms where the catalog lists support; odds + props; **futures** for ucl.
  EPL serves from its v2 API transparently.
- **worldcup** — matches, standings/results, odds, player props, and futures where live.
  Do **not** call `data.pregame_forms.list` for `worldcup` unless the runtime catalog
  explicitly lists `worldcup` in `supported_leagues`; current `resource_unavailable_for_league`
  means use standings/results plus web/news research for pregame form.
- **tennis** (atp/wta) — players, matches, rankings (as `standings`), **head-to-head matchups**,
  match stats, career stats; odds only.
- **mma** — fighters, events (cards), **fight results** (`data.results.list`), fight stats,
  rankings; odds only.
- **f1** — drivers, constructors (`teams`), sessions (`events`), qualifying, results, laps,
  pit stops, driver + team standings, venues; **futures only** (no per-race odds). Much of the
  deep telemetry is plan-gated upstream — expect `resource_unavailable_for_league` if unentitled.
- **pga** — players, tournaments, results, strokes-gained round stats, venues (courses);
  **futures + props**, no match odds.
- **college** (ncaaf/ncaab/ncaaw/cbb) — teams, players, games, standings, plays, conferences,
  **bracket** (ncaab/ncaaw March Madness); odds only, no props.
- **esports** (cs2/lol/dota2) — teams/players/matches (cs2 deepest: match/map stats); **no betting**.

## Betting coverage map

| Markets | Leagues |
|---|---|
| odds + player props | nba, nfl, mlb, nhl, wnba, epl, laliga, seriea, bundesliga, ligue1, mls, ucl, worldcup |
| odds only | ncaaf, ncaab, ncaaw, cbb, mma, atp, wta |
| futures | ucl, worldcup, f1, pga |
| none | cs2, lol, dota2 |

Sportsbook odds/props/futures are **optional context, never the executable price**.
Wayfinder sports views are actionable only against Polymarket or Hyperliquid prediction
market books. Compute edges with `wayfinder_paths.quant.sports_props.market_edge(...)`
or the prediction-market quant helpers against PM/HL order-book prices.

## Conventions that bite

- **Arrays**: list-valued query params just work — `query={"player_ids": [161, 1057262518]}`
  bulk-fetches in one call (the gateway maps to the provider's `key[]` form). Batch a whole
  slate's game logs in ONE `data.player_stats.list` call.
- **Ids are one namespace per sport**: the `player_id` in props, game logs, and players are the
  same id space (newer players just have huge ids). Hydrate names via `data.competitors.list`
  with `player_ids`.
- **Seasons**: integer start-year (`2025` = the 2025-26 NBA season). `season_type`:
  `regular`/`playoffs` where supported; game logs accept `postseason` true/false.
- **Id-scoped resources**: where the error or this doc says per-team/per-player, pass
  `path_params={"team_id": ...}` or `{"player_id": ...}` (NFL rosters, NHL season stats).
- **Snapshot id convention**: prefer `event_id` from scoreboard cards. The backend maps it to
  `game_id` for US team sports, `match_id` for soccer/World Cup props, `fight_id`/`event_id`
  for MMA odds, `tournament_id` for PGA props and tennis tournament odds, and `event_ids[]`
  for F1 futures. `game_id` remains a legacy alias.
- **Provider façade id convention**: use the exact provider filter requested by the endpoint
  (`game_id`, `match_id`, `fight_id`, `tournament_id`, `event_ids[]`, etc.); call catalog or
  read errors instead of guessing.
- **Caching**: non-live data (stats/averages/rosters) is cached server-side for hours — repeats
  are cheap; odds/props/futures stay near-live (~15s). Still batch.
- **Pagination**: `per_page` (max 100) + cursor in `meta.next_cursor` where present.

## Scripted analysis (inside `core_run_script`)

**Betting analysis is a FUNNEL that starts from the executable boards: (1) ENUMERATE
what's tradeable — the Polymarket per-game event (`get_event` on the
`{league}-{away}-{home}-{date}` slug) and Hyperliquid HIP-4 outcomes — into a candidate
table; (2) INFORMATION via sports data and the canned pipelines below where useful
(complete fetches, model features, optional sportsbook-context market math; never pull
odds from the web); (3) TRIAGE candidates by liquidity and fair-value delta
(hypothesized fair probability/range minus executable PM/HL price); use PM/HL
cross-venue differences as venue-noise/liquidity sanity checks, not the main
objective; (4) DEEP-DIVE
survivors with whatever data sharpens the number (full matchup history across seasons,
comparable players vs that opponent, minutes/usage given injuries/lineups — multi-season
`player_stats`, `matchups`, advanced stats), each input a weighted evidence card; (5)
GATE per candidate and answer with the annotated board. MODELING is the agent's
judgment: `game_slate` separates an INFORMATION section (facts) from a labeled
REFERENCE MODEL (one opinion — adjust or replace it; `--data-only` for facts alone),
and your own view is expressed as evidence cards gated through `sports_posterior` over
the executable prior:**

**Broad prop/crossbet scans are sports-first, with novelty second.** For broad "any
props worth taking/selling" requests, first run cheap category discovery across real
sports markets: match outcomes/game lines, visible player/team stat props, goals,
points, totals/bands, exact score, more-markets, and specials. Then scan announcer-word,
broadcast, entertainment, and other bespoke PM/HL props as a secondary novelty bucket,
not the default center of the answer; secondary means scan after sports props, not skip.
Do not stop at the first category that returns results. If search surfaces `more-markets`,
specials, exact-score, or announcer/broadcast event groups, hydrate the top
liquid/relevant event before any global prop conclusion. Hydrate discovered event ladders,
inspect resolution text, and gate by spread/liquidity. It is valid to return
`BUY (heuristic)` / `SELL (heuristic)` when the relative-pricing gap is obvious, but do
not center word/phrase markets unless the user explicitly asked for broadcast props or
they are the best surfaced board after the scan. Include categories scanned/found/hydrated
/skipped/not found/unavailable. A broad `NO EDGE` claim is allowed only after surfaced
categories are hydrated or explicitly skipped with reason; otherwise scope it to checked
categories. For live sportsbook `player_props`, default to `limit=20`, page with
`offset=20` only when useful, and prefer `prop_type`/`vendors` filters over full-board
pulls. Before final BUY/SELL/NO EDGE, the primary should run sports data and research as
bounded lanes after the initial executable board/shortlist exists: sports for event
state/odds/props/supported form inputs/unsupported endpoint notes, research for current
news and resolution facts. If skipped or unavailable, label `sports_state=not_hydrated`,
`research_state=not_hydrated`, or `market/odds-only` and scope the conclusion. Avoid
unsupported true-prob claims.

```
# player props -> ACTIONABLE/WATCH/EXCLUDED EV table
poetry run python -m wayfinder_paths.quant.prop_slate \
  --sport nba --game-id <GAME_ID> --season <SEASON> --out .wayfinder_runs/sports

# game markets (moneyline/total/spread) -> data/model context and optional book context
# (PM/HL order books remain the executable price)
poetry run python -m wayfinder_paths.quant.game_slate \
  --sport nhl --game-id <GAME_ID> --season <SEASON> --date <GAME_DATE> --out .wayfinder_runs/sports
```

```
# futures fields (tournament winner / group winner / reach-final) -> optional book-context fair_p
poetry run python -m wayfinder_paths.quant.futures_slate \
  --sport worldcup --market-type outright --out .wayfinder_runs/sports

# context/model fair vs executable PM/HL disagree? -> dislocation check + gated posterior ledger
poetry run python -m wayfinder_paths.quant.sports_posterior \
  --market <PM_PRICE> --book <FAIR_P> --vendors <N> --overround <O> \
  [--card "davies_out:against:medium:news"]
```

**Composition and autonomy rules:** Every probability, edge, or EV asserted in the final
answer must appear in a shown table or posterior ledger. If the sports worker returns a
rendered table, paste the top rows; if it returns only `dataFiles`, read the artifact and
show the rows. Finish the executable-venue check and top dislocation adjudication
in-session instead of offering them as follow-ups.

**Executable board rule:** Polymarket lists a per-game EVENT (slug
`{league}-{away}-{home}-{YYYY-MM-DD}`, e.g. `mlb-lad-cws-2026-06-12`) but the visible
board may be split across parent and child events. Hydrate the exact event
(`polymarket_read get_event`) and use `sportsBoard`, `childEvents`, and
`categorySummary` as the executable coverage map; child events can hold player props,
more-markets, specials, exact score, and broadcast props even when the parent event only
shows match outcomes. Hydrate or inspect surfaced child categories before saying a
Polymarket prop category is absent, and page candidates with `offset` when the category is
large. `game_slate` emits `alt_lines` (model probabilities for the alt ladder) to price
game lines. "No provider props" never means "nothing executable."
For multi-outcome match boards (soccer/worldcup, tennis sets, MMA method, etc.), preserve
the returned outcome mapping exactly. A three-way soccer board is home/draw/away; never
collapse it into a binary or recommend "buy No on the favorite" unless a binary No token
is explicitly listed. For Hyperliquid HIP-4, only request mids for `#...` assets surfaced
by the market search or prior metadata; for sports/prediction-market discovery call
`wayfinder_hyperliquid_search_hip4(query="...", limit=15)` so perps/spots are filtered
out by construction, and do not derive sibling asset ids by changing the last digit.

**Exact-market hydration rule:** For a named match/fight/event, never conclude "no
Polymarket market" from a search summary alone. If `polymarket_read(action="search")`
returns any plausible `eventSlug`, candidate, or event group for the names/date/card,
immediately hydrate it with `get_event` and inspect the candidate board. If exact-name
search is empty, run bounded fallback queries for **each competitor/team surname or short
name independently** plus the sport/card/date, then hydrate every same-card/same-date
event returned before declaring absence. Broad searches such as "ufc fight" are
truncation-prone discovery only; they are not negative proof unless every plausible event
candidate they surface has been hydrated with `get_event`. Only after those hydrated
checks can you say no executable PM market was found; phrase it as "not surfaced by these
searches" when coverage is incomplete.
Likewise, for a named game/fight/event, still search the direct matchup on PM and
Hyperliquid HIP-4 before calling the analysis complete, even when provider odds are
unavailable.

**UTC-boundary trap:** US evening games can cross the UTC date line. For scoreboard reads,
pass the user's IANA `timezone` with the concrete `date` and inspect the returned
`dateContext` before answering. Before analysis, identify the concrete game id, local/UTC
start, and status; NEVER mix one game's live book odds with another game's pre-game venue
board.

**Odds sourcing rule:** never source betting lines from web search or media pages.
Executable lines come from PM/HL order books. Provider sportsbook odds are optional
context only; if they are empty, unmatched, or auth-blocked, continue with PM/HL boards,
sports data, and model estimates rather than checkpointing. Web research is for news and
qualitative evidence, not odds.

**Dislocation rule:** when an optional sportsbook/context number or model estimate and
the PM/HL executable price for the same outcome disagree enough that
`sports_posterior.dislocation` flags it, never recommend the cheap side on trust — the
prior is the EXECUTABLE price, the context/model number enters as one capped evidence card,
and the gap must be adjudicated (research: "what explains the cheap side?" —
post-line news, resolution-rules mismatch, lockup/flow, stale data, method risk) before
any recommendation. An unexplained dislocation gates to
WATCH with the EV shown — by design. Sub-threshold gaps are VENUE NOISE, not edge:
never describe one as "X points too rich/cheap" — say the market is priced within
normal venue tolerance (a directional view from evidence is "a lean within noise,
not a value call"). Absence of a cross-venue arbitrage path is not a skip reason:
the user cares about fair-value delta versus executable price unless they explicitly
ask for arbs.

For scan questions, CLI-gate every dislocation you headline and fully adjudicate at least
the largest one: ask research "what explains the cheap side?", fold evidence cards back
through `sports_posterior`, and show the final gated verdict. Unexplained dislocations are
candidates, not calls.

### Path-dependent event markets

For tournament winners, group winners, playoff brackets, promotion/relegation,
championship futures, season awards with staged cuts, or any field market where path
matters, do not stop at prediction-market cross-venue spreads or optional book-vs-market
spreads. Build a current-state probability layer; sportsbook odds are optional context,
not a required input.

Default first-pass workflow:

1. Enumerate executable PM/HL boards first, then add bounded sports state/context that
   helps interpret the board: completed results, standings/bracket/cuts if cheap,
   injuries/availability, futures context, and obvious missing path fields.
2. Return the desk-analyst board and value/fade shortlist before any full path model. The
   first pass should include board coverage counts, state classification, and
   `path-model status` such as `not_run_shortlist_first` or `missingPathFields`.
3. After the initial executable board and tentative shortlist/evidence questions exist,
   run sports data and research as parallel bounded lanes when both can move fair value.
   `wayfinder-sports` returns sports state/context; `wayfinder-research` returns a reusable
   `researchInfluencePack` with evidence cards, `researcherOpinion`, `influenceHints`,
   optional `contextPack` / `modelModifiers`, or final-synthesis-only evidence when no pack
   is justified.
4. For shortlisted candidates, or when the user explicitly asks for full modelling first,
   ask `wayfinder-sports` for a sport-neutral `eventStatePack`: participants,
   ratings/form inputs, completed results, standings/bracket/cuts if known, upcoming path,
   futures_slate fair probabilities if available, and executable PM/HL markets.
   Sportsbook/futures auth failure must not block this pack.
5. Run event_sim validation/smoke before any full simulation: low iterations, short
   timeout, and fail fast on missing group slots, impossible wildcard slots,
   unknown participants, or unsupported target shapes. Generated custom-simulator debugging
   gets one repair max.
6. Run `poetry run python -m wayfinder_paths.quant.event_sim --input <event_pack.json>
   --out .wayfinder_runs/sports` or delegate the pack to `wayfinder-quant`.
7. Price simulated probabilities against executable order-book entries/depth and gate with
   `sports_posterior` when dislocations remain large.

The simulator is one model view, not the answer. Final synthesis must distill multiple
views: executable PM/HL prior, sports/context model or rating model, path simulation, and
qualitative evidence/posterior. Do not present the latest sim as final fair value by
itself. If ratings come from outright winner probabilities or other market-implied prices,
label the sim `diagnostic_only`; if the bracket/path is approximate, label `approx_bracket`
and downgrade buy calls to `WATCH` unless another independent model corroborates them.
Use executable entry/depth for trade math: ask/depth for buys, bid/depth for sells.

Executable sports odds should be shared as TTL'd WorkPacks, not repeatedly re-fetched by
every subagent. The first agent that hydrates PM/HL boards should write a compact sports
`surfacePack` and pass `surfacePackRefs` downstream. Reuse unexpired surface packs for
analysis and final synthesis; refresh only shortlisted markets for exact sizing or
execution. Defaults: PM/HL board `ttlSeconds: 60`, exact quote/depth `ttlSeconds: 30`,
standings/results state `ttlSeconds: 300`. If a worker hits max steps after writing useful
packs, resume from those pack refs; do not improvise a final `BUY` from venue spreads or
qualitative reasoning alone. Missing model/fair-value work should be labeled
`WATCH` / `incomplete_fair_value`.

Run the first-pass board now, even early in an event. Do not defer with "run once the group
stage is 50% complete" or similar. Full path simulation is the second-stage validation
step after the shortlist; if the user explicitly asks for modelling first, skip straight
to the path layer. If the official bracket/path is unavailable for validation, run the
best bounded approximation from known rules and label `pathAssumption: "approximate"`;
if a path cannot be represented at all, surface `missingPathFields` as the blocker.
If a final answer claims fair value without `event_sim` or a custom simulator, label it
`WATCH` / `incomplete_fair_value` and state that simulation is not yet run. Before running
a full path model, the event pack must pass the smoke/validation step above.
If validation fails, return `WATCH` / `incomplete_fair_value` / `NEEDS_MORE_STATE` with the
validation issues rather than spending the turn repairing a malformed bracket.

For "scan the field/market" questions, the final answer must be the annotated board before
any single-candidate drilldown. It must include: board coverage counts for each executable
venue, a ranked top-candidate table from the full joined field, each candidate's state
classification, path-model status/artifact (or `missingPathFields`), and order-book depth
only for shortlisted candidates. Never finish with only one or two selected order books.
Under a hard tool budget, use a bounded scan plan and finish anyway: hydrate the main
outright board, search the second executable venue, search group/round boards with enough
candidate coverage to surface multiple event slugs, pull current standings/results using
the canonical sport slug (for World Cup: `worldcup`, not `soccer`) and a generous `limit`
so standings are not silently truncated. Do not call `data.pregame_forms.list` for
`worldcup` unless the catalog explicitly supports it; after one
`resource_unavailable_for_league` for `pregame_forms` + `worldcup`, do not retry it for
later matches and use the research lane for current form/news instead. Then fetch mids for the shortlisted match boards
that surfaced. Prioritize hydrating or directly using group boards for groups with current
results before spending calls on novelty/entertainment match searches. Do not call
`mid_prices` for every encoded outcome in a large field; shortlist top/ambiguous outcomes
first. If group or match boards
surface in search but are not fully hydrated, classify them as `search_surfaced_unhydrated`,
not `not_surfaced`. If group or match boards do not surface, report the search coverage and
move on rather than checkpointing or offering follow-up work.
For broad multi-category scans ("most mispriced across match, group, outright", "scan
the whole field"), cap primary-agent collection at **sixteen external calls**. Reserve one
call for current state/results with a generous limit and one for match-market mids if match boards surface.
After the sixteenth call, stop gathering data and write the final answer from the joined
board/state you have. Missing category coverage is a finding (`not_surfaced` /
`search_surfaced_unhydrated` / `missingModelArtifact`), not a reason to continue.
Never output a progress checkpoint for these scans. Avoid progress-only headings such as
`Goal`, `Constraints`, `Progress`, `Done`, `In Progress`, `Blocked`, `Critical Context`,
and `Next Steps` in the answer. A partial annotated board with honest blockers is the
final deliverable.

The generic pack shape is:

```json
{
  "participants": [
    {"id": "stable-id", "name": "Display Name", "rating": 1800,
     "state": "clean_unplayed|live_conditioned|post_result_stale|dead_signal"}
  ],
  "groups": [
    {"id": "Group/Stage", "participants": ["id1", "id2"],
     "qualifiers": [{"rank": 1, "slot": "G1_1"}],
     "matches": [{"a": "id1", "b": "id2", "status": "completed", "score": [1, 0]}]}
  ],
  "wildcards": [{"source_rank": 3, "count": 2, "slot_prefix": "WC"}],
  "bracket": {"matches": [{"id": "m1", "a": {"slot": "G1_1"}, "b": {"participant": "id3"}}],
              "champion_match": "m_final"},
  "target": {"type": "champion|slot|reach_match|match_winner",
             "slots": ["PROMO1", "PROMO2"], "match": "m_final"},
  "markets": [{"participant_id": "id1", "venue": "polymarket|hyperliquid", "bid": 0.01, "ask": 0.011}],
  "modelProvenance": {"ratingSource": "team-strength source", "bracketSource": "official|approximate"}
}
```

Fill only what is known. If the provider/catalog lacks an official path, return
`missingPathFields`; if you approximate from public rules, set `pathAssumption:
"approximate"` and list assumptions. Use `target` to avoid winner-take-all overfitting:
`champion` for outrights, `slot` for promotion/relegation or staged cuts, `reach_match`
for reach-final/reach-playoff markets, and `match_winner` for a specific stage winner. If
`event_sim` cannot represent the sport/event, a quant worker may build a custom simulator,
save artifacts, and document the model and assumptions. Do not create
event-specific prompt rules.

Classify every surfaced candidate:

- `clean_unplayed`: no relevant result has occurred yet.
- `live_conditioned`: current results/standings are folded into the model.
- `post_result_stale`: comparison still uses pre-result odds/model inputs.
- `dead_signal`: path is mathematically gone or economically irrelevant.

Do not call stale or dead signals value.

### LLM forecasting / prediction-market notes

Use LLMs for retrieval, synthesis, evidence cards, `researchInfluencePack` artifacts, and
model selection, not as raw uncalibrated probability oracles. A strong researcher view can
be a visible desk override candidate, but the consuming agent must ledger whether it was
accepted, rejected, or deferred and must show why it changed the final view. Relevant
research patterns:

- Retrieval + aggregation improves LLM forecasting over zero-shot (`Approaching Human-Level Forecasting with Language Models`,
  https://arxiv.org/abs/2402.18563).
- Live prediction-market evaluation needs timestamp-locked order books, news, and
  execution simulation (`PolyBench`, https://arxiv.org/abs/2604.14199).
- Accuracy is not enough; returns depend on execution, downside when wrong, and agreement
  filters (`Beyond Accuracy: Can LLM Forecasters Profit on Prediction Markets?`,
  https://openreview.net/pdf?id=TSA5kRUKZv).
- LLM confidence is often miscalibrated and domain-sensitive; apply posterior gates and
  calibration checks instead of trusting stated confidence (`KalshiBench`,
  https://arxiv.org/abs/2512.16030).

The pipelines are multi-sport (NBA/NHL/MLB/World Cup verified live). They are context and
model helpers, not gates before PM/HL pricing. MLB notes: props
include one-sided "milestone" quotes (single odds, no under side) — the pipeline skips
these with a visible count; do NOT model them by hand (a single quote cannot be
de-vigged). MLB pitcher props project off outs recorded, batter props off plate
appearances. Soccer notes: moneylines are three-way (1X2 — home/draw/away de-vigged
together; never two-way over home/away); futures quotes carry the whole field's vig
(de-vig across the entire field, never read one quote as a probability); a brand-new
tournament has no completed-game form — game_slate flags `no_form_model` and shows
odds-only views.

One command: fetches props + complete paginated game logs + team pace/defense + injuries,
models with proper distributions and optional book-context probabilities, and prints an
`ACTIONABLE` / `WATCH` (flagged) / `EXCLUDED` (no joinable data) table; writes
`prop_slate_<game>.csv/.json` artifacts. Output fields per pick: `model_p`, `book_p`,
`book_edge`, `book_ev`, `kelly`, `proj`, `n`, `flags`. `book_*` numbers are vs optional
non-executable sportsbook context — informational; the executable stage is
`sports_props.market_edge(pick.model_p, polymarket_price)` against a matching Polymarket market.
Only compare a model/context probability to the same executable outcome. A player
anytime-goal probability is not an edge against a team moneyline; if the matching
executable prop is absent, label the model output context-only / informational-only.

For custom analysis the pipeline doesn't cover (matchup deep-dives, soccer xG, cross-game
studies), write a bounded script. Fetch through `SPORTS_CLIENT` (same backend gateway as the MCP
tools: key-safe, allowlisted, cached — never raw provider URLs), shape with pandas, model with
the quant modules:

```python
import asyncio, pandas as pd
from wayfinder_paths.core.clients.SportsClient import SPORTS_CLIENT
from wayfinder_paths.quant import sports_props as sp          # projections, EV, market_edge
# from wayfinder_paths.quant import polymarket_edge           # prediction-market math

async def main():
    logs = await SPORTS_CLIENT.provider_call(
        endpoint_id="data.player_stats.list", sport="nba",
        query={"player_ids": [161, 1057262518], "seasons": [2025], "per_page": 100})
    df = pd.DataFrame(logs["data"])
    # rolling form, hit rates vs a line, joins to team pace/def_rating, score_prop EV tables...
    df.to_csv(".wayfinder_runs/sports/analysis.csv", index=False)  # artifact -> dataFiles

asyncio.run(main())
```

`SPORTS_CLIENT` methods: `snapshot(action=..., sport=..., ...)`,
`provider_call(endpoint_id=..., sport=..., path_params=..., query=..., body=..., run_id=...)`,
`provider_catalog()`, `backtest_state(action=..., run_id=...)` — all async. Conventions: bulk
arrays over per-player loops; bounded lookbacks (a season, not all history); big tables go to
`.wayfinder_runs/sports/` artifacts (return the paths), summaries go in the response.

Local sports scripts are optional accelerators, not blockers. If a script or direct
`SPORTS_CLIENT` call fails with auth/config errors (`401`, `403`, missing key/config,
provider entitlement), do not retry or defer. Mark the model artifact as
`script_auth_unavailable`, use the already gathered MCP snapshots/PM/HL boards, and produce
a final answer with lower confidence plus explicit `missingPathFields` /
`missingModelArtifact`. For outrights/path markets, still return an `eventStatePack` from
known participants/current state/executable PM/HL markets even when `futures_slate` or
sportsbook futures fail auth. Never turn a script-auth failure into a checkpoint or
follow-up.

Hard rules for prop scoring scripts (learned from real runs):
- Use `sports_props` for the math — `devig_two_way` (never compare against raw vigged implied
  probabilities), `score_prop`/`project_stat`/`prob_over` (proper distributions + shrinkage),
  `prop_value`/`market_edge`. Don't reimplement a simpler model inline.
- `per_page=100` does not hold a slate's season of logs — chunk `player_ids` or follow
  `meta.next_cursor` until every player has rows.
- A player with zero joined logs is a broken join/pagination, not a 0.0 average — exclude or
  refetch, and sanity-check stars' averages before ranking.
- Run with `poetry run python` (plain `python3` lacks pandas and project deps).

## Lab (backtesting) quick sheet — nba/nfl/nhl/mlb only

- Factors: `lab.factors.list` — integer `factor_id`, `slug` (`pp_*` = player-prop factors),
  typed `configurable_params`.
- Create: `lab.models.create` body `{name, sport, bet_type: moneyline|spread|over_under,
  mode: simple|weighted, factors: [{factor_id, parameters, weight}]}` — the key is
  **`parameters`** (not `params`); weighted weights **sum to exactly 100**; prop models need
  `model_type: "player_prop"` + `prop_type` + `pp_*` factors (game models reject `pp_*`).
- Update replaces (PUT semantics — send the full body). Backtest: `lab.performance.run`
  (async job) → `lab.performance.get` (win_rate, roi as a fraction, results_by_confidence).
- Predictions: `lab.predictions.generate` (model id in path) → list via
  `query={"model_id": ...}` (top-level route). Active jobs for a model: `lab.jobs.active`
  (`path_params.id` = model id). Models/jobs are **scoped to your workspace** — foreign ids 404.
- Jobs are async (`pending → running → completed/failed`): kick off, capture
  `run_id`/`job_id`, monitor via `wayfinder_sports_backtest_state` — never tight-loop.

