Skip to main content

Hannibal B-Book System Design

Architecture and Implementation Specification

Version 1.0 | January 2026

✅ IMPLEMENTATION STATUS: COMPLETE

This system has been fully implemented. See backend/src/services/bbook/ for the implementation:

  • bbookConfigService.ts - Configuration management
  • bbookFillService.ts - B-book fill execution
  • bbookSettlementService.ts - Settlement processing
  • bbookStateService.ts - Position and exposure tracking
  • filterEngine.ts - Order routing decisions
  • sharpUserService.ts - Sharp user management

Executive Summary

The B-Book is an optional, independent module that allows Hannibal to take the opposite side of user bets using its own capital pool, rather than routing all bets to external exchanges (Betfair, Pinnacle). When enabled, the B-Book captures margin from recreational bettors while protecting the platform through configurable liability limits and sharp user exclusion.

Key Design Principles:

  1. Optional and Toggleable: Admin can enable/disable the entire B-Book with a single switch
  2. Independent Module: Operates as a separate service that plugs into the order flow
  3. Never Exceeds Limits: Hard liability cap that cannot be breached under any circumstance
  4. Currency Agnostic: Works with Hannibal's points system but abstracts currency for future flexibility
  5. Accounting Isolation: B-Book P&L is tracked separately; agent settlements only apply to exchange orders
  6. Sharp Protection: Known sharp users are always routed to exchanges, never B-Booked

Table of Contents

  1. What is the B-Book?
  2. System Architecture
  3. Configuration and Admin Controls
  4. Order Flow Integration
  5. The Filter Engine
  6. Liability Tracking
  7. Sharp User Management
  8. Settlement and P&L
  9. Accounting Integration
  10. Database Schema
  11. API Endpoints
  12. Risk Monitoring and Alerts
  13. Error Handling and Edge Cases
  14. Currency Abstraction Layer
  15. Performance Considerations
  16. Testing Strategy
  17. Deployment and Operations
  18. Migration Path
  19. Security Considerations
  20. Glossary
  21. Open Questions
  22. Appendix

1. What is the B-Book?

1.1 The Core Concept

When a user places a bet, there are two fundamental approaches:

ApproachDescriptionPlatform Role
A-BookPass the bet to an external exchange (Betfair, Pinnacle)Intermediary (earns commission)
B-BookTake the opposite side of the bet using platform capitalCounterparty (wins or loses with user)

Example:

User bets 1,000 points BACK on Mumbai to Win at odds 2.50.

ApproachWhat HappensIf Mumbai WinsIf Mumbai Loses
A-BookBet goes to BetfairBetfair pays 1,500 points (via platform)Platform collects from user
B-BookPlatform takes oppositePlatform pays user 1,500 pointsPlatform keeps 1,000 points

1.2 Why B-Book?

The B-Book exists because recreational bettors lose money on average. By taking the opposite side of these bets, the platform captures the statistical edge that would otherwise go to the exchange.

Expected Value Breakdown:

User TypeLong-term EVB-Book Outcome
RecreationalNegative (loses ~5-15%)Platform profits
Sharp/ProfessionalPositive (wins consistently)Platform loses

This is why sharp users must be excluded from the B-Book—they would drain the pool over time.

1.3 The Pool

The B-Book operates from a fixed capital pool with a hard liability limit. This limit represents the maximum amount the platform is willing to risk at any given moment.

Pool Characteristics:

  • Starting Capital: Configured by admin (e.g., 10,000,000 points)
  • Maximum Liability: Hard cap that can never be exceeded (e.g., 10,000,000 points)
  • Self-Replenishing: Winning B-Book positions return funds to the pool
  • Real-Time Tracking: Current exposure is calculated continuously

2. System Architecture

2.1 High-Level Architecture

The B-Book is designed as an independent, pluggable module that integrates with the existing order flow.

2.2 Module Independence

The B-Book module is designed to be completely independent:

AspectImplementation
DeploymentCan run as a separate service or within the monolith
StateMaintains its own position state (Redis + PostgreSQL)
ConfigurationOwn configuration table, not mixed with other settings
ToggleSingle bbook_enabled flag controls all B-Book behavior
Failure IsolationIf B-Book fails, orders fall back to exchange routing

2.3 Component Responsibilities

ComponentResponsibility
Filter EngineDecides routing: B-Book vs Exchange based on rules
B-Book State ManagerTracks positions, exposures, and pool utilization
Liability TrackerEnforces all limits (global, per-market, per-selection, per-user)
B-Book Fill HandlerExecutes B-Book fills and creates position records
B-Book P&L TrackerCalculates and records B-Book profit/loss

2.4 Data Flow

Order Placement Flow:

1. User submits order
2. Order Service validates (balance, market open, etc.)
3. IF B-Book disabled → Route entirely to exchange
4. IF B-Book enabled:
a. Filter Engine checks sharp list
b. IF sharp → Route entirely to exchange
c. IF not sharp → Calculate B-Book capacity
d. Split order: B-Book fill + Exchange fill
5. Execute B-Book portion (update positions, deduct user balance)
6. Execute Exchange portion (place on Betfair/Pinnacle)
7. Return combined fill result to user

Settlement Flow:

1. Event result received (Mumbai won/lost)
2. B-Book Settlement:
a. Calculate P&L for all B-Book positions on this selection
b. Credit/debit platform B-Book pool
c. Credit/debit user balances
d. Mark positions as settled
3. Exchange Settlement (existing flow):
a. Receive settlement from exchange
b. Update user balances
c. Record in agent accounting system

3. Configuration and Admin Controls

3.1 Master Switch

The B-Book has a single master switch that enables or disables all functionality:

SettingTypeDefaultDescription
bbook_enabledBooleanfalseMaster switch for B-Book functionality

When bbook_enabled = false:

  • All orders route to exchanges (Betfair/Pinnacle)
  • Filter Engine is bypassed entirely
  • B-Book state is preserved but not updated
  • Existing B-Book positions remain until settled

When bbook_enabled = true:

  • Filter Engine evaluates every order
  • Orders may be split between B-Book and exchange
  • All limits and rules are enforced

3.2 Liability Limits

These limits define the maximum exposure the platform will accept:

SettingTypeDefaultDescription
global_pool_limitDecimal10,000,000Maximum total net exposure across all selections
per_market_limitDecimal2,000,000Maximum net exposure per market
per_selection_limitDecimal500,000Maximum net exposure per selection
per_user_limitDecimal50,000Maximum exposure from any single user

Limit Hierarchy:

Global Pool Limit (10,000,000)
└── Per-Market Limit (2,000,000)
└── Per-Selection Limit (500,000)
└── Per-User Limit (50,000)

All limits are checked on every order. The most restrictive limit applies.

3.3 Routing Controls

These settings control how orders are split between B-Book and exchanges:

SettingTypeDefaultDescription
max_bbook_percentageDecimal70%Maximum percentage of any order that can go to B-Book
min_exchange_percentageDecimal0%Minimum percentage that must go to exchange
prefer_bbookBooleantrueIf true, maximize B-Book fill within limits

Example:

Order: 100,000 points BACK Mumbai at 2.50

With max_bbook_percentage = 70%:

  • Maximum B-Book: 70,000 points
  • Minimum Exchange: 30,000 points (if min_exchange_percentage = 0%)

3.4 Risk Controls

SettingTypeDefaultDescription
side_imbalance_thresholdDecimal3.0Ratio at which to restrict heavy side (e.g., 3:1 BACK:LAY)
auto_hedge_thresholdDecimal80%Pool % at which auto-hedging activates
emergency_shutdown_thresholdDecimal95%Pool % at which B-Book suspends
daily_loss_limitDecimal500,000Daily loss that triggers B-Book suspension

3.5 Admin API for Configuration

All settings are managed via admin API endpoints:

GET  /admin/bbook/config           - Get all B-Book configuration
PUT /admin/bbook/config - Update configuration
POST /admin/bbook/enable - Enable B-Book
POST /admin/bbook/disable - Disable B-Book
GET /admin/bbook/status - Get current pool state and exposure

4. Order Flow Integration

4.1 Integration Point

The B-Book integrates at the order routing stage, after validation but before execution:

4.2 Order Data Extensions

The existing Order model requires new fields to track B-Book routing:

FieldTypeDescription
bbook_fillDecimalAmount filled via B-Book
exchange_fillDecimalAmount filled via exchange
routing_venueEnumPrimary venue: 'bbook', 'exchange', 'split'
is_sharp_at_order_timeBooleanUser's sharp status when order placed
bbook_position_idUUIDReference to B-Book position (if any)

