This repository contains a proof of concept for an exact-fill order book on Nervos CKB. Makers create buy or sell Order Cells. A matching bot finds a compatible pair and submits one transaction that consumes both orders.
Makers sign when they create or cancel their orders. Matching is permissionless: no maker signature is required to settle a valid pair.
The PoC supports full exact matches only. A buy and sell order must specify the same xUDT type, token amount, and total CKB price.
Buy order: maker locks enough CKB for the price and buyer token Cell
Sell order: maker locks the exact xUDT amount
Match:
seller receives the price plus the sell Order Cell capacity
buyer receives the exact xUDT amount
Each match consumes exactly one buy Order Cell and one sell Order Cell. The contract validates both sides. A maker can cancel an order by including a maker-locked input in a transaction that consumes only that one DEX Order Cell. Cancellation must return the order's full capacity, type script, and data in a maker-locked refund cell. Fees are funded separately.
The contract limits the buyer's CKB deduction beyond the agreed price to 10,000 shannons (0.0001 CKB). All capacity above that allowance must remain in the buyer's matching xUDT outputs. The bot and fill helper subtract the calculated network fee and put the rest in the buyer's xUDT Cell.
The current version uses exactly 90 bytes:
| Bytes | Size | Meaning |
|---|---|---|
0 |
1 byte | Format version, currently 1 |
1 |
1 byte | Side: 0 for buy, 1 for sell |
2..34 |
32 bytes | Maker lock script hash |
34..66 |
32 bytes | Expected xUDT type script hash |
66..82 |
16 bytes | Exact token amount as little-endian u128 |
82..90 |
8 bytes | Total CKB price in shannons as little-endian u64 |
The lock args record the terms the contract must enforce. The maker lock hash commits to the complete maker lock script. A matcher may recover the full lock from the order creation transaction's input Cells, then verify its hash against the value in the args.
| Component | Responsibility |
|---|---|
| DEX order lock | Enforces buy and sell terms, exact pair matching, and maker cancellation |
| xUDT type script | Enforces token validity and token conservation |
| Maker lock | Authenticates order creation and cancellation inputs |
| Matching bot | Discovers orders, selects matching pairs, builds and submits settlement transactions |
The bot is not trusted to enforce settlement. The DEX lock scripts and xUDT type script validate the transaction on-chain.
This PoC supports:
- xUDT buy and sell orders paid in CKB.
- Exact full fills with equal token type, amount, and price.
- One buy order and one sell order per settlement transaction.
- Permissionless matching and settlement.
- Maker-authorized cancellation of one order.
It does not support partial fills, price improvement, matching orders with differing amounts, multi-order settlement, or a global on-chain order book. The matching bot and off-chain services provide order discovery and matching.
Architecture design diagram: Excalidraw
┌──────────┐ REST + WebSocket ┌──────────────────────────┐ ┌─────────┐
│ ui/ │◄───────────────────►│ backend/ │◄─►│ MongoDB │
│ React + │ │ Express API + WS │ └─────────┘
│ CCC │ │ Matching bot (poll 5s) │
└────┬─────┘ │ Event ingestion │
│ sign + broadcast └───────────┬──────────────┘
│ orders / cancels │ index cells, build and
│ │ submit settlement txs
▼ ▼
┌───────────────────────────────────────────────────────┐
│ CKB testnet (RPC) │
│ dex-order-lock · xUDT type script · maker locks │
└───────────────────────────────────────────────────────┘
▲
│ mint test xUDT
┌────┴─────┐
│ faucet/ │ (own MongoDB database, per-IP throttle, 24h cooldown)
└──────────┘
Flow:
- A maker creates a buy or sell Order Cell from the UI (or
offchain/scripts), signing with their wallet. The cell is locked bydex-order-lock. - The backend bot polls the chain, indexes Order Cells into MongoDB, and recovers each maker's full lock from the creating transaction.
- The bot finds an exact buy/sell pair, reserves both orders, builds the settlement transaction, and submits it. No maker signature is needed.
- Order state changes are ingested as events and pushed to the UI over WebSocket (market and maker channels), with an initial snapshot on subscribe.
- CKB validates the transaction through the DEX lock and xUDT scripts.
Order statuses in the database: DISCOVERED, LIVE, RESERVED, PENDING,
SETTLEMENT_SUBMITTED, FILLED, CANCELED/CANCELLED, INVALID, ORPHANED.
The UI shows these as Broadcasting, Open, Matched, Submitted, Filled, Cancelled
and Invalid.
| Path | Contents |
|---|---|
smart-contract/ |
Rust CKB contracts (dex-order-lock, lock-script-contract) and tests |
backend/ |
Express + MongoDB API, matching bot, event ingestion, WebSocket broadcaster |
ui/ |
Vite + React 19 trading UI using CCC wallet connector |
faucet/ |
Express service that drips test xUDT with a per-claim cooldown |
offchain/ |
CLI scripts: issue-token, create-order, fill-order |
shared/ |
Deployment validation, client creation, and settlement capacity calculation |
deployment/ |
Runtime deployment manifest and deployment-tool artifacts |
compose.yml |
Docker Compose for MongoDB, backend and faucet |
weekly-progress-logs/ |
Weekly progress reports |
- Wallet connection, buy/sell forms with a review modal before signing.
- Order book with depth chart, recent trades, open orders and history tables with a lifecycle Status column.
- Live updates over WebSocket; distinct loading, empty and demo states.
- Persisted light/dark theme.
- Test token faucet integration.
Base path /api/v1.
| Method | Path | Purpose |
|---|---|---|
| GET | /, /health, /readiness |
Service info and health |
| GET | /markets |
Available markets |
| GET | /order-book |
Bids and asks, best price first |
| GET | /orders, /orders/:txHash/:index |
List or fetch orders |
| GET | /trades |
Trade history |
List endpoints take limit (1-200, default 25) and an opaque cursor; pass the
response's nextCursor to fetch the next page. Invalid input returns 400, and
public reads are limited to 300 requests per minute per client.
| POST | /internal/events | Bot event ingestion (bearer-token protected) |
WebSocket (same port): clients send { "channels": [...] } and receive a
snapshot, then update messages per channel. Channels must be
market:<0xhash>:orderbook, market:<0xhash>:trades or maker:<0xhash>:orders.
Snapshots hold at most 100 rows per side; use the REST API to page further.
Messages are limited to 4 KiB, 8 channels per message, 16 per connection and 20
connections per address.
The faucet exposes GET /api/v1/faucet/health and POST /api/v1/faucet/claim
on port 3100. It only runs on testnet. Each claim reserves its per-address
cooldown and a share of the global daily budget atomically before anything is
sent, and rolls both back if the send fails.
deployment/manifest.json is the runtime source of truth for the UI, backend,
faucet, and offchain scripts. It pins the network genesis hash, CCC client type,
DEX and system scripts, dependency outpoints, and supported token identities.
RPC URLs and private keys stay in environment variables. Startup checks reject
a wrong chain, missing DEX/xUDT dependency cells, or mismatched script identities.
Unconfigured networks are rejected; only testnet is currently listed.
scripts.json and system-scripts.json remain deployment-tool artifacts, not
runtime configuration. The manifest records the currently deployed contract:
dex-order-lock uses code hash
0x8560b80bacb96bad676ce0c75b73ebf93e3a6fb06cda5cf3a89137f11018df3b
(data2).
Requirements: Node.js, MongoDB (or Docker), Rust toolchain for contracts.
Each service has a .env.example; copy it to .env and fill in values.
# Database + backend + faucet
docker compose up --build
# Or run locally, per service (backend, faucet, ui)
cd backend && npm install && npm run dev # API on :3000
cd faucet && npm install && npm run dev # faucet on :3100
cd ui && npm install && npm run dev # Vite dev server
# Off-chain helper scripts
cd offchain && npm install
npm run issue-token # requires the offchain maker/buyer keys
npm run create-order # requires the offchain maker/buyer keys
npm run fill-order # requires the buy and sell order outpoints; no maker signatureKey environment variables:
| Service | Variables |
|---|---|
| backend | MONGO_DB_URL, PORT, CKB_RPC_URL, CKB_NETWORK, INTERNAL_BOT_TOKEN (required), CONFIRMATION_DEPTH (default 10) |
| faucet | MONGO_DB_URL, PORT, CKB_RPC_URL, CKB_NETWORK, FAUCET_ISSUER_PRIVATE_KEY, FAUCET_DRIP_AMOUNT, FAUCET_COOLDOWN_HOURS, FAUCET_DAILY_MAX_CLAIMS, FAUCET_DAILY_MAX_TOKENS |
| ui | VITE_API_URL, VITE_WS_URL, VITE_CKB_RPC_URL, VITE_DEX_NETWORK, VITE_FAUCET_URL |
The demo token's full identity is derived from the manifest's xUDT script and asset args. Token issuance requires that asset's issuer key; creating an order only requires the maker's key and funds. Legacy DEX/issuer environment settings are checked for conflicts rather than allowed to override the manifest.
The backend indexes a block only after CONFIRMATION_DEPTH confirmations,
re-verifies every event against CKB before applying it, and records block
hashes so it can roll back and recover after a reorg.
The backend requires INTERNAL_BOT_TOKEN (at least 32 characters, not a
placeholder) and refuses to start without it. Event ingestion uses MongoDB
transactions, so MongoDB must run as a replica set (compose.yml configures a
single-node one).
The backend needs MongoDB reachable at startup; its "demo mode" fallback does not work reliably yet.
Smart contract source, build instructions, and tests are in smart-contract/.
The DEX lock's validation details and error codes are documented in
smart-contract/contracts/dex-order-lock/README.md.
From smart-contract/, build the DEX contract with:
make run CONTRACT=dex-order-lock TASK=buildRun its tests with:
cargo test -p tests test_dex_Backend tests: cd backend && npm test (type check with npm run typecheck).
UI: npm run lint and npm run build.
- Fills detected outside the bot's submission flow mark orders as filled but do not create a trade record or emit a trade event.
- The backend demo-mode fallback is non-functional.
- No partial fills, market orders, or multi-order atomic matching.