Skip to main content

Bifrost Phase 5 — Fix Resolution Plan

Summary

6 issues reported by Bifrost team (Michael) + 1 critical blocker found during investigation. All issues traced to root causes with specific file:line references.

Worktree

  • Branch: fix/bifrost-phase5 from dev (ee856d0b)
  • Path: .claude/worktrees/bifrost-phase5/

Fix 1: Settlement Version INT4 Overflow (CRITICAL BLOCKER)

Problem: settlementVersion is Int (INT4, max ~2.1B) but Bifrost sends version numbers like 1772641624109. ALL settlement processing fails with "Unable to fit integer value into INT4".

Evidence: Server logs show repeated errors:

FINANCIAL_INTEGRITY: error processing forsyt.bets.outcomes.queue message (attempt 1/3)
Error: Unable to fit integer value '1772641624109' into an INT4

Fix:

  • File: backend/prisma/schema.prisma:182
  • Change: settlementVersion Int @default(0)settlementVersion BigInt @default(0)
  • Run migration: prisma migrate dev --name fix-settlement-version-bigint
  • File: backend/src/exchanges/adapters/bifrost/BifrostBetConsumer.ts:424
  • Update outcome.version ?? 0 usages to handle BigInt properly

Cross-system impact: Schema change requires migration on bhdev DB.


Fix 2: Batch Order Lay Liability — Wrong Formula for Bifrost (CRITICAL)

Problem: placeBatchOrders() uses stake × (odds - 1) for ALL lay bets without checking if it's a Bifrost market. For Indian odds 100, this calculates 9900 instead of 100.

Evidence: Error screenshots: "Insufficient balance for batch: need 9900.00, have 3995.00" and "need 19800.00, have 3995.00"

Root cause: orderService.ts:966-969

// CURRENT (BROKEN):
totalRequired = totalRequired.plus(
new Decimal(o.stake).times(new Decimal(o.odds).minus(1))
);

// FIX:
const isBifrost = isBifrostBettableMarket(String(o.marketId)) || o.isBookmaker === true;
totalRequired = totalRequired.plus(
isBifrost
? new Decimal(o.stake).times(new Decimal(o.odds)).div(100)
: new Decimal(o.stake).times(new Decimal(o.odds).minus(1))
);

Fix 3: Settlement Lay Liability — Wrong Formula for Bifrost (CRITICAL)

Problem: settleOrder() uses decimal formula for lay bet loss/amountDeducted without checking Bifrost.

Root cause: settlement.ts:465,476

// Line 465 - lay loss calculation:
profitLossD = stakeD.times(oddsD.minus(1)).negated(); // WRONG for Bifrost

// Line 476 - amount deducted at placement:
const amountDeducted = order.betType === 'lay' ? stake * (odds - 1) : stake; // WRONG for Bifrost

Fix: Check if order.bookmaker === 'bifrost' and use Indian formula:

  • Lay loss: stakeD.times(oddsD).div(100).negated()
  • Amount deducted: stake * odds / 100

Fix 4: Frontend NO/Lay Potential Payout Display

Problem: Lay bet shows Potential Payout = stake (just profit), while back shows total return. Inconsistent.

Root cause: BetSlipFooter.tsx:39-47

// CURRENT (lay branch):
return sum + Math.round(item.stake * 100); // Only profit

// FIX (lay branch):
const liability = item.isFancy
? item.stake * item.odds / 100
: item.stake * (item.odds - 1);
return sum + Math.round((liability + item.stake) * 100); // liability refund + profit

Also fix betSlipStore.ts:269-282 getTotalPotentialReturn for consistency.


Fix 5: Max Button Exceeds User Balance (MINOR)

Problem: Max button sets stake to item.maxMarket (total market limit) without capping to user's balance.

Root cause: BetSlipCard.tsx:75-78

Fix: Cap maxMarket to user's available balance:

const handleMaxStake = useCallback(() => {
const maxAvailable = Math.min(
item.maxMarket ?? Infinity,
userBalance ?? Infinity
);
if (maxAvailable > 0 && maxAvailable < Infinity) {
updateStake(item.id, Math.floor(maxAvailable));
}
}, [item.id, item.maxMarket, userBalance, updateStake]);

Fix 6: Better Price Not Passed to Customer (MEDIUM)

Problem: WebSocket odds updates refresh maxStake, priceIndex, line in betslip but NOT the actual odds/price.

Root cause: betSlipStore.ts:230+syncBetSlipWithLiveData skips price updates.

Fix: When live price improves (better for the user), auto-update the betslip:

  • Back bet: if new price > current price → update (user gets better odds)
  • Lay bet: if new price < current price → update (user gets better odds)
  • If price worsens → keep stale price (user already locked in, must re-click to accept worse)

Fix 7: Green/Red Highlighting Explanation (NO CODE CHANGE)

Current behavior: Green = price increased since last update, Red = price decreased. For Match Odds: green = drifting (better for back), red = shortening (worse for back). For Fancy: the same logic applies but may appear counterintuitive since Indian odds direction differs.

Action: Communicate to Bifrost team what the colors mean. No code change needed unless they request different behavior.


Commission Clarification (Issue 1 — NO CODE CHANGE)

Finding: We do NOT apply margin on Fancy/Session markets (skipMargin = market.oddsType === 'INDIAN'). Bifrost's prices pass through unmodified.

However: We charge platform commission (currently 5%) on NET MARKET WINNINGS after settlement. This is the Betfair commission model — charged per-market when all orders in a market are settled, only on net profit. This is separate from the margin Bifrost builds into their prices.

Action: Communicate this clearly to Bifrost team.


Implementation Order

  1. Fix 1 (settlementVersion BigInt) — unblocks ALL settlements
  2. Fix 2 (batch liability) — unblocks NO/lay bet placement
  3. Fix 3 (settlement liability) — ensures correct lay settlement
  4. Fix 4 (payout display) — fixes UI inconsistency
  5. Fix 5 (max button) — minor UX fix
  6. Fix 6 (better price) — medium complexity, lower priority

Testing

  • Typecheck: cd backend && npx tsc --noEmit + cd frontend && npx tsc --noEmit
  • DB migration on bhdev after deploy
  • Manual test: place YES and NO bets on Fancy market, verify payout display and balance deduction
  • Verify settlement processes without INT4 overflow