FX / Multi-currency
Rate sources, rate locking, settlement vs display currency, historical rates for reconciliation
Core Concepts
Settlement currency
The currency in which the platform settles with the PSP and holds internal balances. All financial positions, limits, and reconciliation run in settlement currency. Typically USD or EUR, configured per operator at onboarding.
Display currency
The currency the player sees in the UI -- their local currency (e.g. BRL, INR, NGN). Set per player based on GEO or player preference. Display amounts are always converted from settlement currency at the current rate at render time.
Rate lock (rate fixing)
The moment a player initiates a payment, the FX rate is locked for that transaction. This guarantees the player pays exactly what was shown on the checkout screen, regardless of how long PSP processing takes. The locked rate is stored on the transaction record.
Rate refresh cycle
Exchange rates are fetched from an external provider (e.g. Open Exchange Rates, Fixer.io, or ECB) and cached internally. Rates are refreshed every 15 minutes for display and locked at transaction initiation. Stale rates older than 30 minutes must not be used for new transactions.
Rate Sources
| Provider | Tier | Coverage | Cost | Use case |
|---|---|---|---|---|
Open Exchange Rates Recommended | Commercial | 170+ currencies | $12–$97/mo depending on plan | Production -- reliable, wide coverage, SLA |
Fixer.io | Commercial | 170+ currencies | $14–$99/mo | Production -- EUR base, solid European coverage |
ECB (European Central Bank) | Free / Official | 32 currencies (EUR base) | Free | Fallback / audit reference only -- too slow for live rates |
CoinGecko / Binance | Crypto | BTC, ETH, USDT, etc. | Free tier available | Required alongside fiat provider for crypto deposit support |
Rate Lock Flow
Player opens checkout
Frontend calls GET /api/fx/rate?from=USD&to=BRL. Service returns current rate from cache. Player sees amount in display currency (e.g. "500 BRL").
Player submits payment
At the moment of submission, FX Service locks the rate for this transaction. The locked rate is valid for the TTL window (e.g. 10 minutes). If PSP processing takes longer, the locked rate still applies.
Rate stored on transaction
Transaction record gets fx_rate, fx_rate_locked_at, display_currency, display_amount fields populated. These are immutable once set -- no process may change the locked rate after this point.
Orchestrator calls PSP
Payment is submitted to PSP in settlement currency (USD/EUR). PSP never sees the display currency -- conversion is internal to the platform. The amount in settlement currency was calculated from the locked rate.
Wallet Engine credits settlement amount
On deposit success, Wallet credits the settlement_amount (not display_amount). Player balance is always stored in settlement currency. The frontend converts to display currency for rendering.
Data Model
Table fx_rates -- current and historical rates
| Column | Type | Description |
|---|---|---|
id | uuid PK | Record ID |
base_currency | char(3) | ISO 4217 base currency (platform settlement currency, e.g. USD) |
quote_currency | char(3) | ISO 4217 quote currency (player display currency, e.g. BRL) |
rate | numeric(18,8) | Exchange rate: 1 base = rate * quote |
source | text | Rate provider: "openexchangerates" | "fixer" | "ecb" | "manual" |
fetched_at | timestamptz | Timestamp when rate was fetched from provider |
valid_until | timestamptz | Rate expires at this time. System rejects rates beyond this point. |
is_active | boolean | Only one active rate per currency pair. Superseded rates kept for history. |
FX fields on the transactions table
transactions table as part of Phase 3. None of them may be NULL for transactions in currencies other than settlement currency. For transactions in settlement currency -- display_currency = settlement_currency, display_amount = settlement_amount, fx_rate = 1.0.| Column | Type | Description |
|---|---|---|
settlement_currency | char(3) | Currency used for PSP and Wallet (platform base currency) |
settlement_amount | numeric(18,2) | Amount in settlement currency -- what actually moves |
display_currency | char(3) | Player's display currency at time of transaction |
display_amount | numeric(18,2) | Amount shown to player (in display currency) |
fx_rate | numeric(18,8) | Locked rate at transaction time: 1 settlement = fx_rate * display |
fx_rate_locked_at | timestamptz | When the rate was locked -- audit trail |
fx_rate_source | text | Rate provider used for this transaction |
Configuration Cases
Single-currency operator (simplest case)
Operator's settlement currency = player's display currency. FX conversion is a no-op (rate = 1.0). All amounts pass through unchanged. This is the default for EU operators running in EUR.
Settlement: EUR. Player GEO: Germany. Display: EUR. Rate: 1.0. No conversion needed.
Multi-currency operator (standard case)
Operator settles in USD. Players span multiple GEOs with local currencies (BRL, INR, MXN, etc.). FX Service converts each player deposit amount from display currency to USD at the locked rate. Limits, wallets, and reconciliation work in USD.
Player in Brazil deposits "500 BRL". Locked rate: 1 USD = 5.02 BRL. Settlement amount: 99.60 USD. Wallet is credited 99.60 USD.
PSP-currency mismatch
Platform settlement is USD, but the PSP for a given GEO only accepts EUR. Orchestrator detects the mismatch and calls FX Service to convert settlement_amount from USD to EUR before the PSP API call. A second fx_rate (settlement_to_psp) is stored on the transaction.
Settlement: 99.60 USD. PSP requires EUR. Rate: 1 USD = 0.921 EUR. PSP receives: 91.73 EUR. Both rates stored for full audit trail.
Two conversion steps introduce two rate lock moments -- both must be stored on the transaction for reconciliation and dispute resolution.
Crypto deposit (display in crypto, settle in fiat)
Player deposits USDT or BTC. Display amount is in crypto units. FX Service fetches the crypto/USD rate from a crypto provider (CoinGecko/Binance), locks it, and converts to settlement currency. Crypto rate volatility means the lock TTL should be shorter (2–3 minutes vs 10 for fiat).
Player deposits 100 USDT. Rate: 1 USDT = 0.9996 USD. Settlement: 99.96 USD. Lock TTL: 3 minutes.
For volatile assets (BTC, ETH), display the locked rate on the checkout screen with a visible countdown. If TTL expires before submission, refresh the rate and re-display.
Withdrawal in display currency
Player requests withdrawal in their local currency. Wallet holds settlement currency -- FX Service converts at the current (not locked) rate to determine the display amount shown to the player. The actual PSP transfer is in settlement or PSP currency. The rate is locked at the moment the withdrawal is approved, not when the player requests it.
Player balance: 200 USD. Requests withdrawal to BRL. Displayed: "~1,006 BRL" (preview, not locked). On approval: rate locked at 1 USD = 5.03 BRL → 1,006 BRL sent.
Affected Modules
Limits & Rules
All limits (min/max deposit, daily caps, velocity) are defined and evaluated in settlement currency. FX Service converts the player's display amount to settlement before the limits engine checks it.
KYC / AML
AML thresholds (e.g. report transactions above €10,000) are defined in a reference currency. FX Service provides conversion to determine whether a transaction crosses reporting thresholds.
Reconciliation
PSP settles in settlement currency, so reconciliation compares settlement_amount values. The fx_rate stored on each transaction is the source of truth for any historical conversion -- never re-apply current rates to past transactions.
Payment Orchestrator
Orchestrator routes in settlement currency. When a PSP only supports specific fiat currencies (not USD), Orchestrator signals FX Service to convert to the PSP's required currency before submission.
Bonus Engine
Bonus match amounts and wagering requirements are denominated in settlement currency. A player depositing in BRL gets a bonus calculated on the settlement_amount, not the display_amount.
Admin Panel analytics
All revenue and volume metrics in Admin Panel are shown in settlement currency with operator-configured display currency overlay. Cross-operator aggregates are always in settlement currency.
Admin Panel Configuration
Per-operator settlement currency
Set at operator onboarding and stored in operator_config. Cannot be changed after first transaction -- changing settlement currency requires full financial migration. Add a guard in Admin Panel: field is read-only once transactions exist.
Supported display currencies per operator
Each operator configures which display currencies are available for their players (GEO-based or explicit list). Unsupported currencies fall back to settlement currency display. Config stored as JSON array in operator_config.supported_currencies.
FX spread / markup
Platform may add a configurable spread (e.g. 1.5%) on top of the mid-market rate. Spread is configured per operator and applied when generating locked rates. The gross rate shown to the player already includes the spread -- it is never shown separately. Revenue from spread is tracked in the reconciliation fee ledger.
Rate refresh interval
Default: 15 minutes for fiat, 2 minutes for crypto. Configurable per operator in Admin Panel. Shorter intervals = more API calls to rate provider = higher cost. Platform default should be conservative (15 min) with option to reduce for high-volume operators.
Rate lock TTL
Default: 10 minutes for fiat, 3 minutes for crypto. If a transaction is not submitted to PSP within TTL, the locked rate expires and the checkout must refresh. Stored in operator_config.fx_lock_ttl_seconds. Never set above 30 minutes for fiat or 5 minutes for crypto.
Manual rate override
Admin Panel allows entering a manual rate for a currency pair (source = "manual"). Used when the rate provider is down or for testing. Manual rates expire after 1 hour and require a reason field. All manual rate events are audit-logged with admin user ID.