Skip to main content

Bet Panel Research Summary

Condensed findings from 8 research agents + Playwright exploration. Updated 2026-02-12.


Backend Research (Order Placement)

Order Placement API — POST /api/orders

  • Input: fixtureId, marketId, outcomeId, stake, odds, trueOdds, betType, acceptBetterOdds, sportId, display fields
  • Validation: Bookmaker min stakes (Betfair $0.10, Pinnacle $20), max stake $10K, odds 1.01–1000, max 50 open orders, $50K daily volume
  • Flow: Validate → B-Book routing → Agent booking capture → Balance deduction (atomic) → Betslip validation (live odds check) → Margin/slippage → Exchange submission
  • Response: { success, data: { order, submitted, message } }

Order Statuses

pending → accepted | partially_accepted | declined | cancelled → settled

Settlement outcomes: win | lose | void | push | half_win | half_lose

Order Cancellation — POST /api/orders/:id/cancel

  • Only pending orders. Back refund = stake. Lay refund = stake × (odds - 1).
  • Pinnacle does NOT support cancellation.

WebSocket Events

  • balance:update — after any balance change
  • settlement:update — after settlement
  • odds:updated — live odds changes
  • Gap: No order:update event for status changes

Batch Placement — NOT IMPLEMENTED

  • Betfair adapter supports placeOrders({ instructions: PlaceInstruction[] }) (batch)
  • No API endpoint exposing this. Must build POST /api/orders/batch.

Balance

  • Balance deducted atomically at order placement
  • user.balancePoints always reflects available balance (no separate calculation needed)
  • Per-fixture exposure: GET /api/orders/fixture/:id/exposure

Betfair Integration Gaps

Available Data (no changes needed)

  • 3-level back/lay depth with price + size
  • Total matched per market/runner
  • Market status, in-play flag
  • Order status: sizeMatched, sizeRemaining, sizeLapsed, sizeCancelled, sizeVoided
  • Average matched price
  • Account funds (balance + exposure) — exists but not integrated

Critical Gap: Betfair Tick Sizes

Not implemented anywhere in codebase. Required for odds +/- buttons:

1.01-2.00: 0.01 step    6-10: 0.2 step      30-50: 2 step
2-3: 0.02 step 10-20: 0.5 step 50-100: 5 step
3-4: 0.05 step 20-30: 1 step 100-1000: 10 step
4-6: 0.1 step

Gap: Market Limits Not Exposed

  • Min stake: static $0.10 (Betfair) / $20 (Pinnacle) — hardcoded in orderService
  • Max stake: Available from depth sizes but not returned to frontend
  • Max market: totalMatched available but not surfaced as "Max. Mkt"

Gap: Error Messages

  • Betfair error codes passed raw ("INVALID_BET_SIZE" etc.)
  • Need user-friendly mapping

Supported Order Type: LIMIT Only

  • No market orders, fill-or-kill, or bet target types yet

Frontend Research (Current Bet Slip)

Current Architecture

  • BetSlip.tsx (524 lines): Floating overlay, position: fixed, z-50
  • Desktop: right-aligned, w-80 (320px), slides in/out with transform
  • Mobile: bottom sheet, max-h-[60vh]
  • Imported per-page (not layout-level)

Zustand Store — betSlipStore.ts (121 lines)

{ items: BetSlipItem[], isOpen: boolean }
// BetSlipItem stores full Fixture/Market/Outcome objects (heavy)
// Persisted to localStorage as 'hannibal-betslip'

What Works

  • Multi-bet support with duplicate prevention (fixtureId + marketId + outcomeId + betType)
  • Back/Lay bet types with color coding
  • Quick stake buttons (1, 5, 10, 20, 50)
  • Stake input with liability/return calculation
  • Confirmation modal before placement
  • Sequential bet placement
  • Make Offer for no-liquidity markets

What's Missing

  1. No permanent right panel — floating overlay only
  2. No tabs (BETSLIP / OPEN BETS)
  3. No Open Bets display in bet slip
  4. No odds +/- buttons (tick increments)
  5. No min/max/maxMkt display
  6. No batch placement (Place All)
  7. No cancel order from bet slip
  8. No real-time order status updates
  9. Pages have inconsistent widths (max-w-4xl vs max-w-7xl)
  10. No settings panel

Layout Issues

  • Root layout (layout.tsx): No 3-column grid, single column with header/children/bottomNav
  • Live page: max-w-7xl (wider)
  • Fixture page: max-w-4xl (narrower)
  • No permanent sidebar infrastructure

