-
Notifications
You must be signed in to change notification settings - Fork 10
Xaya Architecture
This page gives you the component-level map of a Xaya game running on Polygon: which pieces exist, what each one does, and how data flows between them. After reading it you'll know where the blockchain ends and your code begins, why a "bridge" daemon sits in the middle, and why two strangers running the same game software always compute exactly the same game state. If you're new to the underlying ideas (the chain as an ordered move log, determinism), start with Blockchain Gaming Basics first.
A running Xaya game on Polygon consists of four layers:
- Polygon PoS + the XayaAccounts contract — where names live and moves are recorded.
- XayaX — a bridge daemon that reads the chain and re-exposes it in a chain-agnostic format.
- The GSP (Game State Processor) — a daemon embedding libxayagame plus your game logic, which derives the game state.
- Clients and tools — anything that reads game state from the GSP over JSON-RPC.
flowchart TB
subgraph L1["Layer 1: Polygon PoS mainnet"]
WALLET["Player wallet"]
XA["XayaAccounts contract<br/>0x8C12253F71091b9582908C8a44F78870Ec6F304F<br/>(names as ERC-721, move() calls)"]
WALLET -- "move() transaction" --> XA
end
subgraph L2["Layer 2: Bridge"]
XX["XayaX (eth connector)<br/>watches contract events,<br/>serves Xaya-Core-style RPC + ZMQ"]
end
subgraph L3["Layer 3: Game State Processor"]
GSP["GSP daemon<br/>libxayagame + your game rules<br/>(e.g. moverd, game ID mv)"]
end
subgraph L4["Layer 4: Clients & tools"]
CLI["curl / scripts / bots / game UIs"]
end
XA -- "blocks + moves" --> XX
XX -- "JSON-RPC 2.0 + ZMQ block notifications" --> GSP
GSP -- "JSON-RPC: getcurrentstate,<br/>waitforchange, ..." --> CLI
Note the shape of the arrows: there is exactly one path down through the stack, and a separate path from the player's wallet straight onto the chain. We'll come back to that.
All persistent game data ultimately lives on Polygon PoS mainnet. The anchor is the XayaAccounts contract at 0x8C12253F71091b9582908C8a44F78870Ec6F304F, an ERC-721 contract with two jobs:
-
Names. Every Xaya identity (e.g.
p/alice— namespacepfor players) is an NFT owned by a wallet. Registration costs a flat 1 WCHI and the name is yours permanently. One name can play every Xaya game: the samep/alicesends moves for mover, for XayaShips, for anything else. -
Moves. A game move is simply a
move()call on this contract: the owner of a name submits a JSON string, and the contract emits it as part of the transaction's calldata/logs. The chain doesn't interpret the JSON at all — it just timestamps and orders it.
Polygon's ~2-second block time and very low fees (a move costs well under $0.01 in POL) are why Xaya games run here today. Details of names, registration, and the move() call are on Names and Moves.
XayaX is a daemon that subscribes to Polygon through any EVM RPC node (the Xaya team runs a public one at https://polygon-node.xaya.io), watches the XayaAccounts contract, and translates blocks and moves into the same JSON-RPC + ZMQ interface that the original Xaya Core daemon exposed.
Why does this layer exist at all? Because of history and portability:
- libxayagame — the library every GSP is built on — was originally written against the Xaya Core blockchain's RPC interface (
getblockchaininfo, ZMQ block-attach/detach notifications, and so on). - Rather than teach every GSP about every chain, XayaX presents any chain in that one well-known dialect. The GSP genuinely does not know or care which chain is behind the bridge — from its point of view, it's talking to a Xaya Core node that happens to report
"chain": "polygon".
The practical payoff: the same GSP code runs against Polygon, other EVM chains, or the original Xaya Core chain, just by pointing it at a different XayaX connector. See Other Chains for what (little) changes per chain, and Running XayaX for how to run the bridge yourself.
The Game State Processor is where your game actually lives. It's a daemon that embeds libxayagame plus your own rules code, and it does one thing: deterministically derive the current game state from the ordered sequence of moves on the chain.
Concretely, the GSP:
- connects to XayaX over JSON-RPC and subscribes to its ZMQ block notifications,
- starts from your game's defined genesis block and replays every block since, feeding each block's moves to your
ProcessForwardlogic, - stores the resulting state locally (memory, SQLite, or LMDB), along with per-block undo data so it can rewind cleanly when the chain reorganizes,
- serves the state to clients over its own JSON-RPC server.
Anyone can run a GSP. There is no privileged instance — the one you run in your basement is exactly as authoritative as anyone else's, because they all compute the same thing. The concepts (callbacks, undo data, reorgs, storage) are covered on What Makes a GSP, and you'll build and run a real one in the mover tutorial.
Anything that can speak JSON-RPC over HTTP can read game state from a GSP: curl, a Python script, a bot, a full game frontend. The interface is small — getcurrentstate, getnullstate, getpendingstate, waitforchange, stop — and documented on GSP RPC Interface. A quick taste against a synced mover GSP:
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getnullstate","params":[]}' \
http://127.0.0.1:8602You should see something like:
{"id":1,"jsonrpc":"2.0","result":{"blockhash":"6a4c517105b5b97a7052848c812f62c5ed6dca59950877caa27b285e5d309e09","chain":"polygon","gameid":"mv","height":88370225,"state":"up-to-date"}}The standard client pattern is a long-poll loop: call waitforchange with the last block hash you know, and when it returns (a new block arrived — every ~2–3 seconds on Polygon), call getcurrentstate and repeat.
This is the single most important property of the architecture:
flowchart LR
P["Player wallet"] -- "writes (move tx)" --> C["Polygon chain"]
C -- "reads" --> X["XayaX"]
X -- "reads" --> G["GSP"]
G -- "reads" --> T["Clients / tools"]
- Reads flow down: chain → XayaX → GSP → clients. Each layer only ever consumes from the layer above it.
- The GSP never writes to the chain. It has no wallet, no keys, no way to transact. It is a pure function of chain history.
-
Players write to the chain directly. When you make a move, your wallet sends a
move()transaction to the XayaAccounts contract — the GSP isn't involved in submitting it at all. It just sees the move arrive in a block a couple of seconds later, like everyone else's GSP does.
This separation is what makes the system trustless: the component that interprets the game (the GSP) cannot influence what gets recorded, and the component that records (the chain) doesn't interpret anything.
Since one name can play many games, every on-chain move is wrapped in an envelope that says which game(s) it's for. The outer object has a single key "g" (game moves); inside it, keys are game IDs and values are the game-specific move data. A real, verified example — the move that sends a mover player two steps up:
{"g":{"mv":{"d":"k","n":2}}}-
"mv"is mover's game ID; a single transaction could carry moves for several games at once by listing more game IDs. - The mover GSP receives only the inner part,
{"d":"k","n":2}, together with the sender's name without thep/prefix (e.g."xsv5bob"). XayaX does this unwrapping and filtering per game ID, so your game logic never sees other games' traffic. (The exact structure your code is handed is covered in What Makes a GSP.)
The full envelope rules live on Names and Moves.
How do players agree on the game state if there's no server? By determinism plus replication:
- The chain gives everyone the same totally-ordered list of moves.
- The GSP's rules are a pure, deterministic function of that list: no randomness without an on-chain seed, no wall-clock time, no external I/O.
- Therefore everyone running the same GSP code computes the same state. This isn't just theory — during the verification run for this wiki, two independently-synced
moverdinstances returned byte-identicalgamestateJSON for the same block.
Cheating becomes structurally pointless: an invalid move isn't rejected by anyone, it's simply ignored by every honest GSP's parser, identically. Modifying your own GSP to accept it just means you're now playing a different game alone, while everyone else's state agrees against you. What Makes a GSP digs into the determinism rules you must follow when writing game logic.
In practice the whole backend is two containers on one Docker network, as in the tutorial's ~/mover-polygon/docker/docker-compose.yml:
| Service | Image | Role | Host port |
|---|---|---|---|
xayax |
xaya/xayax |
Bridge: eth connector pointed at https://polygon-node.xaya.io and the XayaAccounts contract |
127.0.0.1:8101 → 8000 |
moverd |
built from the xaya/libxayagame base |
The GSP: mover rules + libxayagame, SQLite storage, pruning |
127.0.0.1:8602 → 8600 |
They share a private network (mover-net, pinned to the subnet 172.16.0.0/24 — the tutorial explains why you must pin it explicitly and pick a range that doesn't collide with your own networks), and the GSP reaches the bridge by Docker service name: its RPC URL is http://xayax:8000, and XayaX advertises its ZMQ block feed as tcp://xayax:28555 — the advertised ZMQ address must be reachable from the GSP container, which is why it uses the service name rather than localhost. Both RPC ports are published only on 127.0.0.1; nothing here needs to face the internet for you to develop and play. Each service keeps its data in a named volume, so containers can be rebuilt without resyncing.
There is no third "chain" container: Polygon itself is reached through a remote RPC node, so a fresh XayaX starts near the current chain tip and is serving RPC within about two minutes, backfilling older blocks on demand when the GSP asks for them.
Two extensions of this picture are worth knowing about now:
-
Other chains. The exact same stack runs on other EVM chains (XayaX's
ethconnector plus an XayaAccounts deployment) and on the original Xaya Core chain (thecoreconnector, where names and moves are native chain features). Your GSP code changes by oneswitchcase. See Other Chains. - Game channels. For real-time gameplay, on-chain moves every ~2 seconds aren't always enough. Game channels add an off-chain fast path: players exchange signed state updates directly, and the chain (and GSP) only get involved for opening, closing, and disputes. XayaShips on Polygon is a live example. See Game Channels for the overview.
-
Names and Moves — register a name, understand the
move()call and envelope in depth. - What Makes a GSP — the callbacks, undo data, and determinism rules behind every GSP.
- Running XayaX — stand up the bridge with Docker.
- Tutorial: Mover, Part 1 — build and sync a complete game backend yourself.
Xaya developer documentation — verified on Polygon mainnet. Questions? Xaya Development Discord