Skip to main content

Bifrost — PAL Integration Plan (v3, Corrected)

Updated 2026-02-17. Corrected from v2 based on product requirements and codebase research. v2 had wrong assumptions about provider toggle, currency, market filtering, and priority.

Scope

Integrate Bifrost's sportsbook (bookmaker) markets for cricket into Hannibal's PAL. Bifrost also passes through Betfair exchange markets — we skip those for now (simple flag to enable later).

Product requirements (corrected):

  • Show Match Odds from Betfair (exchange) — labelled "(exchange)" in UI
  • Do NOT show "Match Odds Including Tie" from Betfair
  • Show ALL sportsbook markets from Bifrost — including Match Odds sportsbook
  • Bifrost sportsbook has back AND lay (confirmed)
  • No provider toggle — both Betfair exchange + Bifrost sportsbook coexist on same fixture page
  • Market ordering: Betfair Match Odds (exchange) first → Bifrost sportsbook markets follow
  • Currency: HKD (1 point = 8 HKD, configurable via admin panel)

How Bifrost Fits in Hannibal

Cricket fixture page (sportId 27 in Hannibal, sportId "2" in Bifrost):
├── Betfair-ex (existing, unchanged)
│ └── Match Odds (exchange) ← labelled "(exchange)" in UI
│ NOTE: "Match Odds Including Tie" is FILTERED OUT

└── Bifrost-sportsbook (NEW)
└── ALL sportsbook markets (14.* prefix):
├── Match Odds (sportsbook — back+lay, coexists with Betfair)
├── 1st Innings Runs Lines (e.g., "1st Innings 20 Overs Line")
├── Fancy Markets (session runs, player runs, etc.)
├── Over/Under markets
├── Player Props
└── Other cricket-specific markets

Both providers coexist on the same fixture. No toggle. Betfair Match Odds exchange market appears first, then all Bifrost sportsbook markets in the existing category structure (Popular, Innings, Overs, Wickets, Players, Other).

Event matching: Bifrost events include externalIds with {provider: "Betfair", eventId: "..."}. We use this to link Bifrost sportsbook markets to existing Betfair fixtures. Same fixture page, multiple providers.

Critical Discovery: Two Market Types in Feed

Actual RabbitMQ data reveals two distinct market ID formats:

FormatExampleTypeCharacteristics
1.xxxxxxx1.254090981Exchange (Betfair pass-through)back/lay with size (liquidity depth)
14.xxxxxxx_N14.2350505_2Sportsbook (bookmaker)back/lay with maxStake per currency + maxStakeCurrencies map

Decision: Filter by market ID prefix. 14.* = sportsbook (our target). 1.* = exchange (skip for now — Betfair handles this natively).

Future toggle: To enable Bifrost exchange markets later, remove the 14.* prefix filter. Architecture doesn't change — just mapper logic and capability flags per market type.

Comparison Table

AspectBetfair (exchange)Pinnacle (bookmaker)Bifrost (sportsbook)
Data sourceREST + WebSocketREST pollingRabbitMQ push (protobuf) → local cache
Bet placementREST (sync result)REST 2-step (line → place)REST 1-step (returns PENDING)
Bet status syncStreaming + pollingPolling job (15s)Queue push (instant)
SettlementPolling job (60s)Polling job (60s)Queue push (instant)
Lay bettingYesNoYes (sportsbook has back+lay)
CancellationYesNoExchange markets only (not sportsbook)
SportsAllSoccer, Basketball, TennisCricket
Stake limitsSize-based (liquidity)Fixed minimumsPer-currency maxStake per price level
Odds formatDecimalDecimalDecimal (confirmed from queue data)
CurrencyGBP (via BetfairClient)USDHKD

Architecture

