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:
| Format | Example | Type | Characteristics |
|---|---|---|---|
1.xxxxxxx | 1.254090981 | Exchange (Betfair pass-through) | back/lay with size (liquidity depth) |
14.xxxxxxx_N | 14.2350505_2 | Sportsbook (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
| Aspect | Betfair (exchange) | Pinnacle (bookmaker) | Bifrost (sportsbook) |
|---|---|---|---|
| Data source | REST + WebSocket | REST polling | RabbitMQ push (protobuf) → local cache |
| Bet placement | REST (sync result) | REST 2-step (line → place) | REST 1-step (returns PENDING) |
| Bet status sync | Streaming + polling | Polling job (15s) | Queue push (instant) |
| Settlement | Polling job (60s) | Polling job (60s) | Queue push (instant) |
| Lay betting | Yes | No | Yes (sportsbook has back+lay) |
| Cancellation | Yes | No | Exchange markets only (not sportsbook) |
| Sports | All | Soccer, Basketball, Tennis | Cricket |
| Stake limits | Size-based (liquidity) | Fixed minimums | Per-currency maxStake per price level |
| Odds format | Decimal | Decimal | Decimal (confirmed from queue data) |
| Currency | GBP (via BetfairClient) | USD | HKD |
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:
- Same fixture page shows Betfair Match Odds (exchange) AND Bifrost sportsbook markets
- Canonical fixture ID remains the Betfair-originated ID — Bifrost markets attach to it
- 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
providerIdneeded (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 betRequestIdsPOST /api/v1/recovery/bet_outcomes— recover settlement resultsPOST /api/v1/recovery/markets— recover market data (catalogue, book, results)POST /api/v1/recovery/events— recover events by sportId, categoryIds, eventIdsPOST /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
| # | Question | Answer |
|---|---|---|
| 1 | Cancel bet endpoint? | Exists but sportsbook only: no cancellation |
| 2 | providerId value? | Removed from spec — not needed |
| 3 | IP whitelist? | 108.61.221.157, 45.76.139.124 (pending activation) |
| 4 | currency? | HKD — we deal in HKD with Bifrost |
| 5 | Odds format? | Decimal (confirmed: 1.75, 2.44, etc.) |
| 6 | Mandatory PT? | No — size = memberSize when PT=0 |
| 7 | priceIndex required? | Optional, 0-indexed on price ladder |
| 8 | Default minStake? | Available in MarketBook as minStake + minStakeCurrencies |
| 9 | Detect live markets? | marketStatus: "OPEN" vs "SUSPENDED" + event status |
| 10 | Distinguish bookmaker vs exchange? | Market ID prefix: 14.* = sportsbook, 1.* = exchange |
| 11 | Data format? | Protobuf — all queue messages are protobuf-encoded |
| 12 | ipAddress 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_TOKENBIFROST_RABBITMQ_HOST,BIFROST_RABBITMQ_PORT,BIFROST_RABBITMQ_VHOSTBIFROST_RABBITMQ_USERNAME,BIFROST_RABBITMQ_PASSWORDBIFROST_QUEUE_PREFIX(default:forsyt)BIFROST_MEMBER_CODE(default:forsyt)BIFROST_IP_ADDRESS(server IP for bet placement)BIFROST_PT_PERCENT(default 0)BIFROST_CREDIT_LIMITBIFROST_CURRENCY(default:HKD)
- Validation: throw if enabled but required vars missing
Phase 2: Types & Protobuf
types.ts— TypeScript interfaces matching protobuf messages aboveproto/— .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)
backDepthfromavailableToBackwithmaxStakeCurrencies.HKDconverted to points assizelayDepthfromavailableToLaywithmaxStakeCurrencies.HKDconverted to points assizelimit=hkdToPoints(maxStakeCurrencies.HKD)(for max bet validation, in points)
- Market ID filtering: only map
14.*markets (sportsbook), skip1.*(exchange)- Configurable: simple boolean flag
BIFROST_INCLUDE_EXCHANGE_MARKETSto enable1.*later
- Configurable: simple boolean flag
- 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
- Sport:
Phase 4: Queue Manager
BifrostQueueManager.ts: amqplib connection to Bifrost RabbitMQ- 6 queue consumers:
forsyt.category.queue→ categoriesforsyt.event.queue→ eventsforsyt.market-catalogue.queue→ market metadataforsyt.market-book.queue→ live pricesforsyt.bets.snapshot.queue→ bet status updatesforsyt.bets.outcomes.queue→ settlement results
- Protobuf decode each message
- Version dedup, manual ack (
basic_nackwithrequeue:trueon 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.tsimplements 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
pnlfrom BetOutcomeSnapshot for settlement amount (in HKD)
Phase 10: PAL Registration & Market Merging
- Register in
exchanges/index.tswhen 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:- Get Betfair markets (existing flow)
- Get Bifrost sportsbook markets (from cache)
- Merge: Betfair Match Odds first, then Bifrost sportsbook markets
- 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
- When
- Add
bifrostto ExternalIds type - Add to BOOKMAKER_MIN_STAKES (use
hkdToPoints(minStakeCurrencies.HKD))
Phase 11: Provider & Exposure
- Seed PlatformProviderState for bifrost (with
currency: 'HKD',exchangeRatefor HKD→points) - Exposure guard at 90% of credit limit
- Add
point_value_hkdplatform setting (default: 8) — admin-configurable
Phase 12: Currency Service Extension
- Add
pointsToHkd(points)andhkdToPoints(hkd)to orderService - Driven by
Provider.exchangeRateorplatformSettings.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.tsxreadsbackDepth/layDepth— data-driven visibility- Lay column auto-shows when
layDepthhas data (Bifrost sportsbook provides both) - Market categorization in
fixture/[id]/page.tsxalready handles cricket-specific grouping (Popular, Innings, Overs, Wickets, Players, Other) maxStakemaps tooutcome.price.limitin canonical model (already in points after conversion)
Changes needed:
-
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
sourcefield to FrontendMarket, frontend appends label conditionally - Sportsbook markets: leave name as-is (no label)
- Add in
-
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) -
Market ordering: Betfair Match Odds (exchange) appears first in the markets list, followed by all Bifrost sportsbook markets in their natural category order
-
BetSlip: Validate against max stake (already in points after HKD→points conversion in mapper)
-
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/amqplibbackend/src/config/index.ts— bifrost config block (RabbitMQ + REST + credentials + currency)backend/src/exchanges/index.ts— register adapter, cricket market mergingbackend/src/exchanges/core/models/canonical.ts—bifrostin ExternalIds,sourcefield on CanonicalMarketbackend/src/exchanges/core/coordinator/ExchangeCoordinator.ts— market merging for multi-provider fixturesbackend/src/services/orderService.ts— BOOKMAKER_MIN_STAKES for bifrost, pointsToHkd/hkdToPointsbackend/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 settingfrontend/src/services/marginService.tsorfrontend/src/app/fixture/[id]/page.tsx— market ordering (exchange first)
Remaining Questions for Bifrost Team
| # | Question | Why it matters | Status |
|---|---|---|---|
| 1 | REST IP whitelisting complete? | All endpoints return 403 currently | Pending |
| 2 | Is priceIndex required for sportsbook bets? | Rejection if missing | Unknown |
| 3 | How does line field work for innings runs markets? | Correct bet placement on fancy markets | Unknown |
| 4 | Recovery endpoint result delivery — via same queues? | Startup data sync strategy | Unknown |
| 5 | Is there a heartbeat/health endpoint? | Health check for FailoverManager | Unknown |
| 6 | maxStake in RunnerPriceSize — is that per-bet or per-market? | Stake validation logic | Unknown |
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
sizeinstead ofmaxStakefor exchange markets (market ID prefix check in mapper) - Handle partial matching:
sizeMatched,sizeRemaining,averageOdds - Enable cancellation for
1.*markets - Set
partialMatching: trueconditionally per market type
Architecture doesn't change — just mapper logic and capability flags per market.
Changelog (v2 → v3)
| # | What changed | v2 (wrong) | v3 (corrected) |
|---|---|---|---|
| 1 | Match Odds from Betfair | Keep | Keep, but filter "Match Odds Including Tie" |
| 2 | Match Odds from Bifrost | Skip (Betfair covers) | Keep — show sportsbook Match Odds too |
| 3 | Provider toggle | Toggle between providers | No toggle — both coexist on same page |
| 4 | Currency | USD | HKD (1 point = 8 HKD, configurable) |
| 5 | Priority | 80 (between Betfair/Pinnacle) | 100 (essential, not fallback) |
| 6 | Caching | Presented as new | Same pattern as BetfairCache (not new) |
| 7 | queueing capability | Not present | Added — distinguishes queue-push from REST/WebSocket |
| 8 | Frontend label | None | "(exchange)" on Betfair Match Odds only |
| 9 | Market ordering | Provider toggle decides | Betfair exchange first, then sportsbook markets |
| 10 | Stake currency | currency: "USD" | currency: "HKD" |
| 11 | streaming field | false | false (queue ≠ streaming, queueing: true instead) |