-
Notifications
You must be signed in to change notification settings - Fork 10
GSP RPC Interface
This page is the reference for the JSON-RPC interface that every Game State Processor (GSP) built on libxayagame exposes. It is how any client — a game UI, a bot, an explorer, a shell script — reads game state: you call a handful of standard methods over plain HTTP and get JSON back. You'll learn the transport, the response envelope, every standard method with real captured outputs, the exact sync-state values, and the standard polling loop, finishing with a complete worked example that tracks a mover player from a bash script.
If you haven't run a GSP yet, do Tutorial-Mover-Part-1-Setup first — the examples below query the moverd instance built there.
The GSP is the read side of a Xaya game. Data flows one way — chain to client — and the RPC interface is the last hop:
flowchart LR
P[Polygon PoS] -->|XayaAccounts events| X[XayaX bridge]
X -->|JSON-RPC + ZMQ| G["GSP<br/>libxayagame + game logic"]
G -->|"JSON-RPC over HTTP<br/>(this page)"| C1[Game UI]
G -->|JSON-RPC| C2[Bot / script]
G -->|JSON-RPC| C3[Explorer / tooling]
Clients never write through the GSP — moves go to the chain directly via the wallet (see Names-and-Moves). The GSP serves a single job here: answer "what is the game state right now?" quickly and consistently.
The interface is plain JSON-RPC 2.0 over HTTP — no WebSockets, no custom framing, no authentication layer. Anything that can POST JSON can be a client.
The GSP's server port is set with the --game_rpc_port flag when starting the GSP:
--game_rpc_port=8600 # GSP's own JSON-RPC server
--game_rpc_listen_locally=false # bind 0.0.0.0 (e.g. inside a container)
By default the server listens only on localhost; --game_rpc_listen_locally=false makes it bind all interfaces, which you need when the GSP runs in Docker and you query it from the host. In the mover tutorial's Docker Compose setup, container port 8600 is published on host port 127.0.0.1:8602, so all examples below talk to http://127.0.0.1:8602.
A request is a standard JSON-RPC 2.0 envelope:
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getnullstate","params":[]}' \
http://127.0.0.1:8602Security note: the RPC server has no authentication, and
stopshuts the GSP down. Never expose the port to untrusted networks; keep it on localhost or behind a reverse proxy if you serve state publicly.
The state-returning methods share a common envelope, assembled by libxayagame:
| Field | Meaning |
|---|---|
gameid |
The game ID this GSP processes (mv for mover) |
chain |
Chain behind XayaX — "polygon" on Polygon mainnet |
state |
The GSP's sync state (exact values below) |
blockhash |
Hash of the block the returned state corresponds to |
height |
Height of that block |
gamestate |
The game-defined state JSON (only in getcurrentstate) |
Two things to know:
-
blockhashandheightare omitted when the GSP has no current state yet (for example while it is still inpregenesis, before the chain reached the game's genesis block). Robust clients check for their presence. -
gamestateis entirely game-defined: libxayagame calls the game'sGameStateToJsonand inserts whatever it returns. For mover it is{"players": {"<name>": {"x": ..., "y": ...}}}(plusdir/stepswhile a player is moving); for an SQLite-backed game it can be anything the game chooses to render. The envelope is the contract; the payload is per-game. See What-Makes-a-GSP for whereGameStateToJsonfits in.
The state field tells you whether the returned data is current. These are the exact strings from libxayagame's Game::StateToString (in game.cpp):
| Value | Meaning |
|---|---|
"unknown" |
State not yet well-defined — briefly at startup, or after a missed ZMQ message forces re-initialization |
"pregenesis" |
The chain (as seen via XayaX) is still below the game's genesis block; no game state exists yet |
"out-of-sync" |
A game state exists but is not at the chain tip; transitions to catching-up almost immediately |
"catching-up" |
The GSP requested and is processing block updates to reach the tip |
"at-target" |
Synced to an explicitly configured target block and holding there; not seen in normal operation |
"up-to-date" |
At the current chain tip — the normal steady state |
"disconnected" |
The connection to XayaX is down or stalled; the GSP is reconnecting |
For most clients the rule is simple: trust gamestate fully when state is "up-to-date"; during "catching-up" the data is valid but historical; anything else means wait.
Every GSP that uses libxayagame's default GameRpcServer (mover does, via DefaultMain) exposes exactly these methods.
Returns the envelope without gamestate — the cheap way to check sync status without serializing the whole game state.
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"}}Use this for health checks, readiness probes, and the "am I synced yet?" loop after first start.
Returns the full envelope including gamestate.
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getcurrentstate","params":[]}' \
http://127.0.0.1:8602You should see something like (after the move sent in Tutorial-Mover-Part-2-Playing):
{"id":1,"jsonrpc":"2.0","result":{"blockhash":"5e10e660e23015470501b6e6a41d88290b76509a544590867d39e2d7d176a05b","chain":"polygon","gameid":"mv","gamestate":{"players":{"xsv5bob":{"x":0,"y":2}}},"height":88370263,"state":"up-to-date"}}Note that the player key is the bare name (xsv5bob), without the p/ namespace prefix — that is how libxayagame hands names to game logic, and mover passes them straight through.
For large games, rendering the entire state on every call can be expensive — which is exactly why such games add their own narrower query methods (see "Game-specific extensions" below).
Long-poll for state changes. Takes one parameter, the block hash you currently know, and returns the (new) best block hash as a plain string:
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"waitforchange","params":["6a4c517105b5b97a7052848c812f62c5ed6dca59950877caa27b285e5d309e09"]}' \
http://127.0.0.1:8602Exact behavior (from GameRpcServer::DefaultWaitForChange and Game::WaitForChange):
- If the current best block already differs from the hash you passed, it returns immediately with the current best block hash.
- An empty or invalid hash is treated as "no known block": the call does not return immediately, but waits for the next change (or the internal timeout below) and then returns the current best hash.
- Otherwise it blocks until the state changes — on Polygon's ~2 second blocks that's typically a 2–3 second wait (verified).
- It does not block forever: the wait has an internal timeout (5 seconds by default, tunable on the GSP with
--xaya_waitforchange_timeout_ms), after which it returns the current hash even if nothing changed. Your client must compare the returned hash with the one it sent and simply call again if they're equal — don't treat every return as a new block. - If the GSP has no state at all yet, it returns an empty string.
A pending waitforchange does not block the server: other RPC calls are answered concurrently (verified — a getnullstate issued while a waitforchange was blocking answered in 7 ms). So a UI can long-poll on one connection and serve instant queries on another.
Returns the state of pending (unconfirmed, mempool) moves. When pending tracking is active, the result carries the usual gameid/chain/state/blockhash/height fields plus a version counter and a game-defined pending field.
Pending tracking requires the upstream daemon to publish pending moves over ZMQ. XayaX does not provide this, so on Polygon pending state is unavailable: the GSP logs Not subscribing to pending moves at startup (verified, even with --pending_moves=true), and getpendingstate answers with a JSON-RPC error ("pending moves are not tracked", from libxayagame's Game::UnlockedPendingJsonState) rather than a result. With ~2 second blocks, polling confirmed state covers nearly every use case anyway.
The long-poll counterpart for pending state: takes the last version number you saw (or 0 to always block) and returns the new pending state when it changes — with the same internal timeout as waitforchange, so compare the returned version with yours. Like getpendingstate, it answers with the same "pending moves are not tracked" error when pending tracking is inactive — which is always the case via XayaX — and is listed here for completeness.
Shuts the GSP down cleanly:
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"stop","params":[]}' \
http://127.0.0.1:8602The GSP flushes storage and exits. This is the supported way to stop a GSP from scripts (and another reason not to expose the port publicly).
Almost every client — UI, bot, or indexer — runs the same loop:
sequenceDiagram
participant C as Client
participant G as GSP
C->>G: getcurrentstate
G-->>C: state + blockhash H1
loop forever
C->>G: waitforchange(H1)
Note over G: blocks until a new block<br/>(or internal timeout)
G-->>C: H2
alt H2 != H1
C->>G: getcurrentstate
G-->>C: new state + H2
Note over C: update UI / act<br/>H1 := H2
else H2 == H1 (timeout)
Note over C: nothing changed, loop again
end
end
This gives you updates within milliseconds of the GSP processing a block, with zero busy-polling. Because concurrent calls are fine, you can run this loop in the background while answering ad-hoc queries on the side.
The six methods above are the standard set, but games are free to extend the server: instead of the stock GameRpcServer, a game can ship its own RPC server class with extra methods (libxayagame even exposes GameRpcServer::DefaultWaitForChange as a static helper so custom servers can reuse the standard behavior). XayaShips, the live game-channels game on Polygon, does exactly this — its GSP adds channel-related methods on top of the standard ones (see Game-Channels). Mover has only the standard set.
So when integrating against a specific game's GSP, check its documentation/source for extra methods — but the six standard ones will always be there for libxayagame-based games.
Putting it together: this script follows one mover player and prints their position every time it changes. It's illustrative (shown here to demonstrate the pattern, not a captured transcript) — but it is exactly the loop the tutorial uses interactively.
#!/usr/bin/env bash
# track-player.sh — follow a mover player's position via the GSP RPC.
# Usage: ./track-player.sh xsv5bob
RPC=http://127.0.0.1:8602
PLAYER="$1"
rpc() {
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":[$2]}" \
"$RPC"
}
known=""
while true; do
# Long-poll until the GSP sees a new block (returns the new best hash).
new=$(rpc waitforchange "\"$known\"" | python3 -c \
'import sys,json; print(json.load(sys.stdin)["result"])')
# Timeout with no change: same hash comes back, just poll again.
[ "$new" = "$known" ] && continue
known="$new"
# Fetch the full state and extract our player.
rpc getcurrentstate "" | python3 -c '
import sys, json
r = json.load(sys.stdin)["result"]
p = r.get("gamestate", {}).get("players", {}).get(sys.argv[1])
where = "({},{})".format(p["x"], p["y"]) if p else "not on the map"
print("block {} [{}]: {} {}".format(r["height"], r["state"], sys.argv[1], where))
' "$PLAYER"
doneRun it while sending a move (as in Tutorial-Mover-Part-2-Playing) and you'll see the player appear at (0,0) and walk to their destination over the following blocks, one line per block. If you prefer jq, the extraction is a one-liner (jq -r .result.gamestate.players.\"$PLAYER\"); python3 is used above because it's preinstalled nearly everywhere.
The same pattern translates directly to any language: an HTTP POST helper, a waitforchange loop with hash comparison, and getcurrentstate when something changed.
- Xaya-Architecture — where the GSP and its RPC fit in the whole stack
-
What-Makes-a-GSP — how
gamestateis produced (GameStateToJson, undo data, determinism) - Tutorial-Mover-Part-1-Setup — build and run the GSP these examples query
- Tutorial-Mover-Part-2-Playing — send the moves you'll watch arrive here
- Running-XayaX — the bridge feeding the GSP
- Glossary — quick definitions of GSP, move, reorg and friends
Xaya developer documentation — verified on Polygon mainnet. Questions? Xaya Development Discord