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:
- Player-prop markets (Shots, Shots on Target, Goals, Tackles, Fouls,
Fouls Won, Passes, Cards) → our model outputs in the
value_betstable. - 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_betsmodel 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=trueandPOST /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-buildertypes 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_betsrows and known odds produces the expected merged selections with correctsourcetagging. - 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).