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
| Component | Repo | What it does |
|---|---|---|
Host GSP (arcaded) | arcade-platform | The on-chain referee: one process, game-code-free, runs every game's rules.wasm in the embedded wasmtime judge. Follows the chain through XayaX. |
| XayaX | xaya/xayax | The Polygon→GSP move bridge the GSP consumes. |
| Relay | xaya-relay | The WebSocket transport that carries every game's channel proofs. |
| games-host | arcade-platform | Serves 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. |
| submissions | arcade-platform | Self-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-arcade | Wallet, identity, game library, embeds, the /submit and /admin/submissions UIs. |
| ArcadeWager | arcade-wager | The 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.
| Variable | Where | Meaning |
|---|---|---|
ADMIN_TOKEN | submissions | Gates the admin endpoints (list / accept / reject / artifact); timing-safe compared. |
GAME_ID | submissions | The namespace the reg/blob admin moves are sent as (xarc on the playground), not the game type. |
GSP_URL | submissions | Polled to confirm the registration landed (the move is fire-and-forget). |
WAGER_ADDRESS | submissions | The 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_DIR | submissions + games-host | Shared bundle registry the two write/read. |
SITE_INTERNAL | submissions | The 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:
| Variable | Meaning |
|---|---|
SUBMISSIONS_INTERNAL | The 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_DIR | The overlay dir merged over the baked content rows (Accept writes here). Unset → baked rows only. |
MEDIA_RUNTIME_DIR | The 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
_startMatchrevert, so no paid match can start, while creates and partial joins still unwind throughabortMatch/expireMatch. There is no pause function and notransferOwnershipon 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.