StatsHub Docs
Reference

Bet Builder — Selection Compatibility ("Padlock") Rules

"Date: 2026-07-09. Exploration/spec for the multi-selection Bet Builder."

Date: 2026-07-09. Exploration/spec for the multi-selection Bet Builder.

Problem

When a user adds a selection to the builder, some other selections must become unavailable ("padlocked") — exactly like bet365 greys-out/suspends cells. A pair (or set) of selections is invalid when:

  • Mutually exclusive — same market, pick one only (France win vs Morocco win).
  • Logically contradictory — no single match outcome can make both win (Under 0.5 goals + Both Teams To Score).
  • Redundant / implied — one selection is guaranteed by another, so adding it is free and pointless (France win ⇒ "France or Draw" double chance). bet365 keeps these selectable but the combined price = the dominant single; we can either allow-and-collapse or hide them.

Everything else is compatible (and, for pricing, may still be correlated — that's a separate concern; see bet365-betbuilder-correlation-findings.md).

Core idea — selections as constraints over the outcome space

Don't hand-code every pair. Model each selection as a predicate over match outcome variables, then:

Two selections are compatible ⟺ there exists at least one match outcome that satisfies all their predicates. Selection B is redundant given A ⟺ every outcome satisfying A also satisfies B (A ⊆ B).

This one rule derives every padlock automatically and correctly, including 3-way and n-way combinations, with no matrix to maintain.

Outcome variables

For a single fixture, an "outcome" is an assignment of:

VarMeaning
hg1, ag1home / away goals, 1st half
hg2, ag2home / away goals, 2nd half
hg = hg1+hg2, ag = ag1+ag2full-time goals
hc, achome / away corners (FT), + hc1/ac1 for 1st half
hk, akhome / away cards
offsides, fouls, etc. as needed

Enumerate over a bounded grid (e.g. each goal count 0..8, corners 0..25) and test satisfiability. The grid is tiny and the check is instant. Variable families that never interact (goals vs corners vs cards) can be checked independently — a goals selection can only conflict with another goals/result selection, etc.

Selection → predicate (goals / result family)

Market selectionPredicate
Match Result — Homehg > ag
Match Result — Drawhg == ag
Match Result — Awayhg < ag
Double Chance — Home/Drawhg >= ag
Double Chance — Home/Awayhg != ag
Double Chance — Draw/Awayhg <= ag
Draw No Bet — Homehg > ag (void if hg==ag, so treat as hg >= ag for feasibility)
Total Goals — Over Lhg + ag > L
Total Goals — Under Lhg + ag < L
Exact Total Goals — Nhg + ag == N
Number of Goals — "2 or 3"hg + ag in {2,3}
BTTS — Yeshg >= 1 && ag >= 1
BTTS — No`hg == 0
Asian Handicap — Home −hhg - ag > h (whole-line push handled at pricing)
Result Handicap (European) — Home −nhg - ag > n
Team Total (Home) — Over Lhg > L
1st-half variantssame predicates on hg1, ag1
Correct Score a–bhg == a && ag == b
Half Time / Full Time X/YHT result on hg1,ag1 AND FT result on hg,ag

Corners / cards markets map the same way onto hc,ac / hk,ak.

Worked examples (all fall out of §2)

Selection ASelection BResultWhy
Match Result HomeMatch Result Awayexclusivehg>aghg<ag unsatisfiable
Under 0.5 goalsBTTS Yescontradictionhg+ag==0hg,ag>=1
Under 0.5 goalsOver 2.5 goalscontradiction==0>2
Over 3.5 goalsUnder 2.5 goalscontradiction>3<2 (any Over L1 with Under L2, L2 ≤ L1)
Exact 2 goalsOver 2.5 goalscontradiction==2>2
Match Result HomeDraw No Bet Awaycontradictionhg>aghg<ag
Match Result HomeDouble Chance Draw/Awaycontradictionhg>aghg<=ag
Match Result HomeDouble Chance Home/Drawredundanthg>aghg>=ag
Match Result HomeDraw No Bet Homeredundanthg>ag ⇒ (home DNB)
Match Result HomeHome −1.5 handicapcompatible (not redundant)home win doesn't imply win by 2
Match Result HomeOver 1.5 goalscompatible2-0, 3-1… satisfy both (positively correlated → pricing, not padlock)
Correct Score 2–0Over 2.5 goalscontradictiontotal is exactly 2
Correct Score 2–0Match Result Homeredundant2–0 ⇒ home win
BTTS NoCorrect Score 1–1contradiction1–1 ⇒ both scored
Over 2.5 cornersMatch Result Homecompatibleindependent variable families

Player props

Player props live on their own variables and are almost always independent of team/goals markets — a player's shot/tackle/foul count doesn't constrain the scoreline. The few real links:

  • Player to score / Anytime scorer ⇒ that team >= 1 goal ⇒ contradicts Under 0.5, Team Total Under 0.5, BTTS No (for that team), Correct Score 0-0, etc. Model as teamGoals >= 1.
  • Two lines of the same player+stat (e.g. Player Shots Over 1.5 and Under 1.5) → mutually exclusive.
  • Otherwise treat as independent/compatible (correlation handled at pricing).

Redundancy / implication handling

For A ⊆ B (B guaranteed by A):

  • Default (match bet365): keep B selectable; when both are in the slip, the combined price = the dominant leg (B contributes odds 1.00). Simplest and least surprising.
  • Optional: grey B with a "already covered" hint instead of a hard padlock.

Implication is detected the same way as contradiction: A ⇒ B iff satisfiable(A ∧ ¬B) is false.

Implementation sketch

type Sel = { market: string; predicate: (o: Outcome) => boolean; vars: VarFamily };

// compatible if some outcome in the (bounded) grid satisfies all predicates,
// restricted to the union of the selections' variable families.
function compatible(sels: Sel[]): boolean {
  const families = union(sels.map(s => s.vars));
  for (const o of enumerateOutcomes(families)) {       // small bounded grid
    if (sels.every(s => s.predicate(o))) return true;
  }
  return false;
}

// when user has `picked`, a candidate is padlocked if:
function isLocked(picked: Sel[], candidate: Sel): boolean {
  if (sameMarket(picked, candidate)) return true;       // one-per-market
  return !compatible([...picked, candidate]);           // no joint outcome
}
  • Grids stay tiny: goals 0..8 each side (81 outcomes), corners 0..25, etc. Only enumerate the families the current selections actually touch.
  • Same-market exclusivity is a cheap short-circuit before the feasibility check.
  • Redundancy check (A ⇒ B) reuses the same engine for the "already covered" UI.

Phasing

  1. Goals/result family first — highest overlap and the most obvious bad parlays (Match Result, Double Chance, DNB, Total Goals, BTTS, Exact Goals, Number of Goals, Handicaps, Correct Score, HT/FT). One predicate set, one grid.
  2. Corners, then cards — same engine, separate variable families.
  3. Player-prop links — the teamGoals >= 1 bridge for scorer markets; same-player+stat exclusivity.
  4. Half-time / full-time consistency — predicates on *1 vars + FT.

Single-selection fair odds (already shipped) + phase 1 padlocks cover the vast majority of logically-broken bets before any correlation modelling.

7b. Markets excluded from the multi entirely (push/refund)

bet365 does not allow markets with a push/refund outcome to join a bet builder at all — you can view their price but not add them to a multi. These are:

  • Draw No Bet (voids on a draw)
  • Asian handicaps — our "Handicap", "Corner Handicap", "Card Handicap" (Spread markets; whole/quarter lines push or half-win)

Implemented via multiEligible: false on those selections (NO_MULTI_TITLES in bet-builder.ts); the UI renders them view-only (non-clickable). This sidesteps the reduced-parlay/void-pricing complexity entirely — void legs simply never enter a slip. (European "Result Handicap" is a clean 3-way with no refund, so it stays eligible.)

Open questions

  • Whole-line pushes (e.g. Asian Handicap -1) are void, not lose, on the exact line — for feasibility we treat >=; for pricing the push is handled separately. Confirm this is the right call for padlocking.
  • Do we hard-lock redundant selections or show "already covered"? (Recommend the latter — matches bet365 and is less frustrating.)
  • Player-prop ⇄ team-goals bridge: is our data rich enough to know which team a player is on at selection time? (Yes — the value-bets-v2 group carries teamId.)

On this page