BifrostAdapter (IExchangeAdapter — same pattern as BetfairAdapter/PinnacleAdapter)
├─ BifrostCache ← in-memory, same pattern as BetfairCache
│ populated by 4 data queues, version-deduped
├─ BifrostQueueManager ← RabbitMQ connection, protobuf decode, 6 consumers
├─ BifrostClient ← REST POST /api/v1/bets/place + /cancel
└─ BifrostMapper ← Bifrost types → canonical (sportsbook: back+lay)

Capabilities

layBetting: true             ← sportsbook has back+lay
inPlay: true
streaming: false ← queue ≠ streaming (Betfair WebSocket pattern)
queueing: true ← NEW FIELD: data arrives via RabbitMQ push
cashOut: false
partialMatching: false ← sportsbook: accepted or rejected (all-or-nothing)
cancellation: false ← sportsbook only (cancel exists for exchange markets)
supportedSports: [27] ← cricket only
priority: 100 ← ESSENTIAL, not a fallback — same as Betfair

Caching Architecture (Same Pattern as Betfair)

This is NOT new architecture. Betfair already uses local in-memory caching:

  • BetfairCache: Event Types (1h TTL), Market Catalogue (5m TTL), Market Book (60s TTL + streaming override)
  • BifrostCache: Same pattern. Categories, Events, MarketCatalogues, MarketBooks — all in-memory maps with version dedup

Key difference: Betfair cache is populated by REST API calls + WebSocket streaming. Bifrost cache is populated by RabbitMQ queue consumers. Both serve the same IOddsProvider interface.

Betfair flow:  REST/WebSocket → BetfairCache → IOddsProvider → ExchangeCoordinator
Bifrost flow: RabbitMQ Queue → BifrostCache → IOddsProvider → ExchangeCoordinator

Latency consideration: RabbitMQ push is inherently low-latency — data arrives as soon as Bifrost publishes. No polling delay. Cache updates trigger redis.publish('odds:updated') via the same pattern Betfair uses, so frontend receives real-time invalidation signals via Socket.IO.

Data Flow

Market Data: Queue → Cache → PAL

RabbitMQ                              BifrostCache                      PAL
─────────── ──────────── ───
forsyt.category.queue → categories map → competitionName lookup
(88 msgs, protobuf) key: categoryId
Example: {id:"1.4_AUS", value: "Australia"
type:"COUNTRY",
sportId:"2"}

forsyt.event.queue → events map → getFixtures()
(201 msgs, protobuf) key: eventId
Example: {eventId:"35272381", competitors: [{fullName,shortName}]
name:"Pheonix v Kings", externalIds: [{provider:"Betfair", eventId:"..."}]
startTime:"2026-02-16T17:00",
sportId:"2",
competitors:[...],
externalIds:[...]}

forsyt.market-catalogue.queue → marketCatalogues map → getMarkets()
(10000+ msgs, protobuf) key: marketId
Example: {id:"14.2350505_2", FILTER: only 14.* markets for sportsbook
eventId:"35262799",
type:"MATCH_ODDS",
name:"Match Odds",
runners:[{id, name, sortPriority}],
tradingMarketType:"...",
variables:{MARKET_TYPE:"..."}}

forsyt.market-book.queue → marketBooks map → getOdds(), getBetslip()
(10000+ msgs, protobuf) key: marketId
Sportsbook: {marketId:"14.2350505_2", FILTER: only 14.* markets
runners:[{id, priceLadder:{
availableToBack:[{price,maxStake,maxStakeCurrencies}],
availableToLay:[{price,maxStake,maxStakeCurrencies}]
}}],
maxMarketCurrencies:{...},
variables:{MARKET_TYPE:"..."}}

Version dedup: cache only accepts msg.version > lastVersion[entityId].

Event Matching: Bifrost ↔ Betfair (CRITICAL)

Bifrost events include externalIds with cross-provider mappings:

Event {
eventId: "35272381",
externalIds: [
{ provider: "Betfair", eventId: "32456789" },
{ provider: "Sportradar", eventId: "sr:match:12345" }
]
}