Key Component Interaction Points

  • ExchangeGrid.tsx (428 lines): Main market display, calls addBet(fixture, market, outcome, betType)
  • FixtureCard.tsx (643 lines): Fixture list cards, same addBet call
  • QuickOddsButton (in fixture page): Header 1X2 buttons

1exch.com UX Reference (from screenshots)

Layout

  • 3-column: Left nav (sports menu) | Center content | Right bet panel (~320px)
  • Bet panel always visible on desktop
  • Content area adjusts — same width on home page and match detail page
  • Panel has subtle border-left separation

BETSLIP Tab

  • "Your Bets: {count}" header with ⚙ (settings) and 🗑 (delete all)
  • Per bet card: LIVE badge, match name, X close, market type, selection with logo, odds with +/-, Your Bet/Min/Max/Max.Mkt limits row, stake input with liability, quick stake buttons (+500/+2500/+5000/+12500), Max button
  • Back bets: blue-ish accent, Lay bets: pink accent
  • Footer: Total Payout (green), Potential Risk, Place Bet button with countdown timer

OPEN BETS Tab

  • Sub-tabs: Matched, Unmatched, Voided, Cashed Out
  • Settings (collapsible): Show bet info, Consolidate, Average Odds, Order By Date
  • Empty state: illustration + "You have no {tab} Bets" message

ONE CLICK Betting

  • Toggle at top of bet panel
  • When ON: clicking odds immediately places bet (no confirmation, no bet slip)
  • CONFIRMED: We will implement this

Points/Currency System (Research Round 2)

  • 1 point = 1 USD (configurable via PlatformSettings.pointValueUsd, default 1)
  • No currency conversion layer — all internal accounting in points
  • USD conversion only at exchange boundary (pointsToUsd/usdToPoints in orderService)
  • Display uses useCurrency hook — formats with $ symbol, no conversion
  • Current quick stakes: [1, 5, 10, 20, 50] — too low for meaningful bets
  • Recommended quick stakes: [10, 25, 50, 100, 500] (in points = USD)
  • Min stakes: Betfair $0.10, Pinnacle $20 — hardcoded in BOOKMAKER_MIN_STAKES

Settings Persistence (Research Round 2)

Industry Practice

  • localStorage is standard for UI preferences (Betfair, DraftKings, bet365 all use client-side persistence)
  • No server round-trip needed for settings — instant, offline-capable
  • Never store auth tokens in localStorage (already using HTTP-only cookies)

Hannibal's Existing Pattern

  • Zustand + persist middleware, key convention: hannibal-{feature}
  • hannibal-auth — user data (with version migration, AUTH_STORE_VERSION = 5)
  • hannibal-referral — referral code
  • hannibal-betslip — bet items (NOT isOpen state)

Recommendation for Bet Panel

PERSIST (localStorage as hannibal-bet-panel-settings):

  • defaultStake (number)
  • oneClickMode (boolean)
  • quickStakes (number[])
  • acceptBetterOdds (boolean)

SESSION-ONLY (reset per session):

  • isOpen state, active tab, validation errors, odds change warnings

Include version field for future migration (like authStore pattern).


Betfair Tick Sizes (Research Round 2)

Confirmed: NOT from API

  • Tick sizes are a fixed client-side rule set — Betfair does NOT expose them via any API endpoint
  • Same for 20+ years — the price ladder is a standard all integrations implement locally
  • Table in 1EXCH_UX_ANALYSIS.md is correct (matches standard Betfair ladder)

Implementation Location

  • Frontend: frontend/src/utils/betfairTicks.ts — for odds +/- buttons, input snapping
  • Backend: backend/src/utils/betfairTicks.ts — for server-side validation before submission
  • Same logic, shared constants

Market Limits from Betfair

  • listMarketBook does NOT provide explicit minStake/maxStake fields
  • Min stake: Hardcoded per bookmaker (BOOKMAKER_MIN_STAKES in orderService)
  • Max stake: Must calculate from backDepth[0].size or layDepth[0].size
  • Max Market: market.totalMatched (already available in responses)
  • Bet delay: betDelay field exists on BetfairMarketBook (2-5s for in-play)

1exch Responsive Behavior (Playwright Exploration)

Breakpoints Observed

