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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
{
"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:
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.
1---2name: using-sports-data3description: 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.4---56## What you need to know (TL;DR)78All sports data flows through three MCP tools backed by the backend gateway (the provider9key never leaves the backend; the surface is provider-agnostic):1011- `wayfinder_sports_snapshot` — bounded live reads (scoreboard/odds/props/injuries/lookups/stats).12- `wayfinder_sports_provider` — the full façade: `action="catalog"` lists every callable13 `endpoint_id` **with `supported_leagues` per data endpoint — that catalog is the runtime14 source of truth for "which sports support what"**; `action="call"` invokes one.15- `wayfinder_sports_backtest_state` — canonical run/job monitoring for Lab backtests.1617An unsupported (resource, sport) combo returns `resource_unavailable_for_league` **with the18leagues that DO support it** — never guess availability, read the error or the catalog.19Within one task, treat that error as a task-local unavailable-resource guard: do not retry20the same `(endpoint_id, sport)` combo for every game/match.2122Leagues: `nba nfl mlb nhl wnba ncaaf ncaab ncaaw cbb epl laliga seriea bundesliga ligue1 ucl23mls worldcup mma f1 atp wta pga cs2 lol dota2`. Lab (backtesting) = **nba/nfl/nhl/mlb only**.24Common wrong guesses → right slug: `fifa`/`fiba` → `worldcup`; `soccer` → a league slug25(`epl`/`laliga`/`ucl`/...); `football` → `nfl`; `tennis` → `atp`/`wta`; `golf` → `pga`.26An invalid slug returns a 400 listing the valid choices — read it, don't keep guessing.2728## Canonical resources (what each returns, key params)2930Resource ids are generic and resolve per-league (e.g. `competitors` = players/fighters/drivers;31`events` = games/matches/MMA events/F1 sessions/golf tournaments). Call shape:32`sports_provider(action="call", endpoint_id="data.<resource>.<list|get>", sport=..., query={...}, path_params={...})`.3334| endpoint_id | Returns | Key params |35|---|---|---|36| `data.events.list` / `data.event.get` | schedule/fixtures + results | `query`: `dates[]`, `seasons[]`, `per_page`; get: `path_params.id` |37| `data.teams.list` / `data.team.get` | teams / clubs / constructors | `query.per_page` |38| `data.competitors.list` / `.get` | players / fighters / drivers | `query`: `search`, `player_ids` (array), `per_page` |39| `data.competitors_active.list` | current rosters only | as above |40| `data.standings.list` | standings / rankings / driver standings | `query.season` (most team sports require it) |41| `data.team_standings.list` | constructor/team standings (F1) | `query.season` |42| `data.player_stats.list` | per-game logs | `query`: `player_ids` (array), `seasons[]`, `dates[]`, `per_page`, `postseason` |43| `data.player_season_stats.list` | player season totals | `query`: `season`, `player_ids` where supported; NHL is per-player: `path_params.player_id` |44| `data.season_averages.list` | per-player season averages (NBA-family) | `query`: `season`, `player_id`; `path_params.category` + `query.type` for categorized |45| `data.team_season_averages.list` | team season averages | `query`: `season`, `season_type` (`regular`/`playoffs`), `type` (`base`/`advanced`); `path_params.category` |46| `data.team_stats.list` | team per-game stats | `query`: `game_id`/`season` |47| `data.team_season_stats.list` | team season stats | `query.season`; NHL per-team: `path_params.team_id` |48| `data.player_advanced_stats.list` | advanced metrics | `query`: `seasons[]`, `player_ids`; NFL: `path_params.category` = `rushing`/`passing`/`receiving` |49| `data.leaders.list` | stat leaders | `query`: `season`, `stat_type` |50| `data.injuries.list` | injury/availability report | `query.per_page` |51| `data.box.list` / `data.box.live` | box scores (historical / live) | `query.date` / none |52| `data.lineups.list`, `data.plays.list` | lineups, play-by-play | `query.game_id` (game-scoped) |53| `data.matchups.list` | tennis head-to-head; MLB batter-vs-pitcher | tennis: `query` player ids; MLB versus: batter/pitcher ids |54| `data.career_stats.list` | career stats (tennis) | `query.player_id` |55| `data.shots.list` | soccer shot maps with **xG** | `query.match_id`/`game_id` depending on league |56| `data.match_events.list` | soccer goals/cards/subs | `query.match_id`/`game_id` depending on league |57| `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 |58| `data.rosters.list` | rosters; NFL depth charts | soccer: `query`; NFL: `path_params.team_id` |59| `data.results.list` | F1 session results / PGA tournament results / MMA fight results | `query`: season/event/fight/tournament filters |60| `data.qualifying.list`, `data.pit_stops.list`, `data.laps.list` | F1 detail (plan-gated upstream) | `query` session/event ids |61| `data.venues.list` | circuits / stadiums / courses | — |62| `data.round_stats.list` | PGA strokes-gained round stats | `query` tournament/player |63| `data.splits.list`, `data.plate_appearances.list` | MLB splits / plate appearances | `query` player/season |64| `data.pitcher_pitch_stats.list`, `data.hitter_pitch_stats.list` | MLB pitch-type breakdowns | `query` player/season |65| `data.conferences.list`, `data.bracket.list` | college conferences / March Madness bracket | `query.season` |66| `data.player_contracts.list`, `data.team_contracts.list` | NBA salaries/payroll (plan-gated upstream) | `query` |67| `data.shot_locations.list` | WNBA shooting zones | `query` player/season |68| `data.odds.list` | odds context | snapshot: `event_id`/`game_id`/`fight_id`/`tournament_id` OR `date`; façade: provider-specific ids |69| `data.player_props.list` | player prop lines + over/under odds | snapshot: `event_id`; façade: `game_id`, `match_id`, or `tournament_id` |70| `data.futures.list` | outright/futures odds | snapshot: `season`, optional `event_id`/`market_type`; façade: provider-specific filters |7172## Which sports have what (highlights — catalog is authoritative)7374- **nba** — richest: full stats family (game logs, season averages + categories75 `general/clutch/shooting/playtype/tracking/hustle/defense/shotdashboard` with `type` sub-param,76 team averages incl. `pace`/`def_rating` under `type=advanced`), advanced stats, box/live box,77 lineups, plays, leaders, injuries, contracts (plan-gated), odds + player props (the only league78 on the absolute v2 betting surface — handled transparently).79- **nfl** — game logs, season stats, team stats + team season stats, advanced80 rushing/passing/receiving (via `category`), per-team depth-chart rosters81 (`path_params.team_id`), plays, injuries, standings, odds + props.82- **mlb** — game logs, season stats, **batter-vs-pitcher matchups**, splits, plate appearances,83 pitch-type stats (pitcher + hitter), lineups, plays, injuries, odds + props.84- **nhl** — box scores, plays, injuries, standings; season stats are **per-player/per-team85 id-scoped** (no flat game-log endpoint); player/team leaders; odds + props.86- **wnba** — NBA-style stats + advanced + **shot_locations**; odds + props.87- **soccer leagues** (epl/laliga/seriea/bundesliga/ligue1/ucl/mls) — matches, rosters,88 injuries, standings, player/team match stats, **xG shots**, match events, momentum and89 pregame forms where the catalog lists support; odds + props; **futures** for ucl.90 EPL serves from its v2 API transparently.91- **worldcup** — matches, standings/results, odds, player props, and futures where live.92 Do **not** call `data.pregame_forms.list` for `worldcup` unless the runtime catalog93 explicitly lists `worldcup` in `supported_leagues`; current `resource_unavailable_for_league`94 means use standings/results plus web/news research for pregame form.95- **tennis** (atp/wta) — players, matches, rankings (as `standings`), **head-to-head matchups**,96 match stats, career stats; odds only.97- **mma** — fighters, events (cards), **fight results** (`data.results.list`), fight stats,98 rankings; odds only.99- **f1** — drivers, constructors (`teams`), sessions (`events`), qualifying, results, laps,100 pit stops, driver + team standings, venues; **futures only** (no per-race odds). Much of the101 deep telemetry is plan-gated upstream — expect `resource_unavailable_for_league` if unentitled.102- **pga** — players, tournaments, results, strokes-gained round stats, venues (courses);103 **futures + props**, no match odds.104- **college** (ncaaf/ncaab/ncaaw/cbb) — teams, players, games, standings, plays, conferences,105 **bracket** (ncaab/ncaaw March Madness); odds only, no props.106- **esports** (cs2/lol/dota2) — teams/players/matches (cs2 deepest: match/map stats); **no betting**.107108## Betting coverage map109110| Markets | Leagues |111|---|---|112| odds + player props | nba, nfl, mlb, nhl, wnba, epl, laliga, seriea, bundesliga, ligue1, mls, ucl, worldcup |113| odds only | ncaaf, ncaab, ncaaw, cbb, mma, atp, wta |114| futures | ucl, worldcup, f1, pga |115| none | cs2, lol, dota2 |116117Sportsbook odds/props/futures are **optional context, never the executable price**.118Wayfinder sports views are actionable only against Polymarket or Hyperliquid prediction119market books. Compute edges with `wayfinder_paths.quant.sports_props.market_edge(...)`120or the prediction-market quant helpers against PM/HL order-book prices.121122## Conventions that bite123124- **Arrays**: list-valued query params just work — `query={"player_ids": [161, 1057262518]}`125 bulk-fetches in one call (the gateway maps to the provider's `key[]` form). Batch a whole126 slate's game logs in ONE `data.player_stats.list` call.127- **Ids are one namespace per sport**: the `player_id` in props, game logs, and players are the128 same id space (newer players just have huge ids). Hydrate names via `data.competitors.list`129 with `player_ids`.130- **Seasons**: integer start-year (`2025` = the 2025-26 NBA season). `season_type`:131 `regular`/`playoffs` where supported; game logs accept `postseason` true/false.132- **Id-scoped resources**: where the error or this doc says per-team/per-player, pass133 `path_params={"team_id": ...}` or `{"player_id": ...}` (NFL rosters, NHL season stats).134- **Snapshot id convention**: prefer `event_id` from scoreboard cards. The backend maps it to135 `game_id` for US team sports, `match_id` for soccer/World Cup props, `fight_id`/`event_id`136 for MMA odds, `tournament_id` for PGA props and tennis tournament odds, and `event_ids[]`137 for F1 futures. `game_id` remains a legacy alias.138- **Provider façade id convention**: use the exact provider filter requested by the endpoint139 (`game_id`, `match_id`, `fight_id`, `tournament_id`, `event_ids[]`, etc.); call catalog or140 read errors instead of guessing.141- **Caching**: non-live data (stats/averages/rosters) is cached server-side for hours — repeats142 are cheap; odds/props/futures stay near-live (~15s). Still batch.143- **Pagination**: `per_page` (max 100) + cursor in `meta.next_cursor` where present.144145## Scripted analysis (inside `core_run_script`)146147**Betting analysis is a FUNNEL that starts from the executable boards: (1) ENUMERATE148what's tradeable — the Polymarket per-game event (`get_event` on the149`{league}-{away}-{home}-{date}` slug) and Hyperliquid HIP-4 outcomes — into a candidate150table; (2) INFORMATION via sports data and the canned pipelines below where useful151(complete fetches, model features, optional sportsbook-context market math; never pull152odds from the web); (3) TRIAGE candidates by liquidity and fair-value delta153(hypothesized fair probability/range minus executable PM/HL price); use PM/HL154cross-venue differences as venue-noise/liquidity sanity checks, not the main155objective; (4) DEEP-DIVE156survivors with whatever data sharpens the number (full matchup history across seasons,157comparable players vs that opponent, minutes/usage given injuries/lineups — multi-season158`player_stats`, `matchups`, advanced stats), each input a weighted evidence card; (5)159GATE per candidate and answer with the annotated board. MODELING is the agent's160judgment: `game_slate` separates an INFORMATION section (facts) from a labeled161REFERENCE MODEL (one opinion — adjust or replace it; `--data-only` for facts alone),162and your own view is expressed as evidence cards gated through `sports_posterior` over163the executable prior:**164165**Broad prop/crossbet scans are sports-first, with novelty second.** For broad "any166props worth taking/selling" requests, first run cheap category discovery across real167sports markets: match outcomes/game lines, visible player/team stat props, goals,168points, totals/bands, exact score, more-markets, and specials. Then scan announcer-word,169broadcast, entertainment, and other bespoke PM/HL props as a secondary novelty bucket,170not the default center of the answer; secondary means scan after sports props, not skip.171Do not stop at the first category that returns results. If search surfaces `more-markets`,172specials, exact-score, or announcer/broadcast event groups, hydrate the top173liquid/relevant event before any global prop conclusion. Hydrate discovered event ladders,174inspect resolution text, and gate by spread/liquidity. It is valid to return175`BUY (heuristic)` / `SELL (heuristic)` when the relative-pricing gap is obvious, but do176not center word/phrase markets unless the user explicitly asked for broadcast props or177they are the best surfaced board after the scan. Include categories scanned/found/hydrated178/skipped/not found/unavailable. A broad `NO EDGE` claim is allowed only after surfaced179categories are hydrated or explicitly skipped with reason; otherwise scope it to checked180categories. For live sportsbook `player_props`, default to `limit=20`, page with181`offset=20` only when useful, and prefer `prop_type`/`vendors` filters over full-board182pulls. Before final BUY/SELL/NO EDGE, the primary should run sports data and research as183bounded lanes after the initial executable board/shortlist exists: sports for event184state/odds/props/supported form inputs/unsupported endpoint notes, research for current185news and resolution facts. If skipped or unavailable, label `sports_state=not_hydrated`,186`research_state=not_hydrated`, or `market/odds-only` and scope the conclusion. Avoid187unsupported true-prob claims.188189```190# player props -> ACTIONABLE/WATCH/EXCLUDED EV table191poetry run python -m wayfinder_paths.quant.prop_slate \192 --sport nba --game-id <GAME_ID> --season <SEASON> --out .wayfinder_runs/sports193194# game markets (moneyline/total/spread) -> data/model context and optional book context195# (PM/HL order books remain the executable price)196poetry run python -m wayfinder_paths.quant.game_slate \197 --sport nhl --game-id <GAME_ID> --season <SEASON> --date <GAME_DATE> --out .wayfinder_runs/sports198```199200```201# futures fields (tournament winner / group winner / reach-final) -> optional book-context fair_p202poetry run python -m wayfinder_paths.quant.futures_slate \203 --sport worldcup --market-type outright --out .wayfinder_runs/sports204205# context/model fair vs executable PM/HL disagree? -> dislocation check + gated posterior ledger206poetry run python -m wayfinder_paths.quant.sports_posterior \207 --market <PM_PRICE> --book <FAIR_P> --vendors <N> --overround <O> \208 [--card "davies_out:against:medium:news"]209```210211**Composition and autonomy rules:** Every probability, edge, or EV asserted in the final212answer must appear in a shown table or posterior ledger. If the sports worker returns a213rendered table, paste the top rows; if it returns only `dataFiles`, read the artifact and214show the rows. Finish the executable-venue check and top dislocation adjudication215in-session instead of offering them as follow-ups.216217**Executable board rule:** Polymarket lists a per-game EVENT (slug218`{league}-{away}-{home}-{YYYY-MM-DD}`, e.g. `mlb-lad-cws-2026-06-12`) but the visible219board may be split across parent and child events. Hydrate the exact event220(`polymarket_read get_event`) and use `sportsBoard`, `childEvents`, and221`categorySummary` as the executable coverage map; child events can hold player props,222more-markets, specials, exact score, and broadcast props even when the parent event only223shows match outcomes. Hydrate or inspect surfaced child categories before saying a224Polymarket prop category is absent, and page candidates with `offset` when the category is225large. `game_slate` emits `alt_lines` (model probabilities for the alt ladder) to price226game lines. "No provider props" never means "nothing executable."227For multi-outcome match boards (soccer/worldcup, tennis sets, MMA method, etc.), preserve228the returned outcome mapping exactly. A three-way soccer board is home/draw/away; never229collapse it into a binary or recommend "buy No on the favorite" unless a binary No token230is explicitly listed. For Hyperliquid HIP-4, only request mids for `#...` assets surfaced231by the market search or prior metadata; for sports/prediction-market discovery call232`wayfinder_hyperliquid_search_hip4(query="...", limit=15)` so perps/spots are filtered233out by construction, and do not derive sibling asset ids by changing the last digit.234235**Exact-market hydration rule:** For a named match/fight/event, never conclude "no236Polymarket market" from a search summary alone. If `polymarket_read(action="search")`237returns any plausible `eventSlug`, candidate, or event group for the names/date/card,238immediately hydrate it with `get_event` and inspect the candidate board. If exact-name239search is empty, run bounded fallback queries for **each competitor/team surname or short240name independently** plus the sport/card/date, then hydrate every same-card/same-date241event returned before declaring absence. Broad searches such as "ufc fight" are242truncation-prone discovery only; they are not negative proof unless every plausible event243candidate they surface has been hydrated with `get_event`. Only after those hydrated244checks can you say no executable PM market was found; phrase it as "not surfaced by these245searches" when coverage is incomplete.246Likewise, for a named game/fight/event, still search the direct matchup on PM and247Hyperliquid HIP-4 before calling the analysis complete, even when provider odds are248unavailable.249250**UTC-boundary trap:** US evening games can cross the UTC date line. For scoreboard reads,251pass the user's IANA `timezone` with the concrete `date` and inspect the returned252`dateContext` before answering. Before analysis, identify the concrete game id, local/UTC253start, and status; NEVER mix one game's live book odds with another game's pre-game venue254board.255256**Odds sourcing rule:** never source betting lines from web search or media pages.257Executable lines come from PM/HL order books. Provider sportsbook odds are optional258context only; if they are empty, unmatched, or auth-blocked, continue with PM/HL boards,259sports data, and model estimates rather than checkpointing. Web research is for news and260qualitative evidence, not odds.261262**Dislocation rule:** when an optional sportsbook/context number or model estimate and263the PM/HL executable price for the same outcome disagree enough that264`sports_posterior.dislocation` flags it, never recommend the cheap side on trust — the265prior is the EXECUTABLE price, the context/model number enters as one capped evidence card,266and the gap must be adjudicated (research: "what explains the cheap side?" —267post-line news, resolution-rules mismatch, lockup/flow, stale data, method risk) before268any recommendation. An unexplained dislocation gates to269WATCH with the EV shown — by design. Sub-threshold gaps are VENUE NOISE, not edge:270never describe one as "X points too rich/cheap" — say the market is priced within271normal venue tolerance (a directional view from evidence is "a lean within noise,272not a value call"). Absence of a cross-venue arbitrage path is not a skip reason:273the user cares about fair-value delta versus executable price unless they explicitly274ask for arbs.275276For scan questions, CLI-gate every dislocation you headline and fully adjudicate at least277the largest one: ask research "what explains the cheap side?", fold evidence cards back278through `sports_posterior`, and show the final gated verdict. Unexplained dislocations are279candidates, not calls.280281### Path-dependent event markets282283For tournament winners, group winners, playoff brackets, promotion/relegation,284championship futures, season awards with staged cuts, or any field market where path285matters, do not stop at prediction-market cross-venue spreads or optional book-vs-market286spreads. Build a current-state probability layer; sportsbook odds are optional context,287not a required input.288289Default first-pass workflow:2902911. Enumerate executable PM/HL boards first, then add bounded sports state/context that292 helps interpret the board: completed results, standings/bracket/cuts if cheap,293 injuries/availability, futures context, and obvious missing path fields.2942. Return the desk-analyst board and value/fade shortlist before any full path model. The295 first pass should include board coverage counts, state classification, and296 `path-model status` such as `not_run_shortlist_first` or `missingPathFields`.2973. After the initial executable board and tentative shortlist/evidence questions exist,298 run sports data and research as parallel bounded lanes when both can move fair value.299 `wayfinder-sports` returns sports state/context; `wayfinder-research` returns a reusable300 `researchInfluencePack` with evidence cards, `researcherOpinion`, `influenceHints`,301 optional `contextPack` / `modelModifiers`, or final-synthesis-only evidence when no pack302 is justified.3034. For shortlisted candidates, or when the user explicitly asks for full modelling first,304 ask `wayfinder-sports` for a sport-neutral `eventStatePack`: participants,305 ratings/form inputs, completed results, standings/bracket/cuts if known, upcoming path,306 futures_slate fair probabilities if available, and executable PM/HL markets.307 Sportsbook/futures auth failure must not block this pack.3085. Run event_sim validation/smoke before any full simulation: low iterations, short309 timeout, and fail fast on missing group slots, impossible wildcard slots,310 unknown participants, or unsupported target shapes. Generated custom-simulator debugging311 gets one repair max.3126. Run `poetry run python -m wayfinder_paths.quant.event_sim --input <event_pack.json>313 --out .wayfinder_runs/sports` or delegate the pack to `wayfinder-quant`.3147. Price simulated probabilities against executable order-book entries/depth and gate with315 `sports_posterior` when dislocations remain large.316317The simulator is one model view, not the answer. Final synthesis must distill multiple318views: executable PM/HL prior, sports/context model or rating model, path simulation, and319qualitative evidence/posterior. Do not present the latest sim as final fair value by320itself. If ratings come from outright winner probabilities or other market-implied prices,321label the sim `diagnostic_only`; if the bracket/path is approximate, label `approx_bracket`322and downgrade buy calls to `WATCH` unless another independent model corroborates them.323Use executable entry/depth for trade math: ask/depth for buys, bid/depth for sells.324325Executable sports odds should be shared as TTL'd WorkPacks, not repeatedly re-fetched by326every subagent. The first agent that hydrates PM/HL boards should write a compact sports327`surfacePack` and pass `surfacePackRefs` downstream. Reuse unexpired surface packs for328analysis and final synthesis; refresh only shortlisted markets for exact sizing or329execution. Defaults: PM/HL board `ttlSeconds: 60`, exact quote/depth `ttlSeconds: 30`,330standings/results state `ttlSeconds: 300`. If a worker hits max steps after writing useful331packs, resume from those pack refs; do not improvise a final `BUY` from venue spreads or332qualitative reasoning alone. Missing model/fair-value work should be labeled333`WATCH` / `incomplete_fair_value`.334335Run the first-pass board now, even early in an event. Do not defer with "run once the group336stage is 50% complete" or similar. Full path simulation is the second-stage validation337step after the shortlist; if the user explicitly asks for modelling first, skip straight338to the path layer. If the official bracket/path is unavailable for validation, run the339best bounded approximation from known rules and label `pathAssumption: "approximate"`;340if a path cannot be represented at all, surface `missingPathFields` as the blocker.341If a final answer claims fair value without `event_sim` or a custom simulator, label it342`WATCH` / `incomplete_fair_value` and state that simulation is not yet run. Before running343a full path model, the event pack must pass the smoke/validation step above.344If validation fails, return `WATCH` / `incomplete_fair_value` / `NEEDS_MORE_STATE` with the345validation issues rather than spending the turn repairing a malformed bracket.346347For "scan the field/market" questions, the final answer must be the annotated board before348any single-candidate drilldown. It must include: board coverage counts for each executable349venue, a ranked top-candidate table from the full joined field, each candidate's state350classification, path-model status/artifact (or `missingPathFields`), and order-book depth351only for shortlisted candidates. Never finish with only one or two selected order books.352Under a hard tool budget, use a bounded scan plan and finish anyway: hydrate the main353outright board, search the second executable venue, search group/round boards with enough354candidate coverage to surface multiple event slugs, pull current standings/results using355the canonical sport slug (for World Cup: `worldcup`, not `soccer`) and a generous `limit`356so standings are not silently truncated. Do not call `data.pregame_forms.list` for357`worldcup` unless the catalog explicitly supports it; after one358`resource_unavailable_for_league` for `pregame_forms` + `worldcup`, do not retry it for359later matches and use the research lane for current form/news instead. Then fetch mids for the shortlisted match boards360that surfaced. Prioritize hydrating or directly using group boards for groups with current361results before spending calls on novelty/entertainment match searches. Do not call362`mid_prices` for every encoded outcome in a large field; shortlist top/ambiguous outcomes363first. If group or match boards364surface in search but are not fully hydrated, classify them as `search_surfaced_unhydrated`,365not `not_surfaced`. If group or match boards do not surface, report the search coverage and366move on rather than checkpointing or offering follow-up work.367For broad multi-category scans ("most mispriced across match, group, outright", "scan368the whole field"), cap primary-agent collection at **sixteen external calls**. Reserve one369call for current state/results with a generous limit and one for match-market mids if match boards surface.370After the sixteenth call, stop gathering data and write the final answer from the joined371board/state you have. Missing category coverage is a finding (`not_surfaced` /372`search_surfaced_unhydrated` / `missingModelArtifact`), not a reason to continue.373Never output a progress checkpoint for these scans. Avoid progress-only headings such as374`Goal`, `Constraints`, `Progress`, `Done`, `In Progress`, `Blocked`, `Critical Context`,375and `Next Steps` in the answer. A partial annotated board with honest blockers is the376final deliverable.377378The generic pack shape is:379380```json381{382 "participants": [383 {"id": "stable-id", "name": "Display Name", "rating": 1800,384 "state": "clean_unplayed|live_conditioned|post_result_stale|dead_signal"}385 ],386 "groups": [387 {"id": "Group/Stage", "participants": ["id1", "id2"],388 "qualifiers": [{"rank": 1, "slot": "G1_1"}],389 "matches": [{"a": "id1", "b": "id2", "status": "completed", "score": [1, 0]}]}390 ],391 "wildcards": [{"source_rank": 3, "count": 2, "slot_prefix": "WC"}],392 "bracket": {"matches": [{"id": "m1", "a": {"slot": "G1_1"}, "b": {"participant": "id3"}}],393 "champion_match": "m_final"},394 "target": {"type": "champion|slot|reach_match|match_winner",395 "slots": ["PROMO1", "PROMO2"], "match": "m_final"},396 "markets": [{"participant_id": "id1", "venue": "polymarket|hyperliquid", "bid": 0.01, "ask": 0.011}],397 "modelProvenance": {"ratingSource": "team-strength source", "bracketSource": "official|approximate"}398}399```400401Fill only what is known. If the provider/catalog lacks an official path, return402`missingPathFields`; if you approximate from public rules, set `pathAssumption:403"approximate"` and list assumptions. Use `target` to avoid winner-take-all overfitting:404`champion` for outrights, `slot` for promotion/relegation or staged cuts, `reach_match`405for reach-final/reach-playoff markets, and `match_winner` for a specific stage winner. If406`event_sim` cannot represent the sport/event, a quant worker may build a custom simulator,407save artifacts, and document the model and assumptions. Do not create408event-specific prompt rules.409410Classify every surfaced candidate:411412- `clean_unplayed`: no relevant result has occurred yet.413- `live_conditioned`: current results/standings are folded into the model.414- `post_result_stale`: comparison still uses pre-result odds/model inputs.415- `dead_signal`: path is mathematically gone or economically irrelevant.416417Do not call stale or dead signals value.418419### LLM forecasting / prediction-market notes420421Use LLMs for retrieval, synthesis, evidence cards, `researchInfluencePack` artifacts, and422model selection, not as raw uncalibrated probability oracles. A strong researcher view can423be a visible desk override candidate, but the consuming agent must ledger whether it was424accepted, rejected, or deferred and must show why it changed the final view. Relevant425research patterns:426427- Retrieval + aggregation improves LLM forecasting over zero-shot (`Approaching Human-Level Forecasting with Language Models`,428 https://arxiv.org/abs/2402.18563).429- Live prediction-market evaluation needs timestamp-locked order books, news, and430 execution simulation (`PolyBench`, https://arxiv.org/abs/2604.14199).431- Accuracy is not enough; returns depend on execution, downside when wrong, and agreement432 filters (`Beyond Accuracy: Can LLM Forecasters Profit on Prediction Markets?`,433 https://openreview.net/pdf?id=TSA5kRUKZv).434- LLM confidence is often miscalibrated and domain-sensitive; apply posterior gates and435 calibration checks instead of trusting stated confidence (`KalshiBench`,436 https://arxiv.org/abs/2512.16030).437438The pipelines are multi-sport (NBA/NHL/MLB/World Cup verified live). They are context and439model helpers, not gates before PM/HL pricing. MLB notes: props440include one-sided "milestone" quotes (single odds, no under side) — the pipeline skips441these with a visible count; do NOT model them by hand (a single quote cannot be442de-vigged). MLB pitcher props project off outs recorded, batter props off plate443appearances. Soccer notes: moneylines are three-way (1X2 — home/draw/away de-vigged444together; never two-way over home/away); futures quotes carry the whole field's vig445(de-vig across the entire field, never read one quote as a probability); a brand-new446tournament has no completed-game form — game_slate flags `no_form_model` and shows447odds-only views.448449One command: fetches props + complete paginated game logs + team pace/defense + injuries,450models with proper distributions and optional book-context probabilities, and prints an451`ACTIONABLE` / `WATCH` (flagged) / `EXCLUDED` (no joinable data) table; writes452`prop_slate_<game>.csv/.json` artifacts. Output fields per pick: `model_p`, `book_p`,453`book_edge`, `book_ev`, `kelly`, `proj`, `n`, `flags`. `book_*` numbers are vs optional454non-executable sportsbook context — informational; the executable stage is455`sports_props.market_edge(pick.model_p, polymarket_price)` against a matching Polymarket market.456Only compare a model/context probability to the same executable outcome. A player457anytime-goal probability is not an edge against a team moneyline; if the matching458executable prop is absent, label the model output context-only / informational-only.459460For custom analysis the pipeline doesn't cover (matchup deep-dives, soccer xG, cross-game461studies), write a bounded script. Fetch through `SPORTS_CLIENT` (same backend gateway as the MCP462tools: key-safe, allowlisted, cached — never raw provider URLs), shape with pandas, model with463the quant modules:464465```python466import asyncio, pandas as pd467from wayfinder_paths.core.clients.SportsClient import SPORTS_CLIENT468from wayfinder_paths.quant import sports_props as sp # projections, EV, market_edge469# from wayfinder_paths.quant import polymarket_edge # prediction-market math470471async def main():472 logs = await SPORTS_CLIENT.provider_call(473 endpoint_id="data.player_stats.list", sport="nba",474 query={"player_ids": [161, 1057262518], "seasons": [2025], "per_page": 100})475 df = pd.DataFrame(logs["data"])476 # rolling form, hit rates vs a line, joins to team pace/def_rating, score_prop EV tables...477 df.to_csv(".wayfinder_runs/sports/analysis.csv", index=False) # artifact -> dataFiles478479asyncio.run(main())480```481482`SPORTS_CLIENT` methods: `snapshot(action=..., sport=..., ...)`,483`provider_call(endpoint_id=..., sport=..., path_params=..., query=..., body=..., run_id=...)`,484`provider_catalog()`, `backtest_state(action=..., run_id=...)` — all async. Conventions: bulk485arrays over per-player loops; bounded lookbacks (a season, not all history); big tables go to486`.wayfinder_runs/sports/` artifacts (return the paths), summaries go in the response.487488Local sports scripts are optional accelerators, not blockers. If a script or direct489`SPORTS_CLIENT` call fails with auth/config errors (`401`, `403`, missing key/config,490provider entitlement), do not retry or defer. Mark the model artifact as491`script_auth_unavailable`, use the already gathered MCP snapshots/PM/HL boards, and produce492a final answer with lower confidence plus explicit `missingPathFields` /493`missingModelArtifact`. For outrights/path markets, still return an `eventStatePack` from494known participants/current state/executable PM/HL markets even when `futures_slate` or495sportsbook futures fail auth. Never turn a script-auth failure into a checkpoint or496follow-up.497498Hard rules for prop scoring scripts (learned from real runs):499- Use `sports_props` for the math — `devig_two_way` (never compare against raw vigged implied500 probabilities), `score_prop`/`project_stat`/`prob_over` (proper distributions + shrinkage),501 `prop_value`/`market_edge`. Don't reimplement a simpler model inline.502- `per_page=100` does not hold a slate's season of logs — chunk `player_ids` or follow503 `meta.next_cursor` until every player has rows.504- A player with zero joined logs is a broken join/pagination, not a 0.0 average — exclude or505 refetch, and sanity-check stars' averages before ranking.506- Run with `poetry run python` (plain `python3` lacks pandas and project deps).507508## Lab (backtesting) quick sheet — nba/nfl/nhl/mlb only509510- Factors: `lab.factors.list` — integer `factor_id`, `slug` (`pp_*` = player-prop factors),511 typed `configurable_params`.512- Create: `lab.models.create` body `{name, sport, bet_type: moneyline|spread|over_under,513 mode: simple|weighted, factors: [{factor_id, parameters, weight}]}` — the key is514 **`parameters`** (not `params`); weighted weights **sum to exactly 100**; prop models need515 `model_type: "player_prop"` + `prop_type` + `pp_*` factors (game models reject `pp_*`).516- Update replaces (PUT semantics — send the full body). Backtest: `lab.performance.run`517 (async job) → `lab.performance.get` (win_rate, roi as a fraction, results_by_confidence).518- Predictions: `lab.predictions.generate` (model id in path) → list via519 `query={"model_id": ...}` (top-level route). Active jobs for a model: `lab.jobs.active`520 (`path_params.id` = model id). Models/jobs are **scoped to your workspace** — foreign ids 404.521- Jobs are async (`pending → running → completed/failed`): kick off, capture522 `run_id`/`job_id`, monitor via `wayfinder_sports_backtest_state` — never tight-loop.