Skip to content

GSP RPC Interface

Andy Colosimo edited this page Jun 12, 2026 · 1 revision

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.

Where the RPC interface sits

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]
Loading

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.

Transport

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:8602

Security note: the RPC server has no authentication, and stop shuts 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 response envelope

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:

  • blockhash and height are omitted when the GSP has no current state yet (for example while it is still in pregenesis, before the chain reached the game's genesis block). Robust clients check for their presence.
  • gamestate is entirely game-defined: libxayagame calls the game's GameStateToJson and inserts whatever it returns. For mover it is {"players": {"<name>": {"x": ..., "y": ...}}} (plus dir/steps while 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 where GameStateToJson fits in.

Sync-state values

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.

Standard methods

Every GSP that uses libxayagame's default GameRpcServer (mover does, via DefaultMain) exposes exactly these methods.

getnullstate

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:8602

You 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.

getcurrentstate

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:8602

You 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).

waitforchange

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:8602

Exact 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.

getpendingstate

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.

waitforpendingchange

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.

stop

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:8602

The 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).

The standard client polling loop

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
Loading

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.

Game-specific extensions

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.

Worked example: track a mover player from a shell script

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"
done

Run 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.

Related pages

Clone this wiki locally