Trading & the DEX
The per-building order books, price matching and trading fees.
Authoritative reference for Taurion's in-game decentralized exchange (DEX). Single source of truth: the Game State Processor (GSP) C++ in the GSP source tree. Display names: the official web client.
Primary source files:
- Move/operation logic:
src/trading.cpp,src/trading.hpp- Tests (canonical JSON examples):
src/trading_tests.cpp,gametest/dex.py- Persistence/queries:
database/dex.cpp,database/dex.hpp- Move routing:
src/moveprocessor.cpp- State JSON exposed to clients:
src/gamestatejson.cpp- RPC surface:
src/pxrpcserver.cpp- Config constants:
proto/roconfig/params.pb.text,proto/config.proto,proto/building.proto,database/amount.hpp,database/inventory.hpp
1. Overview & mental model#
In plain language (for players): Taurion has no single global auction house. Instead, each finished building is its own little marketplace. To buy or sell, you go to a specific building and place an order there: a bid (an offer to buy at a price you name) or an ask (an offer to sell). When a bid and an ask in the same building meet at a workable price, they trade automatically. To sell an item you must first have it physically sitting in that building (dropped off from a vehicle); to buy, you must have enough in-game coin. The coin is called Cubits in the client (it is the burn-sale currency, "vCHI" in the code). Sellers pay a small fee on each sale (see §7); buyers pay no extra fee. Orders you place lock up your coins (for a bid) or items (for an ask) until they fill or you cancel them.
A few terms used throughout: DEX = decentralized exchange (the order-book market); order book = the live list of all open bids and asks for one item in one building; maker = a resting order already on the book; taker = the new order that matches against it; escrow = funds/items locked while an order is open; bps = basis points, where 100 bps = 1%.
Technically: Taurion's market is a per-building, on-chain order-book DEX. There is no global market. Every tradeable building hosts its own independent set of order books, one per item type. A buy order placed in building #42 can only ever match a sell order in building #42 for the same item.
To trade an item you must first have that item inside the building's inventory account (items are deposited via vehicles dropping cargo into the building) and the building must be a completed, non-foundation building. (A foundation is a building that has been started but not yet finished constructing; foundations cannot host a market — see §2.) The currency used is the in-game coin (Cubits / vCHI, §3).
There are exactly four DEX operations, all submitted under the "x" move key
(src/moveprocessor.cpp:452):
| Operation | Internal class | pending/op name |
Discriminator key |
|---|---|---|---|
| Place buy order (bid) | BidOperation |
bid |
bp (bid price) |
| Place sell order (ask) | AskOperation |
ask |
ap (ask price) |
| Cancel order | CancelOrderOperation |
cancel |
single-key c |
| In-building item transfer | TransferOperation |
transfer |
t (to) |
Item transfer is technically a DEX operation (it shares the building/item/qty plumbing) but is not a market order; it is documented in §10.
Source for the operation hierarchy: src/trading.cpp:67-682,
src/trading.hpp:42-102.
2. Where orders live (per building)#
Orders are stored in the dex_orders SQL table. Each row is keyed by a unique
order ID and carries the building it lives in
(database/dex.cpp:84-98, schema columns at database/dex.hpp:37-46):
| Column | Type | Meaning |
|---|---|---|
id |
int64 | Unique order ID (global ID counter, also used for vehicles/buildings/etc.) |
building |
int64 | Building this order lives in |
account |
string | Owner (Xaya name) of the order |
type |
int64 | 1 = BID (buy), 2 = ASK (sell) (database/dex.hpp:63-68) |
item |
string | Internal item code, e.g. raw a (Trimideum); full code→name map in §14 |
quantity |
int64 | Remaining units (decremented on partial fill) |
price |
int64 | Price per unit, in coin sat (see §3) |
Order matching only ever considers (building, item, type) triples
(database/dex.cpp:217-252). Buildings are fully isolated markets.
Foundations cannot trade. Any DEX item operation against a building whose
proto has foundation() set is rejected (src/trading.cpp:121-126). Only
finished buildings have an order book. (In game-state JSON, foundations expose a
construction block instead of inventories/orderbook;
src/gamestatejson.cpp:434-459.)
Ancient buildings (faction ANCIENT, the neutral starter buildings) can
host a DEX but always have a zero owner fee (src/trading.cpp:304-305).
3. Price units & currency#
The currency is the game coin. In code it is Amount (an int64_t,
database/amount.hpp:28), denominated in sat (1 coin = COIN =
100,000,000 sat = 1e8, database/amount.hpp:31). On the user side this coin is
vCHI / Cubits (the burn-sale minted in-game currency; the burn-sale doc and
src/burnsale.hpp:30-37 refer to it as "vCHI"; the trading code comments call
units "Cubits", e.g. src/trading.cpp:250 "the price of the order (in Cubits per
unit)"). The client display layer calls them Cubits. Note database/amount.hpp:27
labels the Amount type "An amount of CHI, represented as Satoshi" — on-chain
WCHI/CHI and the in-game minted vCHI share the same sat denomination.
price in every order and trade is the price per single unit of the item, in
coin-sat. Total cost of n units at price p is n * p sat
(src/trading.cpp:366, computed via the overflow-safe QuantityProduct,
database/inventory.hpp:69-114).
Numeric limits#
| Quantity | Constant | Value | Source |
|---|---|---|---|
Max units per order (n) |
MAX_QUANTITY |
2^50 = 1,125,899,906,842,624 |
database/inventory.hpp:54 |
| Min units per order | — | 1 (must be > 0) |
src/jsonutils.cpp:144 |
Max price per unit (bp/ap) |
MAX_COIN_AMOUNT |
100,000,000,000 sat (= 1000 coins) | src/jsonutils.cpp:38,134 |
| Min price | — | 0 (zero-price orders are legal, see §6/§7) | src/jsonutils.cpp:134 |
A bid total n * p may exceed the entire money supply at validation time only
if you actually have the balance — but an ask may be priced arbitrarily high
even if n * p exceeds the total money supply, because no one is required to
have that money to place a sell (src/trading_tests.cpp:476-496). The price
per unit is still capped at MAX_COIN_AMOUNT.
4. Move JSON schemas (exact)#
All DEX operations are submitted as a JSON array under the top-level move key
"x". The array may contain any mix of the four operation objects; they are
processed in array order (src/moveprocessor.cpp:449-473). DEX operations are
processed early in the move pipeline and work even before an account has chosen a
faction, but only after the game-start fork is active
(src/moveprocessor.cpp:1320-1322).
The full Xaya move envelope is {"g": {"tn": <move>}} for the Taurion game ID
(tn); the schemas below show only <move>.
4.1 Place a bid (buy order)#
(Here "i": "raw a" is the ore Trimideum; see §14 for the full code→display-name
table. The gametest examples use the placeholder item code "foo", which is a
test-only fixture item, not a real game item.)
{
"x": [
{
"b": 42,
"i": "raw a",
"n": 100,
"bp": 250
}
]
}
| Field | Type | Meaning |
|---|---|---|
b |
uint | Building ID |
i |
string | Item code |
n |
int | Quantity to buy (1 … MAX_QUANTITY) |
bp |
int | Bid price per unit, sat (0 … MAX_COIN_AMOUNT) |
The object must have exactly these four keys (b,i,n,bp). Any extra
key, a bp together with ap, or a missing key makes the whole operation
malformed and silently dropped (src/trading.cpp:631-676,
src/trading_tests.cpp:359-378).
4.2 Place an ask (sell order)#
{
"x": [
{
"b": 42,
"i": "raw a",
"n": 100,
"ap": 300
}
]
}
Same shape as a bid, but use ap (ask price) instead of bp.
4.3 Cancel an order#
{
"x": [
{ "c": 12345 }
]
}
The cancel object must have exactly one key c = the order ID
(src/trading.cpp:621-628). Extra keys make it malformed
(src/trading_tests.cpp:821-835).
4.4 Item transfer (see §10)#
{
"x": [
{ "b": 42, "i": "raw a", "n": 30, "t": "recipientName" }
]
}
4.5 Combined example (multiple ops in one move)#
Real example from gametest/dex.py:122-152 (two asks then later two bids):
{ "x": [
{ "b": 42, "i": "foo", "n": 2, "ap": 100 },
{ "b": 42, "i": "foo", "n": 2, "ap": 50 }
] }
{ "x": [
{ "b": 42, "i": "foo", "n": 1, "bp": 10 },
{ "b": 42, "i": "foo", "n": 1, "bp": 20 }
] }
4.6 Parsing & validation rules (exact)#
A DEX op is rejected as malformed (parse returns null) if
(src/trading.cpp:614-682):
- The element is not a JSON object.
- It is not the single-key
{"c": id}form and does not have exactly 4 members. bis not an unsigned integer ID, oriis not a string, ornfailsQuantityFromJson(must be a JSON integer in(0, MAX_QUANTITY]; floats, strings, 0, negatives, overflowing values all fail —src/trading_tests.cpp:166-188).- More than one of
t/bp/apis present (because total key count is fixed at 4, at most one can ever be set —src/trading.cpp:649-675). bp/apis not a JSON integer in[0, MAX_COIN_AMOUNT](src/jsonutils.cpp:128-135).
A well-formed op is then validated and either executed or dropped:
- Building must exist and not be a foundation (
src/trading.cpp:113-126). - Item code must exist in the read-only config (
src/trading.cpp:128-133). - Bid: buyer's available balance must cover
n * bp(src/trading.cpp:360-376). - Ask: seller must hold ≥
nof the item inside that building (src/trading.cpp:439-456).
Invalid operations are silently skipped; the rest of the move continues
(src/moveprocessor.cpp:465-471). There is no error feedback on-chain — a
malformed/invalid order simply does nothing.
5. Escrow / reservation semantics#
Placing an order immediately escrows the cost up front. Funds/items are locked the moment the order rests on the book, not at fill time.
- Bid (buy): when the unmatched remainder rests on the book, the buyer's main
coin balance is debited by
remaining * price(src/trading.cpp:410-417). So a resting bid means the coins are already gone from the spendable balance and held in escrow. - Ask (sell): when the unmatched remainder rests on the book, the seller's
items are removed from their building inventory
(
src/trading.cpp:492-499). The items are held in escrow.
This is why the matching code does not re-debit on fill: for a resting ask,
the items were already taken when the ask was placed, so a later matching bid
just credits the buyer (src/trading.cpp:388-392); and for a resting bid, the
coins were already taken, so a later matching ask just pays the seller
(src/trading.cpp:468-478).
A bid is checked against your liquid (available) balance only — you cannot
spend coins you expect to receive from a fill earlier in the same move. The
BuildingOwnerSells test (src/trading_tests.cpp:798-815) places an ask for 1000
items at price 1, then a bid for 1001 at price 1 fails because the buyer lacks the
1001 liquid coins, even though the matching ask would have funded it. The same test
also shows the building owner can sell on their own building's DEX: the owner
fee they would pay simply lands back in their own balance.
How the client sees escrowed value#
Escrowed value is surfaced separately so clients can show "available vs. reserved vs. total":
- Reserved coins (from open bids), per account, are added into the account
JSON:
balance.reservedandbalance.total = available + reserved(src/gamestatejson.cpp:716-743; computed byGetReservedCoins,database/dex.cpp:278-305).balance.availableis the spendable balance. - Reserved item quantities (from open asks), per account, appear in each
building's
reservedobject (src/gamestatejson.cpp:453-456; computed byGetReservedQuantities,database/dex.cpp:307-344).
Important: the building
inventoriesmap shows only the free (un-escrowed) items. Items locked in open asks are moved out of inventory and shown underreserved. A client must addinventories[acct][item] + reserved[acct][item]to display a user's true holdings in a building.
The official web client already models this: it reads
serverData.reserved and serverData.orderbook, and reads
balance.reserved and computes available + reserved.
6. Matching engine & settlement#
When a new order is placed it is treated as a taker and immediately matched against resting maker orders of the opposite type, best price first, then by oldest order ID (price-time priority). Any unfilled remainder becomes a new resting order.
Matching queries (sort order = priority)#
- A new bid matches resting asks with
price <= bid_price, ordered by ascending price then ascending ID (cheapest, oldest first) —database/dex.cpp:216-233(QueryToMatchBid). - A new ask matches resting bids with
price >= ask_price, ordered by descending price then ascending ID (highest, oldest first) —database/dex.cpp:235-252(QueryToMatchAsk).
Execution price = the resting (maker) order's price#
Each fill executes at the maker's price, not the taker's
(src/trading.cpp:394, :477). Concretely, a bid at price 100 that crosses two
resting asks priced 10 and 20 pays 10 + 20 = 30, not 200
(src/trading_tests.cpp:568-582, FilledBid). The taker therefore gets price
improvement: a buyer never pays more than the resting ask, a seller never gets
less than the resting bid.
Per-fill settlement steps#
Bid taker loop (src/trading.cpp:378-418):
- For each crossing resting ask,
cur = min(remaining, ask.quantity). - Credit
curitems to the buyer's building inventory. cost = cur * ask.price; pay seller (minus fees, §7) and burn/owner-fee; debitcostfrom the buyer's balance.- Record a trade-history row (§8).
ask.ReduceQuantity(cur)(deletes the ask if it hits 0).- Any remaining buyer quantity rests as a new bid, escrowing
remaining * bid_price.
Ask taker loop (src/trading.cpp:458-500) is the mirror image: credit items to
the resting buyer's inventory, debit items from the seller, pay the seller
(minus fees), record history, reduce the bid, rest the remainder (escrowing
items).
Self-trading is allowed#
An account can match its own order. FillingOwnOrder
(src/trading_tests.cpp:634-649) shows domob placing an ask at 3 then a bid at
10 that fills it: items move within their own building accounts and the coins net
out. (In that test the building fee is deliberately offset to 0%, so domob ends
exactly +3; in the real game the seller-side fee in §7 would still be charged on a
self-trade, since the fee path does not check for matching buyer/seller.) There is
no self-trade prevention.
Partial fills#
Fully supported. The resting order's quantity is decremented in place
(database/dex.cpp:134-147); when it reaches 0 the row is deleted on flush
(database/dex.cpp:108-118). A taker that only partially fills the book rests
its leftover (PartialBid/PartialAsk, src/trading_tests.cpp:600-632). A
taker that consumes only part of a resting order leaves that order on the book
with reduced quantity.
Zero-price orders#
Price 0 is valid. A zero-price ask is effectively a giveaway; a zero-price bid
crosses any ask priced 0. gametest/dex.py:234-279 shows a price-0 ask getting
filled by a later bid at price 200 — the trade executes at price 0 (maker
price), so the buyer pays nothing and the seller receives nothing, with a
history row at price: 0, cost: 0 (gametest/dex.py:300-302).
7. Fees#
Fees are charged only on the seller side, deducted from the coins the seller receives on each fill. Buyers never pay a fee on top. There are two components, both expressed in basis points (bps, 1/100 of a percent):
| Component | Source | Where set |
|---|---|---|
| Base fee | params.dex_fee_bps |
Global config proto |
| Building-owner fee | building.config.dex_fee_bps |
Per-building, set by owner |
totalBps = baseBps + ownerBps (src/trading.cpp:300-302).
Constant values#
| Network | params.dex_fee_bps |
Source |
|---|---|---|
| Mainnet | 300 (3.00%) |
proto/roconfig/params.pb.text:19 |
| Testnet/regtest | 1000 (10.00%) |
proto/roconfig/test_params.pb.text:13 |
The per-building owner fee field is int32 dex_fee_bps
(proto/building.proto:95) and defaults to 0. A building owner sets it via a
building-config update move (§9). The owner fee is capped:
| Constant | Value | Source |
|---|---|---|
MAX_DEX_FEE_BPS (owner-settable cap) |
3000 (30%) |
src/moveprocessor.cpp:54 |
Note: the owner fee field is signed and the base fee is added to it, so the effective total can exceed 30% only via the base; and in unit tests a negative owner fee is forced directly into the proto to cancel the base fee, but that is not possible through moves —
MaybeUpdateDexFeeonly accepts unsigned values0 … MAX_DEX_FEE_BPS(src/moveprocessor.cpp:280-294). So the achievable on-chain range is owner ∈ [0%, 30%], total ∈ [base, base+30%].
Fee math (per fill)#
For a fill of value cost = cur * makerPrice (src/trading.cpp:291-328):
total = ceil (cost * totalBps / 10000) // rounded UP, the full fee
owner = floor(cost * ownerBps / 10000) // rounded DOWN, owner's cut
payout = cost - total // what the seller receives
burned = total - owner // base fee is burned (destroyed)
Implemented as integer arithmetic:
total = (cost * totalBps + 9999) / 10000, owner = (cost * ownerBps) / 10000
(src/trading.cpp:315-316).
Rounding rationale (from the source comments, src/trading.cpp:307-313):
- Total is rounded up so sellers cannot dodge fees by splitting into tiny fills — but it also means the maximum fee is capped at 1 coin-sat per fill for sub-1-sat fees.
- Owner's cut is rounded down so there's no incentive to split orders to game the owner-rounding.
Where the money goes#
payout→ the seller (PayToSellerAndFee,src/trading.cpp:327).owner→ the building owner's coin balance, but only for non-ancient buildings (src/trading.cpp:325-326). Ancient buildings haveownerBpsforced to 0 (src/trading.cpp:304-305).burned = total - owner→ destroyed (not paid to anyone). This is implicit: the buyer is debited the fullcost, the seller getspayout, the owner getsowner, and the differencecost - payout - owner = burnedsimply vanishes from the money supply. The base fee is therefore a coin burn.
Worked example (from gametest/dex.py, regtest 10% base + 10% owner = 20%)#
The end-to-end game test (gametest/dex.py) uses regtest base 10% and sets the
building owner fee to xf: 1000 (10%), so total = 20%, owner = 10%. In
gametest/dex.py:245-259 a buyer bid of 4 @ 200 crosses, in priority order, a
resting price-0 ask (1 unit), then a price-100 ask (2 units), leaving 1 unit
resting. Combined with an earlier 1-unit fill at maker price 10, the per-fill fee
math is:
| Fill | cost | total = ceil(cost*0.20) |
owner = floor(cost*0.10) |
seller payout | burned |
|---|---|---|---|---|---|
| 1 @ 10 | 10 | 2 | 1 | 8 | 1 |
| 1 @ 0 | 0 | 0 | 0 | 0 | 0 |
| 2 @ 100 | 200 | 40 | 20 | 160 | 20 |
| sum | 210 | 42 | 21 | 168 | 21 |
The test asserts exactly this: seller: 210 - 2*21 = 168 and building: 21
(gametest/dex.py:255-259). (The "210" is gross receipts; the 2*21 is shorthand
for the 42 total fee, which splits 21 to the owner and 21 burned.)
The clearer unit test is DexFeeTests.BasicFeeDistribution
(src/trading_tests.cpp:727-739) with base 10% + owner 20% = 30% total, two
units traded at price 100 (cost = 200):
| Party | Δ balance | Computation |
|---|---|---|
Buyer (andy) |
−200 | pays full cost |
Seller (domob) |
+140 | payout = 200 − total, total = ceil(200*0.30) = 60 |
Building owner (building) |
+40 | owner = floor(200*0.20) = 40 |
| Burned | 20 | total − owner = 60 − 40 |
Edge cases#
- Ancient building: owner fee = 0; only the base fee (burned) applies
(
AncientBuilding,src/trading_tests.cpp:741-768): 10 units at price 10 → cost 100, base 10% → total 10 burned, seller gets 90, owner balance unchanged. - Zero price: all fees are 0 (
ZeroPrice,src/trading_tests.cpp:770-782). - Rounding on tiny fills: 10 separate asks of 1 unit at price 1 each, filled
by one bid of 10 → buyer pays 10, but
ceil(1 * 0.30) = 1per fill would burn the whole 1; the test shows seller nets 0 and owner gets 0 (Rounding,src/trading_tests.cpp:784-796) — fees can consume 100% of very small fills. Designers: warn users that sub-~3-sat unit prices may lose the entire proceeds to fee rounding.
8. Trade history#
Every individual fill writes one immutable row to dex_trade_history
(src/trading.cpp:398-400, :480-482; persistence database/dex.cpp:359-431).
History rows use a separate log ID counter (database/dex.cpp:360), so they
do not consume normal entity IDs (gametest/dex.py:660-663 / the
TradeHistory unit test at src/trading_tests.cpp:651-701).
Row schema (database/dex.hpp:286-297):
| Column | Meaning |
|---|---|
height |
block height of the trade |
time |
block timestamp |
building |
building ID |
item |
item code |
quantity |
units traded in this fill |
price |
maker price per unit (the settled price) |
seller |
seller account |
buyer |
buyer account |
Note: history records gross trade values (price, quantity); it does not
record fees. For a self-trade both seller and buyer are the same account
(src/trading_tests.cpp:678-687).
History is building+item scoped (QueryForItem, database/dex.cpp:464-477)
and returned oldest-first (ordered by log ID). It is not part of the global
getbuildings/full-state dump — clients must query it explicitly per
(building, item) pair.
9. Setting the building DEX fee (owner action)#
The building owner sets their DEX fee with a building-config update move,
under top-level key "b" (an object or array of objects), each with the building
id and an xf (DEX fee bps) field (src/moveprocessor.cpp:326-341):
{
"b": [
{ "id": 42, "xf": 150 }
]
}
xf is the owner fee in basis points (here 1.50%). Constraints:
- Must be an unsigned integer ≤
MAX_DEX_FEE_BPS(3000) (src/moveprocessor.cpp:280-294). - Only the building owner may update; ancient buildings cannot be updated
(
src/moveprocessor.cpp:385-401). - The same
"b"update object also supportssf(service-fee percent) andsend(transfer building ownership) — orthogonal to DEX fee.
Config updates are delayed#
A building-config update does not take effect immediately. It is scheduled as
an ongoing operation that applies after params.building_update_delay blocks
(src/moveprocessor.cpp:1908-1921; applied via merge in
src/ongoings.cpp:206-218).
| Network | building_update_delay (blocks) |
Source |
|---|---|---|
| Mainnet | 120 | proto/roconfig/params.pb.text (building_update_delay: 120) |
| Regtest | 10 | proto/roconfig/test_params.pb.text (building_update_delay: 10) |
The merge semantics mean unset fields are left untouched, so updating xf alone
does not reset sf (src/ongoings.cpp:213-217).
The current fee is exposed in building JSON as config.dexfee = the bps value
divided by 100 (i.e. a percent, possibly fractional), only present if set
(src/gamestatejson.cpp:335-336). E.g. dex_fee_bps: 150 → "dexfee": 1.5.
10. In-building item transfer (the fourth DEX op)#
A direct, fee-free, instant transfer of fungible items between two accounts'
inventories inside the same building (TransferOperation,
src/trading.cpp:156-225):
{ "x": [ { "b": 42, "i": "raw a", "n": 30, "t": "andy" } ] }
| Field | Meaning |
|---|---|
b |
building ID |
i |
item code |
n |
quantity |
t |
recipient Xaya name |
Rules:
- Sender must hold ≥
nof the item in that building (src/trading.cpp:178-194). - If the recipient account does not exist it is created automatically
(
src/trading.cpp:219-220); the recipient need not have a faction. - Self-transfer (
t== sender) is a no-op (src/trading.cpp:214-217). - Same building/foundation/item validity rules as orders
(
src/trading.cpp:111-138).
Pending JSON: {"op":"transfer","building":B,"item":I,"num":N,"to":NAME}
(src/trading.cpp:197-204, example src/trading_tests.cpp:243-257).
11. Cancelling orders#
{ "c": <orderId> } cancels and fully unwinds escrow
(src/trading.cpp:507-593):
- Only the order's owner can cancel; cancelling someone else's order, or a
nonexistent ID, is rejected (
src/trading.cpp:528-547,src/trading_tests.cpp:837-848). - Cancel a bid: refunds
quantity * pricecoins to the owner's balance (src/trading.cpp:571-577;CancelBidtest refunds 6 for a 2@3 bid,src/trading_tests.cpp:860-871). - Cancel an ask: returns
quantityitems to the owner's building inventory (src/trading.cpp:580-586;CancelAsktest returns 2 items,src/trading_tests.cpp:873-884). - The order row is then deleted (
src/trading.cpp:592).
There is no partial cancel — you cancel the whole remaining order. To resize, cancel and re-place.
12. Building destruction & order cleanup (edge case)#
If a building is destroyed in combat, all its open orders are wound up
(src/combat.cpp:1226-1276):
- Open bids: the reserved coins are refunded to each buyer's main balance
(
src/combat.cpp:1226-1234, viaGetReservedCoins(building)). Coins are always fully recovered. - Open asks: the reserved items are not returned to the seller's
inventory. They are added to the building's total inventory pile
(
src/combat.cpp:1235-1236) which, together with all building inventory, is then probabilistically dropped as ground loot — each item stack has only aBUILDING_INVENTORY_DROP_PERCENT= 30% chance to drop, otherwise it is destroyed (src/combat.cpp:47,:1255-1271). Items in resting asks can therefore be lost when a building is destroyed. - Finally all of the building's orders are deleted
(
orders.DeleteForBuilding,src/combat.cpp:1275,database/dex.cpp:346-355).
Designer/UX implication: bids are safe (coins refunded), but asks carry destruction risk — only 30% of escrowed items survive as ground loot if the hosting building is destroyed. Surface this risk prominently.
13. How a client queries the order book#
13.1 Order book (live, per building) — getbuildings#
The order book is embedded in each building object returned by the getbuildings
RPC (src/pxrpcserver.cpp:514-523 → GameStateJson::Buildings,
src/gamestatejson.cpp:745-750). For each non-foundation building, the building
JSON contains (src/gamestatejson.cpp:442-459):
inventories:{ account: {fungible:{item:qty}} }— free items only.reserved:{ account: {fungible:{item:qty}} }— items locked in open asks.orderbook:{ item: { item, bids:[...], asks:[...] } }.config.dexfee: the building owner fee as a percent (thedex_fee_bpsvalue ÷ 100; if set). This is the owner's cut only — it does not include the network base fee (§7), so the real total fee a seller pays isparams.dex_fee_bps/100 + config.dexfeepercent.
Order book shape (GetOrderbookInBuilding, src/gamestatejson.cpp:343-403):
{
"orderbook": {
"raw a": {
"item": "raw a",
"bids": [
{ "id": 14, "account": "buyer", "price": 20, "quantity": 1 },
{ "id": 13, "account": "buyer", "price": 10, "quantity": 1 }
],
"asks": [
{ "id": 12, "account": "seller", "price": 50, "quantity": 2 },
{ "id": 11, "account": "seller", "price": 100, "quantity": 2 }
]
}
}
}
Sort order, ready for display (src/gamestatejson.cpp:393-400):
bids: descending price (best/highest bid first).asks: ascending price (best/lowest ask first).
So bids[0] and asks[0] are the inside (best) prices; the spread is
asks[0].price - bids[0].price. (Real values verified in
gametest/dex.py:161-196.)
The official web client reads exactly this: its order book comes from
serverData.orderbook and its reserved map from
serverData.reserved?.fungible, fetched via the getbuildings RPC.
13.2 Trade history — gettradehistory#
RPC gettradehistory(building, item) (src/pxrpcserver.cpp:603-614 →
GameStateJson::TradeHistory, src/gamestatejson.cpp:780-786). Returns an array
of trade rows (oldest first), each (src/gamestatejson.cpp:605-626):
[
{
"height": 12345,
"timestamp": 1700000000,
"buildingid": 42,
"item": "raw a",
"price": 100,
"quantity": 2,
"cost": 200,
"seller": "seller",
"buyer": "buyer"
}
]
cost is quantity * price (gross, fees not reflected;
src/gamestatejson.cpp:619-620). (Verified against gametest/dex.py:282-317.)
13.3 Reserved balances — getaccounts#
getaccounts returns each account with a balance object:
{ available, reserved, total } where reserved is coins locked in that
account's open bids across all buildings and total = available + reserved
(src/gamestatejson.cpp:716-743). minted (burn-sale total) is also present
(src/gamestatejson.cpp:313).
13.4 Notes for client devs#
- The full-state dump (
FullState,src/gamestatejson.cpp:788-803) includesaccountsandbuildings(with order books) but not trade history; querygettradehistoryseparately. - Order books update only on confirmed blocks. For optimistic UX, also read the
pending move feed: each DEX op exposes a pending JSON (
ToPendingJson, e.g.{"op":"bid","building":B,"item":I,"num":N,"price":P},src/trading.cpp:330-337) so a client can show in-flight orders before they confirm.
14. Item codes & display names (for market UI)#
Items tradeable on the DEX are identified by their internal code. Display names come from the official web client. Key categories (codes in backticks):
Raw resources (ores):
Trimideum (raw a), Talon (raw b), Henoix (raw c), Orchanum (raw d),
Kalanite (raw e), Voltar (raw f), Ravolute (raw g), Talgarite (raw h),
Liberite (raw i).
Refined materials:
Agarite (mat a), Borolium (mat b), Chronogen (mat c), DARR-4 (mat d),
Exillium (mat e), FORTON-D (mat f), Greophite (mat g), Helion (mat h),
I-77E (mat i).
Artefacts:
Ancient Artefact common (art c), uncommon (art uc), rare (art r),
ultra-rare (art ur).
Vehicles are faction-prefixed (rv*/bv*/gv*), e.g. Raider (rv st),
Barracuda (bv st), Scarab (gv st) — full list in the official client.
Fitments use <size>f <code> keys (size ∈ light lf / medium mf /
heavy hf / very-heavy vhf), e.g. Light Rail Gun (lf gun), Heavy Shield
Enhancer (hf shield), Mobile Refinery (vhf refinery) — full list in
the official client.
Validity check is purely "does the item code exist in the read-only config" (
src/trading.cpp:128). Any configured item — ore, material, vehicle, fitment, artefact, prize — can be listed on the DEX provided it physically sits in the building inventory.
15. Quick reference cheat-sheet#
| Thing | Value | Source |
|---|---|---|
| Move key for DEX ops | "x" (array) |
src/moveprocessor.cpp:452 |
| Bid price field | bp |
src/trading.cpp:664 |
| Ask price field | ap |
src/trading.cpp:670 |
| Cancel field | c (single-key obj) |
src/trading.cpp:621-628 |
| Transfer recipient field | t |
src/trading.cpp:654 |
| Building fee update field | xf (under "b" update) |
src/moveprocessor.cpp:334 |
| Base DEX fee (mainnet) | 300 bps = 3% (burned) | proto/roconfig/params.pb.text:19 |
| Base DEX fee (regtest) | 1000 bps = 10% | proto/roconfig/test_params.pb.text:13 |
| Max owner fee (move) | 3000 bps = 30% | src/moveprocessor.cpp:54 |
| Coin denomination | sat; 1 coin = 1e8 sat | database/amount.hpp:31 |
| Max price/unit | 100,000,000,000 sat (1000 coins) | src/jsonutils.cpp:38 |
| Max quantity/order | 2^50 | database/inventory.hpp:54 |
| Building update delay (mainnet/regtest) | 120 / 10 blocks | params.pb.text / test_params.pb.text |
| Building destruction item-drop chance | 30% per stack | src/combat.cpp:47 |
| Order book RPC | getbuildings → building.orderbook |
src/gamestatejson.cpp:458 |
| Trade history RPC | gettradehistory(building, item) |
src/pxrpcserver.cpp:603 |
| Reserved coins RPC | getaccounts → balance.reserved |
src/gamestatejson.cpp:738 |
Open questions#
- Burn destination of the base fee. The base fee is destroyed implicitly
(buyer debited full cost; seller + owner credited the rest), reducing money
supply. The code does not route it to the configured
burn_addr(params.pb.text); that address is for on-chain WCHI burns, not in-game coin accounting. Confirm whether design intends the in-game burn to be tracked inmoneysupplystats —MoneySupply()is not obviously updated by DEX fee burns in the trading code path (not found insrc/trading.cpp). - Client
dexvs. GSPxkey. The official web client builds its DEX intents under adexwrapper, and those methods are marked as stubs / "Server may not support this action yet". The GSP move key is"x". The translation from the client'sdexwrapper to the on-chain"x"move was not located; client devs should confirm the final emitted move uses"x", notdex. - The client market UI is a placeholder. The official client's market UI
currently shows "DEX Order Book — Coming when marketplace backend is ready"
and its trade-history call is a stub returning
{trades:[]}. The backend (GSP) fully supports the DEX as documented here; the client integration is incomplete. No GSP-side ambiguity, just a client gap to flag. - Order ID exhaustion / reuse. Orders share the global entity ID counter
(
database/dex.cpp:29), while trade-history rows use a separate log-ID counter (database/dex.cpp:360). No practical limit was found, but designers relying on stable order IDs in URLs/links should note IDs are never reused after deletion (monotonic counter).