Implementing the wallet

Your wallet is the system of record for player money. The RGS calls four endpoints on it. This page is the contract; the reference wallet is a working implementation to start from.

The three facts

  1. The RGS retries until it gets a definitive answer, effectively forever.
  2. A replay never moves money twice. Every money movement has an RGS-generated id; use it as your idempotency key.
  3. A 2xx is a promise that the money is durably correct. If you are uncertain, answer 5xx.

Endpoints you serve

The base URL is your wallet ingress URL, registered per environment. If you also integrate with another RGS (for example Engine), give RuneCode its own ingress path or host so the right key is used and round ids from two RGSes never collide.

MethodPathMeaning
POST/v1/balancespendable balance; doubles as the session check when the game opens
POST/v1/debittake a stake (opens or extends a round); may settle a win in the same call
POST/v1/creditpay a win and/or close the round
POST/v1/rollbackreverse one or more debits; optionally close the round
GET/healthany 2xx is healthy; unsigned; body ignored

Verifying the call

Every POST carries x-operator (your slug) and x-signature (Base64 Ed25519 over the raw body). Check the slug, verify the signature against both RGS keys (current and standby) before parsing, then parse the same bytes. See Authentication. SDK helper: verifyWalletRequest / verify_wallet_request / VerifyWalletRequest.

Money

Integers only, in micro-units: 1_000_000 is 1.00. The currency on every money object must equal the session currency (ERR_BAD otherwise). For currencies with more than six decimals the RGS also sends amountMinor (a decimal integer string) and scale, which are authoritative when present; wallets that ignore them stay compliant.

Identifiers

FieldWho makes itNotes
tokenyou (at launch)opaque string; the same on every call of the session
roundthe RGSinteger up to 2^53-1; unique per RGS, so key your rounds by (rgs, round)
debit.id, credit.id, rollbacks[].idthe RGSUUIDs, stable across retries: your idempotency keys
credit.ref, rollbacks[].refthe RGSthe debit.id being settled or reversed
debit_id, credit_id, rollback_ids[]youyour own ids, echoed identically on replays; we store them and you can search by them

POST /v1/balance

{ "token": "9f0c1d2e-…" }   →   { "balance": { "amount": 1000000000, "currency": "USD" } }

Errors: ERR_IS (unknown token), ERR_ATE (bad signature or operator). A pure read.

POST /v1/debit

{ "token": "9f0c1d2e-…", "round": 4119983943, "mode": "BASE", "active": true, "ip": "203.0.113.5",
  "debit":  { "id": "019a1b2c-3d4e-7f60-8a1b-2c3d4e5f6a7b", "amount": 1000000, "currency": "USD" },
  "credit": null }

POST /v1/credit

{ "token": "9f0c1d2e-…", "round": 4119983943, "active": false, "ip": "203.0.113.5",
  "credit": { "id": "019a1b2c-4e5f-7a80-9b1c-2d3e4f5a6b7c", "amount": 2000000, "ref": "019a1b2c-3d4e-7f60-8a1b-2c3d4e5f6a7b", "currency": "USD" } }

POST /v1/rollback

{ "token": "9f0c1d2e-…", "round": 4119983943, "active": false,
  "rollbacks": [ { "id": "019a1b2c-5f60-7b90-8c2d-3e4f5a6b7c8d", "ref": "019a1b2c-3d4e-7f60-8a1b-2c3d4e5f6a7b" } ] }

Errors you answer with

Body: { "code": "<ERR_CODE>", "message": "…" } (note code, unlike the game API).

CodeHTTPMeaningWhat the RGS does
ERR_ATE401bad signature, or x-operator missing or not servedpages our on-call; debits are not re-sent; credits and rollbacks are parked until the key is fixed
ERR_IS400unknown token, or the token is not the opening session'sterminal
ERR_IPB400insufficient balancethe game gets ERR_IPB at once; the debit is fenced (a rollback follows)
ERR_BAD400malformed, currency mismatch, fenced debitterminal
ERR_BNF404round never debitedterminal (but retried while our own debit is still in flight)
ERR_BC400round already closedterminal
ERR_GE500undecidedretried
ERR_UE500unhandled error during a rollbackretried

How the RGS classifies your answer: a 2xx with a valid body is done; a 2xx with an unparseable body is unknown and is retried (your replay returns the stored result); a 4xx with a known code above is terminal; a 4xx with an unknown code, 408, 425 and 429 are retried; any 5xx, timeout, connection or TLS error is retried.

Timing

Health

GET /health is unsigned; any 2xx means healthy. We poll it. Three consecutive failures, or a high share of unknown-outcome debits or slow debits, marks your integration degraded: new plays are paused (the game shows bets paused, launches answer 503 ERR_MAINT) while settlement retries continue. Make /health reflect whether you can really take a debit.

What a real wallet must add

The reference wallet keeps its books in memory, which is fine for learning and for testing your integration, and not for production. A production wallet applies the same rules on durable storage:

Reference wallet

It passes RuneCode's real wallet conformance kit with verdict pass (examples/reference-wallet/wcs.sh): every row, and every capability flag true (single-shot rounds, close-at-play, remembers refusals). See Testing and going live.

examples/reference-wallet/wallet.ts implements everything above in about 200 dependency-free lines, using the SDK's verification. examples/reference-wallet/e2e.sh runs the real RGS against it:

ok  authenticate: the RGS asked the wallet for the balance (signed /v1/balance) …
ok  7 plays, 2 wins: the RGS sent signed /v1/debit and /v1/credit calls
ok  the wallet's books agree with the RGS: 1000000000 -> 994000000 (expected 994000000) …
ok  a signature from another key is refused (401 ERR_ATE)