4.3 Fill Response Structure

Orders return detailed fill information:

interface OrderFillResponse {
orderId: string;
totalStake: number;
fills: {
venue: 'bbook' | 'betfair' | 'pinnacle';
amount: number;
odds: number;
fillId: string;
}[];
status: 'filled' | 'partial' | 'rejected';
limitingFactor?: string; // e.g., 'per_user_limit', 'pool_capacity'
}

4.4 Partial Fill Handling

Orders may be partially filled across venues:

ScenarioB-BookExchangeUser Sees
Full B-Book capacity100%0%Order filled
B-Book at limit0%100%Order filled
Split by limits70%30%Order filled (details in response)
Exchange unavailable, B-Book has capacity100%0%Order filled
Exchange unavailable, B-Book at limit0%0%Order rejected

5. The Filter Engine

5.1 Purpose

The Filter Engine is the decision-making component that determines how each order should be routed. It evaluates:

  1. Is B-Book enabled?
  2. Is the user on the sharp list?
  3. Does B-Book have capacity?
  4. How should the order be split?

5.2 Decision Flow

5.3 Capacity Calculation Algorithm

The Filter Engine calculates available B-Book capacity by finding the minimum across all limits:

function calculateBBookCapacity(order: Order): number {
const config = getBBookConfig();

// Get current exposures
const globalExposure = getGlobalExposure();
const marketExposure = getMarketExposure(order.marketId);
const selectionExposure = getSelectionExposure(order.selectionId);
const userExposure = getUserBBookExposure(order.userId);

// Calculate remaining capacity at each level
const globalRemaining = config.global_pool_limit - globalExposure;
const marketRemaining = config.per_market_limit - marketExposure;
const selectionRemaining = config.per_selection_limit - selectionExposure;
const userRemaining = config.per_user_limit - userExposure;

// Apply percentage cap
const percentageCap = order.stake * config.max_bbook_percentage;

// B-Book capacity is the minimum of all limits
const capacity = Math.min(
globalRemaining,
marketRemaining,
selectionRemaining,
userRemaining,
percentageCap
);

return Math.max(0, capacity);
}

5.4 Balancing Logic

Orders that reduce existing exposure are treated favorably:

function calculateExposureImpact(order: Order): number {
const selection = getSelectionState(order.selectionId);

const currentBack = selection.backLiability;
const currentLay = selection.layLiability;
const currentNet = Math.abs(currentBack - currentLay);

const orderLiability = calculateLiability(order);

let newNet: number;
if (order.betType === 'back') {
newNet = Math.abs((currentBack + orderLiability) - currentLay);
} else {
newNet = Math.abs(currentBack - (currentLay + orderLiability));
}

const exposureChange = newNet - currentNet;

// Negative = reduces exposure (good!)
// Positive = increases exposure (apply limits)
// Zero = perfectly balancing
return exposureChange;
}

Balancing Orders Get Priority:

Exposure ChangeTreatment
Negative (reduces exposure)Accept freely, no capacity limits
Zero (perfectly balances)Accept freely
Positive (increases exposure)Apply all limits

5.5 Side Imbalance Restriction

When one side is significantly heavier, restrict new orders on the heavy side:

function checkSideImbalance(order: Order, selection: SelectionState): boolean {
const config = getBBookConfig();
const ratio = selection.backLiability / selection.layLiability;

if (ratio > config.side_imbalance_threshold) {
// BACK heavy - restrict BACK orders, accept LAY
return order.betType === 'lay';
}

if (ratio < (1 / config.side_imbalance_threshold)) {
// LAY heavy - restrict LAY orders, accept BACK
return order.betType === 'back';
}

return true; // Balanced, accept both
}

6. Liability Tracking

6.1 Understanding Liability

Liability is what the platform stands to lose if an outcome occurs.

BACK Order Liability:

When a user places a BACK bet, the platform (as B-Book) takes the LAY side.

User BACK: stake × (odds - 1) = Platform liability if selection WINS
Example: 1000 pts BACK @ 2.50 = 1000 × (2.50 - 1) = 1,500 pts liability

LAY Order Liability:

When a user places a LAY bet, the platform takes the BACK side.

User LAY: stake = Platform liability if selection LOSES
Example: 1000 pts LAY @ 2.50 = 1,000 pts liability

6.2 Net Exposure Calculation

The actual risk is the net exposure, not gross:

Net Exposure = |BACK liability - LAY liability|

Example:

Selection: Mumbai WinBACK LiabilityLAY LiabilityNet ExposureDirection
Position 110,000010,000Short Mumbai
After LAY order10,0006,0004,000Short Mumbai (reduced!)
More LAY orders10,00010,0000Balanced (no risk!)
Even more LAY10,00015,0005,000Long Mumbai

Key Insight: LAY orders offset BACK orders. A perfectly balanced selection has zero net exposure.

6.3 Position State Tracking

The B-Book maintains real-time position state at multiple levels:

Per-Selection State:

interface SelectionState {
selectionId: string;
marketId: string;
fixtureId: string;

// Liability tracking
backLiability: number; // What we pay if selection wins
layLiability: number; // What we pay if selection loses
netExposure: number; // abs(back - lay)
direction: 'short' | 'long' | 'balanced';

// Capacity
capacityRemaining: number;

// Status
status: 'open' | 'suspended' | 'settled';
updatedAt: timestamp;
}

Per-Market State:

interface MarketState {
marketId: string;
fixtureId: string;

// Aggregate of all selections in market
totalNetExposure: number;
selectionCount: number;

status: 'open' | 'suspended' | 'settled';
}

Per-User State:

interface UserBBookState {
userId: string;

// User's total B-Book exposure
totalBackLiability: number;
totalLayLiability: number;
totalExposure: number;

// Limits
remainingCapacity: number;
}

Global Pool State:

interface PoolState {
// Aggregate exposure
totalNetExposure: number;

// Pool health
utilizationPercent: number; // exposure / limit
capacityRemaining: number;

// Side balance
totalBackLiability: number;
totalLayLiability: number;
imbalanceRatio: number;

// P&L tracking
realizedPnL: number; // Settled positions
unrealizedPnL: number; // Open positions (mark-to-market)

// Status
status: 'active' | 'restricted' | 'suspended';
lastUpdated: timestamp;
}

6.4 Real-Time State Updates

Position state is updated atomically with each B-Book fill:

async function updatePositionState(order: Order, fill: BBookFill): Promise<void> {
const liability = calculateLiability(order, fill.amount);

await transaction(async (tx) => {
// 1. Update selection state
const selection = await tx.getSelectionState(order.selectionId);
if (order.betType === 'back') {
selection.backLiability += liability;
} else {
selection.layLiability += liability;
}
selection.netExposure = Math.abs(selection.backLiability - selection.layLiability);
await tx.saveSelectionState(selection);

// 2. Update market state
await tx.updateMarketExposure(order.marketId);

// 3. Update user state
await tx.updateUserExposure(order.userId, liability, order.betType);

// 4. Update global pool state
await tx.updatePoolState();

// 5. Create position record
await tx.createBBookPosition({
orderId: order.id,
selectionId: order.selectionId,
userId: order.userId,
side: order.betType,
stake: fill.amount,
liability: liability,
odds: order.odds,
status: 'open'
});
});
}

6.5 State Storage Strategy

DataPrimary StorageCacheRationale
Selection StatePostgreSQLRedisFast reads for capacity checks
Market StatePostgreSQLRedisAggregated from selections
User StatePostgreSQLRedisPer-user limit checks
Pool StatePostgreSQLRedis (singleton)Most frequently accessed
Position RecordsPostgreSQLNoneAudit trail, less frequent access

Redis Keys:

bbook:pool                          → Global pool state (hash)
bbook:selection:{selectionId} → Selection state (hash)
bbook:market:{marketId} → Market state (hash)
bbook:user:{userId} → User B-Book exposure (hash)

Consistency:

  1. PostgreSQL is the source of truth
  2. Redis is updated synchronously with database writes
  3. On cache miss, rebuild from PostgreSQL
  4. On startup, rebuild all Redis state from PostgreSQL

7. Sharp User Management

7.1 The Sharp List

Sharp users are professional bettors with positive expected value. They consistently beat the closing line and would drain the B-Book pool over time. These users must be excluded from B-Book.

Sharp List Storage:

interface SharpUser {
userId: string;
addedAt: timestamp;
addedBy: string; // Admin who added
reason: string; // Why classified as sharp
source: 'manual' | 'auto'; // How identified (MVP: always 'manual')
isActive: boolean;
}

