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
- The RGS retries until it gets a definitive answer, effectively forever.
- A replay never moves money twice. Every money movement has an RGS-generated id; use it as your idempotency key.
- A
2xxis a promise that the money is durably correct. If you are uncertain, answer5xx.
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.
| Method | Path | Meaning |
|---|---|---|
| POST | /v1/balance | spendable balance; doubles as the session check when the game opens |
| POST | /v1/debit | take a stake (opens or extends a round); may settle a win in the same call |
| POST | /v1/credit | pay a win and/or close the round |
| POST | /v1/rollback | reverse one or more debits; optionally close the round |
| GET | /health | any 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
| Field | Who makes it | Notes |
|---|---|---|
token | you (at launch) | opaque string; the same on every call of the session |
round | the RGS | integer up to 2^53-1; unique per RGS, so key your rounds by (rgs, round) |
debit.id, credit.id, rollbacks[].id | the RGS | UUIDs, stable across retries: your idempotency keys |
credit.ref, rollbacks[].ref | the RGS | the debit.id being settled or reversed |
debit_id, credit_id, rollback_ids[] | you | your 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 }
active: truekeeps the round open;falsecloses it now (a 0-win close-at-play, or a single-shot round).creditis a concurrent settling credit (id,amount,ref= the debit id,currency) ornull. The stake alone must be coverable: a concurrent credit never funds its own stake (ERR_IPB).- Answer:
{ "debit_id": "…", "credit_id": "…"?, "balance": { … } }(credit_idonly when a credit was sent). - Errors:
ERR_IPB(stake not coverable),ERR_BAD(zero or negative amount, currency mismatch, schema, or a fenced debit),ERR_BC(round already closed),ERR_IS.
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" } }
- A losing close is
"credit": null(never a zero-amount credit). - The round must have at least one debit (
ERR_BNF); a new credit on a closed round isERR_BC; thetokenmust be the one that opened the round (ERR_ISotherwise, even if the round is later continued from another session). - Answer:
{ "credit_id": "…"?, "balance": { … } }. - A credit is never rolled back and never fenced: a win is owed, and the RGS retries it forever.
POST /v1/rollback
{ "token": "9f0c1d2e-…", "round": 4119983943, "active": false,
"rollbacks": [ { "id": "019a1b2c-5f60-7b90-8c2d-3e4f5a6b7c8d", "ref": "019a1b2c-3d4e-7f60-8a1b-2c3d4e5f6a7b" } ] }
- Idempotent on the debit ref: two different rollback ids for the same debit refund once.
- Answer:
{ "rollback_ids": ["…"], "balance": { … } }, in input order. - Fencing. A rollback whose
refyour wallet has never seen answers200and refunds nothing, but you must remember it: a debit with that id that arrives later is refused (ERR_BAD). This is what stops a delayed copy of a debit from charging a round the player was told failed. - A rollback whose
refnames a debit you refused answers200and refunds nothing. ERR_BCif the round was settled by a credit. If the debit is still pending on your side, answer5xxso the RGS retries.
Errors you answer with
Body: { "code": "<ERR_CODE>", "message": "…" } (note code, unlike the game API).
| Code | HTTP | Meaning | What the RGS does |
|---|---|---|---|
ERR_ATE | 401 | bad signature, or x-operator missing or not served | pages our on-call; debits are not re-sent; credits and rollbacks are parked until the key is fixed |
ERR_IS | 400 | unknown token, or the token is not the opening session's | terminal |
ERR_IPB | 400 | insufficient balance | the game gets ERR_IPB at once; the debit is fenced (a rollback follows) |
ERR_BAD | 400 | malformed, currency mismatch, fenced debit | terminal |
ERR_BNF | 404 | round never debited | terminal (but retried while our own debit is still in flight) |
ERR_BC | 400 | round already closed | terminal |
ERR_GE | 500 | undecided | retried |
ERR_UE | 500 | unhandled error during a rollback | retried |
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
- The RGS waits 3 seconds per attempt while a player's request is open, then retries in the background with exponential backoff (1 s, 2 s, 4 s, up to 60 s, with jitter) with no maximum number of attempts.
- A debit that has no answer by the game deadline is stopped and replaced by a fencing rollback, retried the same way. Your wallet may therefore see a debit after the rollback for it. Fencing is what makes that safe.
- After 15 minutes of failure we alert; after 24 hours the transaction is parked and we page, but we keep retrying hourly.
- Retries are byte-identical: the same body and the same signature. That makes it easy to prove, from your side, that two requests are the same transaction.
- Sessions do not expire in your wallet. The RGS stops accepting new bets 24 hours after launch (configurable) but still sends debits, credits and rollbacks for rounds that are open.
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:
- the idempotency record and the money movement commit in the same database transaction, with unique constraints on the ids;
- concurrent debits for one player are serialized (a row lock or a conditional update), so two stakes can never both pass the balance check;
- a refused debit's id is stored as consumed, and answered identically if the same debit arrives again;
- fences are durable;
- a
5xxis the answer to anything you cannot prove committed.
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)