ποΈ Exchange Integration Architecture
The Provider Abstraction Layer (PAL)β
"The best architectures are the ones that make adding the 10th integration as easy as adding the 2nd."
π Table of Contentsβ
- Executive Summary
- The Problem We're Solving
- Architecture Vision
- Core Concepts
- The Four Pillars
- Data Flow & Use Cases
- Provider Deep Dives
- Routing Intelligence
- Failure & Recovery
- Migration Strategy
- Comparison: Why Not Oracle?
- Identity Resolution: The ID Mapping Challenge β NEW
- File Structure
- Glossary
Executive Summaryβ
Hannibal is evolving from a dual-provider system (OddsPAPI + BETS API) to a multi-exchange platform capable of integrating with Betfair, Matchbook, and potentially dozens of future betting exchanges.
This document describes the Provider Abstraction Layer (PAL) β an architecture that:
| Goal | How We Achieve It |
|---|---|
| Add new exchanges in days, not weeks | Plug-in adapter system |
| Sport-specific routing | Intelligent capability-based routing |
| Zero business logic changes | Clean separation via interfaces |
| Graceful degradation | Automatic failover between providers |
| Preserve exchange-specific features | Capability system instead of lowest-common-denominator |
The Problem We're Solvingβ
Today's Realityβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Current Hannibal β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β orderService.ts βββΊ if cricket: betfair-ex β
β βββΊ if soccer: pinnacle β
β β
β oddsPapi.ts βββββΊ OddsPAPI (aggregator) β
β betsApi.ts ββββββΊ BETS API (order placement) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
What happens when we add Betfair direct API?
- Modify
orderService.tswith moreifstatements - Add
betfairApi.tsalongside existing services - Duplicate routing logic across multiple files
- Hope we don't break existing Pinnacle flow
What happens when we add Matchbook?
- Even more
ifstatements - More services, more coupling
- Testing becomes a nightmare
- Every new provider = risk to existing code
The Spaghetti Trajectoryβ
Without proper architecture, this is where we're headed:
ββββββββββββββββ
β orderService β
ββββββββ¬ββββββββ
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
if betfair if pinnacle if matchbook
β β β
βΌ βΌ βΌ
βββββββββββ βββββββββββ βββββββββββ
βbetfair β βoddsPapi β βmatchbookβ
βApi.ts β β.ts β βApi.ts β
βββββββββββ βββββββββββ βββββββββββ
β β β
ββββββββββββββββββΌβββββββββββββββββ
βΌ
π± Maintenance Nightmare
Architecture Visionβ
The Provider Abstraction Layerβ
Instead of scattering provider logic throughout the codebase, we introduce a clean abstraction layer that makes all exchanges look the same to our business logic.
The magic: Our orderService.ts doesn't know or care whether it's talking to Betfair, Matchbook, or a provider that doesn't exist yet. It just asks the Exchange Coordinator to place an order.
Core Conceptsβ
π§ Mental Model: The Universal Translatorβ
Think of the Provider Abstraction Layer as a universal translator for betting exchanges:
| Betfair says... | Matchbook says... | Pinnacle says... | PAL translates to... |
|---|---|---|---|
| "Event Type 4" | "Sport: Cricket" | "Sport ID: 27" | { sport: CRICKET } |
| "Market: 1.234567" | "Event: abc-123" | "Fixture: 27..." | { fixtureId: "..." } |
| "Back @ 2.50" | "Bid @ 2.50" | "Price: 2.50" | { back: 2.50 } |
| "EXECUTION_COMPLETE" | "matched" | "accepted" | { status: FILLED } |
Every exchange speaks a different dialect. PAL ensures our application only needs to understand one language.
π¦ Key Componentsβ
| Component | Responsibility | Analogy |
|---|---|---|
| Exchange Coordinator | Routes requests, handles failover | Air Traffic Control |
| Provider Registry | Knows all available providers | Phone Book |
| Routing Strategy | Decides which provider to use | GPS Navigation |
| Adapters | Translate between PAL and exchange | Language Translators |
| Canonical Models | Internal data format | Universal Language |
The Four Pillarsβ
The architecture rests on four fundamental principles:
Pillar 1: π Canonical Data Modelsβ
All data flows through a single, provider-agnostic format.
Why this matters:
- Business logic never changes when adding providers
- Testing is simplified (mock canonical data)
- Data consistency across the platform
Pillar 2: π Plug-in Adaptersβ
Each exchange is an isolated, self-contained module.
An adapter is responsible for:
| Responsibility | Description |
|---|---|
| Authentication | Handle exchange-specific auth (API keys, SSL certs, OAuth) |
| API Communication | REST, WebSocket, or proprietary protocols |
| Data Transformation | Convert exchange format β canonical format |
| Error Handling | Map exchange errors to standard error types |
| Rate Limiting | Respect exchange-specific limits |
| Health Monitoring | Track connection status, latency |
Adding a new exchange = Creating a new adapter. Nothing else changes.
Pillar 3: π― Capability-Based Routingβ
Not all exchanges are created equal. Our routing respects that.
Capability Examples:
| Provider | Back | Lay | Streaming | In-Play | Cash Out | Sports |
|---|---|---|---|---|---|---|
| Betfair | β | β | β | β | β | Cricket, Soccer, Tennis, ... |
| Matchbook | β | β | β | β | β | Soccer, Tennis, Basketball |
| Pinnacle | β | β | β | β | β | All major sports |
Pillar 4: π‘οΈ Graceful Degradationβ
When one provider fails, the system adapts automatically.
Data Flow & Use Casesβ
Use Case 1: Placing a Cricket Betβ
Scenario: User wants to place a βΉ1000 back bet on India to win at odds of 1.85
Key observations:
Order Servicedoesn't know about BetfairRouting Strategydecided Betfair based on sportBetfair Adapterhandled all translation- If Betfair was down,
Exchange Coordinatorwould failover to Matchbook
Use Case 2: Real-Time Odds Streamingβ
Scenario: User is viewing a live cricket match and needs real-time price updates
Use Case 3: Multi-Provider Price Comparisonβ
Scenario: Admin wants to see odds from all providers for arbitrage detection
Provider Deep Divesβ
π‘ Betfair Exchangeβ
Primary use: Cricket betting (direct integration)
| Aspect | Details |
|---|---|
| Authentication | SSL certificate + session token |
| APIs Used | Betting API (REST), Exchange Stream API (WebSocket) |
| Unique Features | Full market depth, BSP, lay betting, cash out |
| Rate Limits | ~5 concurrent requests, 50 markets per listMarketBook |
| Event Type ID | Cricket = 4 |
Why direct integration?
- Lower latency than going through OddsPAPI
- Access to full market depth (not just best prices)
- Native streaming API for real-time updates
- Better control over rate limits
π΅ Pinnacle (via OddsPAPI/BETS API)β
Primary use: Soccer and other sports
| Aspect | Details |
|---|---|
| Authentication | API key (handled by OddsPAPI) |
| APIs Used | OddsPAPI (odds), BETS API (orders) |
| Unique Features | Sharp lines, high limits, wide coverage |
| Limitations | Back-only (no lay betting), no streaming |
| Sport ID | Soccer = 10 |
Why via aggregator?
- Already integrated and working
- Good for sports where lay betting isn't needed
- Wider market coverage
π’ Matchbook (Future)β
Primary use: Secondary exchange, failover, arbitrage
| Aspect | Details |
|---|---|
| Authentication | Username/password + session |
| APIs Used | REST API, Streaming API |
| Unique Features | Low commission, P2P exchange |
| Potential Use | Failover for Betfair, price comparison |
Routing Intelligenceβ
The Routing Decision Treeβ
Routing Rules Configurationβ
The routing strategy is configurable, not hard-coded:
| Priority | Condition | Provider | Rationale |
|---|---|---|---|
| 100 | sport = cricket | Betfair | Best liquidity, native lay |
| 90 | sport = cricket, betfair_down | Matchbook | Failover |
| 100 | sport = soccer, need_lay | Matchbook | P2P exchange |
| 80 | sport = soccer | Pinnacle | Sharp lines |
| 50 | * | Pinnacle | Default fallback |
Failure & Recoveryβ
Health Statesβ
Each provider can be in one of three states:
Failover Scenariosβ
| Scenario | Detection | Action | Recovery |
|---|---|---|---|
| API Timeout | 5s timeout | Retry once, then failover | Auto-retry after 30s |
| Auth Failure | 401/403 | Refresh session, retry | If fails, mark degraded |
| Rate Limited | 429 | Backoff, route to secondary | Resume after cooldown |
| Connection Lost | Socket close | Immediate failover | Reconnect in background |
| Data Corruption | Validation fail | Reject, log, continue | Investigate manually |
Circuit Breaker Patternβ
Migration Strategyβ
Phase 0: Foundation (Week 1)β
Goal: Create the infrastructure without changing existing behavior
Migration Checklistβ
| Phase | Task | Risk | Rollback Plan |
|---|---|---|---|
| 0 | Create interfaces | None | N/A |
| 1 | Create OddsPAPI adapter | Low | Delete adapter, use services directly |
| 2 | Route through coordinator | Medium | Feature flag to bypass |
| 3 | Add Betfair adapter | Medium | Route cricket back to OddsPAPI |
| 4 | Enable Betfair for cricket | High | Instant rollback via config |
| 5 | Add Matchbook | Low | Just don't enable routing |
Feature Flagsβ
EXCHANGE_PAL_ENABLED=true # Use new architecture
BETFAIR_DIRECT_ENABLED=true # Enable Betfair adapter
MATCHBOOK_ENABLED=false # Future
FAILOVER_ENABLED=true # Auto-failover
ROUTING_MODE=capability # capability | legacy
Comparison: Why Not Oracle?β
The "Oracle" pattern (single facade hiding all providers) seems simpler but has critical flaws:
Head-to-Head Comparisonβ
| Aspect | Oracle Pattern | PAL Architecture |
|---|---|---|
| Adding new provider | Modify Oracle (risky) | Add new adapter (isolated) |
| Provider-specific features | β Lost (lowest common denominator) | β Preserved via capabilities |
| Testing | Hard (everything coupled) | Easy (test adapters independently) |
| Failover | Manual implementation | Built-in with circuit breakers |
| Performance | Bottleneck | Parallel provider calls |
| Team scalability | One team owns Oracle | Teams can own adapters |
| Debugging | "Somewhere in Oracle" | "In Betfair adapter, line 45" |
The Lowest Common Denominator Problemβ
Oracle pattern forces you to only expose features ALL providers support:
Betfair supports: β
Back, β
Lay, β
Stream, β
Cash Out
Matchbook supports: β
Back, β
Lay, β
Stream, β Cash Out
Pinnacle supports: β
Back, β Lay, β Stream, β Cash Out
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Oracle exposes: β
Back, β Lay, β Stream, β Cash Out π
With PAL, we expose all features and route appropriately:
Request: Place lay bet on cricket
PAL: "Betfair supports lay betting for cricket" β Route to Betfair β
Identity Resolution: The ID Mapping Challengeβ
The Problem: Every Exchange Speaks a Different Languageβ
This is one of the most critical challenges in multi-exchange integration. Each exchange has its own universe of identifiers for the same real-world events:
| Exchange | Match: India vs Australia, Jan 25 2026 |
|---|---|
| OddsPAPI | 27010001234567 (fixtureId) |
| Betfair | 32456789 (eventId) + 1.234567890 (marketId) |
| Matchbook | evt-abc-123 (eventId) |
| Your Internal | fixture_uuid_xyz |
The same cricket match has 4+ completely different identifiers!
When a user sees a match from OddsPAPI and wants to bet on it via Betfair, how do we know which Betfair event corresponds to that OddsPAPI fixture?
Solution: The ID Registry Patternβ
We introduce an ID Registry Service that maintains relationships between identifiers across all providers:
The Canonical Fixture Modelβ
Every fixture in our system carries its external IDs:
CanonicalFixture {
id: "fixture_abc123" β Our internal ID (source of truth)
externalIds: {
oddspapi: "27010001234567" β OddsPAPI's ID
betfair: {
eventId: "32456789" β Betfair's event ID
marketIds: {
MATCH_ODDS: "1.234567890" β Betfair's market ID
OVER_UNDER: "1.234567891"
}
}
matchbook: "evt-abc-123" β Matchbook's ID
}
// Match identification (used for fuzzy matching)
sport: CRICKET
competition: "ICC World Cup"
homeTeam: "India"
awayTeam: "Australia"
startTime: 2026-01-25T10:00:00Z β Critical for matching!
}
Three Strategies for ID Resolutionβ
Strategy 1: Explicit Mapping Table (Database)β
Store mappings in a dedicated database table:
| canonical_id | provider | external_id | external_type | confidence |
|---|---|---|---|---|
| fix_abc123 | oddspapi | 27010001234567 | fixture | 1.00 |
| fix_abc123 | betfair | 32456789 | event | 0.95 |
| fix_abc123 | betfair | 1.234567890 | market_MATCH_ODDS | 1.00 |
| fix_abc123 | matchbook | evt-abc-123 | event | 0.92 |
Pros: Reliable, queryable, auditable Cons: Needs to be populated
Strategy 2: Fuzzy Matching (For Discovery)β
When we encounter a new fixture from any provider, we match it to existing data:
Betfair says: "India v Australia, Cricket, 2026-01-25T10:00:00Z"
β
Fuzzy Matcher
β
OddsPAPI has: "India vs Australia, Cricket, 2026-01-25T10:00:00Z"
β
β
Match found! Link IDs.
Matching Criteria (weighted):
| Criterion | Weight | Tolerance |
|---|---|---|
| Sport | Required | Exact match |
| Start Time | High | Β±5 minutes |
| Team Names | High | Fuzzy string match (Levenshtein) |
| Competition | Medium | Fuzzy match |
| Venue | Low | If available |
Fuzzy String Matching Examples:
| Provider A | Provider B | Match? |
|---|---|---|
| "India" | "India" | β Exact |
| "India v Australia" | "India vs Australia" | β 95% similar |
| "Mumbai Indians" | "MI" | β Known alias |
| "Chennai Super Kings" | "CSK" | β Known alias |
| "India" | "Pakistan" | β Different |
Strategy 3: Common External IDs (When Available)β
Some fixtures have shared identifiers that multiple providers use:
| Shared ID Type | Description | Providers Using It |
|---|---|---|
| Betradar ID | Sports data provider | Many bookmakers |
| ESPN ID | Common in US sports | US-focused providers |
| betfairId | Betfair's ID | OddsPAPI already provides this! |
Good news - OddsPAPI already provides some cross-references:
OddsPAPI Fixture {
fixtureId: "27010001234567"
betradarId: "sr:match:12345"
betfairId: "32456789" β Direct Betfair mapping!
pinnacleId: "987654321"
}
When available, these are golden - no fuzzy matching needed.
The Complete ID Resolution Flowβ
When Are Mappings Created?β
Eager Population (Background Job)β
For popular events, we pre-populate mappings:
Lazy Resolution (On Demand)β
For less popular events, resolve when needed:
Recommended: Hybrid Approachβ
| Scenario | Strategy | Rationale |
|---|---|---|
| IPL, World Cup, Premier League | Eager | High traffic, pre-populate |
| Smaller leagues | Lazy | Resolve on first request |
| New provider added | Batch job | Map all existing fixtures |
| Live matches | Real-time | Sync as matches go live |
Handling Market IDsβ
Markets add another layer of complexity. The same market type has different IDs:
| Market Type | OddsPAPI | Betfair | Matchbook |
|---|---|---|---|
| Match Winner | marketId: 101 | marketId: 1.234567 | market: "winner" |
| Over/Under 2.5 | marketId: 201, handicap: 2.5 | marketId: 1.234568 | market: "totals-2.5" |
| Asian Handicap -1.5 | marketId: 301, handicap: -1.5 | marketId: 1.234569 | market: "ah--1.5" |
Solution: Map by market type and parameters, not raw IDs:
MarketTypeMapping {
canonicalType: "OVER_UNDER"
handicap: 2.5
providerFormats: {
oddspapi: { marketId: 201, handicapField: "handicap" }
betfair: { marketType: "OVER_UNDER_25", marketIdPattern: "..." }
matchbook: { marketSlug: "totals-{handicap}" }
}
}
The ID Registry Componentβ
Key Operations:
| Operation | Description | Use Case |
|---|---|---|
registerMapping | Store a new ID relationship | After fuzzy match or explicit link |
getCanonicalId | External ID β Our ID | Receiving data from provider |
getExternalId | Our ID β External ID | Sending request to provider |
resolveFixture | Find/create canonical fixture | New fixture from any provider |
Database Schema for ID Mappingsβ
-- Core fixture ID mappings
CREATE TABLE fixture_id_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
canonical_id VARCHAR(50) NOT NULL,
provider VARCHAR(20) NOT NULL,
external_id VARCHAR(100) NOT NULL,
external_type VARCHAR(30) DEFAULT 'fixture',
confidence DECIMAL(3,2) DEFAULT 1.00,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(provider, external_id, external_type),
INDEX idx_canonical (canonical_id),
INDEX idx_lookup (provider, external_id)
);
-- Market type reference mappings
CREATE TABLE market_type_mappings (
id SERIAL PRIMARY KEY,
canonical_type VARCHAR(30) NOT NULL,
provider VARCHAR(20) NOT NULL,
provider_market_id VARCHAR(50),
provider_market_type VARCHAR(50),
handicap_field VARCHAR(30),
UNIQUE(canonical_type, provider)
);
-- Team name aliases for fuzzy matching
CREATE TABLE team_aliases (
id SERIAL PRIMARY KEY,
canonical_name VARCHAR(100) NOT NULL,
alias VARCHAR(100) NOT NULL,
sport VARCHAR(20),
UNIQUE(alias, sport),
INDEX idx_alias (alias)
);
Example: Team Aliases Tableβ
| canonical_name | alias | sport |
|---|---|---|
| Mumbai Indians | MI | cricket |
| Mumbai Indians | Mumbai | cricket |
| Chennai Super Kings | CSK | cricket |
| Chennai Super Kings | Chennai | cricket |
| Manchester United | Man Utd | soccer |
| Manchester United | Man United | soccer |
| Manchester United | MUFC | soccer |
Updated Architecture with ID Registryβ
Summary: ID Resolutionβ
| Challenge | Solution |
|---|---|
| Different fixture IDs per provider | ID Registry with mapping table |
| Discovering new mappings | Fuzzy matching (teams + time + sport) |
| Cross-provider references | Use betfairId, betradarId when available |
| Market ID differences | Map by market type + parameters |
| Performance | Redis cache for hot mappings |
| Team name variations | Alias table for fuzzy matching |
| Confidence tracking | Store match confidence score |
File Structureβ
Proposed Directory Layoutβ
backend/src/
βββ exchanges/ # π Provider Abstraction Layer
β β
β βββ core/ # Shared infrastructure
β β βββ interfaces/
β β β βββ IOddsProvider.ts # "I can fetch odds"
β β β βββ IOrderProvider.ts # "I can place orders"
β β β βββ IStreamProvider.ts # "I can stream updates"
β β β βββ IExchangeAdapter.ts # Combined interface
β β β
β β βββ models/
β β β βββ canonical.ts # Universal data formats
β β β βββ capability.ts # Provider capability types
β β β βββ events.ts # Stream event definitions
β β β
β β βββ registry/
β β β βββ ProviderRegistry.ts # Provider lookup service
β β β
β β βββ identity/ # π ID Resolution (NEW)
β β β βββ IDRegistry.ts # Main ID mapping service
β β β βββ FuzzyMatcher.ts # Team/fixture matching logic
β β β βββ SyncJob.ts # Background sync for mappings
β β β βββ types.ts # ID mapping types
β β β
β β βββ coordinator/
β β βββ ExchangeCoordinator.ts # Main orchestrator
β β βββ RoutingStrategy.ts # Routing decisions
β β βββ FailoverManager.ts # Health & failover
β β
β βββ adapters/ # Provider implementations
β β β
β β βββ betfair/ # π‘ Betfair Exchange
β β β βββ BetfairAdapter.ts # Main adapter class
β β β βββ auth/
β β β β βββ CertAuth.ts # SSL certificate auth
β β β βββ api/
β β β β βββ BettingApi.ts # REST endpoints
β β β β βββ AccountApi.ts # Account operations
β β β βββ stream/
β β β β βββ ExchangeStream.ts # WebSocket streaming
β β β βββ mappers/
β β β βββ BetfairMapper.ts # Data transformation
β β β
β β βββ matchbook/ # π’ Matchbook (future)
β β β βββ ... # Same structure
β β β
β β βββ oddspapi/ # π΅ Pinnacle (via aggregator)
β β βββ OddsPapiAdapter.ts # Wraps existing services
β β βββ mappers/
β β βββ OddsPapiMapper.ts
β β
β βββ index.ts # Public exports
β
βββ services/ # Existing (unchanged)
β βββ oddsPapi.ts # β Wrapped by OddsPapiAdapter
β βββ betsApi.ts # β Wrapped by OddsPapiAdapter
β βββ orderService.ts # β Now uses ExchangeCoordinator
Interface Relationshipsβ
Glossaryβ
| Term | Definition |
|---|---|
| Adapter | A module that translates between PAL and a specific exchange's API |
| Canonical ID | Our internal identifier for a fixture/market, independent of any provider |
| Canonical Model | The standard internal data format used throughout Hannibal |
| Capability | A feature that a provider may or may not support (e.g., lay betting) |
| Circuit Breaker | A pattern that prevents cascading failures by stopping requests to failing services |
| Coordinator | The central component that routes requests to appropriate providers |
| External ID | An identifier used by a specific provider (e.g., Betfair's eventId) |
| Failover | Automatically switching to a backup provider when the primary fails |
| Fuzzy Matching | Technique to match fixtures across providers using team names, times, etc. |
| ID Registry | Service that maintains mappings between canonical IDs and external provider IDs |
| PAL | Provider Abstraction Layer - the overall architecture described in this document |
| Port | An interface defining what the application needs from external services |
| Provider | An external betting exchange or API (Betfair, Matchbook, Pinnacle) |
| Routing Strategy | The logic that decides which provider handles a given request |
Summaryβ
The Provider Abstraction Layer transforms Hannibal from a rigidly-coupled dual-provider system into a flexible, scalable multi-exchange platform.
Key Takeawaysβ
| Before | After |
|---|---|
| Adding provider = weeks of work | Adding provider = create adapter |
| Provider logic scattered everywhere | Provider logic isolated in adapters |
| Failures cascade through system | Failures contained, auto-failover |
| "Which if statement handles this?" | "Which adapter handles this?" |
| Testing requires all providers | Test adapters independently |
The 10th Provider Testβ
"A good architecture is one where adding the 10th integration is as easy as adding the 2nd."
With PAL:
- Create new adapter directory
- Implement the three interfaces
- Register in ProviderRegistry
- Add routing rules
- Deploy β no other code changes needed
Next Stepsβ
- Review & Approve this architecture document
- Create interfaces (
IOddsProvider,IOrderProvider,IStreamProvider) - Wrap existing services in
OddsPapiAdapter - Build Betfair adapter for cricket
- Enable routing via feature flags
- Monitor & iterate
Document Version: 1.0 Last Updated: January 2026 Author: Architecture Team