7.2 Sharp Detection Criteria (Manual for MVP)

For MVP, ops team manually identifies sharp users based on:

IndicatorThresholdWeight
Win rate over 200+ bets> 55%High
Closing Line Value (CLV)Consistently positiveHigh
Stake patternsPrecise amounts (not round numbers)Medium
TimingBets placed close to event startMedium
Market selectionNiche/low-liquidity marketsMedium

7.3 Sharp Check in Order Flow

async function isSharpUser(userId: string): Promise<boolean> {
// First check Redis cache
const cached = await redis.sismember('bbook:sharp_list', userId);
if (cached) return true;

// Fallback to database
const sharpRecord = await db.sharpUsers.findUnique({
where: { userId, isActive: true }
});

if (sharpRecord) {
// Add to cache
await redis.sadd('bbook:sharp_list', userId);
return true;
}

return false;
}

7.4 Sharp List Management API

GET  /admin/bbook/sharp-list              - List all sharp users
POST /admin/bbook/sharp-list - Add user to sharp list
DELETE /admin/bbook/sharp-list/:userId - Remove user from sharp list
GET /admin/bbook/sharp-candidates - Users flagged for review (future)

7.5 Impact of Sharp Classification

ScenarioBefore ClassificationAfter Classification
New orderEvaluated for B-BookRoutes 100% to exchange
Existing B-Book positionsRemain until settledRemain until settled
Future ordersMay go to B-BookNever go to B-Book

Sharp classification is immediate: The moment a user is added to the sharp list, their next order routes entirely to the exchange.


8. Settlement and P&L

8.1 B-Book Settlement Process

When an event concludes, B-Book positions are settled:

8.2 Settlement Calculation

If Selection WINS:

Position TypeUser OutcomePlatform Outcome
BACKUser wins: stake × (odds - 1)Platform pays liability
LAYUser loses: loses stakePlatform keeps stake

If Selection LOSES:

Position TypeUser OutcomePlatform Outcome
BACKUser loses: loses stakePlatform keeps stake
LAYUser wins: stake × (odds - 1)Platform pays liability

8.3 Settlement Example

Selection: Mumbai Win @ 2.50

Positions:

  • User A: BACK 10,000 pts @ 2.50 (liability: 15,000 pts)
  • User B: BACK 5,000 pts @ 2.60 (liability: 8,000 pts)
  • User C: LAY 8,000 pts @ 2.50 (liability: 8,000 pts)

If Mumbai WINS:

UserPositionStakeOddsUser P&LPlatform P&L
ABACK10,0002.50+15,000-15,000
BBACK5,0002.60+8,000-8,000
CLAY8,0002.50-8,000+8,000
Total+15,000-15,000

If Mumbai LOSES:

UserPositionStakeOddsUser P&LPlatform P&L
ABACK10,0002.50-10,000+10,000
BBACK5,0002.60-5,000+5,000
CLAY8,0002.50+12,000-12,000
Total-3,000+3,000

8.4 Settlement Code Flow

async function settleBBookPositions(
selectionId: string,
won: boolean
): Promise<SettlementResult> {
const positions = await db.bbookPositions.findMany({
where: { selectionId, status: 'open' }
});

let totalPlatformPnL = 0;

await transaction(async (tx) => {
for (const position of positions) {
let userPnL: number;
let platformPnL: number;

if (position.side === 'back') {
if (won) {
// User wins: pay them liability
userPnL = position.liability;
platformPnL = -position.liability;
} else {
// User loses: keep their stake
userPnL = -position.stake;
platformPnL = position.stake;
}
} else { // LAY
if (won) {
// User loses: keep their stake
userPnL = -position.stake;
platformPnL = position.stake;
} else {
// User wins: pay them liability
userPnL = position.liability;
platformPnL = -position.liability;
}
}

// Update user balance
await tx.updateUserBalance(position.userId, userPnL);

// Mark position as settled
await tx.updateBBookPosition(position.id, {
status: 'settled',
settledAt: new Date(),
settlementPnL: userPnL
});

// Record in B-Book P&L
await tx.recordBBookPnL({
positionId: position.id,
selectionId,
platformPnL,
settledAt: new Date()
});

totalPlatformPnL += platformPnL;
}

// Update pool state (release exposure, record P&L)
await tx.updatePoolAfterSettlement(selectionId, totalPlatformPnL);
});

return { positionsSettled: positions.length, platformPnL: totalPlatformPnL };
}

8.5 Void/Cancelled Events

When an event is voided or cancelled:

ActionAll Positions
StakesReturned to users
LiabilitiesReleased
P&LZero (no win/loss)
Pool StateExposure removed

9. Accounting Integration

9.1 Separation of B-Book and Exchange Accounting

Critical Principle: Agent settlements only apply to exchange orders. B-Book is a direct platform-user transaction.

9.2 Why This Separation?

The agent model works as follows:

  • Agents distribute points to punters
  • When punters bet, agents bear the risk of their punters' P&L
  • Weekly settlement reconciles agent ↔ platform

B-Book orders bypass this because:

  1. Platform is the counterparty, not the exchange
  2. There's no external exchange settlement to reconcile
  3. Platform P&L is direct, not through agent

Result: B-Book P&L belongs entirely to the platform, not shared with agents.

9.3 Accounting Treatment by Venue

VenueWho Profits/LosesAgent CommissionAgent NGR Share
B-BookPlatform directlyNoNo
ExchangeAgent (via settlement)YesYes (if NGR > 0)

9.4 Order-Level Tracking

Each order tracks which venue processed it:

interface OrderAccountingData {
orderId: string;

// B-Book portion (platform P&L)
bbookStake: number;
bbookPnL: number; // Populated on settlement
bbookCommission: number; // Always 0 for B-Book

// Exchange portion (agent accounting)
exchangeStake: number;
exchangePnL: number; // Populated on settlement
exchangeCommission: number; // Standard commission on winnings

// Settlement tracking
bbookSettled: boolean;
exchangeSettled: boolean;
}

9.5 Commission Handling

For Exchange Orders:

  • Commission charged on user winnings (existing logic)
  • Commission flows through agent accounting system
  • Agent receives commission share per their configuration

For B-Book Orders:

  • No commission charged to user (platform keeps full margin)
  • Commission field is not applicable
  • Platform profit is the edge on recreational flow

9.6 Ledger Entries for B-Book

B-Book transactions create ledger entries with a distinct account type:

// On B-Book fill
await createLedgerEntry({
transactionRef: generateRef(),
accountType: 'bbook_pool',
accountId: null, // Platform-level
entryType: 'debit', // Exposure increases
amount: liability,
description: `B-Book fill: ${orderId}`,
category: 'bbook_fill',
orderId: order.id
});

// On B-Book settlement (platform wins)
await createLedgerEntry({
transactionRef: generateRef(),
accountType: 'bbook_pool',
accountId: null,
entryType: 'credit', // Pool increases
amount: pnl,
description: `B-Book settlement: ${positionId}`,
category: 'bbook_settlement',
orderId: order.id
});

9.7 B-Book P&L Reporting

The platform needs clear visibility into B-Book performance:

interface BBookReport {
period: {
start: Date;
end: Date;
};

// Volume metrics
totalBBookStakes: number;
totalBBookFills: number;
averageFillSize: number;

// P&L metrics
realizedPnL: number; // Settled positions
unrealizedPnL: number; // Open positions (mark-to-market)
totalPnL: number;

// Pool metrics
peakExposure: number;
averageUtilization: number;
currentUtilization: number;

// Risk metrics
maxDrawdown: number;
sharpeRatio: number;
winRate: number;

// User breakdown
userCount: number;
topWinners: { userId: string; pnl: number }[];
topLosers: { userId: string; pnl: number }[];
}

9.8 Impact on Agent Settlement

Agent settlement calculation explicitly excludes B-Book:

async function calculateAgentSettlement(
agentId: string,
period: SettlementPeriod
): Promise<AgentSettlement> {
// Get all settled orders for agent's punters in this period
const orders = await db.orders.findMany({
where: {
user: { agentId },
settledAt: { gte: period.start, lte: period.end },
status: 'settled',
// IMPORTANT: Only exchange orders
exchangeFill: { gt: 0 }
}
});

// Calculate totals from EXCHANGE portions only
let totalStakes = 0;
let totalPayouts = 0;
let commission = 0;

for (const order of orders) {
// Only count exchange portion, not B-Book
totalStakes += order.exchangeStake;

if (order.settlementOutcome === 'win') {
const payout = order.exchangeStake * order.placedOdds;
totalPayouts += payout;
commission += payout * COMMISSION_RATE;
}
}

const ngr = totalStakes - totalPayouts;

// Rest of settlement calculation...
}

