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
pendingorders. Back refund = stake. Lay refund = stake × (odds - 1). - Pinnacle does NOT support cancellation.
WebSocket Events
balance:update— after any balance changesettlement:update— after settlementodds:updated— live odds changes- Gap: No
order:updateevent 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.balancePointsalways 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:
totalMatchedavailable 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
- No permanent right panel — floating overlay only
- No tabs (BETSLIP / OPEN BETS)
- No Open Bets display in bet slip
- No odds +/- buttons (tick increments)
- No min/max/maxMkt display
- No batch placement (Place All)
- No cancel order from bet slip
- No real-time order status updates
- Pages have inconsistent widths (max-w-4xl vs max-w-7xl)
- 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, callsaddBet(fixture, market, outcome, betType)FixtureCard.tsx(643 lines): Fixture list cards, sameaddBetcallQuickOddsButton(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/usdToPointsin orderService) - Display uses
useCurrencyhook — 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 +
persistmiddleware, key convention:hannibal-{feature} hannibal-auth— user data (with version migration, AUTH_STORE_VERSION = 5)hannibal-referral— referral codehannibal-betslip— bet items (NOTisOpenstate)
Recommendation for Bet Panel
PERSIST (localStorage as hannibal-bet-panel-settings):
defaultStake(number)oneClickMode(boolean)quickStakes(number[])acceptBetterOdds(boolean)
SESSION-ONLY (reset per session):
isOpenstate, 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.mdis 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
listMarketBookdoes NOT provide explicitminStake/maxStakefields- Min stake: Hardcoded per bookmaker (
BOOKMAKER_MIN_STAKESin orderService) - Max stake: Must calculate from
backDepth[0].sizeorlayDepth[0].size - Max Market:
market.totalMatched(already available in responses) - Bet delay:
betDelayfield exists onBetfairMarketBook(2-5s for in-play)
1exch Responsive Behavior (Playwright Exploration)
Breakpoints Observed
| Viewport | Left Sidebar | Center Content | Bet Panel | Notes |
|---|---|---|---|---|
| 1920px | ~190px, full sports nav | Fluid, fills remaining | ~240px, always visible | Full 3-column |
| 1440px | ~190px, same | Fluid, tighter | ~240px, same | 3-column |
| 1024px | Hamburger menu | Fluid, wider (sidebar gone) | ~300px, still visible | 2-column |
| 768px | Hamburger | Cramped, fewer odds columns | Still visible, ~320px | 2-column, content squeezed |
| 375px | Hamburger | Hidden behind overlay | Full-screen overlay | Single column + overlay |
Key Observations
- Left sidebar collapses at ~1024px — replaced by hamburger menu icon
- Bet panel stays visible from 768px up — never hidden on tablet
- Bet panel becomes full-screen overlay on mobile (~480px breakpoint) — content visible but faded behind
- Match detail page: Same 3-column structure, consistent widths. Breadcrumbs + match header + 3-level depth grid.
- Content area never has a max-width — always fluid, adapts to available space
- 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)
- Click odds cell → bet card appears in slip, input focused
- Enter stake → real-time calculation of potential win/liability
- Click "Place Bet {amount}" → confirmation dialog: "Are you sure you want to place your bet(s)?"
- Dialog has Cancel + Place buttons
- On success: bet card removed from slip, balance deducted (verified: 492→392 for stake 100)
- On failure: dialog closes, bet stays in slip, no error feedback shown
- 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):
- Enable One Click Betting — toggle on betslip header
- Choose Your Active Stake — select comfortable stake value
- Place Bets Instantly! — enjoy placing bets without extra effort
- "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-iconwith.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:
- BetPanel rendered once in layout.tsx (not per-page)
- Pages no longer import BetSlip
- Pages no longer set max-width — they fill the grid cell
- Content and bet panel scroll independently
- Mobile/tablet gracefully degrades to overlay/bottom sheet