Playground: disposable chain, test WCHI, wiped without warning
Builder docs

Operating the platform

The platform's components and their configuration.

Test against the public playground and configure the platform services. The component reference follows.

Testing against a playground

The public playground is test-arcade.xaya.io: a disposable fork with test wallets, where you attach your own game and play it before you submit (Testing). Its services are the components below, each with the repository its source is in. For tests that need a forked chain of your own, forked-evm-testing is the minimal substrate the playground is built on.

For local UI development, set PLAYGROUND_ORIGIN=https://test-arcade.xaya.io when running the template's npm run dev:playground. Keep fork-only test keys and impersonation settings away from mainnet configurations.

Registering games into a running platform: Hosting & registration.

The components

ComponentRepoWhat it does
Host GSP (arcaded)arcade-platformThe on-chain referee: one process, game-code-free, runs every game's rules.wasm in the embedded wasmtime judge. Follows the chain through XayaX.
XayaXxaya/xayaxThe Polygon→GSP move bridge the GSP consumes.
Relayxaya-relayThe WebSocket transport that carries every game's channel proofs.
games-hostarcade-platformServes registered UI bundles and each one's arcade-config.json. GAMES_MODE=path (what the public plane runs) mounts every game at /g/<slug>/ on one games origin: a hostname of its own unless the operator sets GAMES_SAME_ORIGIN_FIRST_PARTY=1 (disabled on the public arcade). With {slug} in that origin, path mode gives each game a hostname of its own instead (what the public playground runs); GAMES_MODE=port gives each game its own origin by port. See the games-host README's serving modes.
submissionsarcade-platformSelf-serve upload + moderation queue; Accept runs the go-live steps (on-chain reg, registerBundle, the shell content row). The public arcade does not run it (its /api/submissions answers 503 and /submit serves the closed page); an operator's own plane wires it with SUBMISSIONS_INTERNAL, and AUTO_ACCEPT=1 makes a playground of its own accept an upload without review.
The shell (this site)xaya-arcadeWallet, identity, game library, embeds, the /submit and /admin/submissions UIs.
ArcadeWagerarcade-wagerThe escrow contract: deploy once, then registerGame per wagered game, naming who is paid its fee.

Bring-up order: chain access (Polygon RPC) → XayaX → GSP → relay → games-host → submissions → shell. A fresh GSP needs a start height set to roughly the current chain head minus 100, or it tries to sync from the compiled-in genesis and never catches up (an existing datadir wins over the variable).

games-host configuration

games-host reads its settings at startup and refuses to start when a required one is missing. Every variable, the two serving modes and the manifest format are in games-host/README.md, beside the code; registering a game is Hosting & registration.

Submissions service and the shell's runtime config

The submissions service accepts uploads, moderates them, and, on Accept, signs the on-chain registration, calls registerBundle, and writes the shell content row. It signs with an admin wallet (mounted read-only into the container) and shares the games-host data dir and the shell's runtime dirs.

VariableWhereMeaning
ADMIN_TOKENsubmissionsGates the admin endpoints (list / accept / reject / artifact); timing-safe compared.
GAME_IDsubmissionsThe namespace the reg/blob admin moves are sent as (xarc on the playground), not the game type.
GSP_URLsubmissionsPolled to confirm the registration landed (the move is fire-and-forget).
WAGER_ADDRESSsubmissionsThe plane's ArcadeWager. A registry row's bet ladder and split are read off it, and the service will not start without it.
GAMES_DATA_DIRsubmissions + games-hostShared bundle registry the two write/read.
SITE_INTERNALsubmissionsThe shell URL, set on every plane that runs one. Set → Accept writes the shell content row and card media, then verifies /play/<slug> before marking a game live, and that is the accepted game's play URL. Unset → both are skipped and the play URL is the bare games-host mount /g/<slug>/, which is the only correct answer for a plane with no shell to write a row for.

The shell consumes three runtime variables (all server-side, never NEXT_PUBLIC_: set them to reconfigure without an image rebuild). Each one is purely additive: unset, the site falls back to the baked image's own behaviour, so the same image runs a bare deployment and a full one. Both live planes set GAMES_CONTENT_RUNTIME_DIR and MEDIA_RUNTIME_DIR. The arcade leaves SUBMISSIONS_INTERNAL unset, which is why its /api/submissions answers 503 and /submit serves the closed page there; the public playground sets it with AUTO_ACCEPT=1, so an upload is listed with no site rebuild:

VariableMeaning
SUBMISSIONS_INTERNALThe submissions service base URL the /api/submissions proxy forwards to. Unset → those routes 503 (fail closed) and no page offers an upload door, whatever the plane.
GAMES_CONTENT_RUNTIME_DIRThe overlay dir merged over the baked content rows (Accept writes here). Unset → baked rows only.
MEDIA_RUNTIME_DIRThe dir the /media/community/... route streams uploaded card art from. Unset → that route 404s.

Wagering

Deploy ArcadeWager once; per game follow the three-action enable chain in Hosting & registration: on-chain actions first, bundle flags last.

A queue row is retired by its own amount, not by its payee. The contract's replay key is keccak256(betAmount, numPlayers, matchId, payee, amount): the same five fields the GSP keys a prepayment on. A payment at any other amount therefore marks a different key and leaves the row payable, so nobody can retire a row owed a whole pot by paying one wei of it. Anything that derives that key (the shell's snapshot builder, the SDK's paid-set read) must pass the row's own amount or it will read a still-payable row as settled and snapshot past the queue front.

A paid join names the payout group it consents to. The consent hash covers the match's stored queue snapshot, and those bytes do not change when some other lobby's fill consumes the group they lead with, so a join also sends the matchId of the group the contract's start-time walk would land on right now. If that group moves between reading and filling, the join reverts rather than paying whatever sits behind it. Seats after the first inherit the group the lobby already committed to; a lobby whose front has moved unwinds through leaveMatch/abortMatch/expireMatch with every stake refunded.

The contract does not own the GSP's g/ name. The operator wallet keeps it and grants the contract an ERC-721 approval over it, which is what lets _startMatch author the start move. That distinction matters twice over:

  • It is the emergency stop. Revoking that approval makes _startMatch revert, so no paid match can start, while creates and partial joins still unwind through abortMatch/expireMatch. There is no pause function and no transferOwnership on the contract: this is the only lever.
  • The name never leaves the operator wallet. The end-to-end suite hard-fails if it ever does, because a transferred name would strand every in-flight match.

Registry admin moves are authored under that same g/ name.

Queues are pooled per (tier, seats) across games, and a new combo's first three paid matches pay the operator: see Hosting & registration §4.