10. Database Schema

10.1 New Tables for B-Book

The following Prisma models are added to support B-Book functionality:

// B-Book Configuration (singleton or per-period)
model BBookConfig {
id String @id @default(uuid())

// Master switch
bbookEnabled Boolean @default(false)

// Liability limits (in points)
globalPoolLimit Decimal @default(10000000)
perMarketLimit Decimal @default(2000000)
perSelectionLimit Decimal @default(500000)
perUserLimit Decimal @default(50000)

// Routing controls
maxBbookPercentage Decimal @default(0.70)
minExchangePercentage Decimal @default(0)
preferBbook Boolean @default(true)

// Risk controls
sideImbalanceThreshold Decimal @default(3.0)
autoHedgeThreshold Decimal @default(0.80)
emergencyShutdownThreshold Decimal @default(0.95)
dailyLossLimit Decimal @default(500000)

// Metadata
updatedAt DateTime @updatedAt
updatedBy String?

@@map("bbook_config")
}

// B-Book Position (individual fills)
model BBookPosition {
id String @id @default(uuid())

// References
orderId String
order Order @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])

// Market identifiers
fixtureId String
marketId String
selectionId String

// Position details
side BetType // BACK or LAY
stake Decimal
odds Decimal
liability Decimal // Calculated based on side and odds

// Status
status BBookPositionStatus @default(OPEN)

// Settlement
settledAt DateTime?
settlementPnL Decimal?

// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([selectionId, status])
@@index([userId])
@@index([status])
@@map("bbook_positions")
}

enum BBookPositionStatus {
OPEN
SETTLED
VOIDED
}

// Selection State (aggregate per selection)
model BBookSelectionState {
id String @id @default(uuid())

// Identifiers
fixtureId String
marketId String
selectionId String @unique

// Liability tracking
backLiability Decimal @default(0)
layLiability Decimal @default(0)
netExposure Decimal @default(0)

// Status
status SelectionStatus @default(OPEN)

// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([marketId])
@@index([fixtureId])
@@map("bbook_selection_state")
}

enum SelectionStatus {
OPEN
SUSPENDED
SETTLED
}

// Market State (aggregate per market)
model BBookMarketState {
id String @id @default(uuid())

// Identifiers
fixtureId String
marketId String @unique

// Aggregate exposure
totalNetExposure Decimal @default(0)
selectionCount Int @default(0)

// Status
status MarketStatus @default(OPEN)

// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([fixtureId])
@@map("bbook_market_state")
}

enum MarketStatus {
OPEN
SUSPENDED
SETTLED
}

// User B-Book State (per-user exposure tracking)
model BBookUserState {
id String @id @default(uuid())

userId String @unique
user User @relation(fields: [userId], references: [id])

// Exposure tracking
totalBackLiability Decimal @default(0)
totalLayLiability Decimal @default(0)
totalExposure Decimal @default(0)

// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("bbook_user_state")
}

// Global Pool State (singleton)
model BBookPoolState {
id String @id @default(uuid())

// Exposure tracking
totalNetExposure Decimal @default(0)
totalBackLiability Decimal @default(0)
totalLayLiability Decimal @default(0)

// Utilization
utilizationPercent Decimal @default(0)

// P&L tracking
realizedPnL Decimal @default(0)
unrealizedPnL Decimal @default(0)

// Daily tracking
dailyPnL Decimal @default(0)
dailyResetAt DateTime @default(now())

// Status
status PoolStatus @default(ACTIVE)

// Timestamps
updatedAt DateTime @updatedAt

@@map("bbook_pool_state")
}

enum PoolStatus {
ACTIVE
RESTRICTED
SUSPENDED
}

// Sharp User List
model SharpUser {
id String @id @default(uuid())

userId String @unique
user User @relation(fields: [userId], references: [id])

// Classification
reason String
source SharpSource @default(MANUAL)
isActive Boolean @default(true)

// Audit
addedBy String
addedAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("sharp_users")
}

enum SharpSource {
MANUAL
AUTO
}

// B-Book P&L Records (audit trail)
model BBookPnLRecord {
id String @id @default(uuid())

positionId String
selectionId String

platformPnL Decimal

settledAt DateTime @default(now())

@@index([settledAt])
@@map("bbook_pnl_records")
}

10.2 Order Model Extensions

Add these fields to the existing Order model:

model Order {
// ... existing fields ...

// B-Book routing fields (new)
bbookFill Decimal?
exchangeFill Decimal?
routingVenue RoutingVenue?
isSharpAtOrderTime Boolean?

// B-Book position reference
bbookPosition BBookPosition?

// ... existing relations ...
}

enum RoutingVenue {
BBOOK
EXCHANGE
SPLIT
}

10.3 Index Strategy

Critical indexes for performance:

TableIndexPurpose
bbook_positions(selectionId, status)Fast position lookup for settlement
bbook_positions(userId)User position history
bbook_selection_state(marketId)Market-level aggregation
bbook_pnl_records(settledAt)P&L reporting queries
sharp_users(userId)Fast sharp check

11. API Endpoints

11.1 Admin Configuration APIs

Get B-Book Configuration

GET /admin/bbook/config

Response:
{
"bbookEnabled": true,
"globalPoolLimit": 10000000,
"perMarketLimit": 2000000,
"perSelectionLimit": 500000,
"perUserLimit": 50000,
"maxBbookPercentage": 0.70,
"sideImbalanceThreshold": 3.0,
"autoHedgeThreshold": 0.80,
"emergencyShutdownThreshold": 0.95,
"dailyLossLimit": 500000,
"updatedAt": "2026-01-22T10:00:00Z"
}

Update B-Book Configuration

PUT /admin/bbook/config

Request:
{
"globalPoolLimit": 15000000,
"perUserLimit": 100000
}

Response:
{
"success": true,
"config": { ... updated config ... }
}

Enable/Disable B-Book

POST /admin/bbook/enable
POST /admin/bbook/disable

Response:
{
"success": true,
"bbookEnabled": true/false,
"message": "B-Book enabled successfully"
}

11.2 Pool State APIs

Get Current Pool State

GET /admin/bbook/pool-state

Response:
{
"totalNetExposure": 4500000,
"utilizationPercent": 45.0,
"capacityRemaining": 5500000,
"totalBackLiability": 7000000,
"totalLayLiability": 2500000,
"imbalanceRatio": 2.8,
"realizedPnL": 150000,
"unrealizedPnL": -25000,
"dailyPnL": 15000,
"status": "ACTIVE"
}

Get Market Exposure

GET /admin/bbook/markets/:marketId/exposure

Response:
{
"marketId": "mkt_123",
"totalNetExposure": 800000,
"selections": [
{
"selectionId": "sel_456",
"backLiability": 500000,
"layLiability": 200000,
"netExposure": 300000,
"capacityRemaining": 200000
}
]
}

11.3 Sharp User APIs

List Sharp Users

GET /admin/bbook/sharp-list

Response:
{
"sharpUsers": [
{
"userId": "usr_123",
"username": "pro_bettor_1",
"reason": "Consistent CLV positive, 58% win rate over 500 bets",
"source": "MANUAL",
"addedBy": "admin_001",
"addedAt": "2026-01-15T10:00:00Z"
}
],
"totalCount": 15
}

Add Sharp User

POST /admin/bbook/sharp-list

Request:
{
"userId": "usr_789",
"reason": "Win rate 62% over 300 bets, strong CLV"
}

Response:
{
"success": true,
"sharpUser": { ... }
}

Remove Sharp User

DELETE /admin/bbook/sharp-list/:userId

Response:
{
"success": true,
"message": "User removed from sharp list"
}

11.4 Reporting APIs

B-Book P&L Report

GET /admin/bbook/reports/pnl?startDate=2026-01-01&endDate=2026-01-22

Response:
{
"period": {
"start": "2026-01-01",
"end": "2026-01-22"
},
"totalBBookStakes": 50000000,
"totalBBookFills": 8500,
"realizedPnL": 750000,
"unrealizedPnL": -50000,
"peakExposure": 8500000,
"averageUtilization": 42.5,
"winRate": 54.2,
"maxDrawdown": 200000
}

Position History

GET /admin/bbook/positions?selectionId=sel_123&status=SETTLED

