StatsHub Docs
Specs

Bet Builder (Fair Odds) — Design Spec

"Date: 2026-07-09 Status: Approved (design), pending implementation plan"

Date: 2026-07-09 Status: Approved (design), pending implementation plan

Summary

A new standalone page, Bet Builder, at /bet-builder. A user picks a single football fixture and sees a bet365-style stack of every available betting market for that match, each selection annotated with our fair odds — the "true" price with the bookmaker margin stripped out.

Fair odds come from two sources:

  1. Player-prop markets (Shots, Shots on Target, Goals, Tackles, Fouls, Fouls Won, Passes, Cards) → our model outputs in the value_bets table.
  2. Efficient markets (Match Result / 1X2, Double Chance, Both Teams to Score, Total Goals, Team Goals, etc.) → the market price with the margin removed (devig). These markets are highly efficient, so the margin-removed market price is the best available fair estimate.

This first version shows single selections only. Multi-selection bet building with correlations is explicitly out of scope for now (see Future Work).

Goals

  • Give users a clean, familiar (bet365-like) place to see the fair value of any single selection in a match.
  • Reuse existing infrastructure: the odds-comparison pipeline (raw prices + market enumeration) and the value_bets model outputs (player-prop fair odds).
  • Introduce a small, isolated devig helper that can be upgraded later without touching the UI.

Non-Goals (this version)

  • Multi-selection / accumulator bet building.
  • Correlation modelling between markets.
  • Placing bets or deep-linking out to bookmakers.
  • New model development — we consume existing model outputs as-is.

Placement & Page Shell

  • Route: /bet-builder (Next.js pages router), added to the main nav.
  • Match picker (top): search or pick an upcoming fixture, reusing the same upcoming-fixtures data source the value-bets-v2 page already uses (/api/tournament?upcomingFixtures=true and POST /api/tournament/upcoming-fixtures-multi). Selecting a match sets ?matchId=… in the URL (shareable / deep-linkable).
  • Market stack (below): collapsible market groups in the style of the screenshot (Match Result, Total Goals, BTTS, Player Shots, Cards, …). Each group expands to a list of selections. Each selection shows its fair odds.
  • Display toggle: "Show market price & edge" — off by default (clean fair-odds-only view). When on, each selection additionally shows the best bookmaker price and the edge % (fair vs best price). This keeps the default view low on information overload while letting power users see value at a glance.
  • Later (not this version): a "Bet Builder" entry point button on the fixture page that links to /bet-builder?matchId=… for that match.

Data Flow & Fair-Odds Engine

New API route

GET /api/bet-builder?matchId=<eventId> returns every market for that match with a fair price attached to each selection. It merges two sources:

Source 1 — Player props (model-derived). Read fair odds from the value_bets table for the match's players. Reuse the value-bets-v2 model-selection logic (the same "chosen model" v2 already picks per market — e.g. Theo-preferred for shots/SOT/cards) so the fair number shown here is consistent with the v2 scanner. Each player-prop selection's fairOdds comes straight from the model's fairOverOdds / fairUnderOdds for that line.

Source 2 — Efficient markets (market-derived, devigged). Reuse the existing odds-comparison pipeline (/api/event/[id]/odds-comparison and its MARKET_CATEGORIES enumeration + outcome-parsing) to obtain the raw bookmaker prices and the normalized market list. For each non-player market, take a bookmaker's set of outcome prices, remove the over-round with the devig helper, and convert the resulting fair probability back to fair decimal odds.

Devig helper

A new, isolated utility (one clear responsibility: over-round prices in → fair probabilities out). MVP uses proportional (multiplicative) devig: convert each outcome's decimal odds to an implied probability, sum them (the over-round), divide each by the sum to normalize to 1.0, then invert to fair odds. The devig method is a swappable strategy so we can later add margin-weighted / Shin / logarithmic methods without changing callers or UI.

Devig operates over the complete set of outcomes for a single market (e.g. all three of home/draw/away for 1X2; over+under for a totals line) — the over-round is only meaningful across a complete market, not a single outcome.

Response shape (per selection)

Each returned selection carries at minimum:

  • marketGroup — display category (e.g. "Match Result", "Total Goals", "Player Shots").
  • label — human label for the selection (e.g. "Home", "Over 2.5", "Cole Palmer Over 2.5").
  • line — numeric line/handicap where applicable.
  • fairOdds — decimal fair odds.
  • source"model" | "market".
  • bestPrice — best available bookmaker decimal odds (for the toggle), when available.
  • edge — edge % of fair vs best price (for the toggle), when available.

Fetching

The page fetches /api/bet-builder?matchId=… client-side (SWR), consistent with value-bets-v2. No polling in this version.

Component Breakdown

  • /src/pages/bet-builder.tsx — the page: nav shell + match picker + display toggle + market stack container. Owns page-level state (selected match, toggle, expanded groups).
  • Match picker — a component (reuse/adapt the existing fixture-picker used by v2 where practical) that emits a selected matchId.
  • Market group / selection row components — collapsible group header + selection rows that render fair odds and (when toggled) best price + edge. Kept small and focused; presentational, fed by the API response.
  • /src/pages/api/bet-builder.ts — the new API route implementing the two-source merge described above.
  • /src/lib/devig.ts — the isolated devig helper (+ its own unit tests).
  • Shared types for the bet-builder response live alongside the API route or in a small bet-builder types module.

Error Handling & Edge Cases

  • No match selected → show the picker and an empty-state prompt.
  • Match has no odds yet → show the match with an empty/"odds not available" state per group rather than erroring.
  • Player prop with no model output → omit that player/line (do not fabricate a fair price from the market for player props in this version).
  • Market with incomplete outcomes (can't form a complete market to devig) → skip devig for that market and either hide it or show price without a fair number, clearly.
  • Toggle on but no best price → show fair odds only for that row.
  • Auth/gating follows the same pattern as sibling pages (to be confirmed at plan time — default assumption: same premium gating as value-bets-v2, adjustable).

Testing

  • Devig helper — unit tests: known over-round inputs produce expected fair probabilities/odds; probabilities sum to 1.0; handles 2-way and 3-way markets; handles missing/zero odds gracefully.
  • API route — a match with known value_bets rows and known odds produces the expected merged selections with correct source tagging.
  • Page — drive it in the browser (per the run skill): pick a match, confirm the market stack renders, toggle price/edge on and off.

Future Work (explicitly deferred)

  • Multi-selection bet building.
  • Correlation modelling between markets (the reason the page is match-scoped).
  • Fixture-page entry-point button.
  • Upgraded devig strategies (margin-weighted, Shin).

Open Items to Resolve at Plan Time

  • Access/gating: premium-gated (like v2) vs free funnel page.
  • Exact reuse vs light fork of the fixture-picker component.
  • Whether the bet-builder API calls the odds-comparison route internally or shares its underlying helper functions directly (prefer sharing helpers to avoid an internal HTTP hop).

On this page