This is CRITICAL for coexistence. We map Bifrost events to existing Betfair fixtures using externalIds.Betfair.eventId. This enables:

  1. Same fixture page shows Betfair Match Odds (exchange) AND Bifrost sportsbook markets
  2. Canonical fixture ID remains the Betfair-originated ID — Bifrost markets attach to it
  3. Frontend doesn't know/care which provider supplied which market

Implementation: When BifrostCache receives an event, look up externalIds.Betfair.eventId. If a CanonicalFixture already exists for that Betfair eventId, attach Bifrost sportsbook markets to it. If no Betfair fixture exists (Bifrost-only event), create a new canonical fixture from Bifrost data.

Currency Conversion: Points → HKD → REST

We deal in HKD with Bifrost. 1 point = 8 HKD (configurable via admin panel).

Current system: pointsToUsd() / usdToPoints() in orderService, driven by platformSettings.point_value_usd.

For Bifrost: We need pointsToHkd() / hkdToPoints(). The conversion rate (1 point = 8 HKD) is stored in the Provider model's exchangeRate field (already exists in Prisma schema) or via a new point_value_hkd platform setting. This must be admin-configurable, NOT hardcoded.

Player bets 100 points at 2.5 odds (back) on sportsbook market 14.2350505_2

├─ orderService.placeOrder()
│ ├─ Deduct 100 points from Player.balancePoints
│ ├─ pointsToHkd(100) = HKD 800 (100 × 8)
│ └─ Route to BifrostAdapter via ExchangeCoordinator

├─ BifrostAdapter.placeOrder()
│ ├─ Validate: maxStakeCurrencies.HKD >= 800 at requested price
│ ├─ Apply PT: memberSize = 800, size = 800 × (1 - PT/100)
│ │ (PT=0 currently, so size = memberSize = 800)
│ ├─ POST /api/v1/bets/place
│ │ { sportId:"2", marketId:"14.2350505_2", runnerId:12345,
│ │ size:800, memberSize:800, odds:2.5, side:"BACK",
│ │ memberCode:"forsyt", ipAddress:"108.61.221.157",
│ │ requestId:"uuid-xxx", currency:"HKD" }
│ └─ REST returns { id:67890, status:"PENDING", version:1708099200 }

└─ Order saved: status='submitted', providerOrderId='67890'

Stake display: When showing maxStake limits from Bifrost, use maxStakeCurrencies.HKD and convert to points via hkdToPoints() for the user-facing display.

Bet Status: Queue → DB → WebSocket → Frontend

forsyt.bets.snapshot.queue → BetSnapshot (protobuf)
{ requestId:"uuid-xxx", betId:67890, marketId:"14.2350505_2",
runnerId:12345, size:800, odds:2.5, side:"BACK",
memberCode:"forsyt", status:"PLACED", version:1708099201 }

├─ handleBetSnapshot()
│ ├─ Find order by providerOrderId='67890' OR requestId='uuid-xxx'
│ ├─ Version check: skip if snapshot.version <= order.lastBifrostVersion
│ ├─ Map status:
│ │ PLACED → accepted
│ │ FAILED → declined (refund balance)
│ │ VOIDED → cancelled (refund balance)
│ │ LAPSED → lapsed (refund balance)
│ │ CANCELLED → cancelled (refund balance)
│ │ PARTIALLY_MATCHED → partially_accepted (exchange only, unlikely for sportsbook)
│ │ UNMATCHED → submitted (exchange only)
│ ├─ Update order in DB
│ └─ Publish WebSocket event → frontend invalidates orders query

└─ No polling job needed (queue push is instant)

Settlement: Queue → settlementService → Ledger

forsyt.bets.outcomes.queue → BetOutcomeSnapshot (protobuf)
{ requestId:"uuid-xxx", betId:67890, outcome:"WON", pnl:1250.0,
sizeSettled:800, version:1708185600 }