Response:
{
"positions": [
{
"id": "pos_001",
"userId": "usr_456",
"side": "BACK",
"stake": 10000,
"odds": 2.50,
"liability": 15000,
"status": "SETTLED",
"settlementPnL": -15000,
"createdAt": "2026-01-20T14:30:00Z",
"settledAt": "2026-01-20T17:45:00Z"
}
],
"totalCount": 25
}

12. Risk Monitoring and Alerts

12.1 Real-Time Monitoring Dashboard

The admin dashboard displays live B-Book metrics:

MetricUpdate FrequencyAlert Threshold
Pool Utilization %Real-time (WebSocket)> 80% (warning), > 95% (critical)
Daily P&LReal-time< -500,000 (critical)
Side Imbalance RatioEvery minute> 3.0 (warning)
Per-Market ExposureReal-time> 80% of limit
Per-Selection ExposureReal-time> 80% of limit

12.2 Alert System

Alerts are triggered automatically and sent via configured channels (email, Slack, etc.):

interface BBookAlert {
id: string;
type: AlertType;
severity: 'info' | 'warning' | 'critical';
message: string;
data: Record<string, any>;
triggeredAt: Date;
acknowledgedAt?: Date;
acknowledgedBy?: string;
}

enum AlertType {
POOL_UTILIZATION_HIGH = 'pool_utilization_high',
DAILY_LOSS_LIMIT_APPROACHING = 'daily_loss_limit_approaching',
DAILY_LOSS_LIMIT_BREACHED = 'daily_loss_limit_breached',
SIDE_IMBALANCE = 'side_imbalance',
MARKET_EXPOSURE_HIGH = 'market_exposure_high',
SELECTION_EXPOSURE_HIGH = 'selection_exposure_high',
AUTO_HEDGE_TRIGGERED = 'auto_hedge_triggered',
EMERGENCY_SHUTDOWN = 'emergency_shutdown'
}

12.3 Alert Triggers

AlertTrigger ConditionAction
Pool Utilization Warningutilization > 80%Notify ops team
Pool Utilization Criticalutilization > 95%Suspend B-Book, notify
Daily Loss ApproachingdailyPnL < -80% of limitNotify, restrict new positions
Daily Loss BreacheddailyPnL < -100% of limitSuspend B-Book, notify
Side Imbalanceratio > thresholdRestrict heavy side
Market Concentrationsingle market > 50% of poolWarning to ops

12.4 Auto-Hedge Logic (Future Enhancement)

When pool utilization exceeds the auto-hedge threshold:

async function autoHedge(): Promise<void> {
const poolState = await getPoolState();
const config = await getBBookConfig();

if (poolState.utilizationPercent > config.autoHedgeThreshold * 100) {
// Find largest unbalanced selections
const riskySelections = await findRiskySelections({
minNetExposure: config.perSelectionLimit * 0.5,
limit: 10
});

for (const selection of riskySelections) {
// Calculate hedge amount
const hedgeAmount = selection.netExposure * 0.3; // Hedge 30%

// Place opposing bet on exchange
await placeHedgeBet({
selectionId: selection.selectionId,
side: selection.direction === 'short' ? 'back' : 'lay',
amount: hedgeAmount,
venue: 'betfair'
});

// Record hedge action
await recordHedgeAction({
selectionId: selection.selectionId,
amount: hedgeAmount,
reason: 'auto_hedge_threshold'
});
}
}
}

12.5 Emergency Shutdown

When critical thresholds are breached:

async function triggerEmergencyShutdown(reason: string): Promise<void> {
// 1. Disable B-Book immediately
await updateBBookConfig({ bbookEnabled: false });

// 2. Update pool status
await updatePoolState({ status: 'SUSPENDED' });

// 3. Create alert
await createAlert({
type: AlertType.EMERGENCY_SHUTDOWN,
severity: 'critical',
message: `B-Book emergency shutdown: ${reason}`,
data: {
poolState: await getPoolState(),
timestamp: new Date()
}
});

// 4. Notify all configured channels
await notifyAllChannels({
title: 'B-BOOK EMERGENCY SHUTDOWN',
message: reason,
urgency: 'immediate'
});

// Note: Existing positions remain until settled
// New orders route 100% to exchange
}

13. Error Handling and Edge Cases

13.1 Exchange Unavailability

When Betfair or Pinnacle is unavailable:

ScenarioB-Book EnabledB-Book Disabled
Exchange down, B-Book has capacityFill via B-Book (up to limits)Reject order
Exchange down, B-Book at limitReject orderReject order
Exchange slow (timeout)Fill B-Book portion, retry exchangeRetry or reject
async function handleExchangeUnavailable(
order: Order,
error: ExchangeError
): Promise<OrderResult> {
const config = await getBBookConfig();

if (!config.bbookEnabled) {
return { status: 'rejected', reason: 'exchange_unavailable' };
}

// Check if user is sharp
if (await isSharpUser(order.userId)) {
return { status: 'rejected', reason: 'exchange_unavailable_sharp_user' };
}

// Try to fill entirely via B-Book
const capacity = await calculateBBookCapacity(order);

if (capacity >= order.stake) {
// Can fulfill entirely via B-Book
return await executeBBookFill(order, order.stake);
} else if (capacity > 0) {
// Partial fill via B-Book
return await executeBBookFill(order, capacity);
} else {
return { status: 'rejected', reason: 'no_capacity' };
}
}

13.2 Race Conditions

Problem: Multiple concurrent orders could exceed limits.

Solution: Optimistic locking with Redis:

async function reserveBBookCapacity(
order: Order,
amount: number
): Promise<boolean> {
const lockKey = `bbook:lock:${order.selectionId}`;

// Acquire lock
const lock = await redis.set(lockKey, order.id, 'NX', 'PX', 5000);
if (!lock) {
// Another order is processing - retry or queue
return false;
}

try {
// Double-check capacity under lock
const capacity = await calculateBBookCapacity(order);
if (capacity < amount) {
return false;
}

// Reserve capacity atomically
await redis.hincrby(`bbook:selection:${order.selectionId}`, 'reserved', amount);
return true;
} finally {
await redis.del(lockKey);
}
}

13.3 Partial Fill Edge Cases

ScenarioHandling
B-Book fills 70%, exchange rejects 30%Return partial fill (70%)
B-Book fails, exchange succeedsReturn exchange fill only
Both failReject order, no state changes
B-Book fills, exchange timeoutReturn B-Book fill, log exchange issue

13.4 Settlement Edge Cases

Double Settlement Prevention:

async function settlePosition(positionId: string): Promise<void> {
const position = await db.bbookPosition.findUnique({
where: { id: positionId }
});

if (position.status !== 'OPEN') {
throw new Error(`Position ${positionId} already ${position.status}`);
}

// Use optimistic locking
const result = await db.bbookPosition.updateMany({
where: {
id: positionId,
status: 'OPEN' // Only if still OPEN
},
data: {
status: 'SETTLED',
settledAt: new Date()
}
});

if (result.count === 0) {
throw new Error('Position was modified by another process');
}
}

Missing Event Result:

If an event result is delayed or missing:

  1. Positions remain OPEN
  2. Exposure remains counted against limits
  3. Alert triggered after 24 hours
  4. Manual settlement option available to admins

13.5 Price Slippage

B-Book fills at the requested odds, not market odds. This differs from exchange behavior:

AspectB-BookExchange
PriceFixed at request timeMay slip based on market
Fill guaranteeGuaranteed if capacity existsDepends on liquidity
Better priceNeverSometimes (exchange finds better price)

13.6 User Balance Insufficient

Always check balance before routing decision:

async function processOrder(order: Order): Promise<OrderResult> {
// Step 1: Validate balance FIRST
const user = await getUser(order.userId);
const requiredBalance = order.stake; // For BACK
// For LAY: order.stake * (order.odds - 1)

if (user.balance < requiredBalance) {
return { status: 'rejected', reason: 'insufficient_balance' };
}

// Step 2: Reserve balance atomically
await reserveUserBalance(order.userId, requiredBalance);

try {
// Step 3: Route and fill
return await routeAndFill(order);
} catch (error) {
// Release reserved balance on failure
await releaseUserBalance(order.userId, requiredBalance);
throw error;
}
}

14. Currency Abstraction Layer

14.1 Design Philosophy

The B-Book is built on Hannibal's points system but designed to be currency-agnostic. This allows:

  1. Current operation on points (internal unit)
  2. Future support for real money (fiat currencies)
  3. Potential support for crypto currencies
  4. Easy conversion between units

14.2 Currency Provider Interface