ViewportLeft SidebarCenter ContentBet PanelNotes
1920px~190px, full sports navFluid, fills remaining~240px, always visibleFull 3-column
1440px~190px, sameFluid, tighter~240px, same3-column
1024pxHamburger menuFluid, wider (sidebar gone)~300px, still visible2-column
768pxHamburgerCramped, fewer odds columnsStill visible, ~320px2-column, content squeezed
375pxHamburgerHidden behind overlayFull-screen overlaySingle column + overlay

Key Observations

  1. Left sidebar collapses at ~1024px — replaced by hamburger menu icon
  2. Bet panel stays visible from 768px up — never hidden on tablet
  3. Bet panel becomes full-screen overlay on mobile (~480px breakpoint) — content visible but faded behind
  4. Match detail page: Same 3-column structure, consistent widths. Breadcrumbs + match header + 3-level depth grid.
  5. Content area never has a max-width — always fluid, adapts to available space
  6. Panel and content scroll independently — panel has its own scrollbar

Implication for Hannibal

Our breakpoints should be:

  • >=1024px: 2-column grid (content + bet panel). Left panel empty/reserved for future AI.
  • 768-1023px: Same 2-column but bet panel narrows slightly OR becomes slide-over
  • <768px: Single column, bet panel as bottom sheet with floating trigger button

Hands-On Exploration Findings (Playwright, 2026-02-12)

Live exploration of 1exch.com bet panel with real account (hannibaluser1, balance 505.72). 37 screenshots saved in docs/bet-panel/screenshots/.

CORRECTION: No Odds +/- Buttons

1exch does NOT have odds +/- increment buttons in the bet card. The odds value (e.g., 1.80) is displayed as static text, not editable. This contradicts the earlier 1EXCH_UX_ANALYSIS.md which described [-] 5.6 [+] buttons. For Hannibal, we should still implement odds +/- as a differentiator, using Betfair tick sizes.

Bet Card Anatomy (Verified)

Back bet card (blue accent):

[LIVE 🟢] Atletico MG v Remo                         X
Match Odds (exchange) - Back
[⚽ logo] Atletico MG 1.78
Your Bet Min: 100 Max: 171,037 Max. Mkt: 500,000+
┌──────────────┐ Potential win: 80.00
│ 100 │ (green text)
└──────────────┘
[+ 500] [+ 2500] [+ 5000] [+ 12500]
[ Max ]

Lay bet card (pink accent):

[LIVE 🟢] Chapecoense v Coritiba                     X
Match Odds (exchange) - Lay
[⚽ logo] The Draw 8.00
Your Bet Min: 100 Max: 135,120 Max. Mkt: 500,000+
┌──────────────┐ Liability: 700
│ 100 │ (red border when validation fails)
└──────────────┘
[+ 500] [+ 2500] [+ 5000] [+ 12500]
[ Max ]

Calculation Patterns (Verified)

  • Back bet: Potential win = stake × (odds - 1). e.g., 100 × (1.80 - 1) = 80.00
  • Lay bet: Liability = stake × (odds - 1). e.g., 100 × (8.00 - 1) = 700
  • Footer "Potential Payout" = profit (NOT stake + profit). Same for both Back and Lay.
  • Footer "Place Bet {amount}": For Back = stake. For Lay = liability.

Validation Behavior (Verified)

  • Min stake validation: "Stake must not be less than 100" — red text inline right of stake input, red border on input
  • Insufficient balance: Silent failure — API returns 400, dialog closes, bet remains in slip, NO error toast/message shown. Bad UX — we must show error feedback.
  • Place Bet footer still renders even with validation errors (but disabled)

Bet Placement Flow (Verified)

  1. Click odds cell → bet card appears in slip, input focused
  2. Enter stake → real-time calculation of potential win/liability
  3. Click "Place Bet {amount}" → confirmation dialog: "Are you sure you want to place your bet(s)?"
  4. Dialog has Cancel + Place buttons
  5. On success: bet card removed from slip, balance deducted (verified: 492→392 for stake 100)
  6. On failure: dialog closes, bet stays in slip, no error feedback shown
  7. 5s countdown timer on Place Bet button for live bets (clock icon + "5s")

Settlement in Real-Time (Verified)

  • Mirassol v Cruzeiro MG ended FT 2-2. Back The Draw at 1.14, stake 100.
  • Balance: 392 → 505.72 (= 392 + 100 stake + 14 profit - 0.28 commission)
  • Commission rate: ~2% on net winnings (0.28 / 14 = 2%)
  • Settlement happens automatically, balance updates without page refresh

ONE CLICK Betting (Verified)

