Skip to content

API Reference

The Aspens Market Stack exposes three gRPC services. Source of truth is the protos repository:

ServiceProto fileUsed by
ArborterServicearborter.protoTrading: orders, cancels, streams, withdrawal vouchers
ConfigServicearborter_config.protoRead-only stack configuration
AuthServicearborter_auth.protoAdmin 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:

OrderGivesBudget
ASK (limit or market)basequantity
LIMIT BIDquotequantity * price
MARKET BIDquotequote_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

Arborter Send Order Request Flow

  1. Client signs the encoded Order (EIP-191 on EVM, Ed25519 on Solana) and sends SendOrderRequest. No on-chain transaction.
  2. 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.
  3. 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).
  4. Any unmatched remainder rests in the orderbook against that lot.
  5. SendOrderResponse returns immediately; Trades and Orderbook subscribers are notified in parallel.
  6. Separately, a background settler folds accumulated net deltas on-chain in batches (MidribV3.settleBatch on EVM, settle_batch on Solana).