interface CurrencyProvider {
// Currency identification
readonly code: string; // e.g., 'POINTS', 'INR', 'USDT'
readonly symbol: string; // e.g., '◉', '₹', '$'
readonly decimals: number; // e.g., 2 for fiat, 0 for points

// Balance operations
getBalance(userId: string): Promise<Decimal>;
reserveBalance(userId: string, amount: Decimal): Promise<boolean>;
releaseReserve(userId: string, amount: Decimal): Promise<void>;
debit(userId: string, amount: Decimal, reason: string): Promise<void>;
credit(userId: string, amount: Decimal, reason: string): Promise<void>;

// Pool operations
getPoolBalance(): Promise<Decimal>;
debitPool(amount: Decimal, reason: string): Promise<void>;
creditPool(amount: Decimal, reason: string): Promise<void>;

// Conversion (optional)
convertToBase?(amount: Decimal): Promise<Decimal>;
convertFromBase?(amount: Decimal): Promise<Decimal>;
}

14.3 Points Provider Implementation (Default)

class PointsCurrencyProvider implements CurrencyProvider {
readonly code = 'POINTS';
readonly symbol = '◉';
readonly decimals = 0;

async getBalance(userId: string): Promise<Decimal> {
const user = await db.user.findUnique({ where: { id: userId } });
return user.points;
}

async reserveBalance(userId: string, amount: Decimal): Promise<boolean> {
// Atomic decrement with check
const result = await db.user.updateMany({
where: {
id: userId,
points: { gte: amount }
},
data: {
points: { decrement: amount },
reservedPoints: { increment: amount }
}
});
return result.count > 0;
}

async debit(userId: string, amount: Decimal, reason: string): Promise<void> {
await db.$transaction([
db.user.update({
where: { id: userId },
data: { reservedPoints: { decrement: amount } }
}),
db.transaction.create({
data: {
userId,
type: 'DEBIT',
amount,
description: reason,
currency: 'POINTS'
}
})
]);
}

// ... other methods
}

14.4 Real Money Provider (Future)

class FiatCurrencyProvider implements CurrencyProvider {
constructor(private readonly currency: 'INR' | 'USD' | 'GBP') {}

readonly code = this.currency;
readonly symbol = { INR: '₹', USD: '$', GBP: '£' }[this.currency];
readonly decimals = 2;

async getBalance(userId: string): Promise<Decimal> {
const wallet = await db.userWallet.findUnique({
where: { userId_currency: { userId, currency: this.currency } }
});
return wallet?.balance ?? new Decimal(0);
}

// Similar implementations with appropriate decimal handling
// May integrate with payment gateway for real deposits/withdrawals
}

14.5 B-Book Currency Integration

The B-Book module accepts a currency provider at initialization:

class BBookModule {
constructor(
private readonly currencyProvider: CurrencyProvider,
private readonly config: BBookConfig
) {}

async executeFill(order: Order, amount: Decimal): Promise<FillResult> {
// Use provider for all balance operations
const reserved = await this.currencyProvider.reserveBalance(
order.userId,
amount
);

if (!reserved) {
throw new InsufficientBalanceError();
}

// ... rest of fill logic
}

async settlePosition(position: BBookPosition): Promise<void> {
if (position.settlementPnL > 0) {
await this.currencyProvider.credit(
position.userId,
position.settlementPnL,
`B-Book win: ${position.id}`
);
await this.currencyProvider.debitPool(
position.settlementPnL,
`B-Book payout: ${position.id}`
);
} else {
await this.currencyProvider.creditPool(
Math.abs(position.settlementPnL),
`B-Book collection: ${position.id}`
);
}
}
}

14.6 Multi-Currency Configuration

Future enhancement to support multiple currencies:

interface BBookMultiCurrencyConfig {
// Per-currency pool limits
poolLimits: {
POINTS: Decimal;
INR: Decimal;
USDT: Decimal;
};

// Base currency for aggregation/reporting
baseCurrency: string;

// Exchange rates (if needed for reporting)
exchangeRates: {
[from: string]: {
[to: string]: Decimal;
};
};
}

15. Performance Considerations

15.1 Latency Requirements

OperationTarget LatencyAcceptable Max
Sharp check< 5ms20ms
Capacity calculation< 10ms50ms
B-Book fill< 50ms200ms
Settlement (batch)< 1s per 100 positions5s

15.2 Throughput Requirements

MetricExpectedDesign Target
Orders per second100500
Concurrent markets50200
Active positions10,000100,000
Position settlements per minute5002,000

15.3 Caching Strategy

Redis as Primary Cache:

DataTTLInvalidation
Sharp listNo expiryOn add/remove
Pool state1 secondOn every update
Selection state1 secondOn position change
User exposure5 secondsOn position change
Config60 secondsOn admin update

Cache Warming:

async function warmBBookCache(): Promise<void> {
// Load sharp list
const sharpUsers = await db.sharpUser.findMany({ where: { isActive: true } });
await redis.sadd('bbook:sharp_list', ...sharpUsers.map(u => u.userId));

// Load pool state
const poolState = await db.bbookPoolState.findFirst();
await redis.hset('bbook:pool', flattenObject(poolState));

// Load active selection states
const selections = await db.bbookSelectionState.findMany({
where: { status: 'OPEN' }
});
for (const sel of selections) {
await redis.hset(`bbook:selection:${sel.selectionId}`, flattenObject(sel));
}

console.log(`B-Book cache warmed: ${sharpUsers.length} sharp users, ${selections.length} selections`);
}

15.4 Database Optimization

Partitioning Strategy:

TablePartition ByRationale
bbook_positionscreatedAt (monthly)High volume, mostly historical
bbook_pnl_recordssettledAt (monthly)Reporting access pattern

Connection Pooling:

// Recommended PgBouncer configuration
{
pool_mode: 'transaction',
max_client_conn: 1000,
default_pool_size: 25,
reserve_pool_size: 5
}

15.5 Batching and Bulk Operations

Bulk Settlement:

async function bulkSettleSelections(
selectionIds: string[],
results: Map<string, boolean>
): Promise<BulkSettlementResult> {
const BATCH_SIZE = 100;
const batches = chunk(selectionIds, BATCH_SIZE);

const allResults: SettlementResult[] = [];

for (const batch of batches) {
const batchResults = await Promise.all(
batch.map(selId => settleBBookPositions(selId, results.get(selId)!))
);
allResults.push(...batchResults);
}

return {
totalSelections: selectionIds.length,
totalPositions: allResults.reduce((sum, r) => sum + r.positionsSettled, 0),
totalPlatformPnL: allResults.reduce((sum, r) => sum + r.platformPnL, 0)
};
}

15.6 Horizontal Scaling

The B-Book module can be scaled horizontally with considerations:

ComponentScaling Approach
Filter EngineStateless, scale with load balancer
Position StatePartitioned by selection/market
Settlement WorkersQueue-based, multiple consumers
Cache (Redis)Redis Cluster for high availability

Important: Use distributed locking (Redis/Redlock) for critical sections when scaled.


16. Testing Strategy

16.1 Unit Tests

Filter Engine Tests:

describe('FilterEngine', () => {
describe('calculateBBookCapacity', () => {
it('should return 0 when B-Book is disabled', async () => {
await setBBookEnabled(false);
const capacity = await calculateBBookCapacity(mockOrder);
expect(capacity).toBe(0);
});

it('should return 0 for sharp users', async () => {
await addToSharpList(mockUser.id);
const capacity = await calculateBBookCapacity(mockOrder);
expect(capacity).toBe(0);
});

it('should respect global pool limit', async () => {
await setGlobalExposure(9_000_000);
await setGlobalLimit(10_000_000);
const capacity = await calculateBBookCapacity({ stake: 2_000_000 });
expect(capacity).toBe(1_000_000);
});

it('should respect per-user limit', async () => {
await setUserExposure(mockUser.id, 40_000);
await setPerUserLimit(50_000);
const capacity = await calculateBBookCapacity(mockOrder);
expect(capacity).toBeLessThanOrEqual(10_000);
});

it('should allow balancing orders without limit', async () => {
await createImbalancedPosition('BACK', 100_000);
const layOrder = { ...mockOrder, betType: 'LAY' };
const capacity = await calculateBBookCapacity(layOrder);
expect(capacity).toBeGreaterThan(0); // Balancing is always allowed
});
});
});

Settlement Tests:

