vectors/signing.json has a worked example of each message: key, inputs, bytes and signature. The exchange’s server and the Rust client are both tested against that file; test yours against it too.
Keys and signatures
An account has two Ed25519 keys (see the quickstart):
The signature is sent base64-encoded (standard alphabet, with padding): 64 bytes, so 88 characters. The trading key’s public key is the raw 32 bytes, registered onchain with
new_trading_key_transfer.
Units
- Micro units. Prices and amounts onchain are integers with 6 implied decimals, like USDC: 1 USDC is
1000000, and 2.5 shares is2500000. The/v1API takes the same values as decimal strings with at most 6 decimals ("1","2.5"). - Shares. A share pays $1 if the underlying's price touches the tile's price range during its time window, and $0 otherwise. So share prices are between 0 and 1 USDC.
- Prices are totals. An order carries a total price (USDC) and a total amount (shares). Its unit price is
total_price / total_amount.
Contract ids
A tile is identified by a u256, sent as a decimal string:1790000000 to 1790000300 (a 5-minute window), $109,000 to $109,100:
Order intents
An order is the BCS encoding of one of the contract’s Move structs. The contract checks its signature onchain, against the account’s registered trading key, every time the order fills. BCS rules used here:- fields are laid out in order, with no padding and no length prefixes;
- an
addressis its 32 bytes; u64is 8 bytes little-endian, andu256is 32 bytes little-endian;Option<u64>is00for none, or01followed by the 8-byte value.
Buy: BuyerOrderIntent
It is 129 bytes, or 121 for a market buy.
Sell: SellerOrderIntent
It is 129 bytes, or 121 for a market sell. Selling more than you hold opens a short, which locks $1 of cash per share as collateral.
Limit and market orders
- Limit (
"type": "limit"inPOST /v1/orders): bothtotal_priceandtotal_amountare set. Whatever doesn’t fill right away rests on the book, at unit pricetotal_price / total_amount. - Market: the optional field is none. A market buy spends up to
total_priceat any price; a market sell sellstotal_amountat any price. Nothing rests, and the order is rejected if nothing fills.
What the contract checks on each fill
- Both signatures verify against the two accounts’ registered trading keys.
- Both intents name this exchange, and the fill happens before both expiries.
- The two orders are on the same
contract_idand belong to different accounts. - The prices cross:
buyer.total_price × seller.total_amount ≥ seller.total_price × buyer.total_amount. - Neither intent fills past what it signed. The contract records the amount filled per account and nonce.
taker_fee_bps), paid only by the taker, the order that fills against a resting one. A taking buyer pays it on top of the price: a buy’s signed total_price bounds the price paid, and the fee comes on top. A taking seller pays it out of its proceeds. Makers pay nothing.
Cancels: CancelOrderIntent
A cancel repeats the order’s own fields:
It is 88 bytes. The server checks that it matches the stored order, then takes the order off the book. Nothing is recorded onchain: the signed intent stays valid until its expiry, but only the operator can submit fills, and it only fills orders on the book (see the trust note under bulk cancels). Only a limit order that is resting can be cancelled, and only before its expiry.
GET /v1/accounts/{address}/orders returns each resting order’s nonce and expiry, so a bot can cancel orders it placed before a restart.
Bulk cancels
POST /v1/orders/cancel-all and /cancel-batch take many orders off the book with one signature, over the UTF-8 bytes of exactly this JSON (no spaces, keys in this order):
contract_idis a decimal string, ornullfor all contracts.intent_idsare the orders’ intent ids, in the order sent. Withclient_order_ids, the server looks them up first, so still sign intent ids.nonceis a random u64, as a string, and can be used once.expiryis Unix ms, at most 60 s ahead.
Trust: a bulk cancel only takes orders off the book; it sends no chain transaction. An order cancelled this way still has a valid signed intent onchain until its expiry, but only the operator can submit fills, and it only fills from the book. The same holds for a single cancel. To limit that exposure, use short expiries.
Withdrawals
A withdrawal moves exchange cash to the account’s own Sui address. The trading key signs the UTF-8 bytes of exactly this JSON, with no spaces and the keys in this order:amountis micro USDC, as a string;typeis the USDC coin type (GET /v1/exchange→usdc_type);addressis the account.
Nonces and replay
- Pick every nonce at random from the full u64 range, and never reuse one. The contract keeps one table of nonces per account, shared by orders and trading-key registrations. A reused nonce continues the old intent’s fill count, or is rejected.
- A nonce’s record can be deleted once its expiry has passed. By then the intent itself is expired, so it can’t fill again.
- Expiry is Unix milliseconds and must be in the future. There is no maximum today; an hour is typical.
- Signatures bind the exchange object, so an intent can’t be used on another deployment.