Skip to main content

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:

  1. Inbound: Provider deposit (HKD/GBP/USD) → FP (when admin adds/updates provider balance)
  2. 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.

CurrencyFP per 1 unit1 FP = X units
HKD2.50.4
INR0.254.0
USD200.05
GBP250.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 exchangeRate derivation in CurrencyService.
  • 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 CurrencyService reading from CurrencyRate directly.
  • 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:

  1. How their settlement obligations are expressed in reports (FP amount × rate = INR/HKD amount)
  2. 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

  1. Provider deposits generate FP at the global rate. This is real-money-backed FP.
  2. 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.
  3. FP is NOT limited to deposits. It's limited to provider_pool + bbook_headroom. If bbook_headroom = ∞, FP is effectively unlimited (Forsyt takes all risk).
  4. 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

EventConversionDirectionRate Source
Admin adds provider depositProvider currency → FPInboundCurrencyRate.fpPerUnit
Admin updates provider balanceDelta in provider currency → FP deltaInboundCurrencyRate.fpPerUnit
Hedge bet placed at providerFP → Provider currencyOutboundCurrencyRate.unitPerFp
Hedge bet settled (payout from provider)Provider currency → FPInboundCurrencyRate.fpPerUnit
Agent dashboard (optional display)FP → Agent's settlement currencyDisplay onlyCurrencyRate.unitPerFp
Settlement report generationFP → Agent's settlement currencyReport onlyCurrencyRate.unitPerFp at period snapshot
Player places betNoneEverything in FP
Player sees balance/limitsNoneEverything in FP
Cascade/B-Book routingNoneEverything in FP
Ledger entriesNoneEverything in FP
Take calculationNoneEverything in FP
Commission calculationNoneEverything in FP

Rule: conversion happens at system boundaries only. Internal = FP. Always.


6. Settlement & Currency

Agent Settlement Flow (Updated)

  1. Settlement period closes → Takes frozen (in FP)
  2. For each agent, compute settlement obligation:
    settlement_fp = currentTake (from TakeBalance)
    settlement_currency = settlement_fp × currencyRate.unitPerFp(agent.settlementCurrency)
  3. Settlement report shows both: FP amount + currency equivalent
  4. 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 CurrencyRate values 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 CurrencyRate table (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)

  1. Create CurrencyRate table
  2. Seed with rates derived from existing Provider.exchangeRate values
  3. Update CurrencyService to read from CurrencyRate instead of Provider
  4. Keep Provider.exchangeRate as synced denormalized field (backwards compat)
  5. Add admin UI for rate management
  6. Add CurrencyRateHistory for audit

Phase 2: Agent Currency

  1. Add settlementCurrency to Agent model
  2. Backfill existing agents (default: "INR" or ask admin)
  3. Add currency to agent creation flow
  4. Add optional currency toggle to agent dashboard

Phase 3: B-Book Headroom

  1. Add bbook_headroom to PlatformSettings
  2. Update treasury calculation to include headroom
  3. Update bet placement to fall back to B-Book when provider balance exhausted
  4. Add headroom config to admin UI

Phase 4: Settlement Currency Reports

  1. Add SettlementRateSnapshot
  2. Snapshot rates at period close
  3. Include currency equivalents in settlement reports
  4. Agent settlement view shows both FP and local currency

11. Invariants & Safety

  1. All internal arithmetic is FP. No currency mixing in ledger, Take, cascade, or balance calculations.
  2. Rates are fixed and admin-controlled. No live forex risk.
  3. Rate changes are audited. Every change logged in CurrencyRateHistory.
  4. Settlement uses snapshot rates. No mid-period rate drift affecting settlements.
  5. Provider.exchangeRate is derived, not independent. One currency = one rate = no conflicts.
  6. B-Book headroom is configurable. Forsyt controls risk appetite. Can be infinite.
  7. FP = 0 decimals in player UI. Integer display. Backend uses Decimal(18,4) for precision.
  8. Conversion at boundaries only. If you're converting mid-pipeline, you're doing it wrong.

12. Edge Cases

ScenarioHandling
Rate changed mid-settlement-periodTakes effect next period. Current period uses snapshot.
Provider in currency with no global rateReject provider creation. Currency must exist in CurrencyRate first.
Agent with no settlement currency setDefault to "INR". Migration backfills existing agents.
B-Book headroom exhausted + no provider balanceBet rejected: "Insufficient platform liquidity". Player sees generic error.
Two providers same currency, different manual ratesImpossible — global rate table means one rate per currency. Provider only stores manualBalance.
Agent wants to change settlement currencyAdmin can update. No effect on internal accounting (FP). Only affects reports.
Rate = 0 for a currencyRejected 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

  1. Player bets 1,000 FP on cricket match
  2. V3 Cascade: Agent Mumbai retains 30% (300 FP), forwards 70% (700 FP) to platform
  3. Platform retains 50% (350 FP via B-Book), forwards 50% (350 FP) to hedge
  4. Hedge: 350 FP → Betfair (GBP): 350 × 0.04 = 14 GBP placed at Betfair
  5. Player balance: 10,000 - 1,000 = 9,000 FP

Settlement (Player Wins, Payout = 2,000 FP)

  1. Betfair hedge settles: payout = 28 GBP → 28 × 25 = 700 FP inbound
  2. B-Book platform portion: -350 FP (platform pays from retained pool)
  3. B-Book agent portion: -300 FP (agent pays from retained pool)
  4. Player receives: +2,000 FP. Balance: 9,000 + 2,000 = 11,000 FP
  5. 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)