Skip to content
TAURION
02RPC & REST API

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 state field. Only up-to-date means "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 every get* data method.
  • libxayagame xayagame/game.cpp, xayagame/gamerpcserver.cpp — the standard envelope + long-poll behavior.
  • src/services.cpp, src/pending.cpp — shapes for getserviceinfo and 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 (id 1001), with three test accounts (snailbrain, johnv5, johnagent) holding stocked inventories in the Reubo command-center building (id 5). 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 /gsp proxy — 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:

  • waitforchange is called positionally: params: ["<blockhash>"].
  • getregions / getbuildingshape / getserviceinfo / getregionat / encodewaypoints are 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:

  • -32600 invalid request, -32601 method not found, -32602 invalid params (type mismatch vs the stub spec), -32700 parse error.
  • getpendingstate throws 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. getcurrentstate uses "gamestate"; the targeted getters use "data". This is set by libxayagame (GetCurrentJsonState"gamestate", game.cpp:846; GetCustomStateData second 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:221GameStateJson::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 (accountsgetaccounts, buildingsgetbuildings, 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 data member 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 by GameStateJson::Accounts (gamestatejson.cpp:716-743) using Convert<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,...}}}. The fungible map 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); see gamestatejson_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 of 0 = all regions.
  • Returns: envelope + data = array of region objects.

Limit / error. fromheight must not be more than MAX_REGIONS_HEIGHT_DIFFERENCE below the current tip (= 2*60*24*3 = 8640 blocks ≈ 3 days at ~2 blocks/min; pxrpcserver.cpp:49). Otherwise it throws GETREGIONS_FROM_TOO_LOW (code 3, pxrpcserver.cpp:567-577). For a full snapshot older than that, use getbootstrapdata.

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 — map key → amount. Keys come from MoneySupply::GetValidKeys; always includes burnsale. The gifted key is omitted entirely (the code asserts its value is already 0 and continues past it) unless god_mode is 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}. price is CHI per Cubit (price_sat / COIN, COIN = 100,000,000; gamestatejson.cpp:675, database/amount.hpp:31). Stage values come from params.burnsale_stages in 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) = map prizeName → {number, probability, found, available}. GameStateJson::PrizeStats (gamestatejson.cpp:692-714). Prize set comes from params.prizes in the config proto. probability is the 1/probability chance per prospection. found counts 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 to getregions with fromheight=0 but not subject to the height-difference limit).
  • When: once at startup to load the entire prospection/resource map, then switch to incremental getregions keyed on the last seen height. 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-501GameRpcServer::DefaultWaitForChange, gamerpcserver.cpp:54-73).
  • Param (positional): ["<block hash hex>"]. Pass the blockhash you 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):
    1. The call blocks server-side until the best block differs from knownBlock (game.cpp WaitForChange, declared game.hpp:564).
    2. Race-free fast path: if knownBlock already 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.
    3. It may return spuriously (same hash). Always compare the returned hash to what you have and only refetch if different (game.hpp:542-558).
    4. Requires the ZMQ subscriber to be running; otherwise it returns immediately (game.hpp:560-562).

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 the version from your last getpendingstate/waitforpendingchange result.
  • Returns: the new full pending state object (same shape as getpendingstate). Blocks until version differs from the current pending version (game.hpp:566-576). Passing 0 (WAITFORCHANGE_ALWAYS_BLOCK, game.hpp:352) always blocks. May return spuriously; dedupe on version.
  • 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_ARGUMENT for bad coord, rot outside [0,5], or unknown building type (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-384EncodeWaypoints, movement.cpp:50-72). This is the value that goes into a movement move's wp field.
  • Errors: INVALID_ARGUMENT for a bad waypoint; FINDPATH_ENCODE_FAILED (code 4) if the serialized blob exceeds MAX_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 subsequent findpath calls (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 an inbuilding member are ignored (pxrpcserver.cpp:175-200).
  • Returns: true on success (the value is meaningless; it just confirms processing completed, pxrpcserver.cpp:225-229).
  • Errors: INVALID_ARGUMENT if buildings or characters is 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>...] }. exbuildings lists building IDs to exclude from the obstacle set (e.g. the building you are pathing into). l1range is the max search radius.
  • Returns: { "dist": <int>, "wp": [{x,y}...], "encoded": "<string>" } (pxrpcserver.cpp:353-358). wp are principal-direction waypoints; encoded is the ready-to-use movement blob (same format as encodewaypoints).
  • Errors: INVALID_ARGUMENT (bad coord, faction invalid/ancient, out-of-range l1range, bad exbuildings); 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 by MULTI_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). name must be an existing account; op is exactly the move object a service operation would carry.
  • Behavior: parses the op against current state at height+1 with NO_TIMESTAMP context (pxrpcserver.cpp:632-654), then returns its pending JSON plus a valid flag. Returns the §2 envelope with data = either null (op could not be parsed at all) or the operation preview object.
  • Errors: INVALID_ACCOUNT (code -2) if name does 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 /helper nor /admin exists; the official client refuses those calls outside emulated mode. On mainnet, moves are sent from the player's wallet by calling the XayaAccounts.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: getnullstate as the connection test, then the waitforchange long-poll loop of §4.4 (called positionally: ["<blockhash>"]).
  • On demand: getbuildingshape, getregionat, encodewaypoints (all named params), and getserviceinfo (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.
  • findpath is 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#

  1. getserviceinfo output for non-deterministic ops. Reverse-engineering and prize outcomes are random at execution; the preview shows the intended/expected output. The precise meaning of the previewed output for reveng (expected vs. max) was not exhaustively traced.
  2. 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.
  3. COIN for Cubit vs CHI. database/amount.hpp:31 defines COIN = 100,000,000 and the burnsale price is price_sat / COIN (CHI per Cubit). Whether in-game Cubit balance/price/cost integers 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.