Currency
Crypto wallets switch currency mid-session, so currency is session state, not tenant state. Four locked rules make that safe: queued switches, immutable history, no cross-currency sums, versioned schema.
Declare it at mount
Fidra.mount(el, {
// …
currency: {
code: 'USDT', // REQUIRED: uppercase ticker, 2–8 chars (USD, GEL, USDT, BTC)
symbol: 'USDT', // display symbol; default = code
decimals: 2, // fraction digits for display, 0–8; default 2
position: 'after', // 'before' ($12.00) | 'after' (125.50 USDT)
},
})
No currency declared → the feed behaves exactly as before: bare numbers, two decimals, no event stamp. Tenant config supplies defaults only — the live value is session state.
Switch it mid-session
feedWindow.postMessage({
source: 'fidra-host', proto: 'fidra:v1',
type: 'fidra:v1:currency_update',
payload: { code: 'BTC', symbol: '₿', decimals: 8 },
}, fidraOrigin)
- No open round → applies immediately: display reformats, subsequent events stamp the new code, the session money segment re-baselines.
- Open round → the switch QUEUES and lands right after the round settles — the running round keeps the currency it was placed in. Only the latest queued update wins.
- Invalid payload → ignored, state untouched (standard host-input posture).
- Pair it with a
fidra:v1:wallet_updatecarrying the balance in the new currency —currency_updatefirst, then the balance.
The four rules (locked in CI)
| rule | what it means for you |
|---|---|
| 1 · Queued switches | a running round settles in its own currency; the switch lands after settle — books never mix, the player is never harmed |
| 2 · Immutable history | every event stamps the currency in force at emit time; a later switch never rewrites earlier events |
| 3 · No cross-currency sums | session recap / breather / Wrapped show the current currency segment, labeled — never a sum across currencies (the same refusal discipline as the mixed-currency measurement readout) |
| 4 · Versioned schema | the raw-event envelope gained optional currency → event schemaVersion is now 7; branch on it if you consume raw events |
What carries currency — and what deliberately does not
| surface | carries currency? |
|---|---|
Raw event relay (envelope currency) | YES v7, stamped at track time |
| Feed display (balance, net) | YES code/symbol + decimals |
Semantic fidra:v1 messages | NO shapes are additive-frozen; correlate by roundId — your wallet knows its own currency |
Wallet RPC (bet_intent/settle_intent) | NO unchanged — see Wallet |
| Network pool (clips/ghosts) | NEVER money-free by construction; the allowlist rejects a currency field (tested) |
Outcomes rows (/api/outcomes) | YES its own required currency (v2) — Measurement |
Exact money — integer minor units
decimals is not just a display hint: internally the feed stores and mutates money as an integer count of minor units (10^decimals per whole unit; scale 2 when no currency is declared), converting to decimal only at the wire/display edge. Sub-cent arithmetic is exact at up to 8 decimals — 0.1 + 0.2 of BTC is 30000000 minor units, never 0.30000000000000004 — and outbound bet_intent/settle_intent amounts are snapped to the same grid before posting.
An applied currency_update that changes decimals re-scales the stored integer to the new grid in the same atomic update as the segment re-baseline (upscaling exact, downscaling rounds to the coarser grid).
Stakes follow the currency
Bet presets, min/max and stepper increments are a property of the currency, not the tenant — 25–500 coins is sane for a GEL-class unit and absurd for BTC. Supply config.stakes at mount, keyed by uppercase currency code:
stakes: {
GEL: { presets: [1, 2, 5, 10], min: 1, max: 200, step: 1, defaultBet: 2 },
BTC: { presets: [0.0001, 0.0005, 0.001], min: 0.0001, max: 0.01, step: 0.0001, defaultBet: 0.0005 },
}
The active set follows the active session currency — an applied currency_update switches stakes together with the balance, honoring the same open-round queue. defaultBet seats the dial games and is the fixed ritual stake. A malformed entry is dropped whole; that currency falls back to the built-in coin defaults (chips 25/50/100/250, range 25–500, default 50). presets needs 2–6 values inside [min, max].
Host balance authority (bridge mode): in wallet: 'operator' mode the balance is your number — after an applied switch the feed's re-scaled integer is only a temporary projection. The authoritative value is your next balance assertion (wallet_update, or a bet_result/settle_result balance). If none arrives within 3 s the feed shows the balance as unknown (—) rather than a number your books may not match. Always pair a currency_update with a wallet_update — currency_update first.
docs/integration/CURRENCY.md · apps/web/src/currency.ts
fidra is a B2B software supplier — no gambling services operated, no player funds held.
g.jgerenaia@tomsons.co