Skip to content
TAURION
10Trading & the DEX

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.
  • b is not an unsigned integer ID, or i is not a string, or n fails QuantityFromJson (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/ap is present (because total key count is fixed at 4, at most one can ever be set — src/trading.cpp:649-675).
  • bp/ap is 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 ≥ n of 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.reserved and balance.total = available + reserved (src/gamestatejson.cpp:716-743; computed by GetReservedCoins, database/dex.cpp:278-305). balance.available is the spendable balance.
  • Reserved item quantities (from open asks), per account, appear in each building's reserved object (src/gamestatejson.cpp:453-456; computed by GetReservedQuantities, database/dex.cpp:307-344).

Important: the building inventories map shows only the free (un-escrowed) items. Items locked in open asks are moved out of inventory and shown under reserved. A client must add inventories[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):

  1. For each crossing resting ask, cur = min(remaining, ask.quantity).
  2. Credit cur items to the buyer's building inventory.
  3. cost = cur * ask.price; pay seller (minus fees, §7) and burn/owner-fee; debit cost from the buyer's balance.
  4. Record a trade-history row (§8).
  5. ask.ReduceQuantity(cur) (deletes the ask if it hits 0).
  6. 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 movesMaybeUpdateDexFee only accepts unsigned values 0 … 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 have ownerBps forced to 0 (src/trading.cpp:304-305).
  • burned = total - ownerdestroyed (not paid to anyone). This is implicit: the buyer is debited the full cost, the seller gets payout, the owner gets owner, and the difference cost - payout - owner = burned simply 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) = 1 per 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 supports sf (service-fee percent) and send (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 ≥ n of 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 * price coins to the owner's balance (src/trading.cpp:571-577; CancelBid test refunds 6 for a 2@3 bid, src/trading_tests.cpp:860-871).
  • Cancel an ask: returns quantity items to the owner's building inventory (src/trading.cpp:580-586; CancelAsk test 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, via GetReservedCoins(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 a BUILDING_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-523GameStateJson::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 (the dex_fee_bps value ÷ 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 is params.dex_fee_bps/100 + config.dexfee percent.

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-614GameStateJson::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) includes accounts and buildings (with order books) but not trade history; query gettradehistory separately.
  • 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 getbuildingsbuilding.orderbook src/gamestatejson.cpp:458
Trade history RPC gettradehistory(building, item) src/pxrpcserver.cpp:603
Reserved coins RPC getaccountsbalance.reserved src/gamestatejson.cpp:738

Open questions#

  1. 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 in moneysupply stats — MoneySupply() is not obviously updated by DEX fee burns in the trading code path (not found in src/trading.cpp).
  2. Client dex vs. GSP x key. The official web client builds its DEX intents under a dex wrapper, 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's dex wrapper to the on-chain "x" move was not located; client devs should confirm the final emitted move uses "x", not dex.
  3. 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.
  4. 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).