describe('BBookSettlement', () => {
it('should credit user on winning BACK bet', async () => {
const position = await createPosition({ side: 'BACK', stake: 1000, odds: 2.5 });
await settleBBookPositions(position.selectionId, true); // Selection won

const user = await getUser(position.userId);
expect(user.balance).toBe(initialBalance + 1500); // Won 1000 * (2.5-1)
});

it('should credit platform on losing BACK bet', async () => {
const position = await createPosition({ side: 'BACK', stake: 1000, odds: 2.5 });
const poolBefore = await getPoolState();

await settleBBookPositions(position.selectionId, false); // Selection lost

const poolAfter = await getPoolState();
expect(poolAfter.realizedPnL).toBe(poolBefore.realizedPnL + 1000);
});

it('should prevent double settlement', async () => {
const position = await createPosition({ side: 'BACK', stake: 1000 });
await settleBBookPositions(position.selectionId, true);

await expect(settleBBookPositions(position.selectionId, true))
.rejects.toThrow('already settled');
});
});

16.2 Integration Tests

describe('BBook Integration', () => {
describe('Full Order Flow', () => {
it('should split order between B-Book and exchange', async () => {
await setBBookEnabled(true);
await setMaxBBookPercentage(0.7);

const result = await placeOrder({
userId: recreationalUser.id,
stake: 10_000,
selectionId: 'sel_123',
betType: 'BACK',
odds: 2.0
});

expect(result.bbookFill).toBeLessThanOrEqual(7_000);
expect(result.exchangeFill).toBeGreaterThanOrEqual(3_000);
expect(result.bbookFill + result.exchangeFill).toBe(10_000);
});

it('should route sharp user entirely to exchange', async () => {
await addToSharpList(sharpUser.id);

const result = await placeOrder({
userId: sharpUser.id,
stake: 10_000,
selectionId: 'sel_123',
betType: 'BACK',
odds: 2.0
});

expect(result.bbookFill).toBe(0);
expect(result.exchangeFill).toBe(10_000);
});
});

describe('Exchange Failover', () => {
it('should fill via B-Book when exchange is unavailable', async () => {
mockExchange.setUnavailable(true);

const result = await placeOrder({
userId: recreationalUser.id,
stake: 5_000,
selectionId: 'sel_123',
betType: 'BACK',
odds: 2.0
});

expect(result.bbookFill).toBe(5_000);
expect(result.status).toBe('filled');
});
});
});

16.3 Load Tests

describe('BBook Load Tests', () => {
it('should handle 500 concurrent orders', async () => {
const orders = Array.from({ length: 500 }, (_, i) => ({
userId: `user_${i % 100}`,
stake: 1000,
selectionId: `sel_${i % 10}`,
betType: i % 2 === 0 ? 'BACK' : 'LAY',
odds: 2.0
}));

const startTime = Date.now();
const results = await Promise.all(orders.map(o => placeOrder(o)));
const duration = Date.now() - startTime;

expect(duration).toBeLessThan(10_000); // < 10s for 500 orders
expect(results.filter(r => r.status === 'filled').length).toBe(500);
});

it('should maintain consistency under concurrent settlement', async () => {
// Create 1000 positions across 10 selections
await createManyPositions(1000, 10);

const poolBefore = await getPoolState();

// Settle all 10 selections concurrently
await Promise.all(
selections.map(sel => settleBBookPositions(sel.id, Math.random() > 0.5))
);

const poolAfter = await getPoolState();

// Verify pool state is consistent
expect(poolAfter.totalNetExposure).toBe(0); // All settled
// Verify no positions left open
const openPositions = await db.bbookPosition.count({ where: { status: 'OPEN' } });
expect(openPositions).toBe(0);
});
});

16.4 Critical Test Scenarios

ScenarioTest Approach
Race condition: two orders exceed limitConcurrent test with assertions on final state
Partial fill: B-Book capacity mid-orderMock capacity depletion during order
Settlement during order placementConcurrent settlement + order tests
Redis cache missClear cache, verify DB fallback
Emergency shutdown during settlementTrigger shutdown, verify positions remain OPEN

17. Deployment and Operations

17.1 Deployment Modes

Option A: Monolith Integration

The B-Book module runs within the main Hannibal backend process.

┌─────────────────────────────────────────┐
│ Hannibal Backend │
│ ┌─────────┐ ┌─────────┐ ┌───────────┐ │
│ │ Order │ │ B-Book │ │ Exchange │ │
│ │ Service │ │ Module │ │ Connector │ │
│ └─────────┘ └─────────┘ └───────────┘ │
└─────────────────────────────────────────┘

Pros: Simple deployment, no network overhead Cons: B-Book failure can affect entire backend

Option B: Microservice (Recommended for Scale)

The B-Book runs as a separate service.

┌─────────────────┐     ┌─────────────────┐
│ Hannibal Backend│ │ B-Book Service │
│ │◄───►│ │
│ Order Service │gRPC │ Filter Engine │
│ │ │ Position Mgr │
└─────────────────┘ └─────────────────┘
│ │
└──────────┬───────────┘

┌────────▼────────┐
│ PostgreSQL │
│ Redis │
└─────────────────┘

Pros: Failure isolation, independent scaling Cons: Network latency, operational complexity

17.2 Feature Flags

Use feature flags for gradual rollout:

const BBOOK_FEATURE_FLAGS = {
// Master enable
'bbook.enabled': false,

// Gradual rollout
'bbook.enabled_for_users': [], // Specific user IDs
'bbook.enabled_percentage': 0, // % of users

// Feature-specific flags
'bbook.auto_hedge.enabled': false,
'bbook.side_restriction.enabled': true,

// Emergency
'bbook.emergency_disable': false
};

17.3 Rollback Procedure

If B-Book issues detected:

# 1. Disable B-Book (immediate, no new B-Book fills)
curl -X POST https://api.hannibal.com/admin/bbook/disable

# 2. Verify new orders route to exchange
# Monitor logs for routing decisions

# 3. Existing positions remain until settled
# No manual intervention needed

# 4. Investigate and fix
# Once fixed, re-enable via admin panel

17.4 Monitoring Dashboards

Key Metrics to Display:

PanelMetrics
Pool HealthUtilization %, Capacity remaining, Status
P&LDaily P&L, Realized P&L, Unrealized P&L
VolumeOrders routed to B-Book, Fill rate, Rejection rate
RiskLargest exposures, Side imbalance, Near-limit selections
PerformanceFill latency (p50, p95, p99), Settlement latency

17.5 Alerting Configuration

# Example AlertManager configuration
groups:
- name: bbook_alerts
rules:
- alert: BBookPoolUtilizationHigh
expr: bbook_pool_utilization > 0.80
for: 5m
labels:
severity: warning
annotations:
summary: "B-Book pool utilization above 80%"

- alert: BBookPoolCritical
expr: bbook_pool_utilization > 0.95
for: 1m
labels:
severity: critical
annotations:
summary: "B-Book pool near capacity - auto-suspend may trigger"

- alert: BBookDailyLossHigh
expr: bbook_daily_pnl < -400000
for: 1m
labels:
severity: warning
annotations:
summary: "B-Book daily loss approaching limit"

18. Migration Path

18.1 Prerequisites

Before enabling B-Book on an existing Hannibal installation:

  1. ✅ Database migrations applied (new tables created)
  2. ✅ Redis deployed and accessible
  3. ✅ Admin UI updated with B-Book controls
  4. ✅ Monitoring dashboards configured
  5. ✅ Alerting configured
  6. ✅ Staff trained on B-Book operations

18.2 Database Migration

-- Migration: Add B-Book tables
-- Run in a maintenance window

-- 1. Create new tables
CREATE TABLE bbook_config (...);
CREATE TABLE bbook_positions (...);
CREATE TABLE bbook_selection_state (...);
CREATE TABLE bbook_market_state (...);
CREATE TABLE bbook_user_state (...);
CREATE TABLE bbook_pool_state (...);
CREATE TABLE sharp_users (...);
CREATE TABLE bbook_pnl_records (...);

-- 2. Add columns to orders table
ALTER TABLE orders ADD COLUMN bbook_fill DECIMAL;
ALTER TABLE orders ADD COLUMN exchange_fill DECIMAL;
ALTER TABLE orders ADD COLUMN routing_venue VARCHAR(20);
ALTER TABLE orders ADD COLUMN is_sharp_at_order_time BOOLEAN;

-- 3. Create indexes
CREATE INDEX idx_bbook_positions_selection ON bbook_positions(selection_id, status);
-- ... other indexes

-- 4. Initialize singleton records
INSERT INTO bbook_config (id, bbook_enabled, ...) VALUES (uuid_generate_v4(), false, ...);
INSERT INTO bbook_pool_state (id, ...) VALUES (uuid_generate_v4(), ...);

18.3 Gradual Enablement

