The Xaya Arcade is a shared platform for provably-fair, real-time games. One game-agnostic host runs the rules for many games at once, so building a game here is not building a blockchain backend — it is writing rules and a UI, and handing over two files.
What a game is
A game is exactly two artifacts, and only two:
- A
rules.wasmblob — a freestanding, zero-import WebAssembly reactor that exports thearcade_*ABI. It is the deterministic judge: it decides which moves are legal, whose turn it is, and who won. The chain knows it only by a sha256 of its raw bytes, so "which rules is the chain running?" always has a one-word answer. See The rules blob. - A UI bundle — a Next app built on
@xayaarcade/sdk, plugged in through a singleGameAdapter. The SDK owns the channel protocol, the wallet, the lobby, the relay, and disputes; your game owns its rules, its renderer, and its input. See The SDK.
You never run a server. Everything else is platform plumbing you do not write.
One consequence of the shared games origin is yours to handle. Games are served
from a games origin of their own — a hostname separate from the arcade shell, so
nothing your bundle does can reach the shell's page or storage — but every game shares
that one origin under a different path. So a root-absolute URL —
fetch('/data.json'), <img src="/sprite.png">, url(/bg.png) — resolves to the
games origin's root rather than your game, and your localStorage namespace is shared
with every other game. Keep asset references relative, and set STORAGE_PREFIX to your
own game's key. Make it yours covers both.
How the pieces fit
browser
│ loads your UI bundle (served by the games-host)
▼
your frontend ──ws──► relay ──► host GSP ──loads──► your rules.wasm
(SDK) │ (the game-agnostic referee)
▼
xayax ──► Polygon
-
Shell (
xaya-arcade) — the arcade site: login, the game library, the lobby, and the iframe that embeds a game. This is the app you are reading. -
games-host — serves every registered UI bundle from one games origin, each mounted at its own path
/g/<slug>/, and hands each one its endpoints (gspUrl,relayUrl,chainRpcUrl,arcadeOrigin,gameId) via<basePath>/arcade-config.jsonat serve time. By default that origin is a hostname of its own: the host refuses to start when it equals the shell's, and the shell refuses to frame a bundle served from its own origin, because an iframe on the same origin shares the embedder's realm — its page, wallet plumbing and stored credentials — outright. One operator flag,GAMES_SAME_ORIGIN_FIRST_PARTY=1, declares every hosted bundle first-party and lifts both refusals, and both public planes run with it: arcade.xaya.io and test-arcade.xaya.io serve every game from their own hostname under/g/<slug>/. On such a plane a bundle can reach the shell's realm, which is why the arcade lists only games whose repository has been read — and why an attached bundle on the playground shares that plane'slocalStoragewith its shell and with every other attached bundle: write under your ownstoragePrefixand nowhere else.Two URLs, and they are not the same thing.
/g/<slug>/is where the bundle is mounted — the iframe's src./play/<slug>is the page a player visits, which is the shell wrapping that frame. You build against the first and link people to the second.Your bundle is slug-agnostic:
./scripts/build-export.sh --bundlebakes the fixed placeholder/__arcade_base__and refuses a caller who pre-setsNEXT_PUBLIC_BASE_PATHto anything else (scripts/build-export.sh— the mistake is cheap before the build and expensive after). The platform rewrites that placeholder to/g/<slug>at registration, so the same bytes can be served under any slug with no rebuild — and a bundle whoseindex.htmlcarries no placeholder is rejected at pre-flight rather than silently served broken. -
Host GSP — the referee. It contains no game code; it loads whichever
rules.wasma channel's game type is pinned to and calls thearcade_*ABI. -
relay — the WebSocket transport players exchange signed off-chain states over.
-
xayax → Polygon — the base chain the channel opens, closes, and settles on.
-
ArcadeWager contract — optional, operator-enabled wagering. A game is playable for free without it. Payouts are pooled by stake across the whole arcade: a 10-WCHI win in any game joins one 10-WCHI payout queue and is paid, when it reaches the front, by a later 10-WCHI match in any game — so picking a different game no longer means a different queue. The first three paid matches at a stake and player count nobody has played yet pay their pot to the operator — a one-time fee that gets that stake's payouts started.
The rules of the road
- Free play first. Every game is playable with no stake. Wagering is a separate, optional, operator-enabled module — your game code stays wager-free. See Hosting & registration.
- The shell is the only signer. When your game is embedded in the arcade it
holds no wallet. The shell owns the signature and brokers every move to your
frontend over a
postMessagebridge. Never write wallet integration into a game. - Determinism is the whole game. The same blob must produce the same result on native C++, on wasmtime (the host), and in V8 (the browser judge). The template ships a three-way determinism gate you keep. See Testing.
- Submissions are closed during the curated phase. arcade.xaya.io takes no
uploads: its submissions service is switched off, Submit is hidden and
/api/submissionsanswers 503. The five first-party games there were registered by the operator. Prove your game on the playground — upload at test-arcade.xaya.io/attach, which wipes every Monday 04:10 UTC — then get in touch in #builders on the Xaya Discord with your repository and your four registration values. /repos says what every repository is and whether you can clone it today, and wager enablement stays a manual operator step.
Two planes, and you should use both
There are two of these stacks, running the same code:
| arcade.xaya.io | the real one, in mainnet beta. Polygon, real wallets and real names, five first-party games with WCHI wagering (free / 1 WCHI / 10 WCHI). Curated for now: the moderated submission queue is closed — Submit is hidden and /api/submissions answers 503. Acceptance here is permanent. |
| test-arcade.xaya.io | a disposable playground on its own forked chain. Pre-funded test wallets, uploads at /attach auto-accept, wiped every Monday around 04:10 UTC and without notice in between, reserves nothing. |
The playground runs the same pre-flight, the same bake and the same go-live
pipeline — that identity is the entire point of it. So the honest order is: build and
test offline for free, point your dev server at the playground, dry-run the real
submission there and play a full match, and only then hand the same bytes to the
operator (#builders in Discord). It is the difference between finding out your
bundle will not boot in ten seconds and finding out after the hand-over.
Where to go next
- What you need — the repos, the tooling, and what costs money (almost nothing).
- Getting started — clone, run, and point it at the playground.
- Make it yours — turn the template into your game.
- The rules blob and The SDK — the two things you actually write.
- Testing — the ladder, and what the playground does and does not prove.
- Submitting — the dry run, then the real thing.
Driving an AI agent? /docs/ai-agent is the literal opening prompt,
and /skills hands you the two agent skills — one button, a
zip you unzip into .claude/skills/.