Wallet contract
The bet_intent → bet_result cycle for fidra originals settling on your money rail: idempotency, the 3-second budget, timeouts, and what happens when your wallet does not answer.
Scope — which games use this
- fidra originals (games rendered inside the feed) settle through this contract. The feed never assumes an outcome; your host page is the only party that mutates player balance.
- Your catalog games are NOT in this path. They settle natively on your existing aggregator/provider integration once the player is inside the tokenized game iframe. The feed only asks for a launch URL and emits an attribution event — it has no wallet visibility into that session at all.
Integrating only the catalog? You need nothing from this page except (optionally) wallet_update to keep the feed's balance display in sync.
The four messages
// feed → host
{ source:'fidra', proto:'fidra:v1', type:'fidra:v1:bet_intent',
roundId: string, // idempotency key
gameId: string, amount: number }
// host → feed — reply within 3s
{ source:'fidra-host', proto:'fidra:v1', type:'fidra:v1:bet_result',
roundId: string, ok: boolean,
balance?: number, // finite number, or omit to leave the display unchanged
reason?: string }
// feed → host — losses settle too (payout: 0), so you can close the round record
{ source:'fidra', proto:'fidra:v1', type:'fidra:v1:settle_intent',
roundId: string, // the SAME roundId as the bet
gameId: string, payout: number }
// host → feed — same shape as bet_result
{ source:'fidra-host', proto:'fidra:v1', type:'fidra:v1:settle_result',
roundId: string, ok: boolean, balance?: number }
Rules your wallet service MUST follow
- Echo the
roundId. It correlates intent → result → analytics (bet_placedcarries the same id). - Dedupe by
roundId, against a durable store. A secondbet_intentwith a knownroundIdis a RETRY of the same bet — reply with the original result, without re-debiting. Dedupesettle_intentthe same way. - Reply within 3 seconds (
OPERATOR_WALLET_TIMEOUT_MS = 3000, applied to both intents). After that the feed treats the bet as unconfirmed: the card returns to watch mode, nothing is debited client-side, and a retry re-posts the sameroundId. balanceon either result updates the balance shown in the feed. Losses also settle (payout: 0).
Timeouts and the unknown-outcome case
- Timeout is explicitly not treated as reject — the outcome is unknown (your reply may have been lost). This is exactly why
roundIdidempotency is load-bearing: whoever retries must get the identical result on replay. - A result arriving after the timeout is a documented no-op on the feed side.
- The feed does not automatically retry a timed-out intent today — do not build server logic that depends on the feed retrying for you.
- There is no host-side timeout in the spec; 3000ms is purely the feed's client budget.
Failure reasons — what the player sees
| reason | meaning | feed reaction |
|---|---|---|
timeout | no reply in 3s, or you replied reason:'timeout' | "bet not confirmed — try again" toast; card back to watch mode |
rejected | you replied ok:false with any other reason (or none) | "bet rejected" toast; card back to watch mode |
insufficient | demo wallet only — the operator wire format cannot express it distinctly; any custom string collapses to rejected | inline demo-wallet notice |
Currency
There is no currency field on any wallet message. Currency is established once, out-of-band, via the player context (player: { playerId, currency } in the mount config). The feed does no currency conversion — you interpret amount/payout in the declared currency, and derive multi-currency handling from your own session state, never from the wire. Outbound amounts arrive canonicalized to the session currency's decimals grid (default 2) — never float dust like 88.71000000000001; internally the feed keeps money as integer minor units (see Currency).
Unsolicited balance sync
{ source:'fidra-host', proto:'fidra:v1', type:'fidra:v1:wallet_update',
balance: number } // finite and ≥ 0, else ignored; no ack
Use after out-of-band changes — deposits, bets in another tab.
Currency — unchanged on this wire
There is still no currency field on any wallet message. The session currency is declared at mount (config.currency) and can switch mid-session via fidra:v1:currency_update — but a switch during an open round queues until that round settles, so every wallet message of a round is interpreted in the single currency the round was placed in. Details: Currency.
Test it
The sandbox wallet is a runnable reference implementation of this page, including every failure mode. Work through it before touching your real wallet service.
docs/integration/WALLET.md · apps/web/src/wallet.ts
fidra is a B2B software supplier — no gambling services operated, no player funds held.
g.jgerenaia@tomsons.co