├─ handleBetOutcome()
│ ├─ Find order by providerOrderId='67890'
│ ├─ Idempotency: skip if already settled
│ ├─ Map: WON→win, LOST→lose, VOID→void
│ ├─ Convert pnl: hkdToPoints(1250.0) = 156.25 points
│ ├─ settlementService.settleOrder()
│ │ ├─ Credit balance (156.25 points)
│ │ ├─ Ledger entries (double-entry)
│ │ ├─ Commission calculation
│ │ └─ WebSocket settlement event
│ └─ No polling job needed

Protobuf Message Schemas (from actual queue data + docs)

Category

message Category {
string id = 1; // "1.4_AUS"
string parentId = 2; // parent category for hierarchy
string categoryType = 3; // "COUNTRY" or "COMPETITION"
string value = 4; // "Australia"
string sportId = 5; // "2"
}

Event

message Event {
string eventId = 1;
string name = 2; // "Pheonix Cricketers v Super Kings"
string startTime = 3; // "2026-02-16T17:00"
string sportId = 4;
string categoryId = 5;
repeated Competitor competitors = 6;
repeated EventMapping externalIds = 7;
string status = 8; // "CANCELLED", "POSTPONED", or empty (active)
int64 version = 9;
}

message Competitor {
string id = 1;
string shortName = 2; // "PHO"
string fullName = 3; // "Pheonix Cricketers"
}

message EventMapping {
string provider = 1; // "Betfair", "Sportradar"
string eventId = 2; // provider-specific event ID
}

MarketCatalogue

message MarketCatalogue {
string id = 1; // "1.254090981" or "14.2350505_2"
string eventId = 2;
string marketType = 3; // "MATCH_ODDS", "1ST_INNINGS_RUNS_EX", etc.
string name = 4; // "Match Odds", "1st Innings 20 Overs Line"
string oddsType = 5; // "DECIMAL"
string startTime = 6;
bool prematchOnly = 7;
int64 sortOrder = 8;
string tradingMarketType = 9;
repeated RunnerCatalogue runners = 10;
MarketMetadata metadata = 11;
string sportId = 12;
int64 version = 13;
}

message RunnerCatalogue {
int64 id = 1;
string name = 2;
int32 sortPriority = 3;
map<string, string> metadata = 4;
}

MarketBook

message MarketBook {
string marketId = 1;
string marketStatus = 2; // INACTIVE, OPEN, SUSPENDED, CLOSED, SETTLED
double maxMarket = 3; // max liability in HKD
map<string, double> maxMarketCurrencies = 4;
optional double minStake = 5;
map<string, double> minStakeCurrencies = 6;
repeated Runner runners = 7;
map<string, string> variables = 8; // includes MARKET_TYPE
string tradingMarketType = 9;
string sportId = 10;
int64 version = 11;
}

message Runner {
int64 id = 1;
string status = 2; // "ACTIVE"
RunnerPriceLadder priceLadder = 3;
int64 version = 4;
}

message RunnerPriceLadder {
repeated RunnerPriceSize availableToBack = 1;
repeated RunnerPriceSize availableToLay = 2;
}

message RunnerPriceSize {
double price = 1; // decimal odds
double midPrice = 2;
double size = 3; // exchange: liquidity available
double line = 4; // for handicap/totals
double midLine = 5;
int32 priceIndex = 6; // 0=best, 1=next, etc.
double maxStake = 7; // sportsbook: max stake in HKD
map<string, double> maxStakeCurrencies = 8; // {HKD:25000, USD:3200, INR:267000, ...}
}

BetSnapshot

message BetSnapshot {
string requestId = 1;
int64 betId = 2;
string marketId = 3;
int64 runnerId = 4;
double size = 5;
optional double odds = 6;
optional int32 line = 7;
string side = 8;
string memberCode = 9;
string status = 10;
optional string errorMessage = 11;
optional string errorCode = 12;
int64 version = 13;
double sizeMatched = 14; // exchange only
double sizeRemaining = 15; // exchange only
double sizeLapsed = 16; // exchange only
double sizeCancelled = 17; // exchange only
double sizeVoided = 18; // exchange only
double averageOdds = 19; // exchange only
optional string voidReason = 20;
}