OFF state: [⚡ OFF] ONE CLICK Betting [ⓘ]

  • Dark background, "OFF" label, info icon opens explanatory modal

ON state: [⚡ ON] [110] [550] [1100] [✏️]

  • 3 pre-set stake buttons, active one highlighted green
  • Edit icon (✏️) opens inline editing
  • Clicking any odds cell immediately places bet at selected stake (no confirmation)

Edit mode: [⚡ Stakes:] [___110___] [___550___] [___1100___] [✅]

  • 3 editable input fields for custom stake amounts
  • Green checkmark to save

Info modal (from ⓘ icon):

  1. Enable One Click Betting — toggle on betslip header
  2. Choose Your Active Stake — select comfortable stake value
  3. Place Bets Instantly! — enjoy placing bets without extra effort
  4. "Got it" button to dismiss

Open Bets Tab (Verified)

Sub-tabs: [Matched (1)] [Unmatched] [Voided] [Cashed Out]

  • Active sub-tab has colored pill (green for Matched, orange for Unmatched)
  • Count badge on tabs with bets (e.g., "Matched 1")

Settings panel (collapsible, chevron toggle):

  • GENERAL: ☐ Show bet info
  • BETS: ☐ Consolidate, ☐ Average Odds, ☑ Order By Date

Matched bet card:

⚽ Mirassol v Cruzeiro MG  >  (clickable, navigates to match)
Match Odds
[1.14] The Draw: Back
12/02/2026 04:02:25
Liability: 100.00 Potential win: 14.00
  • Odds in colored circle badge (green/teal)
  • Date/time of placement
  • No cancel button (matched bets can't be cancelled)
  • Match name is clickable (arrow icon → navigates to match detail)

Empty states: Illustration + "You have no {Unmatched} Bets" (sub-tab name in bold/red)

Multi-Bet Behavior (Verified)

  • Header updates: "Your Bets: {count}" (e.g., "Your Bets: 2")
  • Newest bet added to TOP of the list (not bottom)
  • Each card independent: separate stake, separate quick stakes, separate X close
  • Panel scrolls: bet cards area scrolls between sticky header and sticky footer
  • Footer aggregates: "Place {total} | Potential Payout {total}" across all bets
  • Delete All (🗑): Shows confirmation: "Are you sure you want to delete all bets in the betslip?" with Cancel/Confirm
  • Individual X close: Instant removal, NO confirmation

Settings Gear (⚙) on BETSLIP Tab

  • Visible in "Your Bets: {count}" header row alongside 🗑
  • Not explored in detail (opens a settings panel similar to Open Bets settings)

Empty State (Verified)

  • Panel stays at full height — never collapses or hides
  • Shows: illustration (blue line drawing) + "Your bet list is empty! Add bets and start winning with every match."
  • ONE CLICK toggle still visible above tabs

Market Limits Variation

  • High-liquidity markets: show Min, Max, AND Max. Mkt (e.g., "Max. Mkt: 500,000+")
  • Low-liquidity markets: show only Min and Max (no Max. Mkt field)

CSS Classes Discovered (for reference)

  • Close button: i.icon-cross (16×16 icon)
  • Delete all: i.icon-remove-solid
  • Sport icons: app-sport-icon with .bet-icon
  • Bet panel: matches on [class*="betslip"] (8 elements)
  • Active tab: .nav-tab.active
  • ONE CLICK: .icon-pnl-solid

1exch Limitations (NOT in Hannibal)

  • No limit orders → no Unmatched bets → no cancel button
  • No odds +/- buttons on bet card
  • Silent failure on insufficient balance (no error feedback)
  • No odds editing at all (static display only)

Hannibal Differentiators (to implement)

  • Odds +/- buttons with Betfair tick sizes (1exch doesn't have this)
  • Cancel button for unmatched/pending Betfair orders
  • Error feedback on placement failures (toast notifications)
  • Pinnacle-specific UX: $20 min stake warning, no-cancel notice, no lay bets

Key Architecture Decision

The fundamental change is moving the bet panel from a component-level overlay to a layout-level grid element:

CURRENT:  layout.tsx → page → (BetSlip overlay imported per-page)
TARGET: layout.tsx → CSS Grid [content | BetPanel] → page fills content cell

This means:

  1. BetPanel rendered once in layout.tsx (not per-page)
  2. Pages no longer import BetSlip
  3. Pages no longer set max-width — they fill the grid cell
  4. Content and bet panel scroll independently
  5. Mobile/tablet gracefully degrades to overlay/bottom sheet