X-Change Guide

Exchange & the order book

Users can trade XCH against xUSD on an order book — like a real exchange. This page explains the two sides of the market, the matching algorithm, and why trades are "off-chain".

The two players

Maker

Adds an order that rests on the book — waits for someone else. Sets the price.

Taker

Hits an existing order — takes the maker's price, and the trade happens instantly.

Every trade executes at the maker's price. The maker "made" liquidity; the taker "took" it. This is standard exchange behaviour.

The order book structure

The book is a map of price → queue of (trader, amount). Two sides:

apps/node/src/exchange.rs
1pub struct OrderBook {
2 pub bids: BTreeMap<i64, Vec<(String, i64)>>, // buys: highest price is best
3 pub asks: BTreeMap<i64, Vec<(String, i64)>>, // sells: lowest price is best
4}
  • Bids — people wanting to buy. Best bid = highest price.
  • Asks — people wanting to sell. Best ask = lowest price.
  • BTreeMap keeps prices sorted → the "best" level is trivial to find.
  • A Vec queue per price gives FIFO: earlier orders at the same price fill first.
Price-time priority
Rules of the market: (1) higher bid / lower ask wins, and (2) at the same price, the order that arrived first fills first. The data structure enforces both for free.

How matching works

When an order arrives, match_order loops: keep filling against the best opposite level while prices "cross", then whatever's left rests (or is cancelled for market orders).

match_order — the core loop
1let mut remaining = incoming.amount;
2let resting = incoming.side.opposite();
3loop {
4 if remaining == 0 { break; }
5 let Some((price, _)) = book.best(resting) else { break };
6 // A bid crosses if best ask <= its limit price; an ask if best bid >= its limit
7 let crossed = match incoming.side {
8 Side::Bid => price <= incoming.price,
9 Side::Ask => price >= incoming.price,
10 };
11 if !crossed { break; }
12 let (price, takers) = book.take(resting, remaining);
13 for (maker, qty) in takers { /* record a Trade at [price], reduce remaining */ }
14}

Limit vs market

  • Limit order — you name a price. Only fills if someone is willing to trade at it; otherwise it rests.
  • Market order — you take whatever is available. Implemented cleverly: bid price = i64::MAX, ask price = 0, so it always crosses. Any unfilled remainder is cancelled (market orders never rest).

The placement pipeline (funds locking)

Placing an order is more than matching — the node must make sure you can pay. Everything happens in one database transaction:

1 · Lock funds
bid → lock xUSD = price × amount · ask → lock XCH = amount
2 · Insert order
orders table, status='open', RETURNING id
3 · Match in memory
match_order against state.orderbooks
4 · Settle trades
move XCH one way, xUSD the other, insert trades rows
5 · Update order
filled, status: closed / open / cancelled
6 · Refund remainder
unfilled lock goes back to the trader

The in-memory book is rebuilt at boot from orders rows with status='open' — so the book survives restarts even though matching is in-memory.

Why "off-chain"?

Trade settlement writes directly to the balances table — it doesnot create a block or a transaction row. Compare with a wallet transfer:

Wallet transfer

Signed tx → mempool → block → confirmed. Appears on-chain. Uses nonces.

Trade settlement

Matching + direct balance update in one DB tx. No block. Faster, but not part of the "blockchain" ledger.

Honest simplification
Real exchanges settle trades off-chain too, but then periodically commit net balances to the chain. This demo skips that batch-settlement step. Great to know and to admit.

Play with the engine

Live: the matching engine

This is a simplified clone of match_order in the Rust node. Limit orders fill against resting orders at the maker's price; whatever doesn't fill rests on the book. Market orders cross everything.

Order bookPrice / size
Empty book — place the first order

In the real node, a bid locks xUSD = price × amount (quote) and an ask locks XCH before matching, and any unfilled remainder is refunded. Try: sell below the best bid to cross, or place a big market order to sweep the book.

The trading endpoints

  • GET /trade/orderbook — current depth (bids/asks as [price, size]).
  • POST /trade/order (JWT) — place a limit or market order.
  • GET /trade/orders (JWT) — your recent orders.
  • DELETE /trade/order/{id} (JWT) — cancel + refund.
  • GET /trade/trades — recent fills.

All the UI that calls these is on the Frontend trading page, and the tables behind them are Database orders & trades.

Defense summary
"The exchange uses a price-time priority order book. Orders lock funds in a transaction, match against resting liquidity in memory at the maker's price, and the remainder is refunded. Trades are settled directly on balances — they're off-chain, which is how real exchanges keep trading fast."