RPC & REST API
All JSON-RPC methods and REST endpoints the GSP serves, with live examples.
In plain language (for players)#
Taurion runs on a blockchain, so "the game" is really a shared, tamper-proof ledger
of everything that has happened. Your client (the 3D game in your browser) cannot
just ask the blockchain questions quickly. Instead, a helper program called the
Game State Processor — its program name is tauriond, also called the GSP —
reads the blockchain, replays every confirmed move, and keeps an up-to-date copy of
the whole game world (every vehicle, building, ore deposit, market order, etc.) in a
fast local database. The game client talks to this GSP to draw the world.
The way the client talks to the GSP is JSON-RPC: the client sends a small text message naming a "method" (a question, like "list all characters"), and the GSP sends back the answer as text. This document lists every such method. A few practical things a player should know:
- The GSP can only read, never change the game. When you move a vehicle, attack, or place a market order, that is a move sent to the blockchain itself (via your wallet). The GSP just notices the result afterward. There is no "do something" method here — only "tell me what's going on" methods.
- Your vehicles are "characters" in the data. What you see as a Raider (
rv st), Barracuda (bv st) or Scarab (gv st) is internally a "character" piloting a "vehicle" of that type. (This document uses the game UI's display names alongside the on-chain codes.) - Cubit is the in-game currency; CHI / WCHI is the real Xaya coin you spend to mint Cubit in the burnsale.
- The data can be stale. Because the GSP is catching up to the blockchain, every
answer carries a
statefield. Onlyup-to-datemeans "this is the live world".
The rest of this document is the technical contract for client developers.
This document describes every JSON-RPC method that a client can call against
the Taurion Game State Processor (tauriond, the "GSP"). It is the complete API
contract between any UI and the on-chain game state.
Source of truth:
src/pxrpcserver.cpp,src/pxrpcserver.hpp— the GSP RPC server (custom + standard methods).src/rpc-stubs/pxd.json,src/rpc-stubs/nonstate.json— the libjson-rpc-cpp method specs (param names/types are generated from these).src/gamestatejson.cpp— produces the response bodies for everyget*data method.libxayagamexayagame/game.cpp,xayagame/gamerpcserver.cpp— the standard envelope + long-poll behavior.src/services.cpp,src/pending.cpp— shapes forgetserviceinfoand the pending state.
Important architectural note. The GSP RPC server is read-only with respect to game state. There is no RPC method to submit a move. Moves are submitted on-chain via the Xaya
XayaAccounts.move(...)contract call (see the client section and the Moves Reference). The GSP only reads confirmed state, reads the pending mempool state, and offers a few stateless map/path helpers.
0. Quick start (smoke-test your endpoint)#
The cheapest call is getnullstate — it returns just the §2 envelope (no payload),
so it is the fastest way to confirm your GSP endpoint is reachable and how far it has
synced. In every curl example below, # replace 127.0.0.1:8601 with your GSP endpoint — the dev reference port is 8601 (JSON-RPC); the read-only REST mirror is
on 8701 (see §8).
# replace 127.0.0.1:8601 with your GSP endpoint
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getnullstate","params":[],"id":1}' | jq .
Real response captured from a live, fully-synced Taurion GSP (Polygon mainnet, game id
tn):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"gameid": "tn",
"chain": "polygon",
"state": "up-to-date",
"blockhash": "1c1311b11cff7e114716274d5aa08337710d1658d13fd4aed169e29a0b94705f",
"height": 88371499
}
}
If state is "up-to-date" the world data is current. Anything else means the GSP is
still syncing or disconnected — see §2.1. All examples in this document were captured
from this same live GSP; responses are pretty-printed and trimmed where large
(trimmed spots are marked with a // … note or a "...": "… trimmed …" marker).
Note on the live capture used for examples. This particular GSP had one initialised account (
snailbrain) and one character (id1001), with three test accounts (snailbrain,johnv5,johnagent) holding stocked inventories in the Reubo command-center building (id5). Empty-result methods below (e.g.getgroundloot,getongoings,getbootstrapdata) genuinely returned[]/ no regions on this chain because nothing had been dropped/prospected yet; their non-empty shapes are shown as illustrative examples drawn from the source/tests.
1. Transport & framing#
- Protocol: JSON-RPC 2.0 over HTTP
POST. - Single endpoint: all methods are served from one HTTP URL (the GSP listens on a
configurable port; in the reference dev setup that is
http://127.0.0.1:8601, reached by the client through the/gspproxy — see §6). - Request body:
{"jsonrpc":"2.0","method":"<name>","params":<params>,"id":<n>}. - Success:
{"jsonrpc":"2.0","result":<value>,"id":<n>}. - Error:
{"jsonrpc":"2.0","error":{"code":<int>,"message":"<str>"},"id":<n>}.
1.1 Parameter passing: positional vs named#
libjson-rpc-cpp accepts both positional (array) and named (object) params,
matching the shapes declared in src/rpc-stubs/pxd.json. Examples in this doc use
whichever the official client uses. Two concrete gotchas it relies on:
waitforchangeis called positionally:params: ["<blockhash>"].getregions/getbuildingshape/getserviceinfo/getregionat/encodewaypointsare called with named params objects.
The parameter names (for named calls) and order (for positional calls) are fixed by
src/rpc-stubs/pxd.json.
1.2 Error codes#
All custom error codes are defined in src/pxrpcserver.cpp:57-77 (enum class ErrorCode).
These integers are part of the wire protocol and are stable:
| Code | Symbol | Meaning | Thrown by |
|---|---|---|---|
-1 |
INVALID_ARGUMENT |
Malformed argument: bad HexCoord, out-of-range int, invalid faction, bad building type/rotation, invalid waypoint, invalid buildings/characters/exbuildings array. |
pxrpcserver.cpp:62, used throughout |
-2 |
INVALID_ACCOUNT |
getserviceinfo was given a name that does not exist as an account. |
pxrpcserver.cpp:65,649 |
1 |
FINDPATH_NO_CONNECTION |
findpath: no route between source and target within l1range. |
pxrpcserver.cpp:68,323 |
4 |
FINDPATH_ENCODE_FAILED |
findpath/encodewaypoints: the resulting waypoint blob could not be encoded (e.g. too large, > 1 MiB serialized). |
pxrpcserver.cpp:69,350,380 |
2 |
REGIONAT_OUT_OF_MAP |
getregionat: coordinate is outside the game map. |
pxrpcserver.cpp:72,399 |
3 |
GETREGIONS_FROM_TOO_LOW |
getregions: fromheight too far below the current tip (see limit in §3.6). |
pxrpcserver.cpp:75,574 |
Standard libjson-rpc-cpp errors also occur:
-32600invalid request,-32601method not found,-32602invalid params (type mismatch vs the stub spec),-32700parse error.getpendingstatethrows an internal error (ERROR_RPC_INTERNAL_ERROR) with message"pending moves are not tracked"if pending tracking is disabled (game.cpp:883).
2. The state envelope (shared by data getters)#
Every "current state" data method wraps its payload in a standard envelope built by
Game::UnlockedGetInstanceStateJson (game.cpp:752-784) plus a per-method data field.
Understanding the envelope is essential: a client must check state and blockhash
before trusting the data.
Envelope fields:
| Field | Type | Meaning | Source |
|---|---|---|---|
gameid |
string | Game ID ("tn" for Taurion). |
game.cpp:755 |
chain |
string | One of "unknown", "main", "test", "regtest", "polygon", "mumbai", "ganache". Taurion mainnet is "polygon". |
game.cpp:756; names in gamelogic.cpp:25-34 |
state |
string | Sync state — see §2.1. | game.cpp:757 |
blockhash |
string (hex) | Block the returned data corresponds to. Absent if no state is known yet (initial sync). | game.cpp:780 |
height |
integer | Block height of blockhash. Absent when blockhash is. |
game.cpp:781 |
| (data field) | varies | The method-specific payload (e.g. data, gamestate). |
per method |
When the GSP has no state yet (e.g. mid-initial-sync or the Xaya node is down),
blockhash/height are omitted and the data field is omitted entirely — the
callback that produces the payload is skipped (game.cpp:806-807). Clients must
handle a response that is just the envelope with no data.
2.1 state values (game.cpp:131-154)#
| Value | Meaning |
|---|---|
unknown |
Not initialized. |
pregenesis |
Waiting for the genesis block. On polygon (Taurion mainnet) that is height 88,343,264, hash c57f860246...; on ganache it is height 0 with any hash. Only these two chains are supported by GetInitialStateBlock — any other chain aborts (logic.cpp:135-157). |
out-of-sync |
Known state exists but is behind / not confirmed up to date. |
catching-up |
Actively syncing a range of blocks. |
at-target |
Synced to an explicitly-set target block and stopped. |
up-to-date |
Fully synced to the network tip. Only this value means the data is current (IsHealthy() returns true only for this; game.cpp:906-910). |
disconnected |
Lost connection to the Xaya node. |
A UI should treat anything other than up-to-date as "data may be stale / still loading".
3. Stateful data methods (read confirmed game state)#
All methods in this section are PXRpcServer members (pxrpcserver.cpp:503-625).
They route through PXLogic::GetCustomStateData, which takes a DB snapshot under lock
and returns the envelope of §2 with the payload at the named data field. Each takes
no params unless noted. None of them mutate state.
Jargon used below. DEX = the in-game Decentralised EXchange, the player marketplace inside buildings where items are bought/sold (bids = buy orders, asks = sell orders). fungible items are stackable goods counted by quantity (ore, materials, blueprints) as opposed to one-of-a-kind objects. A foundation is a building still under construction. An ancient building (faction
"a") is a neutral, owner-less structure that belongs to the world rather than a player. Prospecting is the act of scanning a region to reveal its mineable resource; mining then extracts it.
The data field name differs by method.
getcurrentstateuses"gamestate"; the targeted getters use"data". This is set by libxayagame (GetCurrentJsonState→"gamestate",game.cpp:846;GetCustomStateDatasecond overload → caller passes"data",logic.cpp:227).
3.0 Method catalog#
| Method | Params | Data field | Use |
|---|---|---|---|
getcurrentstate |
none | gamestate (full state) |
Debug only — huge. Prefer targeted getters. |
getnullstate |
none | (none) | Cheapest health/sync probe. |
getbootstrapdata |
none | data.regions (all regions) |
One-time startup load. |
getaccounts |
none | data (array) |
All player accounts. |
getbuildings |
none | data (array) |
All buildings + inventories + DEX orderbooks. |
getcharacters |
none | data (array) |
All characters / vehicles. |
getgroundloot |
none | data (array) |
Loot piles on the ground. |
getongoings |
none | data (array) |
In-progress timed operations. |
getregions |
{fromheight} |
data (array) |
Regions changed since a height (incremental). |
getmoneysupply |
none | data (object) |
Cubit supply + burnsale stages. |
getprizestats |
none | data (object) |
Prospecting prize counters. |
gettradehistory |
{item, building} |
data (array) |
DEX trade history for one item in one building. |
(getserviceinfo is also a stateful, DB-reading method that returns the §2 envelope,
but it takes a service-operation argument and is documented with the other service/preview
helpers in §5.6.)
3.1 getcurrentstate#
- Params: none.
- Returns: envelope (§2) with
gamestate= the full game state (logic.cpp:221→GameStateJson::FullState,gamestatejson.cpp:788-803):{accounts, buildings, characters, groundloot, ongoings, moneysupply, regions, prizes}. - When: debugging/testing only. The source explicitly warns this is not for
production (
gamestatejson.hpp:144-149); it returns all regions and is expensive. Use the targeted getters instead.
Example (real capture, heavily trimmed). The full response was ~340 KB even on this
near-empty chain; do not call this from a UI. The example slices to just the
gamestate keys and their array sizes so you can see the shape without the bulk:
# WARNING: huge response — pipe through jq and never store it in a hot loop
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getcurrentstate","params":[],"id":1}' \
| jq '{state: .result.state,
height: .result.height,
keys: (.result.gamestate | keys),
sizes: {accounts: (.result.gamestate.accounts | length),
buildings: (.result.gamestate.buildings | length),
characters: (.result.gamestate.characters | length),
groundloot: (.result.gamestate.groundloot | length),
ongoings: (.result.gamestate.ongoings | length),
regions: (.result.gamestate.regions | length)}}'
{
"state": "up-to-date",
"height": 88371557,
"keys": [
"accounts", "buildings", "characters", "groundloot",
"moneysupply", "ongoings", "prizes", "regions"
],
"sizes": {
"accounts": 1,
"buildings": 133,
"characters": 1,
"groundloot": 0,
"ongoings": 0,
"regions": 0
}
}
Each sub-array/object is identical to what its dedicated getter returns (accounts →
getaccounts, buildings → getbuildings, etc.); moneysupply and prizes match
getmoneysupply/getprizestats. Prefer those targeted getters in real clients.
3.2 getnullstate#
- Params: none.
- Returns: the envelope of §2 with no data field (the
datamember is built then removed;game.cpp:860-870). i.e.{gameid, chain, state, blockhash?, height?}. - When: the cheapest possible call to check the GSP's health and current block — used by the official client as its connection test.
Example — see §0 Quick start for the full curl + a real captured response. The
result payload is exactly the §2 envelope with no data field:
{
"gameid": "tn",
"chain": "polygon",
"state": "up-to-date",
"blockhash": "1c1311b11cff7e114716274d5aa08337710d1658d13fd4aed169e29a0b94705f",
"height": 88371499
}
3.3 getaccounts#
- Returns: envelope +
data= array of account objects. Built byGameStateJson::Accounts(gamestatejson.cpp:716-743) usingConvert<Account>(gamestatejson.cpp:305-327).
Per-account fields:
| Field | Type | Notes |
|---|---|---|
name |
string | Player name (Xaya p/ name). |
minted |
integer | Cubit minted by this account via burnsale (burnsale_balance). |
balance.available |
integer | Spendable Cubit. |
balance.reserved |
integer | Cubit locked in open DEX bids (gamestatejson.cpp:722-739). |
balance.total |
integer | available + reserved. |
faction |
string | "r"/"g"/"b"; only present if account is initialised (has chosen a faction). |
kills |
integer | Only if initialised. |
fame |
integer | Only if initialised. Defaults to 100 for a fresh account. |
Realistic example (from gamestatejson_tests.cpp:571-597):
[
{
"name": "bar",
"faction": null,
"balance": { "available": 42, "reserved": 6, "total": 48 },
"minted": 10
},
{
"name": "foo",
"faction": "r",
"balance": { "available": 0, "reserved": 0, "total": 0 },
"minted": 0
}
]
(An uninitialised account omits faction/kills/fame; the test fixture serializes
the absent faction as null.)
Example (real capture):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getaccounts","params":[],"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"gameid": "tn",
"chain": "polygon",
"state": "up-to-date",
"blockhash": "fcf5bd391acefd988da8de8fe4aa6eb68948dab4a27bfccb6791b1e3c1c45953",
"height": 88371510,
"data": [
{
"name": "snailbrain",
"faction": "r",
"fame": 100,
"kills": 0,
"minted": 0,
"balance": { "available": 1000, "reserved": 0, "total": 1000 }
}
]
}
}
This GSP had a single initialised account; data is the full array. Note this is an
initialised account, so faction/fame/kills are present (contrast the
uninitialised-account shape above).
3.4 getbuildings#
- Returns: envelope +
data= array of building objects.GameStateJson::Buildings(gamestatejson.cpp:745-750) →Convert<Building>(gamestatejson.cpp:407-468).
Per-building fields:
| Field | Type | Notes |
|---|---|---|
id |
integer | Building ID. |
type |
string | Building type code (e.g. "checkmark", "r rt"). |
foundation |
bool | Present only if this is an unfinished foundation. |
faction |
string | "r"/"g"/"b"/"a" (ancient). |
owner |
string | Present unless faction is ancient. |
centre |
{x,y} |
Centre hex coordinate. |
rotationsteps |
integer | 0–5, 60° per step. |
config.servicefee |
integer | Service fee percent (omitted if unset). |
config.dexfee |
number | DEX fee in percent (= dex_fee_bps / 100; omitted if unset). |
tiles |
[{x,y}...] |
All occupied hex tiles. |
combat |
object | See §3.5 combat sub-object (HP/attacks/target). |
construction |
object | Only for foundations: {ongoing?, inventory:{fungible:{...}}} (materials deposited). |
inventories |
object | Only for finished buildings: map name → {fungible:{...}} of stored player inventories. |
reserved |
object | Finished only: map name → {fungible:{...}} of item quantities locked in that account's open DEX asks. |
orderbook |
object | Finished only: see below. |
age.founded |
integer | Block height founded. |
age.finished |
integer | Block height construction finished (omitted for foundations). |
Orderbook (gamestatejson.cpp:347-403): map keyed by item code; each entry
{item, bids:[...], asks:[...]}. Each order is {id, account, quantity, price}. Bids
are sorted best (highest price) first; asks lowest price first.
Foundation example (gamestatejson_tests.cpp:678-712):
{
"id": 3,
"foundation": true,
"construction": { "ongoing": null, "inventory": { "fungible": { "bar": 10 } } }
}
Orderbook example (gamestatejson_tests.cpp:790-866, abbreviated):
{
"id": 1,
"orderbook": {
"foo": {
"item": "foo",
"bids": [ { "id": 103, "account": "domob", "quantity": 1, "price": 3 } ],
"asks": [ { "id": 104, "account": "domob", "quantity": 1, "price": 8 } ]
}
}
}
Example (real capture). getbuildings returns all buildings (133 on this chain,
all ancient starter structures), so the example fetches the full list and slices it with
jq to a single building — the Reubo Blue command-center (id 5), which is stocked with
test inventories:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getbuildings","params":[],"id":1}' \
| jq '.result.data[] | select(.id==5)'
{
"id": 5,
"type": "b cc",
"faction": "a",
"centre": { "x": 636, "y": 2446 },
"rotationsteps": 0,
"config": {},
"age": { "founded": 0, "finished": 0 },
"combat": {
"hp": {
"max": { "armour": 1000000, "shield": 1000000 },
"current": { "armour": 1000000, "shield": 1000000 },
"regeneration": { "armour": 0, "shield": 10.0 }
}
},
"tiles": [
{ "x": 637, "y": 2441 },
{ "x": 638, "y": 2441 },
{ "x": 639, "y": 2441 }
// … 89 tiles total, trimmed …
],
"reserved": {},
"orderbook": {},
"inventories": {
"snailbrain": {
"fungible": {
"art c": 100, "art r": 100, "art uc": 100, "art ur": 100,
"raw a": 100000000, "raw b": 100000000, "raw c": 100000000,
"lf gun": 2, "lf gun bpo": 1, "vhf refinery": 2, "vhf refinery bpo": 1
// … 290 item codes for this owner, trimmed …
}
},
"johnv5": { "fungible": { "...": "… same stocked set, trimmed …" } },
"johnagent":{ "fungible": { "...": "… same stocked set, trimmed …" } }
}
}
Notes from the live data: ancient buildings (faction:"a") have no owner field;
age.founded/age.finished are 0 for world-seeded structures. On this chain no
building had any open DEX orders, so every orderbook was {} and reserved was
{} — the non-empty orderbook shape is shown in the illustrative example above.
3.5 getcharacters#
- Returns: envelope +
data= array of character objects.GameStateJson::Characters(gamestatejson.cpp:752-757) →Convert<Character>(gamestatejson.cpp:251-303).
Per-character fields:
| Field | Type | Notes |
|---|---|---|
id |
integer | Character ID. |
owner |
string | Owning player name. |
faction |
string | "r"/"g"/"b". |
vehicle |
string | Vehicle type code. Red examples: "rv st" = Raider, "rv s" = Looter, "rv l" = Marauder; Blue: "bv st" = Barracuda; Green: "gv st" = Scarab. |
fitments |
[string...] |
Installed fitment ("module") item codes, may be empty. Examples: "lf gun" = Light Rail Gun, "vhf shield" = Very Heavy Shield Enhancer, "vhf refinery" = Mobile Refinery. |
position |
{x,y} |
Present when on the map. |
inbuilding |
integer | Building ID; present instead of position when inside a building. |
enterbuilding |
integer | Target building ID if a "enter building" intent is set. |
combat |
object | {target?, attacks?, hp, attackers?} — see below. |
speed |
integer | Base speed (milli-hexes/block). |
inventory.fungible |
object | item → count. |
cargospace |
object | {total, used, free} (gamestatejson.cpp:199-210). |
movement |
object | Present if moving; {partialstep?, blockedturns?, chosenspeed?, waypoints?:[{x,y}...]}. |
busy |
integer | The ongoing-operation ID this character is blocked on (if any). |
mining |
object | {rate:{min,max}, active, region?} (region only when actively mining). |
prospectingblocks |
integer | Blocks remaining to prospect (present only while prospecting). |
refining.inefficiency |
integer | Mobile-refinery input multiplier applied to 100 (e.g. 200 = double input). |
Combat sub-object (gamestatejson.cpp:131-194):
target:{id, type:"character"|"building"}(only if a target is locked).attacks: array of{range?, area?, friendlies?:true, damage:{min,max}?}.hp:{max:{armour,shield}, current:{armour,shield}, regeneration:{armour,shield}}. HP values are integers unless they carry milli-HP, in which case they are fractional (e.g.5.001) (gamestatejson.cpp:75-82).attackers: array of character IDs currently on this character's damage list (present only on characters, not buildings;gamestatejson.cpp:182-194).
Example (gamestatejson_tests.cpp:107-122):
[
{ "id": 1, "owner": "domob", "faction": "r", "speed": 750, "position": {"x": -5, "y": 2} },
{ "id": 2, "owner": "andy", "faction": "g", "inbuilding": 100 }
]
Example (real capture). The live chain had a single character (id 1001, owned by
snailbrain, sitting inside building 6):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getcharacters","params":[],"id":1}' \
| jq '.result.data[] | select(.id==1001)'
{
"id": 1001,
"owner": "snailbrain",
"faction": "r",
"vehicle": "rv st",
"fitments": [ "lf gun" ],
"inbuilding": 6,
"speed": 4500,
"inventory": { "fungible": {} },
"cargospace": { "total": 1000000, "used": 0, "free": 1000000 },
"combat": {
"attacks": [ { "range": 5, "damage": { "min": 5, "max": 15 } } ],
"hp": {
"max": { "armour": 20, "shield": 30 },
"current": { "armour": 20, "shield": 30 },
"regeneration": { "armour": 0, "shield": 1.0 }
}
},
"mining": { "active": false, "rate": { "min": 0, "max": 2000000 } },
"prospectingblocks": 10
}
Because the character is inbuilding, it has no position field (the two are
mutually exclusive). The shield regeneration is fractional (1.0) — combat values
carry milli-HP and serialize as floats when not whole (see §3.5 combat sub-object).
3.6 getgroundloot#
- Returns: envelope +
data= array; only non-empty piles (GameStateJson::GroundLoot/QueryNonEmpty,gamestatejson.cpp:759-764). - Each entry:
{position:{x,y}, inventory:{fungible:{item:count,...}}}. Thefungiblemap is keyed by internal item codes — e.g."raw a"= Trimideum,"raw e"= Kalanite,"mat a"= Agarite (display names as shown in the game UI). The example codes"foo"/"bar"below are only test placeholders. - Items are sorted by position. Note item key
""(empty string) is a valid item code (raw resources/blocks); seegamestatejson_tests.cpp:1028,1056.
Example (gamestatejson_tests.cpp:1035-1061):
[
{ "position": {"x": -1, "y": 20}, "inventory": { "fungible": { "foo": 10 } } },
{ "position": {"x": 1, "y": 2}, "inventory": { "fungible": { "foo": 5, "bar": 42, "": 100 } } }
]
Example (real capture). Only non-empty piles are returned; on this chain nothing
had been dropped, so data was the empty array:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getgroundloot","params":[],"id":1}' \
| jq '.result | {state, data}'
{
"state": "up-to-date",
"data": []
}
(The non-empty shape is the illustrative example above.)
3.7 getongoings#
- Returns: envelope +
data= array of ongoing (timed) operations.Convert<OngoingOperation>(gamestatejson.cpp:481-570).
Common fields: id, start_height, end_height, plus exactly one of characterid
/ buildingid, plus operation and operation-specific fields:
operation |
Extra fields | Source |
|---|---|---|
prospecting |
— | gamestatejson.cpp:504-506 |
armourrepair |
— | gamestatejson.cpp:508-509 |
bpcopy |
account, original (BP original type), output:{<copyType>:count} |
gamestatejson.cpp:512-529 |
construct |
account, output:{<itemType>:count}, original? (BP original consumed) |
gamestatejson.cpp:531-552 |
build |
— (building construction) | gamestatejson.cpp:554-556 |
config |
newconfig:{servicefee?,dexfee?} (building config update) |
gamestatejson.cpp:558-561 |
Important: end_height is the real completion height. For multi-step ops
(blueprint copies, multi-item construction) the operation's stored height is the time
for one unit; the JSON adds a delta for the remaining units
(gamestatejson.cpp:498-567). Example: a 42-copy bpcopy started at height 1 with
1000 blocks per copy reports end_height: 42001 (gamestatejson_tests.cpp:1172-1184).
Example (gamestatejson_tests.cpp:1210-1228):
[
{ "id": 1, "operation": "construct", "output": { "bow": 42 }, "start_height": 1, "end_height": 1001 },
{ "id": 2, "operation": "construct", "output": { "bow": 5 }, "original": "bow bpo", "start_height": 1, "end_height": 5001 }
]
Example (real capture). No timed operations were in flight on this chain, so data
was empty:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getongoings","params":[],"id":1}' \
| jq '.result | {state, data}'
{
"state": "up-to-date",
"data": []
}
(The non-empty shape is the illustrative example above.)
3.8 getregions#
- Params (named):
{ "fromheight": <int> }. Returns regions modified at or after that block height (gamestatejson.cpp:773-778,QueryModifiedSince). The official client also calls it with{}, using the server default of0= all regions. - Returns: envelope +
data= array of region objects.
Limit / error.
fromheightmust not be more thanMAX_REGIONS_HEIGHT_DIFFERENCEbelow the current tip (=2*60*24*3= 8640 blocks ≈ 3 days at ~2 blocks/min;pxrpcserver.cpp:49). Otherwise it throwsGETREGIONS_FROM_TOO_LOW(code 3,pxrpcserver.cpp:567-577). For a full snapshot older than that, usegetbootstrapdata.
Per-region fields (Convert<Region>, gamestatejson.cpp:572-603):
| Field | Type | Notes |
|---|---|---|
id |
integer | Region ID (from the static region map). |
prospection.inprogress |
integer | Character ID currently prospecting (if any). |
prospection.name |
string | Player who prospected it (once done). |
prospection.height |
integer | Block height prospected. |
resource.type |
string | Mineable resource code (only after prospection). |
resource.amount |
integer | Units of resource remaining. |
Example (gamestatejson_tests.cpp:1346-1359):
[
{ "id": 10, "prospection": { "name": "bar", "height": 107 } },
{ "id": 20, "prospection": { "inprogress": 42 } }
]
Example (real capture). Pass fromheight within the 8640-block window of the tip.
On this chain no region had been prospected yet, so data was empty — a region only
appears here once it has prospection/resource state:
# fromheight must be >= (tip - 8640); use a recent height
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getregions","params":{"fromheight":88371547},"id":1}' \
| jq '.result | {state, count: (.data | length)}'
{
"state": "up-to-date",
"count": 0
}
Passing a fromheight further than 8640 blocks below the tip throws
GETREGIONS_FROM_TOO_LOW (code 3) — a real capture of that error:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getregions","params":{"fromheight":88271557},"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 3,
"data": null,
"message": "fromHeight 88271557 is too low for current block height 88371564, needs to be at least 88362924"
}
}
3.9 getmoneysupply#
- Returns: envelope +
data(object) = Cubit supply + burnsale stage breakdown.GameStateJson::MoneySupply(gamestatejson.cpp:643-690).
Fields:
total— total Cubit in existence.entries— mapkey → amount. Keys come fromMoneySupply::GetValidKeys; always includesburnsale. Thegiftedkey is omitted entirely (the code asserts its value is already 0 andcontinues past it) unlessgod_modeis enabled in config (gamestatejson.cpp:651-657) — i.e. it is absent on mainnet, present on dev.burnsale— array of stages, each{stage, price, total, sold, available}.priceis CHI per Cubit (price_sat / COIN,COIN = 100,000,000;gamestatejson.cpp:675,database/amount.hpp:31). Stage values come fromparams.burnsale_stagesin the config proto.
Example (gamestatejson_tests.cpp:1442-1482): four stages priced 0.0001 / 0.0002 /
0.0005 / 0.0010 CHI per Cubit; first two fully sold.
{
"total": 25000000000,
"entries": { "burnsale": 25000000000 },
"burnsale": [
{ "stage": 1, "price": 0.0001, "total": 10000000000, "sold": 10000000000, "available": 0 },
{ "stage": 3, "price": 0.0005, "total": 10000000000, "sold": 5000000000, "available": 5000000000 }
]
}
Example (real capture, full data). On this chain no Cubit had been minted yet, so
total/sold are 0; the four burnsale stages and their prices come straight from the
mainnet config proto:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getmoneysupply","params":[],"id":1}' \
| jq '.result.data'
{
"total": 0,
"entries": { "burnsale": 0, "gifted": 0 },
"burnsale": [
{ "stage": 1, "price": 0.0001, "total": 10000000000, "sold": 0, "available": 10000000000 },
{ "stage": 2, "price": 0.00020000000000000001, "total": 10000000000, "sold": 0, "available": 10000000000 },
{ "stage": 3, "price": 0.00050000000000000001, "total": 10000000000, "sold": 0, "available": 10000000000 },
{ "stage": 4, "price": 0.001, "total": 20000000000, "sold": 0, "available": 20000000000 }
]
}
Two things to note from the live data: (1) the price values are IEEE-754 doubles, so
0.0002/0.0005 print as 0.00020000000000000001 etc. — clients should round for
display. (2) entries.gifted is present and 0 here even on polygon; §7 notes it
is suppressed when god mode is off, but this GSP returned it (god mode is evidently on
for this dev/test deployment).
3.10 getprizestats#
- Returns: envelope +
data(object) = mapprizeName → {number, probability, found, available}.GameStateJson::PrizeStats(gamestatejson.cpp:692-714). Prize set comes fromparams.prizesin the config proto.probabilityis the1/probabilitychance per prospection.foundcounts how many were awarded;available = number - found.
Example (gamestatejson_tests.cpp:1506-1531):
{
"gold": { "number": 3, "probability": 100, "found": 1, "available": 2 },
"silver": { "number": 1000, "probability": 10, "found": 10, "available": 990 },
"bronze": { "number": 1, "probability": 1, "found": 0, "available": 1 }
}
Example (real capture, trimmed). The live mainnet prize set has ~55 named prizes;
shown here are a representative few. found was 0 for all (no prospecting prizes
awarded yet), so available == number:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getprizestats","params":[],"id":1}' \
| jq '.result.data'
{
"1% Player Shares": { "number": 100, "probability": 2655, "found": 0, "available": 100 },
"3 Spaceships": { "number": 1, "probability": 130289, "found": 0, "available": 1 },
"100 TKT": { "number": 250, "probability": 1109, "found": 0, "available": 250 },
"Syphon": { "number": 200, "probability": 1374, "found": 0, "available": 200 },
"Starter Pack": { "number": 20, "probability": 11582, "found": 0, "available": 20 },
"cash": { "number": 10, "probability": 350000, "found": 0, "available": 10 }
// … ~55 prizes total, trimmed …
}
Recall probability is the 1/probability chance per prospection (so larger = rarer);
e.g. "3 Spaceships" at 130289 is far rarer than "100 TKT" at 1109.
3.11 gettradehistory#
- Params (named):
{ "item": "<itemCode>", "building": <buildingId> }(pxd.json:75-82). Both are required. - Returns: envelope +
data= array of completed DEX trades for that item in that building, newest first.Convert<DexTrade>(gamestatejson.cpp:605-626).
Per-trade fields: height, timestamp (Unix seconds of the block), buildingid,
item, quantity, price (per unit), cost (= quantity×price), seller, buyer.
Example (gamestatejson_tests.cpp:1564-1588, for item="foo", building=42):
[
{ "height": 10, "timestamp": 1024, "buildingid": 42, "item": "foo",
"quantity": 2, "price": 3, "cost": 6, "seller": "domob", "buyer": "andy" },
{ "height": 9, "timestamp": 987, "buildingid": 42, "item": "foo",
"quantity": 5, "price": 3, "cost": 15, "seller": "andy", "buyer": "domob" }
]
A missing item/building returns [] (gamestatejson_tests.cpp:1554-1560).
Example (real capture). No DEX trades had completed in building 5, so data was
empty — the same empty result you get for a missing item/building pair:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"gettradehistory","params":{"item":"raw a","building":5},"id":1}' \
| jq '.result | {state, data}'
{
"state": "up-to-date",
"data": []
}
(The populated trade-history shape is the illustrative example above.)
3.12 getbootstrapdata#
- Params: none.
- Returns: envelope +
data={ "regions": [ ...all regions ] }(GameStateJson::BootstrapData,gamestatejson.cpp:805-812; equivalent togetregionswithfromheight=0but not subject to the height-difference limit). - When: once at startup to load the entire prospection/resource map, then switch to
incremental
getregionskeyed on the last seenheight. Flagged in the source as potentially expensive with a large result (gamestatejson.hpp:151-156).
Example (real capture). data.regions is the full region list. On this chain
nothing had been prospected, so it returned [] — once regions carry
prospection/resource state this array grows large (hence the gzip REST mirror in §8):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getbootstrapdata","params":[],"id":1}' \
| jq '.result | {state, height, region_count: (.data.regions | length)}'
{
"state": "up-to-date",
"height": 88371557,
"region_count": 0
}
Each populated entry has the same per-region shape as getregions (§3.8). For the live
chain the populated form is therefore shown there.
4. Pending state & long-polling (the live-update pattern)#
A responsive UI does not poll the data getters in a tight loop. It uses the two long-poll methods to block until something changes, then re-fetches only what it needs.
4.1 waitforchange — block until a new confirmed block#
- Signature:
waitforchange(knownBlock: string) -> string(pxrpcserver.cpp:496-501→GameRpcServer::DefaultWaitForChange,gamerpcserver.cpp:54-73). - Param (positional):
["<block hash hex>"]. Pass theblockhashyou last processed. Pass""(empty) if you have none yet. - Returns: the new best block hash as a hex string, or
""if no state is known yet (still in initial sync;gamerpcserver.cpp:67-69). - Behavior (the pattern):
- The call blocks server-side until the best block differs from
knownBlock(game.cppWaitForChange, declaredgame.hpp:564). - Race-free fast path: if
knownBlockalready differs from the current best block at entry, it returns immediately (game.hpp:546-551). This prevents missing an update that landed between your last return and your next call. - It may return spuriously (same hash). Always compare the returned hash to what
you have and only refetch if different (
game.hpp:542-558). - Requires the ZMQ subscriber to be running; otherwise it returns immediately
(
game.hpp:560-562).
- The call blocks server-side until the best block differs from
Canonical client loop:
let known = ""; // no state yet
while (true) {
const newHash = await rpc.call('waitforchange', [known]); // blocks
if (newHash && newHash !== known) {
known = newHash;
// refetch only what changed, e.g. getcharacters / getregions(lastHeight) / ...
}
}
Note: HTTP clients should set a generous read timeout; if no block arrives the server holds the connection open. (libxayagame also caps the wait internally so the call does return periodically.)
Example (real capture, fast path). To get an immediate return for documentation/
testing, pass a knownBlock that differs from the current tip (here the all-zero hash)
— the race-free fast path (§4.1 behavior #2) returns the current best hash at once
instead of blocking:
# returns immediately because the passed hash != current tip;
# in production you pass your last-seen blockhash and the call blocks.
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"waitforchange","params":["0000000000000000000000000000000000000000000000000000000000000000"],"id":1}' \
| jq .
{
"jsonrpc": "2.0",
"id": 1,
"result": "ccb70560c5c52b86108d2e9a18172e05561ee161f94c9f3dfc4ec031df339ad6"
}
The result is the current best block hash. If you instead pass the current tip hash,
the call blocks until the next block (so wrap it in a generous timeout, e.g.
timeout 30 curl ..., when testing).
4.2 getpendingstate — read the mempool's effect on game state#
- Params: none. Returns a pending envelope (note: different fields than §2).
Game::UnlockedPendingJsonState(game.cpp:879-904). - Errors: internal error
"pending moves are not tracked"if pending processing is disabled (game.cpp:882-884).
Envelope fields: version (int, bumps on every pending change), gameid, chain,
state, blockhash?, height?, and pending = the decoded pending state
(PendingState::ToJson, pending.cpp:517-...).
pending contains the intended effects of unconfirmed moves:
| Key | Shape | Source |
|---|---|---|
characters |
array of {id, waypoints?, enterbuilding?, exitbuilding?:{building}, drop, pickup, prospecting?, mining?, foundbuilding?, changevehicle?, fitments?}. drop/pickup are always present (bool). enterbuilding is emitted as JSON null when the move clears a pending enter-building intent (building id EMPTY_ID); prospecting/mining are region IDs. changevehicle is a vehicle code (e.g. "rv st" = Raider). |
pending.cpp:397-442,523 |
buildings |
array of {id, newconfig?, sentto?} |
pending.cpp:382-394,522 |
accounts |
array of {name, coinops?:{minted,burnt,transfers}, serviceops?:[...], dexops?:[...]} |
pending.cpp:453-489,524 |
newcharacters |
array of {name, creations:[{faction}]} (pending vehicle creations) |
pending.cpp:526-538 |
serviceops/dexops entries are the same objects produced by getserviceinfo
(see §5.6) — they describe the in-flight service or DEX operation.
Example (real capture). This GSP was launched without pending tracking, so the call returns the documented internal error rather than a pending envelope:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getpendingstate","params":[],"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"data": null,
"message": "INTERNAL_ERROR: : pending moves are not tracked"
}
}
When pending tracking is enabled, the result is the pending envelope described
above — illustrative success shape:
{
"version": 42,
"gameid": "tn",
"chain": "polygon",
"state": "up-to-date",
"blockhash": "…",
"height": 88371600,
"pending": {
"characters": [
{ "id": 1001, "drop": false, "pickup": false, "waypoints": [ {"x":1,"y":2} ] }
],
"buildings": [],
"accounts": [],
"newcharacters": []
}
}
4.3 waitforpendingchange — block until the pending state changes#
- Signature:
waitforpendingchange(oldVersion: int) -> object(pxrpcserver.cpp:489-494,pxd.json:27-30). - Param (positional):
[<version>]. Pass theversionfrom your lastgetpendingstate/waitforpendingchangeresult. - Returns: the new full pending state object (same shape as
getpendingstate). Blocks untilversiondiffers from the current pending version (game.hpp:566-576). Passing0(WAITFORCHANGE_ALWAYS_BLOCK,game.hpp:352) always blocks. May return spuriously; dedupe onversion. - When: to show "incoming"/optimistic UI (a player's move that is in the mempool but not yet confirmed) without waiting for a block.
Example (real capture). Like getpendingstate, this fails on a GSP without pending
tracking — captured here passing a high oldVersion (which would otherwise block until
the version advances):
# wrapped in a timeout because, with tracking enabled, this blocks until a pending change
timeout 8 curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"waitforpendingchange","params":[999999],"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"data": null,
"message": "INTERNAL_ERROR: : pending moves are not tracked"
}
}
With tracking enabled the result is the same pending-envelope shape shown for
getpendingstate (§4.2), returned once version advances past the one you passed.
5. Stateless helper methods (map / path / service preview)#
These are implemented by NonStateRpcServer (pxrpcserver.cpp:110-457) and exposed
on the same endpoint via thin forwarders on PXRpcServer (pxrpcserver.hpp:217-255).
They do not read the game-state database (except getserviceinfo, which does), so
they return a bare value — no §2 envelope. They are also intended to work for
Charon/offline clients.
5.1 getversion#
- Params: none. Returns:
{ "package": "<PACKAGE_VERSION>", "git": "<GIT_VERSION>" }(pxrpcserver.cpp:447-457). Use to verify GSP build compatibility.
Example (real capture). This is a bare value (no §2 envelope):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getversion","params":[],"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"package": "0.4",
"git": "531d8dd-dirty"
}
}
(The -dirty git suffix means this build had uncommitted local changes — expected for a
dev deployment.)
5.2 getregionat#
- Params (named):
{ "coord": {"x":<int>,"y":<int>} }. - Returns:
{ "id": <regionId>, "tiles": [{x,y}...] }— the region containing the coordinate and all its tiles (pxrpcserver.cpp:386-414). - Errors:
INVALID_ARGUMENT(bad coord),REGIONAT_OUT_OF_MAP(code 2) if off-map. - Note: returns geometry only, not prospection/resource status — for that use
getregions/getbootstrapdata.
// request params: { "coord": { "x": 10, "y": -5 } }
{ "id": 350146, "tiles": [ {"x":10,"y":-5}, ... ] }
Example (real capture) — the region containing building 6's centre (1919,-2605):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getregionat","params":{"coord":{"x":1919,"y":-2605}},"id":1}' \
| jq '{id: .result.id, tile_count: (.result.tiles | length), first_tiles: (.result.tiles[0:3])}'
{
"id": 298859,
"tile_count": 69,
"first_tiles": [
{ "x": 1911, "y": -2602 },
{ "x": 1911, "y": -2601 },
{ "x": 1911, "y": -2600 }
]
}
A valid coordinate that lies off the map returns REGIONAT_OUT_OF_MAP (code 2) — real
capture:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getregionat","params":{"coord":{"x":3000,"y":3000}},"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": 2, "data": null, "message": "coord is outside the game map" }
}
(A malformed coordinate — e.g. one whose axial sum is out of HexCoord range — instead
gives INVALID_ARGUMENT code -1, "coord is not a valid coordinate".)
5.3 getbuildingshape#
- Params (named):
{ "type": "<buildingType>", "centre": {x,y}, "rot": <0..5> }. - Returns: array of
{x,y}tiles the building would occupy (pxrpcserver.cpp:416-445). - Errors:
INVALID_ARGUMENTfor bad coord,rotoutside[0,5], or unknown buildingtype(pxrpcserver.cpp:425-435). - When: previewing/placing a building in the UI before submitting a found-building move.
// request params: { "type": "r rt", "centre": {"x":0,"y":0}, "rot": 0 }
[ {"x":0,"y":0}, {"x":1,"y":0}, ... ]
Example (real capture) — the footprint of a Red Refinery (r rt) at the origin,
rotation 0 (returns 19 tiles, sliced here):
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getbuildingshape","params":{"type":"r rt","centre":{"x":0,"y":0},"rot":0},"id":1}' \
| jq '.result[0:6]'
[
{ "x": 0, "y": -2 },
{ "x": 1, "y": -2 },
{ "x": 2, "y": -2 },
{ "x": -1, "y": -1 },
{ "x": 0, "y": -1 },
{ "x": 1, "y": -1 }
// … 19 tiles total, trimmed …
]
5.4 encodewaypoints#
- Params (named):
{ "wp": [ {x,y}, {x,y}, ... ] }. - Returns: a single string: a base64-encoded, compressed JSON blob of the
waypoint list (
pxrpcserver.cpp:361-384→EncodeWaypoints,movement.cpp:50-72). This is the value that goes into a movement move'swpfield. - Errors:
INVALID_ARGUMENTfor a bad waypoint;FINDPATH_ENCODE_FAILED(code 4) if the serialized blob exceedsMAX_WAYPOINT_SIZE=1 << 20= 1,048,576 bytes (movement.cpp:45).
// request params: { "wp": [ {"x":1,"y":2}, {"x":5,"y":2} ] }
// result:
"<base64 string>"
Example (real capture). The result is the opaque base64+gzip blob to put in a
movement move's wp field:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"encodewaypoints","params":{"wp":[{"x":1,"y":2},{"x":5,"y":2}]},"id":1}' \
| jq .
{
"jsonrpc": "2.0",
"id": 1,
"result": "i65WqlCyMtRRqlSyMqrVAfNMobxYAA=="
}
5.5 findpath and setpathdata#
Client policy: the official web client does not call
findpath— it always computes paths locally. Documented here for completeness; other clients may still choose to use it.
setpathdata (pxrpcserver.cpp:202-230):
- Params (named):
{ "buildings": [...], "characters": [...] }. Seeds the server's dynamic-obstacle map for subsequentfindpathcalls (decoupled from real game state so it works for offline clients).- Each building:
{ id, type, rotationsteps:0..5, centre:{x,y} }(pxrpcserver.cpp:124-173). - Each character:
{ position:{x,y} }; entries with aninbuildingmember are ignored (pxrpcserver.cpp:175-200).
- Each building:
- Returns:
trueon success (the value is meaningless; it just confirms processing completed,pxrpcserver.cpp:225-229). - Errors:
INVALID_ARGUMENTifbuildingsorcharactersis malformed or buildings overlap.
findpath (pxrpcserver.cpp:232-359):
- Params (named):
{ source:{x,y}, target:{x,y}, faction:"r"|"g"|"b", l1range:<int>, exbuildings:[<id>...] }.exbuildingslists building IDs to exclude from the obstacle set (e.g. the building you are pathing into).l1rangeis the max search radius. - Returns:
{ "dist": <int>, "wp": [{x,y}...], "encoded": "<string>" }(pxrpcserver.cpp:353-358).wpare principal-direction waypoints;encodedis the ready-to-use movement blob (same format asencodewaypoints). - Errors:
INVALID_ARGUMENT(bad coord, factioninvalid/ancient, out-of-rangel1range, badexbuildings);FINDPATH_NO_CONNECTION(code 1) if no route within range;FINDPATH_ENCODE_FAILED(code 4). Vehicles in the obstacle map don't block but slow movement byMULTI_VEHICLE_SLOWDOWN= 8× (pxrpcserver.cpp:314-315,movement.hpp:43).
Example — setpathdata (real capture). Seeding an empty obstacle map returns the
meaningless true:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"setpathdata","params":{"buildings":[],"characters":[]},"id":1}' | jq .
{ "jsonrpc": "2.0", "id": 1, "result": true }
Example — findpath (real capture). A short red-faction path near building 6
(excluding building 6 itself from obstacles). dist is the path cost; encoded is a
ready-to-use movement blob:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"findpath","params":{"source":{"x":1919,"y":-2605},"target":{"x":1925,"y":-2605},"faction":"r","l1range":50,"exbuildings":[6]},"id":1}' \
| jq '.result'
{
"dist": 1998,
"wp": [
{ "x": 1919, "y": -2605 },
{ "x": 1925, "y": -2605 }
],
"encoded": "i65WqlCyMrQ0tNRRqlSy0jUyMzCt1YEKGpkiCcYCAA=="
}
5.6 getserviceinfo — preview a building service operation#
- Params (named):
{ "name": "<playerName>", "op": <serviceOpObject> }(pxrpcserver.cpp:627-664).namemust be an existing account;opis exactly the move object a service operation would carry. - Behavior: parses the op against current state at
height+1withNO_TIMESTAMPcontext (pxrpcserver.cpp:632-654), then returns its pending JSON plus avalidflag. Returns the §2 envelope withdata= eithernull(op could not be parsed at all) or the operation preview object. - Errors:
INVALID_ACCOUNT(code -2) ifnamedoes not exist.
The op shape (the t field selects the operation) and the response come from
services.cpp. The t short codes are dispatched in ServiceOperation::Parse
(services.cpp:1207-1233). The op also always carries b = the building ID
(services.cpp Parse). Op types:
op.t |
Operation | Op fields | Preview type |
Source |
|---|---|---|---|---|
"ref" |
Refine | {b:buildingId, i:itemCode, n:amount, t:"ref"} |
refining |
services.cpp:1217 |
"fix" |
Armour repair | {b, c:characterId, t:"fix"} |
armourrepair |
services.cpp:1219 |
"rve" |
Reverse-engineer | {b, i, n, t:"rve"} |
reveng |
services.cpp:1221 |
"cp" |
Blueprint copy | {b, i, n, t:"cp"} |
bpcopy |
services.cpp:1223 |
"bld" |
Construct item | {b, i, n, t:"bld"} |
construct |
services.cpp:1226 |
The ref/rve/cp/bld types parse via ParseItemAmount<...> (so they share the
i item-code + n amount fields); fix parses via RepairOperation::Parse and targets a
character. The response type strings are set in each SpecificToPendingJson
(services.cpp:231,397,538,703,907).
Response object (ServiceOperation::ToPendingJson, services.cpp:1105-1125, plus the
specific SpecificToPendingJson):
| Field | Type | Notes |
|---|---|---|
type |
string | refining / armourrepair / reveng / bpcopy / construct. |
building |
integer | Building ID (if applicable). |
character |
integer | Character ID (if applicable). |
input |
object | {itemCode: amount} consumed (refining/reveng). |
output |
object | {itemCode: amount} produced. |
cost.base |
integer | Base Cubit cost of the operation. |
cost.fee |
integer | Service fee paid to the building owner. |
valid |
bool | Added by the RPC: whether the op is fully valid right now (pxrpcserver.cpp:660). |
Example request + response (refining; matches services.cpp:227-246,1105-1125):
// params:
{ "name": "domob", "op": { "b": 123, "i": "raw a", "n": 100, "t": "ref" } }
// data:
{
"type": "refining",
"building": 123,
"input": { "raw a": 100 },
"output": { "ref a": 50, "ref b": 20 },
"cost": { "base": 10, "fee": 5 },
"valid": true
}
(raw a is Trimideum in the UI.)
Example (real capture). Previewing a refine of 10000 raw a for snailbrain in
the stocked building 5. Note the §2 envelope is returned with the preview under data:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getserviceinfo","params":{"name":"snailbrain","op":{"b":5,"i":"raw a","n":10000,"t":"ref"}},"id":1}' \
| jq '.result.data'
{
"type": "refining",
"building": 5,
"input": { "raw a": 10000 },
"output": { "mat a": 1000, "mat b": 300 },
"cost": { "base": 1, "fee": 0 },
"valid": true
}
The refine step for raw a is 10000 input units → 1000 mat a + 300 mat b per step.
If n is not a multiple of the step size the op still parses but comes back
valid: false with zeroed output — real capture for n: 100:
{
"type": "refining",
"building": 5,
"input": { "raw a": 100 },
"output": { "mat a": 0, "mat b": 0 },
"cost": { "base": 0, "fee": 0 },
"valid": false
}
An unknown name returns INVALID_ACCOUNT (code -2) — real capture:
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"getserviceinfo","params":{"name":"no_such_account_xyz","op":{"b":5,"i":"raw a","n":10000,"t":"ref"}},"id":1}' | jq .
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -2, "data": null, "message": "account does not exist: no_such_account_xyz" }
}
6. How the official client uses this API#
The official web client is just one consumer of this API — everything it renders comes from the methods documented above. The notes here are for anyone building an alternative client or tool.
6.1 Endpoint routing#
Browsers cannot usually call tauriond directly across origins, so a web frontend
serves itself and proxies a small set of paths to its backends. The official client's
routing:
| Browser path | Proxied to | Purpose |
|---|---|---|
/gsp |
tauriond JSON-RPC |
All GSP read/long-poll methods documented here. |
/chain |
an EVM JSON-RPC node | Contract calls: name ownership, WCHI balance, submitting on-chain moves. |
/helper |
a dev "helper" RPC | Emulated/testnet only. Move submission & test fixtures (sendmove, getname, transfertoken, syncgsp, validatecharacterstate). |
/admin |
helper admin → GSP game_sendupdates fallback |
Emulated only. God-mode admin commands. |
On mainnet neither
/helpernor/adminexists; the official client refuses those calls outside emulated mode. On mainnet, moves are sent from the player's wallet by calling theXayaAccounts.move(...)contract directly; the resulting state changes are observed by polling the GSP.
6.2 GSP methods the official client calls#
- World snapshot at startup:
getbuildings,getaccounts,getregions(named{fromheight}, or{}for the server default),getgroundloot,getongoings,getbootstrapdata,getcharacters. - Liveness and updates:
getnullstateas the connection test, then thewaitforchangelong-poll loop of §4.4 (called positionally:["<blockhash>"]). - On demand:
getbuildingshape,getregionat,encodewaypoints(all named params), andgetserviceinfo(named; an RPC error is treated as{data:{valid:false}}).
6.3 GSP methods the official client does not use (but exist server-side)#
getcurrentstate,getpendingstate,waitforpendingchange,getmoneysupply,getprizestats,gettradehistory,getversion,findpath,setpathdata,stop.- The official client currently substitutes fixed defaults for the money-supply,
prize-stats and trade-history data instead of calling the server. A new client
should wire these to the real GSP methods (
getmoneysupply,getprizestats,gettradehistory) — they are fully implemented server-side (§3.9–3.11). There is no name-registry method on the GSP: name ownership lives on-chain. findpathis deliberately avoided in favor of pathfinding computed locally in the client (see §5.5).
6.4 stop#
- Params: none, no result. Calls
game.RequestStop()(pxrpcserver.cpp:461-466). This shuts the daemon down — operational/admin only, never expose to a public client.
Example — illustrative only, DO NOT RUN against a live GSP. This call shuts the daemon down; it was deliberately not executed when capturing the examples in this document. The shape is:
# DANGER: this stops the GSP daemon. Do not run against a server you need.
curl -s -X POST http://127.0.0.1:8601 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"stop","params":[],"id":1}'
// illustrative — the daemon begins shutting down; the JSON-RPC result is null:
{ "jsonrpc": "2.0", "id": 1, "result": null }
7. Mainnet vs testnet / regtest differences#
| Aspect | Mainnet (polygon) |
Dev / emulated (ganache) |
|---|---|---|
chain envelope value |
"polygon" |
"ganache" |
| Genesis | height 88,343,264, hash c57f860246... (logic.cpp:141-145) |
height 0, any hash (logic.cpp:147-152) |
moneysupply.entries.gifted |
suppressed/null (god mode off) |
present when god mode on (gamestatejson.cpp:653-657) |
/helper, /admin endpoints |
absent — client throws | available for move submission & god-mode testing |
| Burnsale stage values, prize set, service fees | from the mainnet config protos in proto/roconfig/* |
may differ per the regtest config |
The actual numeric game constants (burnsale prices, prize counts, fees) come from the
config protos under proto/roconfig/ and are surfaced verbatim by getmoneysupply /
getprizestats / per-building config. Treat the live getmoneysupply/getprizestats
output for the target chain as authoritative rather than hard-coding.
8. REST endpoints (read-only HTTP mirror)#
Alongside the JSON-RPC port, the GSP exposes a small read-only REST surface on a
separate port (the dev reference is http://127.0.0.1:8701). These are plain GETs and
are convenient for health checks, simple /state polling, and the gzip bootstrap blob.
| Path | Method | Returns |
|---|---|---|
/healthz |
GET |
Liveness probe — HTTP 200 + body ok when healthy. |
/state |
GET |
The §2 envelope (same as getnullstate's result), as bare JSON. |
/bootstrap.json.gz |
GET |
Gzipped getbootstrapdata payload (the full region map). |
8.1 /healthz#
# replace 127.0.0.1:8701 with your GSP REST endpoint
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8701/healthz
curl -s http://127.0.0.1:8701/healthz
Real capture:
HTTP 200
ok
8.2 /state#
Returns the same sync envelope as getnullstate, but as a bare object (no JSON-RPC
wrapper) — ideal for a lightweight reverse-proxy health/sync check:
curl -s http://127.0.0.1:8701/state | jq .
{
"gameid": "tn",
"chain": "polygon",
"state": "up-to-date",
"blockhash": "bcbb1003cc653f4fbd3f2dcdf0cdd51c76f78965eb8dbc2a3902e545c5b998f0",
"height": 88371633
}
8.3 /bootstrap.json.gz#
A gzip-compressed copy of the getbootstrapdata response (decompresses to the §2
envelope with data.regions). Fetch it once at startup instead of the JSON-RPC call when
you want the smaller wire payload. Only GET is supported (a HEAD returns
405 Method Not Allowed).
# check size without dumping the (potentially large) body
curl -s -o /dev/null -w 'status=%{http_code} size=%{size_download} bytes type=%{content_type}\n' \
http://127.0.0.1:8701/bootstrap.json.gz
# to actually use it: curl -s http://127.0.0.1:8701/bootstrap.json.gz | gunzip -c | jq .
Real capture (on this chain no region was prospected, so the gzip blob is tiny — it grows to many KB/MB once the region map fills in):
status=200 size=159 bytes type=application/json+gzip
Open questions#
getserviceinfooutputfor non-deterministic ops. Reverse-engineering and prize outcomes are random at execution; the preview shows the intended/expected output. The precise meaning of the previewedoutputforreveng(expected vs. max) was not exhaustively traced.- HTTP read-timeout for
waitforchange. libxayagame caps the server-side wait so the call eventually returns, but the exact cap is in libxayagame internals not read here; client implementers should set a comfortably-large HTTP timeout and just re-issue. COINfor Cubit vs CHI.database/amount.hpp:31definesCOIN = 100,000,000and the burnsalepriceisprice_sat / COIN(CHI per Cubit). Whether in-game Cubitbalance/price/costintegers are themselves in 1e-8 sub-units or in whole Cubit was not definitively resolved from the RPC code alone; the economy document should confirm the Cubit unit scale.