Phase 1: Shadow Mode (1 week)

  • B-Book logic runs but doesn't affect routing
  • Log what would have happened
  • Verify capacity calculations are accurate

Phase 2: Limited Rollout (2 weeks)

  • Enable for 5% of recreational users
  • Low pool limits (1,000,000 points)
  • High monitoring, quick disable if issues

Phase 3: Expanded Rollout (2 weeks)

  • Enable for 25% of recreational users
  • Increase pool limits (5,000,000 points)
  • Fine-tune sharp detection

Phase 4: Full Enablement

  • Enable for all recreational users
  • Full pool limits (10,000,000 points)
  • Ongoing monitoring and optimization

18.4 Rollback Plan

If issues arise during migration:

IssueAction
Database migration failsRollback migration, fix, retry
B-Book fills incorrectlyDisable B-Book, manual review
Performance degradationReduce B-Book percentage, investigate
Exposure calculation wrongDisable B-Book, rebuild state from positions

19. Security Considerations

19.1 Access Control

Admin APIs require elevated permissions:

API CategoryRequired Role
View B-Book configadmin, ops
Update B-Book configadmin
Enable/Disable B-Bookadmin
View pool stateadmin, ops
Manage sharp listadmin, risk
View positionsadmin, ops, support
Manual settlementadmin
// Example middleware
function requireBBookAdminAccess(req, res, next) {
const user = req.user;
const action = req.path;

const requiredRoles = getBBookPermissions(action);
if (!user.roles.some(r => requiredRoles.includes(r))) {
return res.status(403).json({ error: 'Insufficient permissions' });
}

next();
}

19.2 Audit Logging

All B-Book administrative actions are logged:

interface BBookAuditLog {
id: string;
action: BBookAuditAction;
performedBy: string;
timestamp: Date;
details: Record<string, any>;
ipAddress: string;
userAgent: string;
}

enum BBookAuditAction {
CONFIG_UPDATED = 'config_updated',
BBOOK_ENABLED = 'bbook_enabled',
BBOOK_DISABLED = 'bbook_disabled',
SHARP_USER_ADDED = 'sharp_user_added',
SHARP_USER_REMOVED = 'sharp_user_removed',
MANUAL_SETTLEMENT = 'manual_settlement',
EMERGENCY_SHUTDOWN = 'emergency_shutdown',
LIMIT_OVERRIDDEN = 'limit_overridden'
}

19.3 Fraud Prevention

Detecting Manipulation Attempts:

RiskDetectionMitigation
Coordinated bettingMultiple users, same selection, short timeAggregate limit per selection
Account abuseUser creates multiple accountsKYC verification, device fingerprinting
Insider informationUnusual bet patterns before eventsReal-time monitoring, sharp classification
ArbitrageSimultaneous opposite betsPer-user netting (already handled)

19.4 Data Protection

Sensitive Data Handling:

DataClassificationProtection
User balancesConfidentialEncrypted at rest, access logged
Betting historyConfidentialAccess restricted, audit logged
Sharp listInternalAccess restricted to risk team
Pool stateInternalNo PII, aggregate data only
P&L recordsConfidentialAccess restricted to finance team

20. Glossary

TermDefinition
A-BookOrders passed through to external exchanges (Betfair, Pinnacle)
B-BookPlatform takes the opposite side of user bets using own capital
BACK betBetting that an outcome will happen
LAY betBetting against an outcome (acting as bookmaker)
LiabilityMaximum amount platform could lose on a position
Net ExposureAbsolute difference between BACK and LAY liability
Sharp userProfessional bettor with positive expected value
Recreational userCasual bettor, negative expected value overall
Filter EngineComponent that decides order routing
NettingOffsetting opposite positions to reduce exposure
HedgePlacing opposite bet on exchange to reduce B-Book exposure
CLVClosing Line Value - how bet price compares to final price
NGRNet Gaming Revenue - stakes minus payouts
PoolCapital reserved for B-Book operations
UtilizationCurrent exposure as percentage of pool limit

21. Open Questions

The following decisions require stakeholder input before final implementation:

21.1 Business Questions

#QuestionOptionsRecommendation
1Should agents receive any share of B-Book profits?a) No share b) Small share c) Same as exchangeNo share - B-Book is platform risk
2What should the initial pool limit be?a) 5M b) 10M c) 20M pointsStart conservative at 5M
3Should users be informed their bet is B-Booked?a) Yes, transparent b) No, seamlessNo - seamless UX, internal routing
4How aggressive should sharp detection be?a) Conservative (few false positives) b) AggressiveConservative initially
5Should B-Book be market-type specific?a) All markets b) Only pre-match c) Exclude in-playStart with pre-match only

21.2 Technical Questions

#QuestionOptionsRecommendation
1Deployment model?a) Monolith b) MicroserviceMonolith for V1, extract later
2State rebuild frequency?a) On-demand b) Scheduled c) NeverScheduled nightly + on-demand
3Redis failure handling?a) Degrade to DB b) Reject ordersDegrade to DB (slower but safe)
4Position archival policy?a) 90 days b) 1 year c) Forever90 days hot, archive to cold storage
5Auto-hedge in V1?a) Yes b) No (manual only)No - manual hedging for V1

21.3 Operational Questions

#QuestionOptionsRecommendation
1Who can add/remove sharp users?a) Admin only b) Risk team c) AutomatedRisk team with admin override
2B-Book on/off approval process?a) Single admin b) Two-person ruleTwo-person rule for safety
3Alert escalation path?Define on-call rotationDefine before launch
4Reconciliation frequency?a) Daily b) Weekly c) MonthlyDaily automated, weekly manual review

22. Appendix

A. Sample Configuration

{
"bbookEnabled": true,
"globalPoolLimit": 10000000,
"perMarketLimit": 2000000,
"perSelectionLimit": 500000,
"perUserLimit": 50000,
"maxBbookPercentage": 0.70,
"minExchangePercentage": 0.00,
"preferBbook": true,
"sideImbalanceThreshold": 3.0,
"autoHedgeThreshold": 0.80,
"emergencyShutdownThreshold": 0.95,
"dailyLossLimit": 500000
}

B. Example Order Flow

Scenario: Recreational user places 100,000 pts BACK on Mumbai @ 2.50

1. Order received: { stake: 100000, odds: 2.50, betType: 'BACK', selectionId: 'mumbai_win' }

2. B-Book check:
- B-Book enabled: true
- User sharp: false

3. Capacity calculation:
- Global remaining: 8,000,000 pts
- Market remaining: 1,500,000 pts
- Selection remaining: 400,000 pts
- User remaining: 30,000 pts ← Most restrictive!
- Percentage cap: 70,000 pts

4. Routing decision:
- B-Book fill: 30,000 pts (user limit)
- Exchange fill: 70,000 pts

5. B-Book execution:
- Liability: 30,000 × (2.50 - 1) = 45,000 pts
- Update selection state: backLiability += 45,000
- Update user state: totalExposure += 45,000
- Create position record

6. Exchange execution:
- Place 70,000 BACK @ 2.50 on Betfair
- Record in agent accounting

7. Response to user:
{
orderId: 'ord_123',
status: 'filled',
totalStake: 100000,
fills: [
{ venue: 'bbook', amount: 30000, odds: 2.50 },
{ venue: 'betfair', amount: 70000, odds: 2.50 }
]
}

C. Settlement Example

Scenario: Mumbai wins

1. Event result received: { selectionId: 'mumbai_win', won: true }

2. Get B-Book positions:
- Position 1: BACK 30,000 @ 2.50 (User A) → Liability: 45,000
- Position 2: BACK 20,000 @ 2.40 (User B) → Liability: 28,000
- Position 3: LAY 25,000 @ 2.50 (User C) → Liability: 25,000

3. Settlement calculations:
| Position | Side | Stake | Outcome | User P&L | Platform P&L |
|----------|------|-------|---------|----------|--------------|
| 1 | BACK | 30,000 | WIN | +45,000 | -45,000 |
| 2 | BACK | 20,000 | WIN | +28,000 | -28,000 |
| 3 | LAY | 25,000 | LOSE | -25,000 | +25,000 |
| **Total** | | | | **+48,000** | **-48,000** |

4. Execute settlements:
- Credit User A: 45,000 pts
- Credit User B: 28,000 pts
- User C already paid stake (reserved), no action
- Pool P&L: -48,000 pts

5. Update state:
- Selection state: SETTLED
- All positions: SETTLED
- Pool realizedPnL += -48,000
- Pool totalNetExposure -= settled exposure

Document Version: 1.0 Last Updated: January 2026 Author: Hannibal Engineering Team