API Reference
The Aspens Market Stack exposes three gRPC services. Source of truth is the protos repository:
| Service | Proto file | Used by |
|---|---|---|
ArborterService | arborter.proto | Trading: orders, cancels, streams, withdrawal vouchers |
ConfigService | arborter_config.proto | Read-only stack configuration |
AuthService | arborter_auth.proto | Admin login, JWT issuance |
This page documents the trading service. The full proto bundle is at github.com/aspensprotocol/protos.
ArborterService
service ArborterService {
rpc SendOrder (SendOrderRequest) returns (SendOrderResponse);
rpc CancelOrder (CancelOrderRequest) returns (CancelOrderResponse);
rpc Trades (TradeRequest) returns (stream Trade);
rpc Orderbook (OrderbookRequest) returns (stream OrderbookEntry);
rpc Withdraw (WithdrawRequest) returns (WithdrawResponse);
}SendOrder
message SendOrderRequest {
Order order = 1;
bytes signature_hash = 2; // EIP-191 (EVM) / Ed25519 (Solana) over the encoded Order
}The envelope signature is the only thing that authenticates order
entry, and it covers every input the arborter needs. Nothing rides
alongside the signed Order: both the committed budget and the canonical
order id are derived server-side from the message you signed.
message Order {
Side side = 1; // SIDE_BID | SIDE_ASK
string quantity = 2; // BASE units at pair decimals
optional string price = 3; // present = LIMIT, absent = MARKET
string market_id = 4; // base_chain_id::token_addr::quote_chain_id::token_addr
string base_account_address = 5;
string quote_account_address = 6;
ExecutionType execution_type = 7; // _UNSPECIFIED (=DIRECT) | _DISCRETIONARY
repeated bytes matching_order_ids = 8; // discretionary only — each a resting order's 32-byte id
bool post_only = 9; // limit orders only; must rest or be rejected
bool hidden = 10; // matched normally, never shown in any stream
optional string quote_budget = 11; // market BID only — see below
uint64 nonce = 12; // caller-chosen; folded into the order id
}
enum Side { SIDE_UNSPECIFIED = 0; SIDE_BID = 1; SIDE_ASK = 2; }
enum ExecutionType { EXECUTION_TYPE_UNSPECIFIED = 0; EXECUTION_TYPE_DISCRETIONARY = 1; }The budget rule
One rule governs every order: it commits a budget, denominated in the asset it gives. Three of the four (side, type) combinations derive that budget from fields already present:
| Order | Gives | Budget |
|---|---|---|
| ASK (limit or market) | base | quantity |
| LIMIT BID | quote | quantity * price |
| MARKET BID | quote | quote_budget — nothing else bounds it |
A market BID has no price to convert with, so it must state outright what
it is prepared to spend. quote_budget is in the quote token's native
base units (not pair decimals), decimal-string u128. It is required
on a market BID and rejected everywhere else, where a caller-supplied
figure could only disagree with the derived one. It sits inside Order
so signature_hash covers it — this number authorises spending.
Order ids
The arborter derives the canonical order id itself, from the signed
Order plus the signer's own address and nonce. You cannot choose one.
Because the derivation hashes the caller's address, an order signed by an
attacker can never derive to your id, so pre-claiming an id is not
expressible. Compute the same id locally with
aspens::orders::derive_order_id(...) if you need it — every input is in
the message you signed — or read it back from SendOrderResponse.order_id,
which returns that same full 32-byte id (0x-prefixed hex on the clients).
The cancel and discretionary handles are this id too: OrderToCancel.order_id
and Order.matching_order_ids each carry a 32-byte order id.
Reusing a nonce derives the same id and is refused as a replay.
message SendOrderResponse {
bool order_in_book = 1;
optional Order order = 2; // unfilled remainder, if any
repeated Trade trades = 3; // matches produced by this submit
repeated TransactionHash transaction_hashes = 4;
repeated OrderbookEntry current_orderbook = 5;
bytes order_id = 6; // the 32-byte canonical order id
}CancelOrder
message CancelOrderRequest {
OrderToCancel order = 1;
bytes signature_hash = 2;
}
message OrderToCancel {
string market_id = 1;
Side side = 2;
string token_address = 3;
bytes order_id = 4; // the 32-byte order id from SendOrderResponse.order_id
}Withdraw
Withdrawal is not self-service. On-chain balances still include collateral reserved behind resting orders — those reservations live off-chain — so only the arborter knows the withdrawable amount. It freezes the funds and returns a single-use voucher that the holder submits on-chain.
message WithdrawRequest {
string network = 1; // network key, e.g. "flare-coston2"
string token = 2; // token contract address on `network`
string account = 3; // withdrawer's wallet address on `network`
string amount = 4; // token base units, decimal-string u128
bytes signature = 5; // by `account` over "network|token|account|amount"
}
message WithdrawResponse {
string account = 1;
string token = 2;
string amount = 3;
uint64 nonce = 4; // per-account replay key
uint64 expiry = 5; // unix seconds
bytes signature = 6; // TEE (instance-owner) EIP-712 signature over the voucher
}Submit the response fields to MidribV3.withdraw(voucher, signature) on
EVM, or withdraw_voucher on Solana. The voucher pays account
regardless of who broadcasts it, so a relayer can cover the gas. On-chain
the contract enforces bounded backstops of its own: the owner signature,
the expiry, one-shot per (account, nonce), never more than the balance
on hand, and a per-token per-epoch rate limit.
Trades / Orderbook (streams)
message TradeRequest {
bool continue_stream = 1;
string market_id = 2;
optional bool historical_closed_trades = 3;
optional string filter_by_trader = 4;
}
message OrderbookRequest {
bool continue_stream = 1;
string market_id = 2;
optional bool historical_open_orders = 3;
optional string filter_by_trader = 4;
}Stream entries:
message OrderbookEntry {
uint64 timestamp = 1;
bytes order_id = 2; // the 32-byte order id
string price = 3;
string quantity = 4;
Side side = 5;
string maker_base_address = 6;
string maker_quote_address = 7;
string market_id = 8;
OrderState state = 9; // PENDING | CONFIRMED | MATCHED | CANCELED | SETTLED
}
message Trade {
uint64 timestamp = 1;
string price = 2; // settled, net of fees
string qty = 3;
string maker_id = 4;
string taker_id = 5;
string maker_base_address = 6;
string maker_quote_address = 7;
string taker_base_address = 8;
string taker_quote_address = 9;
TradeRole buyer_is = 10; // MAKER | TAKER
TradeRole seller_is = 11;
bytes order_hit = 12; // the maker order's 32-byte id
}Order request flow
- Client signs the encoded
Order(EIP-191 on EVM, Ed25519 on Solana) and sendsSendOrderRequest. No on-chain transaction. - Arborter verifies the envelope signature with the curve derived from the chain architecture, then derives the order's budget and canonical id from the signed message.
- The engine reserves that budget against the user's off-chain ledger
balance as a per-order collateral lot; if matchable,
Trades are produced and balances move in the ledger immediately (no on-chain tx, no block wait). - Any unmatched remainder rests in the orderbook against that lot.
SendOrderResponsereturns immediately;TradesandOrderbooksubscribers are notified in parallel.- Separately, a background settler folds accumulated net deltas
on-chain in batches (
MidribV3.settleBatchon EVM,settle_batchon Solana).