CONTRACT · wallet: operator

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.

LAST UPDATED 2026-07-27

Scope — which games use this

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

  1. Echo the roundId. It correlates intent → result → analytics (bet_placed carries the same id).
  2. Dedupe by roundId, against a durable store. A second bet_intent with a known roundId is a RETRY of the same bet — reply with the original result, without re-debiting. Dedupe settle_intent the same way.
  3. 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 same roundId.
  4. balance on either result updates the balance shown in the feed. Losses also settle (payout: 0).

Timeouts and the unknown-outcome case

Failure reasons — what the player sees

reasonmeaningfeed reaction
timeoutno reply in 3s, or you replied reason:'timeout'"bet not confirmed — try again" toast; card back to watch mode
rejectedyou replied ok:false with any other reason (or none)"bet rejected" toast; card back to watch mode
insufficientdemo wallet only — the operator wire format cannot express it distinctly; any custom string collapses to rejectedinline demo-wallet notice
A bet failure is never an error state and never a phantom win/loss — the card simply returns to watch mode with a toast.

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.

source of truth: 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