BetOutcomeSnapshot

message BetOutcomeSnapshot {
string requestId = 1;
int64 betId = 2;
string outcome = 3; // "WON", "LOST", "VOID"
double pnl = 4; // in HKD
int64 version = 5;
double sizeSettled = 6; // in HKD
}

REST API Endpoints (from Swagger spec)

POST /api/v1/bets/place

  • Auth: Authorization: <token> (ApiToken scheme)
  • Body: { sportId, marketId, runnerId, priceIndex?, size, memberSize, odds, line?, side, memberCode, currency:"HKD", ipAddress, requestId? }
  • Response: { id, requestId, memberCode, status, side, size, memberSize, odds, line, version }
  • Status always returns PENDING; real result via bets.snapshot.queue
  • No providerId needed (removed from spec)

POST /api/v1/bets/cancel

  • Body: { marketId, betId }
  • Response: { id, status, version }
  • Sportsbook only: cancellation NOT supported (confirmed by Bifrost team)

POST /api/v1/bets/outcomes

  • Body: { marketIds[], pageSize, pageNumber }
  • Paginated bet outcomes (REST fallback for queue)

Recovery Endpoints (async)

  • POST /api/v1/recovery/bets — recover by marketIds, betIds, or betRequestIds
  • POST /api/v1/recovery/bet_outcomes — recover settlement results
  • POST /api/v1/recovery/markets — recover market data (catalogue, book, results)
  • POST /api/v1/recovery/events — recover events by sportId, categoryIds, eventIds
  • POST /api/v1/recovery/categories — recover categories

All recovery endpoints return { requestId, error } — results delivered async via queues.

Connection Details (Preprod)

RabbitMQ:
host: mq-preprod-bifrost-api.bifrost.bet
port: 5572
ssl: false
vhost: /customers
username: forsyt_preprod
password: N49wBBb7GLfyPYKj
queue prefix: forsyt

REST API:
base: https://preprod-bifrost-betting-api.bifrost.bet
auth: Bearer forsyt_preprod
member: forsyt

Whitelisted IPs: 108.61.221.157, 45.76.139.124
(REST currently returning 403 — Bifrost team to complete IP whitelisting)

Answers to Previous Questions

#QuestionAnswer
1Cancel bet endpoint?Exists but sportsbook only: no cancellation
2providerId value?Removed from spec — not needed
3IP whitelist?108.61.221.157, 45.76.139.124 (pending activation)
4currency?HKD — we deal in HKD with Bifrost
5Odds format?Decimal (confirmed: 1.75, 2.44, etc.)
6Mandatory PT?No — size = memberSize when PT=0
7priceIndex required?Optional, 0-indexed on price ladder
8Default minStake?Available in MarketBook as minStake + minStakeCurrencies
9Detect live markets?marketStatus: "OPEN" vs "SUSPENDED" + event status
10Distinguish bookmaker vs exchange?Market ID prefix: 14.* = sportsbook, 1.* = exchange
11Data format?Protobuf — all queue messages are protobuf-encoded
12ipAddress field?Server IP (108.61.221.157), not end-user IP

Implementation Phases

Phase 0: Tracer Bullet

Consume forsyt.market-book.queue → decode protobuf → filter 14.* markets → cache prices → serve via getOdds(). No betting. Prove queue→cache→PAL pipeline end-to-end. Attach sportsbook markets to existing Betfair cricket fixtures via event matching.

