Matching engine
The Go engine — binaries, the reject-never-round contract, and the build flag that eats your binary.
server/engine — deterministic off-chain matching. Go.
Binaries (cmd/)
| Binary | Role |
|---|---|
engine | The matching engine + REST API + admin firehose |
simulator | Drives synthetic activity against a running engine |
flatten | Closes every open position (test-stack reset) |
go build ./cmd/a ./cmd/b does not write binaries. With multiple packages, go build only
compiles them as a check and discards the output. Build one package at a time with -o:
go build -o ./engine ./cmd/engine
go build -o ./simulator ./cmd/simulatorThis costs people an afternoon roughly once per team.
Quickstart
From the repo root: build contracts, upload the DAR, create the Vault, seed counters.
Generate test users and on-chain deposits, then restart the engine so it picks up keys.json.
Run the engine. ADMIN_TOKEN enables the indexer firehose and the admin UI on :8080.
Drive activity from another terminal with simulator.
Optionally flatten everyone to reset.
REST API
| Route | Auth | Purpose |
|---|---|---|
POST /v1/session-auth | wallet signature | Open a trading session |
POST /v1/orders | session signature | Place |
DELETE /v1/orders/:id | session signature | Cancel |
POST /v1/tpsl | session signature | Arm TP/SL on a position |
GET /v1/orderbook?depth | public | Book snapshot |
GET /v1/user/me | session signature + signed body | User snapshot |
GET /v1/admin/events/stream | bearer ADMIN_TOKEN | SSE firehose |
GET /v1/admin/users/{party}/orders?market= | bearer | Live open orders |
?market= on the admin order read is mandatory since c879f7f (400 market_required), and
the reserved value all only exists from that same commit. The BFF depends on both.
Reject, never round
The engine validates against the market grid and rejects. It never silently adjusts an order.
| Rule | Constraint |
|---|---|
| Size | Exact multiple of 10^-szDecimals |
| Price | Integer, or ≤5 significant figures and ≤6 - szDecimals decimals |
| Notional | price × size ≥ minNotional, opening leg only — a reduce is exempt |
Rejections arrive as HTTP 200 with ok: false.
Rejecting rather than rounding is the right call for a derivatives venue: a rounded size changes
the margin requirement the user just previewed. Better to refuse and let the client snap
deliberately — which is exactly what market-grid.ts does.
Key concepts
| Concept | Detail |
|---|---|
| Session keys | Ed25519, bound to a party by fingerprint. Every order signed with one |
| Nonces | Per-operation. A reused nonce is rejected even for an identical payload |
| WAL | Signed write-ahead log. The indexer and the SSE firehose both read it |
| Position epochs | pos-<32 hex>. A flip closes one epoch and opens another |
| Triggers | Position-attached TP/SL. armed → fired | cancelled. No stop-entry trigger |
| Funding | Periodic long↔short payment, fundingIntervalSeconds per market |
| Liquidation | Triggered on maintenance margin fraction, priced off the oracle index |
There is no stop-entry trigger. TP/SL can only attach to an existing position — which is why
the interface parks LIMIT-order TP/SL in stores/pending-tpsl and arms it after the fill.
Commitments
Fills become leaves in a per-user merkle tree. On cadence the engine commits a UserBatch root
per user and a global BatchManifest. Withdrawals are evaluated against the last committed
batch, which is why cadence is directly the withdrawal latency.
Configuration
| Var | Purpose |
|---|---|
ADMIN_TOKEN | Enables the firehose + admin UI |
LEDGER_API_URL | Canton ledger |
REDIS_URL | Oracle index price source |
MARKETS_FILE | Path to markets.json |
keys.json holds generated test-user keys and is read at startup. Generating users while the
engine runs does nothing until you restart it — a classic "why does the deposit not show up".