This whole page is operator-only. Registering a game is an operator action: it uses the games-host CLI plus operator-controlled admin wallets. A builder hands over artifacts and facts (see Submitting your game); an operator runs everything below.
1. Export the bundle (builder produces, operator receives)
./scripts/build-export.sh --bundle
# → dist/bundle.tar.gz + dist/bundle.tar.gz.sha256
A deterministic, content-addressed tarball of the static export in out/, with a
detached sha256 sidecar so the operator can verify the bytes. build-frontend-image.sh
builds the standalone frontend image and prints the exact FRONTEND_IMAGE=… line for
the platform's compose.
The bundle must be built with NEXT_PUBLIC_GAME_ID set to the target plane's shared
move namespace (the gameId its /arcade-config.json serves: xarc on the
playground, arcbeta on arcade.xaya.io), not the game's own GAME_KEY, or it is accepted but refuses
to boot on the namespace mismatch: see Submitting your game.
2. Register the bundle with the games-host
npm run register -w games-host -- \
--data <data-dir> \
--slug <slug> \
--game-id <gameId> \
--game-type <gameType> \
--title "<Display Title>" \
--seats <min>..<max> \
--file dist/bundle.tar.gz \
--sha256 <bundle-sha256> \
--bake \
[--touch-from <game-checkout>] \
[--wager-address 0x…]
The games-host README is the reference for this command: the register CLI
lists every flag (--deactivate <slug> takes a game down), declaring touch play
explains --touch-from, and every registration is a full-row replace,
so a flag left off clears what the row held: omitting --wager-address on a
re-registration clears it.
Production runs games-host in path mode
(serving modes): every game is mounted at /g/<slug>/ on a games
origin separate from the shell's, and registration bakes that mount into the bundle
(path baking).
3. Register on chain
Two admin moves, from the platform repo. First the blob move stores the rules
body ({sha256, z}); then the reg move (a full-row replace of the game's
registry row) references that hash and carries the seat range and cfgSuffix:
(cd $ARCADE_PLATFORM && env TN_RPC=<fork rpc> TN_KEY=<keyfile> XBM_GAME_ID=<gameid> \
npx tsx scripts/onchain-e2e.ts \
--register-blob $GAME_REPO/blob/rules.wasm \
--reg-game-type=<gameType> \
--reg-min-players=2 --reg-max-players=3 \
--reg-cfg-suffix=<hex or omitted>)
On a real-chain plane, do not use onchain-e2e.ts. It registers a fork deployment's short
dispute windows (disputeBlocks 10, graceBlocks 15). The deploy-day registrar is
scripts/register-rules.ts, the path the beta's games were registered with:
(cd $ARCADE_PLATFORM && env TN_RPC=<chain rpc> TN_KEY=<registrar keyfile> \
GSP_URL=<gsp rpc> GAME_ID=<g/ namespace> \
WAGER_ADDRESS=<the plane's ArcadeWager> \
npx tsx scripts/register-rules.ts \
--game-type <gameType> \
--min-players 2 --max-players 4 \
--wasm $GAME_REPO/blob/rules.wasm \
--cfg-suffix <hex, or '' for none>)
It carries the human-scale windows (disputeBlocks 300, graceBlocks 150,
timeoutBlocks 900: HUMAN_SCALE_WINDOWS in submissions/src/chain.ts) plus the bet
ladder and split read off that wager contract, adding nothing of its own to the move.
A paid registration without WAGER_ADDRESS is refused; --free-only registers the
zero stake alone and needs no contract. --cfg-suffix is required: before sending
anything, the registrar opens the blob at every seat count of the range under that
suffix and refuses a range or suffix the referee would not start. It reports OK only
once the GSP has indexed the row.
The registration move is fire-and-forget. Every GSP-side rejection is a
LOG(WARNING)invisible to the sender: a bad move looks identical to a good one from the caller's side. Verify by polling the GSP game state until the registry reflects the change:curl -s -X POST $GSP_URL -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","method":"getcurrentstate","params":[],"id":1}' \ | python3 -c 'import json,sys; \ print(json.load(sys.stdin)["result"]["gamestate"]["gameregistry"]["<gameType>"]["rulesBlobSha256"])'
The cfgSuffix landmine. The host builds cfg = LE32(channel seed) ++ cfgSuffix
and hands it opaquely to the blob. If your blob expects a suffix but the registry
carries an empty one, the host skips the seed prefix and hands the blob 0 bytes,
and every channel opened under it wedges part-filled: a channel builds its board
when it fills, from the suffix it opened with, so registering the right one fixes only
channels opened after it. Register the exact bytes the blob expects, or an empty
suffix only if the blob takes an empty config.
4. Enable wagering (optional, fails closed)
Wagering is off until three operator actions all land:
registerGame(gameType, freeOnly, minPlayers, maxPlayers, feeTo)on the deployed ArcadeWager contract (thearcade-wagerrepo), using the same wire id and seat range as the GSP registration.feeTois who is paid the game's fee: the builder's address for a game taken in, the platform's for its own. The builder can move it later withsetGameFeeRecipient, and so can the contract's owner.- The reg-move wagering block: the GSP registry row records the ArcadeWager game type it settles against (on-chain consensus state).
--wager-address 0x…on the bundle registration (step 2 above), pointing the row's--game-typeat the contract, which the host folds into/arcade-config.json.
The frontend gate is action 3 alone: wagerConfigured() reads the wagerAddress
from /arcade-config.json, so without it the lobby renders exactly as free play,
byte-identically. Actions 1 and 2 are enforced on chain: skip either and the
enable chain fails closed at the contract/GSP layer (stakes are refused, never
half-taken), but the staking UI will still render, so do the on-chain actions
FIRST and flip the bundle flags last (and strip the flags first when disabling).
Enabling a stake tier or seat count nobody has used yet costs the operator the
first three pots there. A payment queue is keyed (tier, num_players) with no
game level, so one FIFO is pooled across every registered game: a 10-WCHI win in
any game is paid, once it reaches the front, by a later 10-WCHI match in any game,
one group per start. Each brand-new combo is
seeded three operator groups deep, once ever, when the first reg move whose
seat range reaches it is processed; a second game registering at an already-seeded
combo seeds nothing. Those seed groups are payable to the operator, so the first
three paid matches at a cold (tier, seats) combo pay their pot to the operator
as a one-time bootstrap fee. Registering at a tier/seat combination that is already
live carries no such cost. Three groups also sets the concurrency: up to three paid
matches can be in flight at one combo at once, and an abandoned match normally
costs one unit of that only until the abandon reaper pushes its group back, so it
heals on its own.
One edge does not heal, and it is worth knowing because it is silent: a channel
the reaper never reaches (one carrying an open dispute is deliberately excluded
from reaping, since a disputed channel is being contested rather than abandoned)
holds its group for as long as it stays in that state. An abandoned match gives
its group back to its own players, so it needs no registry row, even for a game
whose row is gone. If a combo ever ends up shallower than it should be, the fix is
the seed move below rather than another reg.
Deepening a live combo is the seed admin move, never another reg. Once a
combo is seeded, no further registration adds to it; the operator's lever is
{"cmd":{"seed":{t,n,addr,burn,fee}}}, authored as g/<GAME_ID>, which adds one
group to an existing (tier, seats) queue. The move carries no amount: the GSP
derives the queued amount from the tier, the seat count and the burn/fee
parameters. Use it to raise concurrency at a combo busy enough that matches wait.
5. Upgrade and rollback
A blob or cfgSuffix change can be registered at any time, and it applies
only to channels opened afterwards: each channel keeps the blob hash and the
cfgSuffix it opened with, so it finishes under the rules it started with. If your
client bundles one blob and refuses any other, let its open channels finish before
you switch.
Rollback = re-register a prior hash: content-addressed bodies are never deleted, so
an old blob is always still there to point back at.
6. The shell content row (runtime overlay: no rebuild)
A game appears in this site once its content row exists: /play/<slug> and the
library read the registry, and a content/games/<slug>.json row (schema:
content/games/_schema.json in the xaya-arcade shell repo) is what lists it.
A brand-new slug needs no site rebuild. When the shell is run with
GAMES_CONTENT_RUNTIME_DIR set (a bind-mounted dir), the registry merges rows
from that dir over the baked ones (a baked row wins on a slug collision) and
re-reads them live. Writing <slug>.json into the overlay dir (and any card image
under MEDIA_RUNTIME_DIR/community/<slug>/, served at /media/community/...) makes
the game show up within the registry's short refresh window. This is exactly what
the submissions service's Accept writes; the operator does not hand-edit it.
Which planes set these variables is in Operating.