Phase 1: Infrastructure

  • Deps: amqplib, protobufjs, @types/amqplib
  • No Docker RabbitMQ needed — Bifrost provides hosted RabbitMQ
  • Config (env vars):
    • BIFROST_ENABLED, BIFROST_API_URL, BIFROST_API_TOKEN
    • BIFROST_RABBITMQ_HOST, BIFROST_RABBITMQ_PORT, BIFROST_RABBITMQ_VHOST
    • BIFROST_RABBITMQ_USERNAME, BIFROST_RABBITMQ_PASSWORD
    • BIFROST_QUEUE_PREFIX (default: forsyt)
    • BIFROST_MEMBER_CODE (default: forsyt)
    • BIFROST_IP_ADDRESS (server IP for bet placement)
    • BIFROST_PT_PERCENT (default 0)
    • BIFROST_CREDIT_LIMIT
    • BIFROST_CURRENCY (default: HKD)
  • Validation: throw if enabled but required vars missing

Phase 2: Types & Protobuf

  • types.ts — TypeScript interfaces matching protobuf messages above
  • proto/ — .proto files compiled with protobufjs (or use dynamic loading)
  • Key types: Category, Event, Competitor, EventMapping, MarketCatalogue, MarketBook, Runner, RunnerPriceLadder, RunnerPriceSize, BetSnapshot, BetOutcomeSnapshot

Phase 3: Mapper

  • mappers/BifrostMapper.ts:
    • Sport: "2" → 27 (throw on unknown)
    • Event → CanonicalFixture (use competitors for homeTeam/awayTeam, externalIds for cross-mapping)
    • MarketCatalogue + MarketBook → CanonicalMarket
      • Back AND lay prices (both available on sportsbook)
      • backDepth from availableToBack with maxStakeCurrencies.HKD converted to points as size
      • layDepth from availableToLay with maxStakeCurrencies.HKD converted to points as size
      • limit = hkdToPoints(maxStakeCurrencies.HKD) (for max bet validation, in points)
    • Market ID filtering: only map 14.* markets (sportsbook), skip 1.* (exchange)
      • Configurable: simple boolean flag BIFROST_INCLUDE_EXCHANGE_MARKETS to enable 1.* later
    • BetStatus → OrderStatus
    • BetOutcome → SettlementResult
    • MarketStatus mapping: OPEN→open, SUSPENDED→suspended, CLOSED→closed, SETTLED→settled, INACTIVE→closed
    • Market source tagging: Set market.source = 'bifrost-sportsbook' so frontend can distinguish

Phase 4: Queue Manager

  • BifrostQueueManager.ts: amqplib connection to Bifrost RabbitMQ
  • 6 queue consumers:
    1. forsyt.category.queue → categories
    2. forsyt.event.queue → events
    3. forsyt.market-catalogue.queue → market metadata
    4. forsyt.market-book.queue → live prices
    5. forsyt.bets.snapshot.queue → bet status updates
    6. forsyt.bets.outcomes.queue → settlement results
  • Protobuf decode each message
  • Version dedup, manual ack (basic_nack with requeue:true on error)
  • Reconnect with backoff (1s→30s)
  • On market book update: emit event → BifrostCache update → redis.publish('odds:updated') (same pattern as BetfairAdapter)

Phase 5: Cache (Same Pattern as BetfairCache)

  • BifrostCache.ts: in-memory maps with version dedup
  • Maps: categories, events, marketCatalogues, marketBooks
  • Indexes: eventId→marketIds, categoryId→eventIds, betfairEventId→bifrostEventId (cross-provider)
  • Filter: only cache 14.* market IDs (sportsbook) by default
  • Periodic sweep: remove SETTLED markets, old events (>24h)
  • Emit 'marketUpdate' on price changes → triggers redis.publish('odds:updated')

Phase 6: REST Client

  • BifrostClient.ts:
    • placeBet(): POST /api/v1/bets/place, Bearer auth, currency: "HKD"
    • cancelBet(): POST /api/v1/bets/cancel (throw "not supported for sportsbook")
    • getOutcomes(): POST /api/v1/bets/outcomes (fallback for queue)
    • Recovery methods (for startup data sync)
  • PT applied: size = memberSize × (1 - PT/100)
  • All amounts in HKD
  • 10s timeout, 4xx error handling

