Currency Conversion & FP System — Specification
1. Overview
Forsyt Points (FP) is the sole internal unit across the entire platform. All balances, bets, limits, P&L, and settlements operate in FP. Real-currency conversion happens at exactly two boundaries:
- Inbound: Provider deposit (HKD/GBP/USD) → FP (when admin adds/updates provider balance)
- Outbound: FP → Provider currency (when a hedge bet is placed at Betfair/Seven)
Everything in between — player views, agent views, cascade, B-Book, ledger, Take, settlement — is FP.
┌─────────────────────────────────────────────────────────┐
│ PROVIDER LAYER │
│ Betfair (GBP) Seven (HKD) Pinnacle (USD) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ CONVERSION BOUNDARY (Global Rates) │ │
│ │ 1 GBP = 25 FP 1 HKD = 2.5 FP ... │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ FORSYT TREASURY (FP) │ │
│ │ FP Pool = Σ(provider deposits × rate) │ │
│ │ + B-Book headroom │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Agent A │ │ Agent B │ │ Agent C │ │
│ │ (INR) │ │ (HKD) │ │ (USD) │ │
│ │ sees: FP │ │ sees: FP │ │ sees: FP │ │
│ │ settles │ │ settles │ │ settles │ │
│ │ in INR │ │ in HKD │ │ in USD │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [Players] [Players] [Players] │
│ see FP only see FP only see FP only │
└─────────────────────────────────────────────────────────┘
2. Global Rate Table
Design
A single global rate table managed by admin. Not per-provider — one canonical rate per currency.
| Currency | FP per 1 unit | 1 FP = X units |
|---|---|---|
| HKD | 2.5 | 0.4 |
| INR | 0.25 | 4.0 |
| USD | 20 | 0.05 |
| GBP | 25 | 0.04 |
Rates are illustrative. Admin sets the actual values.
Key Properties
- Admin-managed, fixed rates. Not live forex. Changed manually when business decides.
- Single source of truth. Replaces the current per-provider
exchangeRatederivation inCurrencyService. - Immutable during a settlement period. Rate changes take effect at next period boundary to avoid mid-period accounting inconsistencies.
- Audit trail. Every rate change is logged with timestamp, old rate, new rate, changed_by.
Schema: CurrencyRate
model CurrencyRate {
id String @id @default(uuid())
currencyCode String @unique @map("currency_code") // "HKD", "INR", "USD", "GBP"
fpPerUnit Decimal @map("fp_per_unit") @db.Decimal(18, 6) // 1 HKD = X FP
unitPerFp Decimal @map("unit_per_fp") @db.Decimal(18, 6) // 1 FP = X HKD (denormalized inverse)
isActive Boolean @default(true) @map("is_active")
updatedAt DateTime @updatedAt @map("updated_at")
updatedBy String? @map("updated_by")
createdAt DateTime @default(now()) @map("created_at")
@@index([currencyCode])
@@map("currency_rates")
}
model CurrencyRateHistory {
id String @id @default(uuid())
currencyCode String @map("currency_code")
oldFpPerUnit Decimal @map("old_fp_per_unit") @db.Decimal(18, 6)
newFpPerUnit Decimal @map("new_fp_per_unit") @db.Decimal(18, 6)
changedBy String @map("changed_by")
changedAt DateTime @default(now()) @map("changed_at")
reason String?
@@index([currencyCode, changedAt])
@@map("currency_rate_history")
}
Relationship to Provider.exchangeRate
The existing Provider.exchangeRate field becomes derived from the global rate table:
Provider.exchangeRate = CurrencyRate.fpPerUnit WHERE currencyCode = Provider.currency
When admin updates the global rate for HKD, all HKD-denominated providers automatically use the new rate. The Provider.exchangeRate column can either:
- Option A: Be removed entirely, with
CurrencyServicereading fromCurrencyRatedirectly. - Option B: Be kept as a denormalized cache, synced on rate change.
Recommendation: Option A — remove the per-provider rate. It's the source of the current "multiple providers with different rates for same currency" warning in CurrencyService. One currency = one rate. Clean.
3. Agent Currency Assignment
Design
Each agent is onboarded with a settlement currency — the real-world currency they deal in with Forsyt. This determines:
- How their settlement obligations are expressed in reports (FP amount × rate = INR/HKD amount)
- What currency the optional dashboard conversion shows
It does NOT affect any internal accounting. Internal accounting is 100% FP.
Schema Change: Agent
// Add to Agent model:
settlementCurrency String @default("INR") @map("settlement_currency") // "INR", "HKD", "USD"
Inheritance Rule
- Agent's settlement currency applies to the entire downline (sub-agents + players).
- Sub-agents can have their own settlement currency only if they deal with Forsyt independently (rare).
- Default: inherit from parent. Override: explicit set on agent creation.
Agent Dashboard — Optional Currency Toggle
Players always see FP. Agents see FP by default, with an optional toggle to view amounts in their settlement currency:
Balance: 5,000 FP ← default view
Balance: 5,000 FP (₹20,000) ← toggled on (FP × unitPerFp for INR)
This is a display-only conversion. No data changes. Frontend multiplies amount × currencyRate.unitPerFp for the agent's settlementCurrency.
4. FP Pool & Generation
Current System
Treasury FP = Σ(provider.manualBalance × provider.exchangeRate)
FP is hard-capped by provider deposits. If deposits run out, no more FP can be allocated.
New System: Provider Deposits + B-Book Headroom
Available FP = Provider Pool + B-Book Headroom
Provider Pool = Σ(provider.manualBalance × currencyRate.fpPerUnit) ← real money backing
B-Book Headroom = Forsyt's risk appetite (configurable or infinite) ← virtual liquidity
How it works
- Provider deposits generate FP at the global rate. This is real-money-backed FP.
- When provider balances are exhausted (all hedge capacity used), Forsyt's B-Book itself becomes the provider. Bets are 100% retained by the B-Book cascade — no hedge needed.
- FP is NOT limited to deposits. It's limited to
provider_pool + bbook_headroom. Ifbbook_headroom = ∞, FP is effectively unlimited (Forsyt takes all risk). - Error condition: Only when BOTH provider pool is exhausted AND B-Book headroom is breached does the player see "insufficient liquidity."
Schema: Platform-level B-Book headroom config
PlatformSettings key: "bbook_headroom"
value: { "amount": 1000000, "unlimited": false }
If unlimited: true, the system never rejects bets due to FP shortage (only agent/player credit limits apply).
Treasury Balance Calculation (Updated)
Treasury.balancePoints = Provider Pool + B-Book Headroom - Downline Allocation
When a provider deposit increases → treasury grows. When B-Book headroom is raised → treasury grows. When admin allocates to agent → treasury shrinks.
Bet Placement Decision Tree
Player places bet (stake in FP)
│
├─ V3 Cascade routes bet through agent hierarchy
│ Each level retains their portion (B-Book)
│ Remainder = hedge amount
│
├─ IF hedge amount > 0:
│ ├─ Check provider balance (in provider currency)
│ │ hedge_in_currency = hedge_fp × currencyRate.unitPerFp
│ │
│ ├─ IF provider has sufficient balance:
│ │ └─ Place hedge at provider ✓
│ │
│ ├─ IF provider balance insufficient:
│ │ ├─ Check B-Book headroom
│ │ ├─ IF headroom available:
│ │ │ └─ Absorb remaining hedge into Forsyt B-Book ✓
│ │ │ (platform becomes the counterparty)
│ │ │
│ │ └─ IF headroom exhausted:
│ │ └─ REJECT bet with "insufficient liquidity" ✗
│ │
├─ IF hedge amount = 0 (100% retained by B-Book):
│ └─ Accept bet, no provider interaction needed ✓
5. Conversion Points — Where & When
| Event | Conversion | Direction | Rate Source |
|---|---|---|---|
| Admin adds provider deposit | Provider currency → FP | Inbound | CurrencyRate.fpPerUnit |
| Admin updates provider balance | Delta in provider currency → FP delta | Inbound | CurrencyRate.fpPerUnit |
| Hedge bet placed at provider | FP → Provider currency | Outbound | CurrencyRate.unitPerFp |
| Hedge bet settled (payout from provider) | Provider currency → FP | Inbound | CurrencyRate.fpPerUnit |
| Agent dashboard (optional display) | FP → Agent's settlement currency | Display only | CurrencyRate.unitPerFp |
| Settlement report generation | FP → Agent's settlement currency | Report only | CurrencyRate.unitPerFp at period snapshot |
| Player places bet | None | — | Everything in FP |
| Player sees balance/limits | None | — | Everything in FP |
| Cascade/B-Book routing | None | — | Everything in FP |
| Ledger entries | None | — | Everything in FP |
| Take calculation | None | — | Everything in FP |
| Commission calculation | None | — | Everything in FP |
Rule: conversion happens at system boundaries only. Internal = FP. Always.
6. Settlement & Currency
Agent Settlement Flow (Updated)
- Settlement period closes → Takes frozen (in FP)
- For each agent, compute settlement obligation:
settlement_fp = currentTake (from TakeBalance)
settlement_currency = settlement_fp × currencyRate.unitPerFp(agent.settlementCurrency) - Settlement report shows both: FP amount + currency equivalent
- Transfer & Settle operates in FP (internal). The currency amount is for real-world wire/payment reference.
Rate Snapshot at Settlement
When a settlement period transitions to grace:
- Snapshot the current
CurrencyRatevalues alongside the Take snapshots - This ensures the FP → currency conversion used in settlement reports matches the rate at period close
- Prevents disputes from rate changes during grace period
// Add to PeriodSettlementSnapshot or create new model:
model SettlementRateSnapshot {
id String @id @default(uuid())
periodId String @map("period_id")
currencyCode String @map("currency_code")
fpPerUnit Decimal @map("fp_per_unit") @db.Decimal(18, 6)
unitPerFp Decimal @map("unit_per_fp") @db.Decimal(18, 6)
snapshotAt DateTime @default(now()) @map("snapshot_at")
period SettlementPeriod @relation(fields: [periodId], references: [id])
@@unique([periodId, currencyCode])
@@map("settlement_rate_snapshots")
}
7. API Changes
New Endpoints
GET /admin/currency-rates — List all currency rates
PUT /admin/currency-rates/:code — Update rate for a currency
POST /admin/currency-rates — Add new currency
GET /admin/currency-rates/history — Audit trail of rate changes
Modified Endpoints
POST /admin/providers — Remove exchangeRate from input; derived from global rate
PATCH /admin/providers/:id — Same; rate comes from CurrencyRate table
GET /admin/providers/total-credit — Uses global rates for computation
POST /agents — Add optional settlement_currency field
PATCH /agents/:id — Allow updating settlement_currency
GET /agents/:id/dashboard — Include optional currency conversion in response
GET /agents/:id/settlement-report — Include currency-equivalent amounts
Agent Creation (Updated)
POST /agents
{
"name": "Agent Mumbai",
"code": "AGT-MUM-001",
"settlement_currency": "INR", // NEW — defaults to "INR" if omitted
...
}
8. CurrencyService Refactor
Current
- Reads rates from
Provider.exchangeRate(per-provider) - Multiple providers can disagree on rates for the same currency (logged as warning)
New
- Reads from
CurrencyRatetable (single source of truth) - Provider.currency → lookup in CurrencyRate → get rate
- No ambiguity, no warnings
class CurrencyService {
// Rate cache: currency_code → { fpPerUnit, unitPerFp }
private rates = new Map<string, { fpPerUnit: Decimal; unitPerFp: Decimal }>();
// Convert FP to any supported currency
fpToCurrency(fp: number, currencyCode: string): number { ... }
// Convert currency to FP
currencyToFp(amount: number, currencyCode: string): number { ... }
// Convert FP to provider's operating currency (for hedge placement)
fpToProvider(fp: number, providerId: string): number { ... }
// Convert provider currency to FP (for deposit/settlement inbound)
providerToFp(amount: number, providerId: string): number { ... }
// Get rate for agent's settlement currency display
getAgentDisplayRate(agentId: string): { currencyCode: string; unitPerFp: number } { ... }
}
9. Frontend Changes
Player-Facing
No changes. Everything already in FP. No currency symbols, no conversion.
Agent Dashboard (Optional Enhancement)
Add a toggle in agent settings:
Display Currency: FP (default) | INR | HKD | USD
When a non-FP currency is selected, amounts show as:
Balance: 5,000 FP (₹20,000)
P&L Today: +200 FP (+₹800)
Credit Limit: 10,000 FP (₹40,000)
Implementation: frontend-only multiplication using rate fetched from /admin/currency-rates. No backend changes for display.
Admin Panel
New Currency Rates section in Settings:
┌─────────────────────────────────────────────┐
│ Global Currency Rates │
│ │
│ Currency 1 Unit = FP Last Updated │
│ ───────── ────────── ───────────── │
│ HKD 2.50 FP 2026-03-15 │
│ INR 0.25 FP 2026-03-15 │
│ USD 20.00 FP 2026-03-15 │
│ GBP 25.00 FP 2026-03-15 │
│ │
│ [+ Add Currency] │
└─────────────────────────────────────────────┘
Provider creation form: remove exchangeRate input. Show derived rate from global table based on selected currency.
10. Migration Path
Phase 1: Global Rate Table (No Breaking Changes)
- Create
CurrencyRatetable - Seed with rates derived from existing
Provider.exchangeRatevalues - Update
CurrencyServiceto read fromCurrencyRateinstead ofProvider - Keep
Provider.exchangeRateas synced denormalized field (backwards compat) - Add admin UI for rate management
- Add
CurrencyRateHistoryfor audit
Phase 2: Agent Currency
- Add
settlementCurrencytoAgentmodel - Backfill existing agents (default: "INR" or ask admin)
- Add currency to agent creation flow
- Add optional currency toggle to agent dashboard
Phase 3: B-Book Headroom
- Add
bbook_headroomtoPlatformSettings - Update treasury calculation to include headroom
- Update bet placement to fall back to B-Book when provider balance exhausted
- Add headroom config to admin UI
Phase 4: Settlement Currency Reports
- Add
SettlementRateSnapshot - Snapshot rates at period close
- Include currency equivalents in settlement reports
- Agent settlement view shows both FP and local currency
11. Invariants & Safety
- All internal arithmetic is FP. No currency mixing in ledger, Take, cascade, or balance calculations.
- Rates are fixed and admin-controlled. No live forex risk.
- Rate changes are audited. Every change logged in
CurrencyRateHistory. - Settlement uses snapshot rates. No mid-period rate drift affecting settlements.
- Provider.exchangeRate is derived, not independent. One currency = one rate = no conflicts.
- B-Book headroom is configurable. Forsyt controls risk appetite. Can be infinite.
- FP = 0 decimals in player UI. Integer display. Backend uses Decimal(18,4) for precision.
- Conversion at boundaries only. If you're converting mid-pipeline, you're doing it wrong.
12. Edge Cases
| Scenario | Handling |
|---|---|
| Rate changed mid-settlement-period | Takes effect next period. Current period uses snapshot. |
| Provider in currency with no global rate | Reject provider creation. Currency must exist in CurrencyRate first. |
| Agent with no settlement currency set | Default to "INR". Migration backfills existing agents. |
| B-Book headroom exhausted + no provider balance | Bet rejected: "Insufficient platform liquidity". Player sees generic error. |
| Two providers same currency, different manual rates | Impossible — global rate table means one rate per currency. Provider only stores manualBalance. |
| Agent wants to change settlement currency | Admin can update. No effect on internal accounting (FP). Only affects reports. |
| Rate = 0 for a currency | Rejected at API level. fpPerUnit must be > 0. |
13. Example: End-to-End Flow
Setup
- Global rates: 1 HKD = 2.5 FP, 1 INR = 0.25 FP
- Provider "Betfair" (GBP): manualBalance = 10,000 GBP, rate = 25 FP/GBP → 250,000 FP
- Provider "Seven" (HKD): manualBalance = 100,000 HKD, rate = 2.5 FP/HKD → 250,000 FP
- B-Book headroom: 500,000 FP
- Total Treasury: 250,000 + 250,000 + 500,000 = 1,000,000 FP
- Agent "Mumbai" (settlementCurrency: INR, creditLimit: 100,000 FP)
- Player under Agent Mumbai (creditLimit: 10,000 FP)
Bet Placement
- Player bets 1,000 FP on cricket match
- V3 Cascade: Agent Mumbai retains 30% (300 FP), forwards 70% (700 FP) to platform
- Platform retains 50% (350 FP via B-Book), forwards 50% (350 FP) to hedge
- Hedge: 350 FP → Betfair (GBP): 350 × 0.04 = 14 GBP placed at Betfair
- Player balance: 10,000 - 1,000 = 9,000 FP
Settlement (Player Wins, Payout = 2,000 FP)
- Betfair hedge settles: payout = 28 GBP → 28 × 25 = 700 FP inbound
- B-Book platform portion: -350 FP (platform pays from retained pool)
- B-Book agent portion: -300 FP (agent pays from retained pool)
- Player receives: +2,000 FP. Balance: 9,000 + 2,000 = 11,000 FP
- Agent Take updated. At settlement period close:
- Take in FP: -300 FP (agent lost on this bet)
- Settlement report: -300 FP = -₹1,200 (at 1 FP = ₹4 INR)