A provably-fair crash game over a seamless single-wallet API. Player funds stay with you — the RGS never custodies money. You implement four wallet endpoints; we call them during a round. This page is the full contract.
You (the operator) hold the licence, KYC/AML and the wallet. The RGS (our remote game server) runs the game. You call us to launch and to verify; we call you to move money.
💶
Money = integer minor units (cents) + an ISO 4217 currency — always JSON integers, never float. Every wallet success returns the post-operation balance.
DIRECTION — who calls whom
operator ─ POST /v1/sessions ─────────▶ RGS launch (you call us)
player ─ opens launch_url ──────────▶ RGS browser / client
RGS ─ POST {wallet_base}/bet ────▶ operator debit (we call you)
RGS ─ POST {wallet_base}/win ────▶ operator credit
RGS ─ POST {wallet_base}/rollback▶ operator void a bet
anyone ─ GET /v1/verify/{round_id} ─▶ RGS public provably-fair proof
Signing requests
Every request in both directions carries four headers. The signature covers a hash of the raw body, so it survives proxies that re-serialize. Reject anything older than 30 s or with a replayed nonce, and compare the signature in constant time. TLS required in production.
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
// verify an inbound RGS wallet call with your WALLET secret
function verify(secret, rawBody, h) {
const ts = Number(h["x-zs-timestamp"]);
if (Math.abs(Date.now() - ts) > 30_000) return false; // STALE_REQUEST
if (seenNonce(h["x-zs-nonce"])) return false; // replay → STALE_REQUEST
const bodyHash = createHash("sha256").update(rawBody).digest("hex");
const expect = createHmac("sha256", secret)
.update(`${ts}.${h["x-zs-nonce"]}.${bodyHash}`)
.digest("hex");
const a = Buffer.from(expect);
const b = Buffer.from(h["x-zs-signature"] || "");
return a.length === b.length && timingSafeEqual(a, b); // constant-time
}
Two secrets
Two separate shared secrets per operator, both rotatable. Keep them distinct — a leaked launch secret must not let anyone sign wallet calls.
launch secret
POST /v1/sessions
operator → RGS (inbound)
wallet secret
/balance /bet /win /rollback
RGS → operator (outbound)
Anti-replay and idempotency are orthogonal: a logical retry reuses the same request_id but with a fresh nonce + timestamp. Dedupe on request_id (money); reject a repeated nonce (auth).
POST/v1/sessionsoperator → RGS· you call ussigned with launch secret
Launch a session
The one call you make to start play. Sign it with your launch secret, then redirect the player to the returned launch_url. Every later wallet call carries this session_id.
Body parameters
operator_id
stringrequired
Must equal the authenticated signer (X-ZS-Account) — otherwise 401 INVALID_SIGNATURE.
player_id
stringrequired
Your opaque id for the player. The RGS never receives PII.
game_id
stringrequired
The identifier we assign you at onboarding; use verbatim.
currency
stringrequired
ISO 4217, 3-letter, e.g. "EUR".
mode
stringrequired
"real" or "demo". demo never touches your wallet endpoints.
server_seed_reveal and crash_point are null until the round has crashed — revealing early would leak the result.
→
After crash: sha256(server_seed_reveal) === server_seed_commit, and crash_point recomputes from the revealed seed + client_seed.
→
Unknown round → 404 SESSION_NOT_FOUND.
POST{wallet_base}/balanceRGS → operator· you implementsigned with wallet secret
Balance
The RGS asks you for the current wallet balance for an in-play session. Verify the signature with your wallet secret; return the balance in minor units.
POST{wallet_base}/betRGS → operator· you implementsigned with wallet secret
Bet — debit
Fires when the round is derived. Debit the player and return the post-operation balance. Idempotent on request_id — a replay returns the same result and never double-debits.
Notable fields
request_id
stringrequired
RGS-generated, globally unique — the ONLY dedupe key.
amount
integerrequired
Stake in minor units. Reject unaffordable with 402 INSUFFICIENT_FUNDS.
Reject an unaffordable bet with 402 INSUFFICIENT_FUNDS — definitive, no retry.
POST{wallet_base}/winRGS → operator· you implementsigned with wallet secret
Win — credit
Fires at settlement. Always sent — even amount:0 on a loss — so every round closes with a clean bet+settle pair. Idempotent on request_id; the RGS retries until acknowledged, so a credit is never dropped.
/win never fails terminally. The RGS retries with backoff until you ack — a credit survives restarts.
POST{wallet_base}/rollbackRGS → operator· you implementsigned with wallet secret
Rollback — void a bet
Voids a previously applied bet. Its own request_id is derived as <bet request_id>:rollback; ref_tx is the bet being reversed. Note there is no game_id in this body.
NOOP rule: if the referenced bet never applied (timed out and never landed) or is already voided, rollback is a no-op success with status:"NOOP" — not an error. Return the current balance.
Idempotency & retries
The money-critical core. request_id is generated by the RGS, globally unique, and is the only dedupe key. Persist processed request_ids and, on replay, return the original stored result without re-applying.
How the RGS classifies your response
200 + numeric balance
success
Success. Money moved (or was already applied on a replay).
5xx or code INTERNAL
ambiguous
Ambiguous — the RGS retries the same request_id.
4xx business error
definitive
Definitive — you rejected; no money moved.
timeout / network error
ambiguous
Ambiguous — the RGS retries.
/bet ambiguous, retries exhausted
The RGS issues /rollback (NOOP-safe), then fails the bet to the player. A player is never debited for a round that didn't run.
/win never fails terminally
The RGS retries with backoff until acknowledged. A credit not yet acked is retried later and survives restarts.
Round lifecycle
One round = exactly one /bet and one /win (plus at most one /rollback voiding the bet). That pairing is the reconciliation key.
ONE ROUND
client place_bet(amount)
│
├─▶ /bet (debit) ── fails ▶ reject bet to client (no round)
│ │ OK
├─▶ engine derives round ── errors ▶ /rollback(bet) ▶ reject
│ │ crash point committed · round state frozen
│ … round plays; player cashes out, or it busts …
├─▶ engine settles → outcome
└─▶ /win (credit, amount maybe 0, round_final:true) ── retries until acked
Reconciliation
Both sides ledger every transaction. Reconcile by round_id: each round shows the RGS's (bet, win[, rollback]) against your wallet movements; any mismatch is alerted. See the worked example below.
Error codes
Any non-OK body is { "error": { "code":"…", "message":"…" } } with an appropriate 4xx/5xx. The 10 canonical codes:
INVALID_SIGNATUREBad HMAC, or unknown/inactive account.
STALE_REQUESTTimestamp outside the 30 s window, or a replayed nonce.
SESSION_EXPIREDSession is past its TTL.
SESSION_NOT_FOUNDUnknown session_id (or unknown round on verify).
TX_ALREADY_PROCESSEDInformational; return the original stored result.
MALFORMED_REQUESTBad JSON, missing field, or non-integer amount.
INTERNALYour transient failure — the RGS treats it as ambiguous and retries.
Onboarding
The RGS resolves the operator per X-ZS-Account on every inbound call. We set up your integration record together — the two secrets are generated and exchanged over a secure channel, your wallet base URL is registered, and your account is activated. Secrets are stored encrypted and rotatable at any time.
account id
Matches X-ZS-Account on every inbound call.
launch secret
Verifies your POST /v1/sessions.
wallet secret
Signs the RGS outbound wallet calls to you.
wallet base URL
Where the RGS sends /bet /win /rollback /balance.
active flag
An unknown OR inactive account is rejected identically (401 INVALID_SIGNATURE).
Worked example
One session → one bet → one win → reconciliation. Player starts with 10 000.00 EUR (1000000), bets 10.00 (1000), cashes out at 2.00× → win 20.00 (2000).