Phase 7: Adapter

  • BifrostAdapter.ts implements IExchangeAdapter
  • IOddsProvider reads from cache (back AND lay)
  • getMarkets(): Returns sportsbook markets for a fixture, already converted to canonical format with points-denominated sizes
  • placeOrder: Convert points→HKD → REST → return 'submitted'
  • cancelOrder: throw "not supported for sportsbook markets"
  • getBetslip: cache lookup for price + maxStake limits (from maxStakeCurrencies.HKD, converted to points)
  • supportsSport(27) → true

Phase 8: Bet Status Consumer

  • bets.snapshot.queue → find order → update DB → WebSocket notify
  • Status mapping: PLACED→accepted, FAILED→declined, VOIDED→cancelled, LAPSED→lapsed
  • Refund on failure (FAILED, VOIDED, LAPSED, CANCELLED)
  • Version-based dedup (skip if version <= last processed version)

Phase 9: Settlement Consumer

  • bets.outcomes.queue → find order → settlementService
  • Outcome mapping: WON→win, LOST→lose, VOID→void
  • Convert HKD pnl to points: hkdToPoints(pnl) before crediting balance
  • Idempotency, financial integrity logging
  • Use pnl from BetOutcomeSnapshot for settlement amount (in HKD)

Phase 10: PAL Registration & Market Merging

  • Register in exchanges/index.ts when BIFROST_ENABLED
  • Cricket routing: Both betfair-ex AND bifrost serve cricket (sportId 27)
    • Betfair-ex: Match Odds exchange market
    • Bifrost: ALL sportsbook markets (including sportsbook Match Odds)
  • Market merging in ExchangeCoordinator:
    • When getOdds() is called for a cricket fixture:
      1. Get Betfair markets (existing flow)
      2. Get Bifrost sportsbook markets (from cache)
      3. Merge: Betfair Match Odds first, then Bifrost sportsbook markets
      4. Filter out "Match Odds Including Tie" from Betfair
    • This is similar to how Roanuz cricket data is already merged — data from multiple providers on the same fixture
  • Add bifrost to ExternalIds type
  • Add to BOOKMAKER_MIN_STAKES (use hkdToPoints(minStakeCurrencies.HKD))

Phase 11: Provider & Exposure

  • Seed PlatformProviderState for bifrost (with currency: 'HKD', exchangeRate for HKD→points)
  • Exposure guard at 90% of credit limit
  • Add point_value_hkd platform setting (default: 8) — admin-configurable

Phase 12: Currency Service Extension

  • Add pointsToHkd(points) and hkdToPoints(hkd) to orderService
  • Driven by Provider.exchangeRate or platformSettings.point_value_hkd
  • Admin panel: Add HKD point value setting alongside existing USD setting
  • Not hardcoded — configurable per-provider via admin panel

Frontend Integration

The frontend is already provider-agnostic for odds display. Key points:

  • ExchangeGrid.tsx reads backDepth/layDepth — data-driven visibility
  • Lay column auto-shows when layDepth has data (Bifrost sportsbook provides both)
  • Market categorization in fixture/[id]/page.tsx already handles cricket-specific grouping (Popular, Innings, Overs, Wickets, Players, Other)
  • maxStake maps to outcome.price.limit in canonical model (already in points after conversion)

Changes needed:

  1. Market name labelling: Betfair Match Odds → "Match Odds (exchange)" in display

    • Add in marginService.transformMarket() when source is betfair-ex and type is match_odds
    • OR: Add source field to FrontendMarket, frontend appends label conditionally
    • Sportsbook markets: leave name as-is (no label)
  2. Filter "Match Odds Including Tie": In the odds route or marginService, filter out Betfair markets where name === "Match Odds Including Tie" for cricket (sportId 27)

  3. Market ordering: Betfair Match Odds (exchange) appears first in the markets list, followed by all Bifrost sportsbook markets in their natural category order

  4. BetSlip: Validate against max stake (already in points after HKD→points conversion in mapper)

  5. No provider toggle needed — both providers' markets appear together on same fixture

