Skip to main content

πŸ—οΈ 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​

  1. Executive Summary
  2. The Problem We're Solving
  3. Architecture Vision
  4. Core Concepts
  5. The Four Pillars
  6. Data Flow & Use Cases
  7. Provider Deep Dives
  8. Routing Intelligence
  9. Failure & Recovery
  10. Migration Strategy
  11. Comparison: Why Not Oracle?
  12. Identity Resolution: The ID Mapping Challenge ⭐ NEW
  13. File Structure
  14. 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:

GoalHow We Achieve It
Add new exchanges in days, not weeksPlug-in adapter system
Sport-specific routingIntelligent capability-based routing
Zero business logic changesClean separation via interfaces
Graceful degradationAutomatic failover between providers
Preserve exchange-specific featuresCapability 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.ts with more if statements
  • Add betfairApi.ts alongside existing services
  • Duplicate routing logic across multiple files
  • Hope we don't break existing Pinnacle flow

What happens when we add Matchbook?

  • Even more if statements
  • 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​

ComponentResponsibilityAnalogy
Exchange CoordinatorRoutes requests, handles failoverAir Traffic Control
Provider RegistryKnows all available providersPhone Book
Routing StrategyDecides which provider to useGPS Navigation
AdaptersTranslate between PAL and exchangeLanguage Translators
Canonical ModelsInternal data formatUniversal 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:

ResponsibilityDescription
AuthenticationHandle exchange-specific auth (API keys, SSL certs, OAuth)
API CommunicationREST, WebSocket, or proprietary protocols
Data TransformationConvert exchange format ↔ canonical format
Error HandlingMap exchange errors to standard error types
Rate LimitingRespect exchange-specific limits
Health MonitoringTrack 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:

ProviderBackLayStreamingIn-PlayCash OutSports
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 Service doesn't know about Betfair
  • Routing Strategy decided Betfair based on sport
  • Betfair Adapter handled all translation
  • If Betfair was down, Exchange Coordinator would 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)

AspectDetails
AuthenticationSSL certificate + session token
APIs UsedBetting API (REST), Exchange Stream API (WebSocket)
Unique FeaturesFull market depth, BSP, lay betting, cash out
Rate Limits~5 concurrent requests, 50 markets per listMarketBook
Event Type IDCricket = 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

AspectDetails
AuthenticationAPI key (handled by OddsPAPI)
APIs UsedOddsPAPI (odds), BETS API (orders)
Unique FeaturesSharp lines, high limits, wide coverage
LimitationsBack-only (no lay betting), no streaming
Sport IDSoccer = 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

AspectDetails
AuthenticationUsername/password + session
APIs UsedREST API, Streaming API
Unique FeaturesLow commission, P2P exchange
Potential UseFailover for Betfair, price comparison

Routing Intelligence​

The Routing Decision Tree​

Routing Rules Configuration​

The routing strategy is configurable, not hard-coded:

PriorityConditionProviderRationale
100sport = cricketBetfairBest liquidity, native lay
90sport = cricket, betfair_downMatchbookFailover
100sport = soccer, need_layMatchbookP2P exchange
80sport = soccerPinnacleSharp lines
50*PinnacleDefault fallback

Failure & Recovery​

Health States​

Each provider can be in one of three states:

Failover Scenarios​

ScenarioDetectionActionRecovery
API Timeout5s timeoutRetry once, then failoverAuto-retry after 30s
Auth Failure401/403Refresh session, retryIf fails, mark degraded
Rate Limited429Backoff, route to secondaryResume after cooldown
Connection LostSocket closeImmediate failoverReconnect in background
Data CorruptionValidation failReject, log, continueInvestigate manually

Circuit Breaker Pattern​


Migration Strategy​

Phase 0: Foundation (Week 1)​

Goal: Create the infrastructure without changing existing behavior

Migration Checklist​

PhaseTaskRiskRollback Plan
0Create interfacesNoneN/A
1Create OddsPAPI adapterLowDelete adapter, use services directly
2Route through coordinatorMediumFeature flag to bypass
3Add Betfair adapterMediumRoute cricket back to OddsPAPI
4Enable Betfair for cricketHighInstant rollback via config
5Add MatchbookLowJust 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​

AspectOracle PatternPAL Architecture
Adding new providerModify Oracle (risky)Add new adapter (isolated)
Provider-specific features❌ Lost (lowest common denominator)βœ… Preserved via capabilities
TestingHard (everything coupled)Easy (test adapters independently)
FailoverManual implementationBuilt-in with circuit breakers
PerformanceBottleneckParallel provider calls
Team scalabilityOne team owns OracleTeams 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:

