# RuneCode RGS: operator integration guide

> Generated from `docs/` by `tools/build-docs.mjs`; edit the pages, not this file.

## Contents

- [Start here](#00-start-here)
- [Quickstart](#01-quickstart)
- [Authentication and signing](#02-authentication)
- [Launching games](#03-launching-games)
- [Catalogue](#04-catalogue)
- [Implementing the wallet](#05-wallet)
- [Rounds and reconciliation](#06-rounds-and-reconciliation)
- [Errors and limits](#07-errors-and-limits)
- [SDKs](#08-sdks)
- [Testing and going live](#09-testing-and-going-live)
- [Engine operators](#10-engine-operators)
- [Availability](#11-availability)
- [Glossary](#12-glossary)

<a id="00-start-here"></a>

# RuneCode RGS: integration guide

This guide is for the engineers of an **operator** (a casino or lobby that shows our games to its players) or an
**aggregator** (a platform that fronts many operators) who want to offer RuneCode games. It describes the integration
as it works, the SDKs that implement it, and what is live today versus what is still planned.

If you already integrate games through Stake Engine, read [Engine operators](#10-engine-operators) first: the
operator API and the wallet contract are the same, and moving over is mostly configuration.

## The model in one page

Two parties and one rule decide everything.

- The **RGS** (Remote Game Server, run by us) decides every outcome. It never holds player money.
- The **operator wallet** (run by you) is the system of record for player money.
- **A `2xx` from your wallet is a promise that the money is durably correct.** If you are not sure, answer `5xx`:
  the RGS retries, effectively forever, and every retry is safe because every money movement carries an id you use as an
  idempotency key.

There are three surfaces, and you build two of them:

| Surface | Direction | You build | Where it is described |
|---|---|---|---|
| **Operator API** | you → RGS | calls to launch games and look rounds up | [Launching games](#03-launching-games), [Catalogue](#04-catalogue), [Rounds and reconciliation](#06-rounds-and-reconciliation) |
| **Wallet API** | RGS → you | four endpoints on your side: balance, debit, credit, rollback | [Implementing the wallet](#05-wallet) |
| **Game API** | player's browser → RGS | nothing: our game front ends talk to it | (internal; Engine-compatible) |

Both directions are authenticated with **Ed25519 signatures** over the exact bytes of each request. You never share a
secret: you give us your public key and we give you ours. See [Authentication and signing](#02-authentication).

## What a round looks like

```
 your lobby                 RuneCode RGS                         your wallet
     │  POST /v1/game/url        │                                    │
     │  (signed, token T) ─────► │ stores T, mints a session          │
     │ ◄───── {url, fair} ───────│                                    │
     │  redirect the player      │                                    │
     │  to url (valid ~120 s)    │                                    │
     │                           │ player opens the game              │
     │                           │ ── POST /v1/balance {token:T} ───► │  (the session check)
     │                           │ ◄──────── {balance} ────────────── │
     │                           │ player spins                       │
     │                           │ ── POST /v1/debit  {round, id…} ─► │  stake taken
     │                           │ ◄──────── {debit_id, balance} ──── │
     │                           │ outcome drawn, round settles       │
     │                           │ ── POST /v1/credit {round, id…} ─► │  win paid (omitted for a loss)
     │                           │ ◄──────── {credit_id, balance} ─── │
```

The player's browser never sees your token: it only gets an RGS-minted session id inside a one-time, signed URL.

## What you need

1. An Ed25519 key pair for each environment (`openssl genpkey -algorithm ed25519`). Keep the private key; send the public key.
2. A wallet endpoint, reachable by us, that implements four calls. We provide a [reference wallet](#05-wallet) to start from.
3. The list of lobby origins that will frame the games (we restrict framing to them).
4. For production: licences per jurisdiction, and the onboarding steps in [Testing and going live](#09-testing-and-going-live).

## Where to go next

- [Quickstart](#01-quickstart): from zero to a launched game in your own environment, step by step.
- [SDKs](#08-sdks): TypeScript, Python, Go, Java, Rust, PHP and C#. They sign requests, call the operator API and verify our wallet calls.
- [Availability](#11-availability): exactly what is live today and what is planned. Read this before you plan a schedule.


---

<a id="01-quickstart"></a>

# Quickstart

You will launch a game, serve a wallet, and watch real signed calls cross in both directions. Everything below runs on
your own machine against local copies of our services; nothing here needs production access.

## 1. Make a key pair

```sh
openssl genpkey -algorithm ed25519 -out operator.key        # PRIVATE: never share it
openssl pkey -in operator.key -pubout -out operator.pub     # PUBLIC: this goes to us
```

Use a **different pair for every environment**. A sandbox key must never be able to sign a production call.

## 2. Pick an SDK

| Language | Package | Notes |
|---|---|---|
| TypeScript / Node 22+ | `typescript/` | `node:crypto` and `fetch`, no dependencies |
| Python 3.9+ | `python/` | standard library only (pure-Python Ed25519) |
| Go 1.22+ | `go/` | standard library only |
| Java 17+ | `java/` | JDK only (`java.net.http`, built-in Ed25519) |
| Rust | `rust/` | signing, verification and request building; you bring the HTTP client |
| PHP 8.1+ | `php/` | `ext-sodium` and `ext-json` only (both ship with PHP) |
| C# / .NET 8+ | `csharp/` | the .NET base library only (pure Ed25519 over `System.Numerics`) |

Each SDK passes the same known-answer vectors (`vectors/signing.json`) and is tested against a real `operator-api`
(`interop/run.sh`), so a signature that one SDK produces is the one every other produces. Usage is in [SDKs](#08-sdks).

## 3. Call the operator API

Your operator id is the slug we give you at registration (for example `acme`, and `acme-sbx` for sandbox).

```ts
import { OperatorClient } from "@runecode/operator-sdk";

const rgs = new OperatorClient({
  baseUrl: process.env.RUNECODE_OPERATOR_API!,        // from your onboarding email
  operatorId: "acme",
  privateKeyPem: process.env.RUNECODE_OPERATOR_PRIVATE_KEY_PEM!,
  fullRequestSigning: true,                           // recommended: timestamped, covers method, path and query
});

const games = await rgs.games("runecode");            // what you may launch
const { url } = await rgs.launch({
  product: "runecode", publisher: "wizards", game: "royal-tiger-v2",
  user: "player-48213",                               // your stable, opaque player id
  token: crypto.randomUUID(),                         // one-time session token; comes back on every wallet call
  currency: "USD", language: "en", device: "desktop",
});
// redirect the player's browser to `url` (single use, valid about 120 seconds). Never log it.
```

## 4. Serve the wallet

The RGS calls you back on `/v1/balance`, `/v1/debit`, `/v1/credit` and `/v1/rollback`. Start from the reference wallet,
which already enforces the rules that matter (signature before parsing, idempotency on every id, fencing):

```sh
cd examples/reference-wallet
RGS_PUBLIC_KEYS=rgs.pem OPERATOR_SLUG=acme PORT=8080 node wallet.ts
```

`rgs.pem` is the RGS wallet public key we give you (both the `current` and the `standby` key; accept either at all times).
The full contract is in [Implementing the wallet](#05-wallet).

## 5. See it work, end to end

`examples/reference-wallet/e2e.sh` starts the real RGS (`play-core`) next to the reference wallet, creates a player
session, plays rounds, and checks that the wallet's books equal what the RGS paid. It prints one line per step:

```
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)
```

`interop/run.sh` does the same for the operator API with every SDK, including the refusal cases.

## 6. Next

Read [Testing and going live](#09-testing-and-going-live) for the onboarding flow and the go-live checklist.


---

<a id="02-authentication"></a>

# Authentication and signing

Every call in both directions carries an Ed25519 signature. There are no API keys, bearer tokens or shared secrets.

## The keys

| Key | Who holds the private half | What it signs | How the other side gets the public half |
|---|---|---|---|
| **Operator key** | you | your calls to the operator API | you upload it (PEM) when you register |
| **RGS wallet key** | us | our calls to your wallet | you download it (PEM and a SHA-256 fingerprint) and confirm the fingerprint out of band |

- Ed25519 only, as PEM: private keys in PKCS#8 (`-----BEGIN PRIVATE KEY-----`), public keys as SPKI
  (`-----BEGIN PUBLIC KEY-----`). Both are what `openssl genpkey -algorithm ed25519` and `openssl pkey -pubout` produce.
  RSA is not accepted.
- **Two active keys per operator** at any time, so you can rotate without downtime: a signature valid under either key is accepted.
- **We always publish two RGS wallet keys, `current` and `standby`.** Your wallet must accept a signature from either, at all
  times. On a compromise we switch to `standby` immediately with no action on your side. A planned rotation is announced at
  least 14 days ahead.
- One key per purpose per environment. Never reuse a sandbox key in production.

## Signing a request to the operator API

Send these headers:

| Header | Value |
|---|---|
| `X-Operator` | your operator id (the slug you were given; distinct per environment) |
| `X-Signature` | standard Base64 (with padding, 88 characters) of the Ed25519 signature |
| `Content-Type` | `application/json` on POST |

There are two signing forms. Use the second one if you can.

### Engine form: the body only

The signature covers **the exact bytes of the request body**. For a `GET` it covers the empty string. This is the form
Engine uses, so an Engine-written client keeps working. It proves who sent the call but not when, and it does not cover
the path or query, so the RGS only uses query parameters for read filters.

### Full-request form (A9): method, path, query and a timestamp

Add the header `RuneCode-Timestamp: <unix seconds>` and sign this string (UTF-8):

```
METHOD "\n" PATH "\n" CANONICAL_QUERY "\n" TIMESTAMP "\n" BODY
```

- `PATH` is the path as sent, without host.
- `CANONICAL_QUERY`: decode each `name=value` pair (`+` means space), sort by name then value (byte order), then re-encode with
  RFC 3986 (letters, digits and `-._~` stay literal, everything else is `%XX` in upper case) and join with `&`. Empty if there is no query.
- `TIMESTAMP` is the same text you send in the header, and must be within **60 seconds** of our clock.
- Required for every `/runecode/v1/*` call. Optional but recommended on the Engine endpoints (`/v1/*`): when a request carries
  `RuneCode-Timestamp`, the signature must be the full-request form. An operator can make it mandatory.

The SDKs implement both; `fullRequestSigning: true` turns the second one on for the Engine endpoints too.

**Serialize the JSON body once, sign those bytes, and send those same bytes.** Re-serializing between signing and sending is the
most common cause of `401`.

### Reference (Node, no libraries)

```ts
import { createPrivateKey, sign } from "node:crypto";
const key = createPrivateKey(process.env.OPERATOR_PRIVATE_KEY_PEM!);
const body = JSON.stringify(payload);                          // serialize ONCE
const signature = sign(null, Buffer.from(body), key).toString("base64"); // null = pure Ed25519, no pre-hash
// send `body` with X-Operator and X-Signature
```

## Verifying a request from the RGS (your wallet)

The RGS sends `x-operator` (your slug) and `x-signature` on every wallet call except `GET /health`. The signature covers the
**raw body bytes**.

1. Read the raw body.
2. Check `x-operator` is your slug; otherwise answer `401 ERR_ATE`.
3. Verify `x-signature` over the raw bytes against **each** RGS key (current and standby); accept if any verifies.
4. Only then parse the JSON.

A signature proves origin, not freshness. Safety against replays comes from idempotency on the transaction ids, not from
timestamps (see [Implementing the wallet](#05-wallet)).

## Test vectors

`vectors/signing.json` holds a **test** key and known answers: Ed25519 is deterministic, so the same body always gives the same
signature. Every SDK must reproduce them exactly; use them to check your own implementation.

## Common mistakes

- Signing a pretty-printed or re-ordered body, then sending a different serialization.
- Using the standard Base64 alphabet *without* padding, or URL-safe Base64. It is standard, with padding.
- Pre-hashing the body (Ed25519 here is the pure form, RFC 8032, no pre-hash).
- Verifying after parsing instead of before.
- Trusting only one RGS key, which breaks during an emergency rotation.


---

<a id="03-launching-games"></a>

# Launching games

## Real-money launch: `POST /v1/game/url`

You call this when a player clicks a game in your lobby. We return a single-use URL; you redirect the player's browser to it.

```json
{
  "product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
  "user": "player-48213", "token": "9f0c1d2e-5a7b-4c3d-8e1f-0a1b2c3d4e5f",
  "currency": "USD", "language": "en", "device": "desktop", "tag": "real"
}
```

| Field | Required | Notes |
|---|---|---|
| `product`, `publisher`, `game` | yes | the three slugs from the catalogue, copied verbatim. Never build or split them yourself |
| `user` | yes | your player id: opaque, stable per player, at most 128 characters. Do not mint a new one per launch |
| `token` | yes | one-time session token, at most 256 characters (a UUID is recommended). Comes back as `token` on **every wallet call** of this session; the player's browser never sees it |
| `currency` | yes | upper case; must be enabled for you |
| `language` | no | BCP-47, default `en`; unknown values fall back to `en` |
| `device` | no | `desktop` (default), `mobile`, `tablet` |
| `tag` | no | `real` (default), `affiliate`, `streamer`, `employee`, `qa`. Non-`real` tags are accepted only for users you pre-registered, are excluded from production accounting and are reported separately |

Response:

```json
{ "url": "https://play.<rgs-domain>/l/9b2c…", "fair": true }
```

- **`url`** is single use, valid about **120 seconds**, and redirects once to the game. It carries session material: never log it.
- **`fair`** says whether the session's math supports provably-fair verification.

### Rules worth knowing

- **Retries are safe.** A byte-identical repeat of a launch request within 10 minutes, before the URL has been opened, returns the
  same response. The same `token` with a *different* body is `409 ERR_TOKEN_REUSED`. You can also send an `Idempotency-Key` header
  (1 to 255 visible ASCII characters): the same key and body returns the stored response for 24 hours; the same key with a
  different request is `409 ERR_IDEMPOTENCY`.
- **No wallet call happens at launch.** Your wallet's balance is read when the game opens, so a slow wallet does not fail the launch.
  If your wallet is marked degraded, the launch answers `503 ERR_MAINT` instead of a URL that would fail on the first spin.
- **The game's release is fixed at launch** and never changes during the session.
- **Framing:** the game is served with a CSP that allows only the lobby origins you registered. Register every origin before going live.
- **Jurisdiction:** an operator enabled for one jurisdiction profile resolves to it. An operator with several must send `country`
  (ISO 3166-1 alpha-2) on every launch, or have a complete brand-to-profile map. Anything we cannot resolve is refused (`400 ERR_JUR`),
  never guessed. Countries on the platform deny list are refused (`403 ERR_JUR`).

### Optional fields (extensions)

Safe to omit. Send the header `RuneCode-Extensions: v1` to receive extra fields in the response (`runecode.sessionID`,
`expiresAt`, the resolved release and jurisdiction, a request id). `brand`, `country`, `ip`, `links`, `session`, `metadata`
and `timestamp` are accepted as described in the specification. `responsibleGaming` and `displayCurrency` are refused with `400`
until implemented rather than silently ignored.

## Demo launch: `POST /v1/game/url/demo`

Play money, no wallet, no token reaches you.

```json
{ "product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2", "currency": "USD", "language": "en", "device": "desktop" }
```

Response `{ "url": "…" }`. Demo sessions are signed with a separate demo key, rate-limited per player IP, and never reach any wallet.

## After the launch

When the player opens the URL the game authenticates against the RGS, which calls your wallet's `/v1/balance` with your `token`.
From then on every bet is a `/v1/debit` and every payout a `/v1/credit`: see [Implementing the wallet](#05-wallet).

## Errors

| HTTP | `code` | Meaning |
|---|---|---|
| 400 | `ERR_BAD` | not JSON, schema violation, unknown enum value, field too long |
| 400 | `ERR_NAV` | the game has no approved release for your operator, jurisdiction or currency |
| 400 | `ERR_CUR` | currency not enabled for you |
| 400 | `ERR_JUR` | jurisdiction cannot be resolved |
| 401 | `ERR_ATE` | missing or invalid signature, unknown operator, revoked key, stale timestamp |
| 403 | `ERR_FBD` | operator suspended, source IP not allowed, endpoint not enabled for you |
| 403 | `ERR_JUR` | blocked in the player's country or jurisdiction |
| 404 | `ERR_GNF` | unknown game, or not approved for you |
| 409 | `ERR_TOKEN_REUSED` / `ERR_IDEMPOTENCY` | see the rules above |
| 429 | `ERR_RATE` | rate limit; honour `Retry-After` |
| 503 | `ERR_MAINT` | maintenance or bets paused; honour `Retry-After` |

More in [Errors and limits](#07-errors-and-limits).


---

<a id="04-catalogue"></a>

# Catalogue

Two calls tell you what you may launch. Both are signed like every operator API call and are rate limited per operator.

## `GET /v1/product`

The products you may launch from: `[{ "product": "runecode", "name": "RuneCode" }]`.

## `GET /v1/{product}/game?after=<int64>`

```json
{ "updated": 1792000000,
  "games": [ { "product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
               "name": "Royal Tiger v2", "rtp": "96.20", "updated": 1792000000, "image": "https://…/cover.webp" } ] }
```

- The `(product, publisher, game)` triple identifies a game; pass it unchanged to the launch call.
- `rtp` is the return-to-player of the release assigned to **your** operator, as an exact decimal string (for example `"96.20"`),
  never a floating-point number.
- `image` is omitted, not `null`, when there is none.
- **Incremental sync:** pass the `updated` of your previous response as `after`. The boundary is inclusive, so de-duplicate by the
  slug triple. Incremental sync never reports removals: do a full refresh (no `after`) periodically, for example daily.
- The list contains exactly the games a launch would currently accept for at least one of your currencies and jurisdictions: a
  listed game is never unlaunchable.
- The response carries a strong `ETag` and `Cache-Control: private, max-age=3600`; send `If-None-Match` to get `304`.
- An unknown product is `404`.

The SDKs: `rgs.products()` and `rgs.games(product, after?)`.

## Rate limit

The catalogue allows about one request per second per operator (burst 5). Cache it; do not call it per player request.

## Planned: the richer catalogue

A paginated `GET /runecode/v1/catalogue` with media, mode, volatility, bet-level, jurisdiction and release fields is specified but
**not served yet**: see [Availability](#11-availability).


---

<a id="05-wallet"></a>

# 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](#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.

| 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](#02-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`

```json
{ "token": "9f0c1d2e-…" }   →   { "balance": { "amount": 1000000000, "currency": "USD" } }
```
Errors: `ERR_IS` (unknown token), `ERR_ATE` (bad signature or operator). A pure read.

## `POST /v1/debit`

```json
{ "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: true` keeps the round open; `false` closes it now (a 0-win close-at-play, or a single-shot round).
- `credit` is a concurrent settling credit (`id`, `amount`, `ref` = the debit id, `currency`) or `null`. The stake alone must be
  coverable: **a concurrent credit never funds its own stake** (`ERR_IPB`).
- Answer: `{ "debit_id": "…", "credit_id": "…"?, "balance": { … } }` (`credit_id` only 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`

```json
{ "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 is `ERR_BC`; the `token` must be the one
  that **opened** the round (`ERR_IS` otherwise, 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`

```json
{ "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 `ref` your wallet has never seen answers `200` and 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 `ref` names a debit you **refused** answers `200` and refunds nothing.
- `ERR_BC` if the round was settled by a credit. If the debit is still pending on your side, answer `5xx` so 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 `5xx` is 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](#09-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)
```


---

<a id="06-rounds-and-reconciliation"></a>

# Rounds and reconciliation

Your wallet is the system of record for balances. The RGS keeps the authoritative record of what it sent and what you confirmed.
These calls let you check the two against each other, answer a player's dispute, and show a round's replay.

All three use the full-request signing form and are scoped to your operator: another operator's round is a plain `404`.

## One settled round: `GET /v1/bet/{id}`

`{id}` is the round id (the wallet's `round`). Returns the round once it is settled (`CLOSED`, `VOIDED` or `CLOSED_UNSEEN`); an
open or unknown round is `404`. SDK: `rgs.bet(id)`.

## `POST /runecode/v1/rounds/get`

Select a round by **one** of:

```json
{ "round": 4119983943 }
{ "transaction": "019a1b2c-3d4e-7f60-8a1b-2c3d4e5f6a7b" }      // an RGS debit, credit or rollback id
{ "operatorTransaction": "wal-77812" }                           // the debit_id / credit_id your wallet returned
```

The answer carries the round's status (`PENDING_DEBIT`, `ACTIVE`, `CLOSED`, `VOIDING`, `VOIDED`, `CLOSED_UNSEEN`), your `user`,
the `game`, the release (front and math versions), the mode, `currency`, `bet` and `payout` as decimal strings, open and close
times, every wallet leg with its state, `operatorId` and attempt count, and a **`replayUrl`**. SDK: `rgs.roundsGet(selector)`.

### Replay

`replayUrl` is a public link that replays the round exactly as it was played. It needs no session and contains no player data,
and it stays valid for as long as the game version exists. Use it in your round history or support tools.

## `POST /runecode/v1/transactions/get`

By `{ "id": "<RGS id>" }` or `{ "operatorId": "<your id>" }`. Returns the leg, its round, the masked token (last six characters),
the **exact signed bytes** the RGS sent to your wallet (Base64) and each attempt's status, code and latency. This is the tool
for "did you charge this?": it also shows that retries are byte-identical. SDK: `rgs.transactionsGet(selector)`.

## Round states

| State | Meaning |
|---|---|
| `PENDING_DEBIT` | the debit is being sent |
| `ACTIVE` | the stake is taken; the round is open |
| `CLOSED` | settled |
| `VOIDING`, `VOIDED` | the debit was refused or its outcome was unknown, so the RGS is reversing it |
| `CLOSED_UNSEEN` | a debit landed after the RGS gave up on it, so a real round was played that the player never saw; reconciled as played |

## Reconciling

Reconcile daily. The RGS will run the reconciliation itself and open a **break** for any mismatch (missing at the operator,
missing at the RGS, amount, state), with an agreed response time. You provide the data either as a daily transaction file or by
making your wallet's transactions searchable by RGS id. Until the feeds are live (see [Availability](#11-availability)),
reconcile with `transactions/get` and `rounds/get` by operator transaction id.

Retention: lookups are synchronous inside a hot window (14 days by default); older rounds are served by an asynchronous job.
Transaction ids stay resolvable forever.


---

<a id="07-errors-and-limits"></a>

# Errors and limits

## Error format

Errors on the operator API are, by default, **plain-text status lines**, as in Engine (`Unauthorized`). Branch on the **HTTP
status** first; the code refines it. To get a code and message, ask for one:

| `Accept` header | Body |
|---|---|
| (none) | plain text: the HTTP reason phrase |
| `application/json` | `{ "code": "ERR_GNF", "message": "game not found" }` (the same envelope as the wallet API) |
| `application/problem+json` | RFC 9457 problem details with the same `code` |

Every error response also carries `RuneCode-Error-Code: <code>`. Every response, success or error, carries `X-Request-Id`
(a UUIDv7): log it and quote it to support. New codes can be added without a new major version, so treat an unknown code by its
status. The SDKs send `Accept: application/json` and raise an error object with `status`, `code`, `message` and `requestId`.

## Codes

See the table in [Launching games](#03-launching-games). In addition: `404 ERR_NF` is an unknown round or transaction
on a lookup, and `500 ERR_GE` is an unexpected error that is safe to retry with the same body.

## Rate limits (per operator)

| Group | Sustained | Burst |
|---|---|---|
| Launch (`/v1/game/url`, `/v1/game/url/demo`) | 50 per second | 200 |
| Catalogue (`/v1/product`, `/v1/{product}/game`) | 1 per second | 5 |
| Lookups (`/v1/bet`, `/runecode/v1/*`) | 20 per second | 40 |

Over the limit: `429 ERR_RATE` with `Retry-After`. Back off and retry; do not retry in a tight loop. The limits are configured per operator.

## Request limits

Request bodies are limited to 64 KiB. JSON must be one object with no duplicate keys; unknown top-level fields are ignored
(extension objects reject unknown fields).

## Retrying safely

| You called | Safe to retry? |
|---|---|
| `GET` anything | yes |
| `launch` | yes, with the same body (within 10 minutes) or the same `Idempotency-Key` (24 hours): you get the same response |
| `launchDemo` | yes (a new demo session each time) |
| lookups | yes |

On `5xx` and timeouts, retry with the same body. On `4xx` (except `429`) fix the request first.


---

<a id="08-sdks"></a>

# SDKs

Seven SDKs, one behaviour. Each one:

- signs requests in both forms (Engine body-only and full-request) and produces exactly the signatures in `vectors/signing.json`;
- calls the operator API (launch, demo, catalogue, round and transaction lookups) and raises a typed error with the HTTP status, code, message and request id;
- verifies the RGS's signed calls on your wallet and builds the wallet's error and balance answers.

They have no third-party dependencies. All are tested against the shared vectors, and all (except that the Rust crate leaves HTTP to
you) are tested against a real `operator-api` by `interop/run.sh`.

| Language | Directory | Needs | HTTP |
|---|---|---|---|
| TypeScript | `typescript/` | Node 22.6+ (runs the `.ts` directly; compile with `tsc` for older runtimes) | `fetch` |
| Python | `python/` | Python 3.9+ | `urllib` |
| Go | `go/` | Go 1.22+ | `net/http` |
| Java | `java/` | Java 17+ | `java.net.http` |
| Rust | `rust/` | Rust 1.80+ | none: the crate builds the request, you send it |
| PHP | `php/` | PHP 8.1+ with `ext-sodium` and `ext-json` | PHP streams |
| C# | `csharp/` | .NET 8+ | `HttpClient` |

For any other language, the signing scheme in [Authentication](#02-authentication) is short enough to implement directly, and
the vectors tell you when you have it right.

## The same four things in every language

### Call the operator API

```ts
// TypeScript
const rgs = new OperatorClient({ baseUrl, operatorId: "acme", privateKeyPem, fullRequestSigning: true });
const { url } = await rgs.launch({ product: "runecode", publisher: "wizards", game: "royal-tiger-v2",
                                   user: "player-1", token: crypto.randomUUID(), currency: "USD" });
```
```python
# Python
rgs = OperatorClient(base_url, "acme", private_key_pem, full_request_signing=True)
url = rgs.launch({"product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
                  "user": "player-1", "token": str(uuid.uuid4()), "currency": "USD"})["url"]
```
```go
// Go
rgs, _ := operator.NewClient(operator.ClientOptions{BaseURL: base, OperatorID: "acme", PrivateKeyPEM: pem, FullRequestSigning: true})
res, err := rgs.Launch(map[string]any{"product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
    "user": "player-1", "token": uuid, "currency": "USD"}, "")
```
```java
// Java
OperatorClient rgs = new OperatorClient(base, "acme", pem, true);
String json = rgs.launch(Map.of("product", "runecode", "publisher", "wizards", "game", "royal-tiger-v2",
    "user", "player-1", "token", uuid, "currency", "USD"), null).body();
```
```php
// PHP
$rgs = new OperatorClient($base, 'acme', $privateKeyPem, true);
$res = $rgs->launch(['product' => 'runecode', 'publisher' => 'wizards', 'game' => 'royal-tiger-v2',
    'user' => 'player-1', 'token' => $uuid, 'currency' => 'USD']);   // $res['url']
```
```csharp
// C#
var rgs = new OperatorClient(baseUrl, "acme", privateKeyPem, fullRequestSigning: true);
var res = await rgs.LaunchAsync(new JsonObject { ["product"] = "runecode", ["publisher"] = "wizards", ["game"] = "royal-tiger-v2",
    ["user"] = "player-1", ["token"] = uuid, ["currency"] = "USD" });   // res["url"]
```
```rust
// Rust: the crate prepares the call; send it with any HTTP client
let s = Signer::new("acme", &pem)?;
let req = s.prepare("POST", "/v1/game/url", "", body_json.as_bytes(), true, now_unix_secs);
// send req.method to base + req.path_and_query with req.headers and req.body
```

Other calls: `products()`, `games(product, after?)`, `launchDemo(...)`, `bet(id)`, `roundsGet(selector)`, `transactionsGet(selector)`
(snake_case in Python; `LaunchDemo`, `RoundsGet`, … in Go; Rust prepares any of them with `prepare`).

### Handle errors

A non-2xx answer raises `RgsError` (TypeScript, Python), returns `*RgsError` (Go) or throws `RgsException` (Java), with `status`,
`code`, `message` and `requestId`. Branch on `status` first. Keep `requestId` in your logs.

### Verify a wallet call from the RGS

```ts
const check = verifyWalletRequest(rawBody, req.headers, { operatorSlug: "acme", rgsPublicKeys: [current, standby] });
if (!check.ok) return reply(check.error.response());          // 401 { code: "ERR_ATE", message: … }
```
```python
verify_wallet_request(raw_body, headers, "acme", [current, standby])   # raises WalletError("ERR_ATE")
```
```go
if e := operator.VerifyWalletRequest(raw, r.Header, "acme", []string{current, standby}); e != nil { /* e.Status(), e.Body() */ }
```
```java
WalletError.verifyRequest(rawBody, headers, "acme", List.of(current, standby));   // throws WalletError
```
```rust
verify_wallet_request(raw_body, x_operator, x_signature, "acme", &[current, standby])?;
```
```php
WalletError::verifyRequest($rawBody, $headers, 'acme', [$current, $standby]);   // throws WalletError
```
```csharp
WalletError.VerifyRequest(rawBody, headers, "acme", new[] { current, standby });   // throws WalletError
```
Always pass **both** RGS keys, and pass the **raw bytes**, not a re-serialized object.

### Answer a wallet call

Use the helpers for the balance body (`balance(amount, currency)`) and for errors (`WalletError(code, message)` with the right
HTTP status). The full contract is in [Implementing the wallet](#05-wallet); the reference wallet shows all of it together.

## Verifying an SDK yourself

```sh
node --test typescript/test/                       # TypeScript
python3 -m unittest discover -s python/tests -t python
(cd go && go test ./...)
(cd java && javac -d out src/main/java/io/runecode/operator/*.java src/test/java/io/runecode/operator/*.java && java -cp out io.runecode.operator.VectorTests)
(cd rust && cargo test)
php php/tests/run.php                              # PHP (docker run --rm -v "$PWD":/w -w /w/php php:8.3-cli-alpine php tests/run.php if PHP is not installed)
(cd csharp && dotnet run --project tests -c Release)   # C# (or the dotnet/sdk:8.0 image; see interop/run.sh)
interop/run.sh                                     # every SDK against a real operator-api (needs a Core checkout)
```


---

<a id="09-testing-and-going-live"></a>

# Testing and going live

## The onboarding flow

```
 1  Commercial and compliance   contract, KYB, licences per jurisdiction, revenue terms
 2  Registration                you get an operator id per environment (sandbox, production)
 3  Key exchange (sandbox)      you upload your Ed25519 public key; you download the RGS wallet keys (current and standby)
 4  Sandbox configuration       wallet ingress URL, currencies, lobby origins, test users
 5  Build                       launch and catalogue against the sandbox; the wallet against the RGS
 6  Conformance                 the wallet and operator-API suites pass in the sandbox and produce a signed report
 7  Production request          you request go-live with the report and the certificates checklist
 8  Production keys and IPs     a NEW key pair (never reuse the sandbox keys); our six egress IPs allow-listed on your side
 9  Production smoke test       qa-tagged sessions, real tiny bets, one rollback drill
10  Ring 0, then general availability
```

Sandbox and production share nothing secret. A sandbox key can never sign a production call, and sandbox IP addresses are never
to be allow-listed in production firewalls.

## What you can test today, on your own machine

You do not need our environments to build and verify most of an integration. With a checkout of the RGS (Core) and these SDKs:

| What | How | Proves |
|---|---|---|
| Your signing code | the vectors in `vectors/signing.json` | your signatures are byte-identical to ours |
| Your operator-API client | `interop/run.sh`, adapted to your client | launch, catalogue and lookups work against the real service, including the refusals |
| Your wallet against the conformance kit | `wcs` as above | the wallet contract, row by row, with a signed report |
| Your wallet against the real RGS | start `play-core` with `walletUrl` pointing at your wallet, as `examples/reference-wallet/e2e.sh` does | the RGS's signed balance, debit and credit calls are verified and your books agree with ours |
| The whole path: launch, play, wallet | `examples/reference-wallet/launch-e2e.sh` | your launch call, the game API reading that session in another process, and your wallet being debited and credited by signed calls, on one database |
| Retry and fencing behaviour | make your wallet answer `5xx`, time out, or refuse a debit, and watch the RGS retry the identical bytes and roll back | idempotency, fencing and refused-debit handling |

## The wallet conformance kit

Before go-live your wallet must pass RuneCode's **wallet conformance suite** against your sandbox wallet. The kit calls your wallet
exactly as the RGS does (signed bodies, `x-operator`, frozen bytes re-sent verbatim on retry) and writes a **signed report** with
a result per row and the capability flags it derives for your wallet. The report is the evidence for the go-live items on wallet
conformance, refusal handling and the standby key.

```sh
# from the RGS repository (Node 24 or newer; no runtime dependencies)
pnpm --filter @runecode/wallet-conformance wcs \
  --wallet-url https://sandbox-wallet.example.com --operator-slug acme \
  --signing-key ./rgs-sandbox.key --standby-key ./rgs-sandbox-standby.key \
  --dev-session-url https://sandbox-wallet.example.com/v1/dev/session --out ./wcs-report
```

- **Rows:** authentication and `x-operator`, the standby key, validation, balance, debit (including single-shot, instant loss and
  replays), credit, rollback (including fencing and multi-debit), ordering and idempotency, concurrency, forward compatibility,
  byte-identical replays, and refused-debit handling.
- **Verdicts:** `fail` (a MUST row failed) is worse than `incomplete` (a MUST row skipped), then `pass-with-warnings` (a SHOULD
  row failed), then `pass`.
- **Safety gate:** the kit refuses to run (exit 3) unless your wallet offers the dev-only session helper `POST /v1/dev/session`
  (`{currency, balance}` returns `{token}`; off by default and **never in production**), or you pass your own test-account tokens.
  The helper's presence is the proof that the target is a sandbox.
- **Capability flags** such as `remembersRefusals`, `supportsSingleShot` and `supportsCloseAtPlay` tell the RGS how it may drive your
  wallet. A flag whose rows did not run is "not tested", never assumed.

The reference wallet passes it: `examples/reference-wallet/wcs.sh` runs the real kit against the reference wallet (verdict `pass`,
every row, every capability true). Use that wallet as your starting point and the kit as your acceptance test.

The wallet simulator (`wallet-sim`) is our reference for the other direction: it behaves like an Engine-contract wallet and is
what the RGS is tested against.

## Go-live checklist

A blocker must be done before production. Items marked *if used* apply only when you use the feature.

| # | Item | Blocker |
|---|---|---|
| 1 | Contract, KYB and revenue terms signed | yes |
| 2 | Licences and jurisdictions confirmed; a jurisdiction profile assigned; counterparty compliance record complete | yes |
| 3 | Certificates in place for every (game, release, jurisdiction) you enable | yes |
| 4 | Wallet conformance passed against your staging wallet, within 30 days, same build as production | yes |
| 5 | Operator-API conformance passed | yes |
| 6 | Capability suites passed for each capability you enable (promotions, jackpots, webhooks, crypto) | per capability |
| 7 | Production key pair uploaded; both RGS wallet keys (current **and** standby) trusted; fingerprints confirmed out of band | yes |
| 8 | Refusal handling passed: a rollback of a refused debit answers `200` and refunds nothing; refused debit ids are idempotent | yes |
| 9 | All six production egress IPs allow-listed and reachable | yes |
| 10 | Wallet ingress URL specific to RuneCode, TLS valid, `/health` green | yes |
| 11 | Test helpers unreachable in production | yes |
| 12 | Lobby origins registered; launch tested in the real lobby on desktop and mobile | yes |
| 13 | Currencies, bet levels and limits reviewed and approved | yes |
| 14 | Webhook endpoint verifies signatures and de-duplicates by id | *if used* |
| 15 | 24/7 contact for wallet incidents, escalation path, status subscription | yes |
| 16 | One day of transactions pulled and matched against your ledger; daily reconciliation agreed | yes |
| 17 | Production smoke test passed | yes |
| 18 | Player round history shown in your lobby where the jurisdiction requires it | where required |
| 19 | Multi-jurisdiction operators: `country` sent on every launch, or a complete brand-to-profile map | if more than one profile |

## Operating in production

- Log `X-Request-Id` on every operator-API response, and quote it in any support request.
- Never log launch URLs, tokens or private keys.
- Watch `/health` on your wallet as we do: three failures pauses bets for your players.
- Plan key rotation: keep two operator keys active during a switch, and trust both RGS wallet keys at all times.
- Reconcile daily.


---

<a id="10-engine-operators"></a>

# If you integrate with Stake Engine today

The operator API and the wallet contract are the same. The RGS follows Engine's `/v1` operator API and wallet contract
byte for byte, and its game API is compatible with unmodified Engine front ends. Moving a game library over is mostly
configuration.

| Item | Engine | RuneCode | What you do |
|---|---|---|---|
| Operator API base URL | Engine's | ours (given at onboarding) | change the setting |
| `X-Operator` | your Engine operator id | your RuneCode operator id | change the setting |
| Your key pair | Ed25519 (or legacy RSA) | Ed25519 only | upload the public key (you may reuse your Engine pair; we recommend a new one) |
| RGS public key (wallet `x-signature`) | Engine's | ours | add it; keep both, and pick by ingress URL |
| Wallet ingress URL | configured at Engine | configured with us | register it |
| `/v1/game/url`, `/v1/game/url/demo`, `/v1/product`, `/v1/{product}/game`, `/v1/bet/{id}` | as documented by Engine | identical | nothing |
| Wallet `/v1/balance`, `/v1/debit`, `/v1/credit`, `/v1/rollback` | Engine's contract | identical | nothing |
| Game ids | `product`/`publisher`/`game` slugs | the same, with our slugs | map your lobby entries |
| Network allow-listing | Engine's IPs | our six production egress IPs | firewall change |
| Extensions | none | optional and additive | ignore them if you want |

## What differs, so you are not surprised

- **Ed25519 only.** There is no RSA fallback.
- **Retry safety on launch.** A byte-identical repeat within 10 minutes returns the same URL, and an `Idempotency-Key` header is
  supported. Engine says a launch is not idempotent; ours never changes the result of a first call.
- **Full-request signing is available** (timestamped, covers method, path and query). Engine-style body-only signing keeps working.
- **Errors:** plain-text status lines by default, as in Engine; ask for `Accept: application/json` to get a code.
- **If one wallet serves both Engine and RuneCode,** you must give RuneCode its own ingress URL (a distinct path or host), so the
  right key verifies and round ids from the two RGSes (unique only per RGS) never collide: key rounds by `(rgs, round)`.
- **A refused debit is fenced.** After answering `ERR_IPB`, the RGS sends a rollback for that debit and expects `200` with nothing
  refunded. Engine's reference wallet forgets refused debit ids; yours should store them as consumed.


---

<a id="11-availability"></a>

# Availability

This page says what works today and what does not, so you can plan. "Verified" means exercised by an automated test or by the
end-to-end scripts in this repository against the real RGS code. Dates are not promised on this page.

## Works and verified

| Capability | Status | Evidence |
|---|---|---|
| Operator API: launch, demo launch, products, catalogue (incremental, `ETag`), settled-round lookup, round and transaction lookups | implemented in `operator-api` | its test suite; `interop/run.sh` runs all five SDKs against it |
| Request signing: Engine body-only and full-request forms, two keys per operator, timestamp freshness, constant-time verification | implemented | the same suites, plus the shared vectors |
| Launch safety: token reuse refused, idempotent retries, `Idempotency-Key`, rate limits, error negotiation | implemented | `interop/run.sh` and the service tests |
| Wallet calls from the RGS: balance, debit, credit, rollback, signed per operator, byte-identical retries, fencing of refused and timed-out debits | implemented in `play-core` | `examples/reference-wallet/e2e.sh` drives the real RGS against the reference wallet |
| Game API (Engine-compatible): authenticate, balance, play, end-round, event, replay | implemented | the conformance suite and the end-to-end script |
| Wallet conformance kit (WCS): signed report, capability flags | implemented in the RGS repository (`conformance/wallet`) | its own tests against the wallet simulator; the reference wallet passes it (`examples/reference-wallet/wcs.sh`) |
| Reference wallet and SDKs (TypeScript, Python, Go, Java, Rust, PHP, C#) | in this repository | vectors, unit tests, interop, the conformance kit |

## Launch to play across processes

| Capability | Status | Evidence |
|---|---|---|
| A launch from the operator API is a session the game API (a different process) can use: the session is written to Postgres, `play-core` finds it, authenticates it with your wallet and plays | implemented in the RGS and **verified end to end**; held for release approval | `examples/reference-wallet/launch-e2e.sh`: your SDK launches, `play-core` authenticates and plays on the same database, signed balance, debit and credit calls reach the wallet, the books agree, and a session id that was never issued is refused |

What that session carries is the operator, player, game, user, token, currency, IP, language and launch time. The launch's
jurisdiction, release pin, tag, brand, country, device, new-bet window and metadata are resolved at launch but not yet stored with
the session, so the game takes its release from its own configuration for now.

## Implemented, but not yet joined for production

These pieces exist but run with in-memory adapters, so a deployed, multi-instance service needs the production adapters first.

| Gap | Effect today |
|---|---|
| Production registries (operators, releases, rounds) and replay/idempotency/rate-limit state in a shared database | one operator-API process only; no multi-replica |
| KMS signing of launch tokens and wallet requests | local keys only |
| The launch's jurisdiction, release pin, tag and other fields stored with the session | see above |
| Edge pieces: the launcher host that redirects the launch URL, and game hosting | the edge code exists; the production wiring is not part of this repository |

## Specified, not built yet

| Capability | Notes |
|---|---|
| Hosted sandbox environment with the wallet simulator, fault toggles and forced outcomes | until it exists, run the local stack (see [Testing](#09-testing-and-going-live)) |
| Back office: operator registration, key upload, IP lists, go-live requests | keys and operators are configuration today |
| Webhooks (round, balance and key events) | specified, not served |
| Feeds and reports (finished rounds, transactions), daily automated reconciliation | until then, reconcile with the lookup calls |
| `GET /runecode/v1/catalogue` (rich catalogue), `GET /game/list` (deprecated alias) | `GET /v1/{product}/game` is available |
| Responsible-gaming restrictions and session revocation, `displayCurrency` | refused with `400` until implemented, never ignored |
| Promotions, free rounds, jackpots, tournaments | specified separately |
| `detailsUrl` support pages, archived-round jobs (older than the hot window) | lookups cover the hot window |
| Operator-API conformance suite (OCS) | the interop script in this repository is the starting point; the wallet kit above is available |
| Hosted docs portal with an API reference and signature debugger | this guide is the first version |

If something you need is in the last table, tell us early: several of these are decided and only waiting for the first partner's
priorities.


---

<a id="12-glossary"></a>

# Glossary

| Term | Meaning |
|---|---|
| **RGS** | Remote Game Server: decides outcomes, never holds player money |
| **Operator** | the casino or lobby that offers games to players and holds their money |
| **Aggregator** | a platform that fronts many operators; can be given read-only access across them |
| **Operator API** | the calls you make to the RGS (launch, catalogue, lookups) |
| **Wallet API** | the four endpoints the RGS calls on your side (balance, debit, credit, rollback) |
| **Token** | your one-time session token, chosen at launch and echoed on every wallet call |
| **Session id** | the RGS's own id for the player's browser; never your token |
| **Round** | one game round; the RGS gives it an integer id (the wallet's `round`) |
| **Debit / credit / rollback** | stake taken / win paid / stake reversed |
| **Idempotency key** | an id that makes a replay harmless: `debit.id`, `credit.id`, the rollback's `ref` |
| **Fence** | the wallet's record that a debit id was reversed before it arrived, so a late copy is refused |
| **Micro-units** | integer money: `1_000_000` is `1.00` |
| **Engine form / A9** | the two signing forms: body only, or method, path, query, timestamp and body |
| **Current / standby key** | the two RGS wallet public keys you must always trust |
| **Slug triple** | `product`, `publisher`, `game`: the identity of a game |
| **Tag** | `real`, `affiliate`, `streamer`, `employee`, `qa`: how a session is accounted |
| **Break** | a reconciliation mismatch between the RGS's record and yours |


---