Files to Create

backend/src/exchanges/adapters/bifrost/
├── BifrostAdapter.ts
├── BifrostClient.ts
├── BifrostCache.ts
├── BifrostQueueManager.ts
├── types.ts
├── index.ts
├── proto/
│ ├── category.proto
│ ├── event.proto
│ ├── market_catalogue.proto
│ ├── market_book.proto
│ ├── bet_snapshot.proto
│ └── bet_outcome.proto
└── mappers/
└── BifrostMapper.ts

Files to Modify

  • backend/package.json — amqplib, protobufjs, @types/amqplib
  • backend/src/config/index.ts — bifrost config block (RabbitMQ + REST + credentials + currency)
  • backend/src/exchanges/index.ts — register adapter, cricket market merging
  • backend/src/exchanges/core/models/canonical.tsbifrost in ExternalIds, source field on CanonicalMarket
  • backend/src/exchanges/core/coordinator/ExchangeCoordinator.ts — market merging for multi-provider fixtures
  • backend/src/services/orderService.ts — BOOKMAKER_MIN_STAKES for bifrost, pointsToHkd/hkdToPoints
  • backend/src/services/marginService.ts — "(exchange)" label for Betfair Match Odds, filter "Match Odds Including Tie"
  • backend/prisma/seed.ts — PlatformProviderState entry for bifrost, point_value_hkd setting
  • frontend/src/services/marginService.ts or frontend/src/app/fixture/[id]/page.tsx — market ordering (exchange first)

Remaining Questions for Bifrost Team

#QuestionWhy it mattersStatus
1REST IP whitelisting complete?All endpoints return 403 currentlyPending
2Is priceIndex required for sportsbook bets?Rejection if missingUnknown
3How does line field work for innings runs markets?Correct bet placement on fancy marketsUnknown
4Recovery endpoint result delivery — via same queues?Startup data sync strategyUnknown
5Is there a heartbeat/health endpoint?Health check for FailoverManagerUnknown
6maxStake in RunnerPriceSize — is that per-bet or per-market?Stake validation logicUnknown

Future: Full Exchange Integration

The 1.* market IDs in the feed are Betfair exchange pass-throughs. If we later want these:

  • Set BIFROST_INCLUDE_EXCHANGE_MARKETS=true (flip the flag)
  • Use size instead of maxStake for exchange markets (market ID prefix check in mapper)
  • Handle partial matching: sizeMatched, sizeRemaining, averageOdds
  • Enable cancellation for 1.* markets
  • Set partialMatching: true conditionally per market type

Architecture doesn't change — just mapper logic and capability flags per market.

Changelog (v2 → v3)

#What changedv2 (wrong)v3 (corrected)
1Match Odds from BetfairKeepKeep, but filter "Match Odds Including Tie"
2Match Odds from BifrostSkip (Betfair covers)Keep — show sportsbook Match Odds too
3Provider toggleToggle between providersNo toggle — both coexist on same page
4CurrencyUSDHKD (1 point = 8 HKD, configurable)
5Priority80 (between Betfair/Pinnacle)100 (essential, not fallback)
6CachingPresented as newSame pattern as BetfairCache (not new)
7queueing capabilityNot presentAdded — distinguishes queue-push from REST/WebSocket
8Frontend labelNone"(exchange)" on Betfair Match Odds only
9Market orderingProvider toggle decidesBetfair exchange first, then sportsbook markets
10Stake currencycurrency: "USD"currency: "HKD"
11streaming fieldfalsefalse (queue ≠ streaming, queueing: true instead)