ExchangeMatch: India vs Australia, Jan 25 2026
OddsPAPI27010001234567 (fixtureId)
Betfair32456789 (eventId) + 1.234567890 (marketId)
Matchbookevt-abc-123 (eventId)
Your Internalfixture_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_idproviderexternal_idexternal_typeconfidence
fix_abc123oddspapi27010001234567fixture1.00
fix_abc123betfair32456789event0.95
fix_abc123betfair1.234567890market_MATCH_ODDS1.00
fix_abc123matchbookevt-abc-123event0.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):

CriterionWeightTolerance
SportRequiredExact match
Start TimeHighΒ±5 minutes
Team NamesHighFuzzy string match (Levenshtein)
CompetitionMediumFuzzy match
VenueLowIf available

Fuzzy String Matching Examples:

Provider AProvider BMatch?
"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 TypeDescriptionProviders Using It
Betradar IDSports data providerMany bookmakers
ESPN IDCommon in US sportsUS-focused providers
betfairIdBetfair's IDOddsPAPI 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:

ScenarioStrategyRationale
IPL, World Cup, Premier LeagueEagerHigh traffic, pre-populate
Smaller leaguesLazyResolve on first request
New provider addedBatch jobMap all existing fixtures
Live matchesReal-timeSync as matches go live

Handling Market IDs​

Markets add another layer of complexity. The same market type has different IDs:

Market TypeOddsPAPIBetfairMatchbook
Match WinnermarketId: 101marketId: 1.234567market: "winner"
Over/Under 2.5marketId: 201, handicap: 2.5marketId: 1.234568market: "totals-2.5"
Asian Handicap -1.5marketId: 301, handicap: -1.5marketId: 1.234569market: "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:

OperationDescriptionUse Case
registerMappingStore a new ID relationshipAfter fuzzy match or explicit link
getCanonicalIdExternal ID β†’ Our IDReceiving data from provider
getExternalIdOur ID β†’ External IDSending request to provider
resolveFixtureFind/create canonical fixtureNew 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_namealiassport
Mumbai IndiansMIcricket
Mumbai IndiansMumbaicricket
Chennai Super KingsCSKcricket
Chennai Super KingsChennaicricket
Manchester UnitedMan Utdsoccer
Manchester UnitedMan Unitedsoccer
Manchester UnitedMUFCsoccer

Updated Architecture with ID Registry​

Summary: ID Resolution​

ChallengeSolution
Different fixture IDs per providerID Registry with mapping table
Discovering new mappingsFuzzy matching (teams + time + sport)
Cross-provider referencesUse betfairId, betradarId when available
Market ID differencesMap by market type + parameters
PerformanceRedis cache for hot mappings
Team name variationsAlias table for fuzzy matching
Confidence trackingStore 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​

TermDefinition
AdapterA module that translates between PAL and a specific exchange's API
Canonical IDOur internal identifier for a fixture/market, independent of any provider
Canonical ModelThe standard internal data format used throughout Hannibal
CapabilityA feature that a provider may or may not support (e.g., lay betting)
Circuit BreakerA pattern that prevents cascading failures by stopping requests to failing services
CoordinatorThe central component that routes requests to appropriate providers
External IDAn identifier used by a specific provider (e.g., Betfair's eventId)
FailoverAutomatically switching to a backup provider when the primary fails
Fuzzy MatchingTechnique to match fixtures across providers using team names, times, etc.
ID RegistryService that maintains mappings between canonical IDs and external provider IDs
PALProvider Abstraction Layer - the overall architecture described in this document
PortAn interface defining what the application needs from external services
ProviderAn external betting exchange or API (Betfair, Matchbook, Pinnacle)
Routing StrategyThe 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​

BeforeAfter
Adding provider = weeks of workAdding provider = create adapter
Provider logic scattered everywhereProvider logic isolated in adapters
Failures cascade through systemFailures contained, auto-failover
"Which if statement handles this?""Which adapter handles this?"
Testing requires all providersTest 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:

  1. Create new adapter directory
  2. Implement the three interfaces
  3. Register in ProviderRegistry
  4. Add routing rules
  5. Deploy β€” no other code changes needed

Next Steps​

  1. Review & Approve this architecture document
  2. Create interfaces (IOddsProvider, IOrderProvider, IStreamProvider)
  3. Wrap existing services in OddsPapiAdapter
  4. Build Betfair adapter for cricket
  5. Enable routing via feature flags
  6. Monitor & iterate

Document Version: 1.0 Last Updated: January 2026 Author: Architecture Team