Orderbook API
The Orderbook API exposes Hydra App's OrderbookService — initialised markets, the live orderbook, your own orders + trades, and two streaming surfaces (SubscribeMarketEvents for public market data, SubscribeDexEvents for your account activity).
JSON-RPC namespace: orderbook
Endpoints
Markets
- Init Market
- Get Initialized Markets
- Get Markets Info
- Get Market Info
- Get Market Fee Rates — this node's rates, discounts applied
- Get Currencies — the operator's asset allowlist
- Get Orderbook Balances
- Get Orderbook
Fees & discounts
- Get Fee Discount Programs — the ladders, as configured
- Get Fee Discount — your standing on them
Orders
- Estimate Order
- Create Order
- Cancel Order
- Cancel All Orders
- Get Order
- Get Order By Client Id
- Get Own Orders
- Get All Own Orders
Trades (public + per-account)
- Get Trade History — public trades for one pair
- Get Pair Market Trades — your trades on one pair
- Get All Market Trades — your trades across every pair
- Get Pair Swap Trades — your swap trades for one currency pair
- Get All Swap Trades — your swap trades across every pair
Streams
- Subscribe Market Events — public orderbook + trade stream for one pair
- Subscribe DEX Events — your balance updates, order lifecycle, fills, swap progress
Shared types
OrderbookCurrency
Identifies an asset for orderbook RPCs (note: distinct from Network — flat shape).
| Field | Type | Notes |
|---|---|---|
protocol | Protocol enum | PROTOCOL_BITCOIN or PROTOCOL_EVM |
network_id | string | Magic bytes (Bitcoin) or decimal chain ID (EVM) |
asset_id | string | See asset_id format below |
asset_id format
Verified live against a running Hydra App; do not use the ticker.
| Asset | Form |
|---|---|
| Native asset (BTC, ETH, …) | Zero-padded 32-byte hex: 0x0000000000000000000000000000000000000000000000000000000000000000 |
| ERC-20 token | erc20:<lowercase-contract-address> (e.g. erc20:0x8cd0da3d001b013336918b8bc4e56d9dda1347e0) |
The asset RPCs (asset_getAsset / asset_getNativeAsset) return the canonical form; if in doubt, fetch from there.
OrderAmount (oneof)
Used as the amount field on order requests and as the persisted-form remaining_amount / unmatched_amount / failed_amount on response orders. Exactly one variant:
| Variant | Payload | Meaning |
|---|---|---|
base | { amount: DecimalString } | Amount denominated in the base currency |
quote | { amount: DecimalString } | Amount denominated in the quote currency |
DecimalString is always human-readable (e.g. "0.001" BTC, never satoshis). See Common Patterns → Amounts & Decimals.
⚠️OrderAmountechoes the denomination you placed the order in
OrderAmountis aoneofprecisely so it can carry either denomination. The side of an order (buy/sell) does not determine it. When you create aLimitOrderorMarketOrder, you choosebaseorquotefreely — a buy can be sized in base or quote, and so can a sell. On response orders,remaining_amount,unmatched_amount, andfailed_amountcome back in the same variant you submitted, reduced as the order fills. A buy placed in base reports base remaining; a buy placed in quote reports quote remaining.Always discriminate on the
oneofvariant actually set — never on the side:
- Python:
WhichOneof('amount')(hasattris useless ononeofs — it's always true).- TypeScript:
getAmountCase()/hasBase()/hasQuote().- Go: type-switch on
*pb.OrderAmount_Base_vs*pb.OrderAmount_Quote_.- Rust:
match amount.amount { Some(order_amount::Amount::Base(_)) | Some(order_amount::Amount::Quote(_)) => … }.Do not confuse this with
LiquidityPosition.amountin the public orderbook (GetOrderbook). That field is a single scalar reporting offered liquidity, so it genuinely isbase-on-sell /quote-on-buy. Theremaining_amount/unmatched_amount/failed_amounton your own order do not follow that rule — they follow your placement choice.
Enum values (full names)
JSON-RPC and gRPC responses use the full proto constant names. Match these exactly:
| Enum | Values |
|---|---|
OrderSide | ORDER_SIDE_UNSPECIFIED (0), ORDER_SIDE_BUY (1), ORDER_SIDE_SELL (2) |
OrderType | ORDER_TYPE_UNSPECIFIED (0), ORDER_TYPE_LIMIT (1), ORDER_TYPE_LIQUIDITY (2), ORDER_TYPE_MARKET (3), ORDER_TYPE_SWAP (4) |
AmountSide | AMOUNT_SIDE_UNSPECIFIED (0), AMOUNT_SIDE_BASE (1), AMOUNT_SIDE_QUOTE (2) |
SwapRole | SWAP_ROLE_UNSPECIFIED (0), SWAP_ROLE_LAST_MAKER (1), SWAP_ROLE_INTERMEDIATE_MAKER (2), SWAP_ROLE_TAKER (3) |
SwapStatus | SWAP_STATUS_UNSPECIFIED (0) → SWAP_STATUS_SWAP_FAILED (9). See proto for the full list. |
CandlestickInterval | CANDLESTICK_INTERVAL_UNSPECIFIED (0), ..._ONE_MINUTE (1), ..._THREE_MINUTES (2), … ..._ONE_MONTH (15) |
SWAP_ROLE_TAKERwas renumbered (0 → 3) on 2026-05-03. Re-map if you persisted raw integers.
Init Market
Initialise a new trading market. Idempotent — repeat calls with the same pair return the existing MarketInfo.
Method: InitMarket
| Param | Type | Description |
|---|---|---|
first_currency | OrderbookCurrency | One side of the pair |
other_currency | OrderbookCurrency | The other side |
Response: { market_info?: MarketInfo } — present when the market was successfully initialised; absent if the pair is unsupported.
Get Initialized Markets
List every market initialised on this Hydra App.
Method: GetInitializedMarkets
Params: none.
Response: { markets: MarketInfo[] }.
Get Markets Info
Like GetInitializedMarkets but additionally fetches live MarketInfo (fees, precision, min amounts) for every market.
Method: GetMarketsInfo
Params: none.
Response: { markets: MarketInfo[] }.
MarketInfo:
| Field | Type | Description |
|---|---|---|
base | CurrencyInfo | Base currency + on-chain decimals |
quote | CurrencyInfo | Quote currency + on-chain decimals |
taker_base_fee | DecimalString | List taker fee ratio on the base side — see the note below |
taker_quote_fee | DecimalString | List taker fee ratio on the quote side |
maker_base_fee | DecimalString | List maker fee ratio on the base side. Negative = a rebate paid to the maker |
maker_quote_fee | DecimalString | List maker fee ratio on the quote side |
base_precision | uint32 | Decimal places of precision for base amounts |
quote_precision | uint32 | Decimal places of precision for quote amounts |
min_base_amount | DecimalString | Structural minimum, base side — see below |
min_quote_amount | DecimalString | Structural minimum, quote side |
min_place_base_amount | DecimalString? | (2026-08-15) Advisory placement-floor snapshot, base side — see below |
min_place_quote_amount | DecimalString? | (2026-08-15) Advisory placement-floor snapshot, quote side |
order_min_notional_usd | DecimalString? | (2026-08-15) The pair's USD placement floor. Absent when disabled. |
max_taker_discount | DecimalString? | (2026-09-04) Deepest discount a taker's fee on this market is struck at. 0 = takers pay list here |
max_maker_discount | DecimalString? | (2026-09-04) Deepest discount a maker's rate on this market is struck at. 0 = makers trade at list here |
These are list rates, not yours
MarketInfoadvertises the fee ratios that apply to every client alike. What you are charged is these with your own fee-tier discount struck off. Don't derive that by hand — callGetMarketFeeRates, which returns a market's four ratios already priced for this node in both roles. A discount makes a fee shrink and a rebate deepen, and the naivelisted × (1 − discount)gets the rebate case backwards.The three
optionalfields marked?above are genuinely absent on a server that does not stamp them — distinguish "absent" from"0".
Two different minimums
An order has to clear both floors, and they behave differently:
Structural minimum — min_base_amount / min_quote_amount. The market's grid minimum: the smallest amount a single fill may carry, and the minimum size of an order of any type. It is stable and authoritative. A partially consumed maker whose remainder would fall below it is evicted at match time, which is what guarantees that any resting size ≥ this minimum is fully takeable.
Advisory placement floor — min_place_base_amount / min_place_quote_amount. A snapshot of the hub's USD placement floor (order_min_notional_usd) converted to base/quote units at the hub's last oracle read. It is advisory only — it moves with oracle prices, so treat it as a sizing hint, not a contract. It is absent when the floor is disabled, when a side is unpriceable, or when the serving endpoint does not stamp it (notably the market-list RPC, GetMarketsInfo); size by the structural minimums in that case.
The authoritative placement floor is the rejection, not the snapshot. When an order is refused for being too small, the rejection carries the live
min_place=value. Re-read it from there and refetchGetMarketInfo— see Errors → Placement rejections. Sizing purely off a cachedmin_place_*will intermittently fail as prices move.
Get Market Info
Same shape as GetMarketsInfo, scoped to one (first_currency, other_currency) pair.
Method: GetMarketInfo
Params: { first_currency: OrderbookCurrency, other_currency: OrderbookCurrency }
Response: { market_info?: MarketInfo }.
Get Market Fee Rates
(2026-09-03) The four fee ratios this node trades a market at — the market's published rates with this node's own discounts already struck off, each role at its own figure. Read from the same market copy the node's own estimates price against.
Method: GetMarketFeeRates
Params: { first_currency: OrderbookCurrency, other_currency: OrderbookCurrency }
Response: { fee_rates?: MarketFeeRates } — absent when the pair is not a market.
MarketFeeRates:
| Field | Type | Description |
|---|---|---|
taker_base_fee | DecimalString | Charged on the base this node receives when it takes liquidity |
taker_quote_fee | DecimalString | Charged on the quote this node receives when it takes liquidity |
maker_base_fee | DecimalString | Charged on the base this node receives when one of its resting orders fills. Negative = a rebate paid to this node |
maker_quote_fee | DecimalString | Charged on the quote this node receives when one of its resting orders fills |
taker_discount | DecimalString | The discount the taker rates above were struck at, after bounding by MarketInfo.max_taker_discount. 0 when this node holds no rung or the market honours none |
maker_discount | DecimalString | The discount the maker rates were struck at, after bounding by MarketInfo.max_maker_discount |
This is a snapshot. Your discounts move as you settle volume and as the operator edits the ladders, and a fill is priced at whatever you hold when it fills. Refresh on a
fee_discount_updateevent rather than caching indefinitely.
Get Currencies
(2026-09-02) The orderbook operator's asset allowlist — every currency it trades, with its ticker, class and lifecycle state.
Method: GetCurrencies
Params: none.
Response: { currencies: ListedCurrency[] } — ordered by network, then ticker.
ListedCurrency:
| Field | Type | Description |
|---|---|---|
currency | OrderbookCurrency | The currency itself |
ticker | string? | The currency's identity on the orderbook — what fee rules and holding ladders are keyed on. Absent for a currency found in a persisted market but no longer listed |
class | CurrencyClass | CURRENCY_CLASS_STANDARD (1) or CURRENCY_CLASS_STABLECOIN (2) |
state | CurrencyState | ..._ENABLED (1), ..._CANCEL_ONLY (2), ..._FROZEN (3) |
min_notional_usd | DecimalString? | This currency's own USD placement floor. Policy input, not the floor an order is checked against |
min_native_amount | DecimalString? | A floor in the currency's own units, always in effect |
The same ticker on two networks is one asset bridged across chains — that is the point of the ticker, and it is deliberately distinct from the symbol the chain reports. A holding ladder for
HDNcounts your HDN wherever it lives.
Lifecycle states:
| State | New orders | Cancels & in-flight settlement | Swap routing |
|---|---|---|---|
ENABLED | ✅ | ✅ | ✅ |
CANCEL_ONLY | ❌ | ✅ | ✅ |
FROZEN | ❌ | ✅ | ❌ |
Both enums reserve their zero value and are never sent. JSON drops a default-valued field, so a real variant at
0would be indistinguishable from an absent one — treatCURRENCY_CLASS_UNSPECIFIED/CURRENCY_STATE_UNSPECIFIEDas a conversion error, not a default. A currency with notickeris alwaysCANCEL_ONLYand carriesSTANDARD(having no configured class) plus none of the floors.
Currency floors, most specific first: the currency's own min_notional_usd wins over the network's, which wins over the orderbook-wide one. min_native_amount is taken alongside the USD floor and binds whenever it is the larger of the two — so a market keeps a minimum even when the oracle is absent or wrong. To size an actual order, read the market's min_base_amount / min_place_base_amount; these are the policy inputs behind them.
Fee discounts
(2026-09-02, reshaped 2026-09-04) The operator runs up to two kinds of discount ladder, and what a fill costs you depends on which rungs you hold. Three RPCs cover it:
| RPC | Answers |
|---|---|
GetFeeDiscountPrograms | What ladders exist, as configured |
GetFeeDiscount | Where you stand on them |
GetMarketFeeRates | What one market costs you, both roles, already priced |
The arithmeticA discount is a fraction applied to your own rate in that role, in your favor:
effective = listed − discount × |listed|Applying it to a signed rate is the whole trick: a positive rate (a fee) shrinks, a negative one (a rebate) deepens. The intuitive
listed × (1 − discount)is only correct for fees — on a maker rebate it moves the number the wrong way.A discount never touches a counterparty's rate, and each market bounds each role's discount by its own published maximum (
MarketInfo.max_taker_discount/max_maker_discount). Your volume and holding rungs sum.
Get Fee Discount Programs
The ladders the operator runs, as configured. An absent volume or an empty holdings means that program does not run.
Method: GetFeeDiscountPrograms
Params: none.
Response:
| Field | Type | Description |
|---|---|---|
volume | VolumeDiscountProgram? | The volume ladder |
holdings | HoldingDiscountProgram[] | One entry per holding ladder |
max_taker_discount | DecimalString? | Operator's default ceiling on a market's taker discount. Absent = only the ladders' own maxima bound it |
max_maker_discount | DecimalString? | Same for the maker discount |
A market's own terms may replace either ceiling for that market — MarketInfo.max_taker_discount / max_maker_discount is the result.
VolumeDiscountProgram — rolling weighted settled USD volume buys a rung: { window_days: uint32, tiers: VolumeDiscountTier[] } (ascending).
VolumeDiscountTier — { from_volume_usd, maker_discount, taker_discount }; from_volume_usd is an inclusive threshold.
HoldingDiscountProgram — holding an asset above a threshold buys a rung: { ticker, currencies: OrderbookCurrency[], tiers: HoldingDiscountTier[] }. currencies is exactly the set whose balances count as holding the asset, summed across them.
HoldingDiscountTier — { from_amount, maker_discount, taker_discount }, in the asset's native units, inclusive.
Get Fee Discount
Your own standing on those programs, and the two figures your fills are priced at.
Method: GetFeeDiscount
Params: none.
Response: { standing: FeeDiscountStanding }
FeeDiscountStanding is a oneof — exactly one is set:
| Variant | Meaning |
|---|---|
no_program | The operator runs no discount program: every fill is list price |
excluded | This node is excluded: list price in both roles, and its settled volume earns nothing |
active | ActiveFeeDiscount — your standing on the programs that run |
ActiveFeeDiscount:
| Field | Type | Description |
|---|---|---|
taker_discount | DecimalString | The taker columns of your volume rung and every holding rung, summed — what your taker fills are priced at on a market with no ceiling |
maker_discount | DecimalString | The same for your maker fills |
volume | VolumeDiscountStanding? | Absent when the operator runs no volume ladder |
holdings | HoldingDiscountStanding[] | One per configured holding ladder; empty when none run |
taker_discount/maker_discounthere are figures to show, not to price with. Every market bounds them by its own maximum. To price an order, callGetMarketFeeRates— it returns the market's rates already struck at the bounded figure.
VolumeDiscountStanding: { window_days, settled_volume_usd, tier: uint32, maker_discount, taker_discount, next_tier?: VolumeDiscountTier }. tier is 1-based; 0 is the undiscounted base. next_tier is absent at the top of the ladder. settled_volume_usd counts every settled hop for its taker and its maker alike, at its own notional scaled by its market's volume weight; a self-trade counts once.
HoldingDiscountStanding: { ticker, held_amount, by_currency: CurrencyHolding[], tier: uint32, maker_discount, taker_discount, next_tier?: HoldingDiscountTier, measured_at?: Timestamp }. tier 0 means below the first threshold; measured_at is absent until the first measurement.
CurrencyHolding: { currency, channel_amount, onchain_amount? } — what the hub measured of one counted currency. channel_amount is your own side of your channels with the hub. onchain_amount is the token balance at your node's wallet address, and is absent on networks whose node ids are not wallet addresses (Bitcoin), or when the last read failed with no earlier figure to carry.
Your standing arrives by subscription, not by polling. The hub opens the private DEX stream with your standing and pushes the whole standing again whenever it moves — see
fee_discount_update. Silence means unchanged; the last message received is the one to hold. CallGetFeeDiscountto seed a UI, then follow the stream.
Get Orderbook Balances
The orderbook balances your account currently holds reserved against open orders.
Method: GetOrderbookBalances
Params: none.
Response: { balances: map<string, CurrencyBalance> } — keyed by an opaque currency identifier.
Get Orderbook
Fetch the current snapshot of one orderbook.
Method: GetOrderbook
Params: { base: OrderbookCurrency, quote: OrderbookCurrency }
Response: { orderbook?: Orderbook }.
Orderbook:
| Field | Type | Description |
|---|---|---|
info | MarketInfo | Market configuration |
orders | map<string, LiquidityPosition> | Active liquidity positions, keyed by order_id |
LiquidityPosition:
| Field | Type | Description |
|---|---|---|
client_pubkey | bytes | Ed25519 public key of the position owner |
side | OrderSide | Buy or sell |
price | DecimalString | Price (quote per base) |
amount | DecimalString | Offered — base amount on sell orders, quote on buy orders |
matched_amount | DecimalString | Amount already matched |
pending_cancel | bool | True if a cancellation is in flight |
Example:
import { GetOrderbookRequest } from './proto/orderbook_pb'
const SIGNET_BTC = { protocol: 1, networkId: '0a03cf40',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000' }
const SEPOLIA_USDC = { protocol: 2, networkId: '11155111',
assetId: 'erc20:0x8cd0da3d001b013336918b8bc4e56d9dda1347e0' }
const req = new GetOrderbookRequest()
req.setBase(SIGNET_BTC)
req.setQuote(SEPOLIA_USDC)
const resp = await orderbook.getOrderbook(req, {})
const ob = resp.getOrderbook()
if (ob) {
ob.getOrdersMap().forEach((pos, orderId) => {
console.log(orderId, pos.getSide(), pos.getPrice()?.getValue(), pos.getAmount()?.getValue())
})
}
Estimate Order
Dry-run an order — see the matching it would produce without actually creating it.
Method: EstimateOrder
Params: { order_variant: OrderVariant } — same OrderVariant shape as CreateOrder.
Response: { order_match?: OrderMatch }. Absent when no matching is possible.
Create Order
Place an order on the orderbook.
Method: CreateOrder
| Param | Type | Required | Description |
|---|---|---|---|
order_variant | OrderVariant | YES | Exactly one of LimitOrder / MarketOrder / SwapOrder |
client_order_id | string | NO | Added 2026-05-03. Your own identifier, max 64 chars. See idempotency note below. |
Response: { order_id: string }.
Idempotent order creation. When you set
client_order_idand it's unique among your open orders, the orderbook stores it on the order and returns the originalorder_idon any retry with the sameclient_order_id. A network blip orDEADLINE_EXCEEDEDretry won't double-place the order. See Errors → Idempotency.
⚠️AddLiquiditywas removed fromCreateOrder(2026-06-11)The
add_liquidityrequest variant — the range-basedmin_buy_price/mid_price/max_sell_price/remove_on_fillform — no longer exists.OrderVariantnow creates onlylimit_order,market_order, orswap_order.Provide maker / passive liquidity by placing limit orders (one or more
LimitOrders at your chosen prices) — a resting limit order is a maker order and pays maker fees when filled. TheLiquidityOrdermessage andORDER_TYPE_LIQUIDITYstill exist as the persisted / returned form (PairOrder.liquidity_order) for positions created that way, so you can still read, hold, and cancel pre-existing liquidity orders — you just can't create new ones through this call.
OrderVariant — the three variants
Exactly one variant must be set. To settle a leg on-chain instead of through a channel, see On-chain settlement below.
LimitOrder — fixed price, take it or wait (and the maker / liquidity path)
| Field | Type | Description |
|---|---|---|
base / quote | OrderbookCurrency | Pair |
side | OrderSide | ORDER_SIDE_BUY or ORDER_SIDE_SELL |
price | DecimalString | Quote per base |
amount | OrderAmount | Base or quote denomination |
MarketOrder — fill at the best available price
| Field | Type | Description |
|---|---|---|
base / quote | OrderbookCurrency | Pair |
amount | OrderAmount | Base or quote denomination |
side | OrderSide | ORDER_SIDE_BUY or ORDER_SIDE_SELL |
SwapOrder — multi-hop cross-currency swap
| Field | Type | Description |
|---|---|---|
from_currency | OrderbookCurrency | Source currency |
currency_path | OrderbookCurrency[] | Intermediate hop currencies (may be empty) |
to_currency | OrderbookCurrency | Destination currency |
amount | SwapAmount | { from: { amount } } or { to: { amount } } |
On-chain settlement
Added 2026-06-11 / 2026-06-15.
By default every swap leg settles through a payment channel. A taker order (market_order / swap_order) can opt individual legs onto on-chain HTLC settlement instead, so you can trade a currency you hold on-chain without first opening a channel for it. This is signed into the order as an OrderSettlement (from currency.proto):
| Field | Type | Description |
|---|---|---|
sending | LegSettlement | How you send to the hub: CHANNEL (0, default), ONCHAIN (1), or CHANNEL_OR_ONCHAIN (2, orderbook picks — prefers channel) |
receiving | LegSettlement | How you receive from the hub (same values) |
route_filter | uint32 (optional) | Taker-only route-wide method bitmask (bit 0 = Channel, bit 1 = On-chain). Absent = no constraint. Must be absent on a resting maker order. |
Absent settlement ⇒ channel on both legs (the previous behavior). On-chain is valid only for taker orders — resting maker orders are channel-only. The on-chain refund/claim addresses are not declared here; the node supplies them later at the orderbook's PrepareSwap, and the per-hop details surface on the matched-order route (SwapHop.sending_onchain / receiving_onchain) and as simple-swap milestones. See the channel-vs-on-chain settlement model for the full picture.
Example — market sell 0.0005 BTC for USDC:
import { CreateOrderRequest, OrderVariant, OrderSide, OrderAmount } from './proto/orderbook_pb'
const market = new OrderVariant.MarketOrder()
market.setBase(SIGNET_BTC)
market.setQuote(SEPOLIA_USDC)
market.setSide(OrderSide.ORDER_SIDE_SELL)
const amt = new OrderAmount()
amt.setBase({ amount: { value: '0.0005' } })
market.setAmount(amt)
const variant = new OrderVariant()
variant.setMarketOrder(market)
const req = new CreateOrderRequest()
req.setOrderVariant(variant)
req.setClientOrderId('bot:bid-2026-06-08:0001') // idempotent retry
const resp = await orderbook.createOrder(req, {})
console.log('order id:', resp.getOrderId())
Cancel Order
Method: CancelOrder
Params: { order_id: string }
Response: { removed: bool } — true when the order was resting and has been taken off the book.
removed: falseis a successful call, not an error. It means there was nothing to cancel: the order had already filled, already been cancelled, or never existed. Treat it as "the order is not on the book", which is the state you were asking for either way.
Cancel All Orders
Cancel every open order belonging to the caller.
Method: CancelAllOrders
Params: none.
Response: empty.
Get Order
Method: GetOrder
Params: { order_id: string }
Response: { order?: Order }. Returns empty when the order is not open (a completed order whose idempotency entry has been pruned will not be found).
Order — the persisted form
The Order message is a oneof over PairOrder (limit / market / liquidity) and SwapOrder (cross-currency), plus the client_order_id you supplied:
Order {
oneof order {
PairOrder pair_order = 1; // limit_order | market_order | liquidity_order
SwapOrder swap_order = 2;
}
optional string client_order_id = 3;
}
PairOrderis aoneofofLiquidityOrder(passive provision),LimitOrder(fixed price), orMarketOrder(best-available).SwapOrdercarriesfrom_currency/currency_path[]/to_currencyand detailed fill / fee accounting.
Field-level shapes are in orderbook.proto — they're long, exhaustive, and best read there rather than mirrored here.
⚠️LimitOrder(and friends) — same name, different shape on request vs responseThe proto reuses the names
LimitOrder/MarketOrder/SwapOrderfor two distinct messages:
- The request form is
OrderVariant.LimitOrder(nested underOrderVariant) — it carries the order spec you submit toCreateOrder/EstimateOrder.- The response / persisted form is the top-level
LimitOrder— it carries the order as it lives in the orderbook, withcreated_at,remaining_amount, and avariant: OrderSideVariantfield instead of a flatOrderSide side.The two have non-trivially different fields. In particular, the response form's
variantis itself aoneof(OrderSideVariant) ofBuy { bought_base_amount, sold_quote_amount, paid_base_fee }orSell { sold_base_amount, bought_quote_amount, paid_quote_fee }. A responseLimitOrderdoes not carry a flatside: OrderSidefield —WhichOneof('side')onvariantis the discriminator, and the fill / fee accounting fields differ per side.In practice:
- When writing an order, you reference the nested
OrderVariant.LimitOrder(see Create Order → OrderVariant).- When reading an order back, you reference the top-level
LimitOrder(this section).- Same applies to
MarketOrderandSwapOrder.Some clients (e.g. when generated Python imports both into one namespace) will alias-collide here; rename or scope the imports to keep the two distinct.
Get Order By Client Id
Added 2026-05-03. Pair with idempotent order creation.
Method: GetOrderByClientId
Params: { client_order_id: string }
Response: { order?: Order }. Empty when no open order matches.
Get Own Orders
The caller's open orders for one pair.
Method: GetOwnOrders
Params: { base: OrderbookCurrency, quote: OrderbookCurrency }
Response: { orders: map<string, Order> } — keyed by order_id.
Get All Own Orders
The caller's open orders across every pair.
Method: GetAllOwnOrders
Params: none.
Response: { orders: map<string, Order> } — keyed by order_id.
Get Trade History
Public trade history for one market — anyone trading the pair, paginated.
Method: GetTradeHistory
Params: { base: OrderbookCurrency, quote: OrderbookCurrency, pagination: PaginationRequest }
Response: { trade_history: Trade[], pagination: PaginationResponse }, newest first.
The field is
trade_history, nottrades— only the caller's own trade calls (GetPairMarketTrades,GetPairSwapTrades) usetrades.
Trade:
| Field | Type | Description |
|---|---|---|
taker_order_id | string | The order that crossed the book |
base_amount | DecimalString | Base amount of the fill |
quote_amount | DecimalString | Quote amount of the fill |
price | DecimalString | Pre-fee execution price |
final_price | DecimalString | After-fee effective price |
timestamp | Timestamp | Fill time |
maker_order_side | OrderSide | The maker side of the trade |
Get Pair Market Trades
The caller's market trades for one pair.
Method: GetPairMarketTrades
Params: { base: OrderbookCurrency, quote: OrderbookCurrency, pagination: PaginationRequest }
Response: { trades: ClientMarketTrade[], pagination: PaginationResponse }.
ClientMarketTrade:
| Field | Type | Description |
|---|---|---|
swap_id | string | The DEX swap ID backing the fill |
order_id | string | Caller's order that produced the fill |
base_amount, quote_amount | DecimalString | Filled volumes |
base_fee, quote_fee | DecimalString | Fees paid |
price, final_price | DecimalString | Pre- and after-fee prices |
timestamp | Timestamp | Fill time |
order_side | OrderSide | Side of the caller's order |
order_type | OrderType | ORDER_TYPE_LIMIT / ORDER_TYPE_MARKET / ORDER_TYPE_LIQUIDITY |
Get All Market Trades
The caller's market trades across every pair. Same response shape as GetPairMarketTrades.
Method: GetAllMarketTrades
Params: { pagination: PaginationRequest }
Response: { trades: ClientMarketTrade[], pagination: PaginationResponse }.
Get Pair Swap Trades
The caller's multi-currency swap trades for one (from, to) pair.
Method: GetPairSwapTrades
Params: { from_currency: OrderbookCurrency, to_currency: OrderbookCurrency, pagination: PaginationRequest }
Response: { trades: ClientSwapTrade[], pagination: PaginationResponse }.
ClientSwapTrade:
| Field | Type | Description |
|---|---|---|
swap_id | string | DEX swap ID |
order_id | string | Caller's order that produced the swap |
from_currency_amount | DecimalString | Amount sent in the source currency |
to_currency_amount | DecimalString | Amount received in the destination currency |
to_currency_fee | DecimalString | Fee paid (in destination currency) |
timestamp | Timestamp | Fill time |
Get All Swap Trades
The caller's swap trades across every (from, to) pair. Same shape as GetPairSwapTrades.
Method: GetAllSwapTrades
Params: { pagination: PaginationRequest }
Subscribe Market Events
Public market data for one pair — orderbook deltas, trades, daily stats, candlesticks. Server-streaming.
Method: SubscribeMarketEvents
Params: { base: OrderbookCurrency, quote: OrderbookCurrency }
Stream of MarketEvent:
MarketEvent {
oneof update {
bool is_synced = 1;
OrderbookUpdate orderbook_update = 2; // {updated_orders, removed_orders}
Trade trade_update = 3; // public Trade
MarketDailyStats daily_stats_update = 4;
CandlestickUpdate candlestick_update = 5;
}
}
Subscribe before you act. If you place an order before the stream attaches, you can miss the fill notification. See Streaming guide.
Available over gRPC as a server-stream, or over JSON-RPC as a WebSocket subscription (
orderbook_subscribeMarketEvents). A one-shot HTTP POST cannot carry it.
Subscribe DEX Events
Your personal account activity — balance updates, order lifecycle, fills, ongoing swap progress. Server-streaming.
Method: SubscribeDexEvents
Params: none.
Stream of DexEvent:
DexEvent {
google.protobuf.Timestamp timestamp = 1;
oneof update {
bool is_synced = 2;
BalanceUpdate balance_update = 3;
OrderUpdate order_update = 4; // OrderCreated / OrderUpdated / OrderCompleted / OrderCanceled
MatchedOrder order_matched = 5;
SwapUpdate swap_update = 6;
MarketTradeUpdate market_trade_update = 7;
SwapTradeUpdate swap_trade_update = 8;
FeeDiscountStanding fee_discount_update = 9; // 2026-09-03
}
}
MarketTradeUpdate carries a ClientMarketTrade (same shape as GetPairMarketTrades); SwapTradeUpdate carries a ClientSwapTrade. Use these to populate a live "my trades" view.
fee_discount_update (2026-09-03) carries the full FeeDiscountStanding — your fee-discount standing changed, in either role. The hub opens this stream with your standing and pushes the whole standing again whenever it moves, so silence means unchanged and the last message received is the one to hold. Nothing needs to poll GetFeeDiscount; a dropped stream heals by reconnecting into a fresh opening standing. Refresh any displayed rates — and any cached GetMarketFeeRates — when one arrives.
Same two transports as
SubscribeMarketEvents— gRPC server-stream, or theorderbook_subscribeDexEventsWebSocket subscription.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Invalid sending currency: Conversion error | Used assetId: "BTC" or mixed-case ERC20:0xAbC… | Use the canonical forms — see asset_id format |
| Order placed, no fill events | Subscribed to SubscribeMarketEvents (or SubscribeDexEvents) after placing the order | Subscribe at startup, then act |
Two orders placed after a DEADLINE_EXCEEDED retry | CreateOrder is not idempotent without client_order_id | Always set client_order_id on retry-prone paths |
Streaming call returns -32603 Internal error via JSON-RPC | Subscribed over a one-shot HTTP POST, which cannot carry a stream | Open a WebSocket to the same URL and subscribe there — see Subscriptions over WebSocket — or use gRPC |
Bot decodes SwapRole integer 0 as taker | SwapRole was renumbered on 2026-05-03 (TAKER moved 0 → 3) | Re-map persisted integers; or compare against the string form |
| Fees charged don't match what you computed | Derived your fee from MarketInfo's list rates, or applied listed × (1 − discount) to a maker rebate | Call GetMarketFeeRates. The formula is listed − discount × |listed| — signed, so a rebate deepens |
| A maker rebate came out smaller after earning a tier | Same sign bug, in the arithmetic | See the arithmetic |
| Bot reads a fill amount as zero or as the wrong currency | Read OrderAmount.base.amount (or .quote.amount) without checking which variant is set | OrderAmount echoes the denomination you placed the order in — always discriminate on the oneof variant, not on side. Confusing the per-order remaining/unmatched/failed_amount with the public LiquidityPosition.amount (which is base-on-sell / quote-on-buy) leads to the same bug. See the OrderAmount callout |
Buy-side LimitOrder fields look empty in the response | Mixed up the request-form OrderVariant.LimitOrder (has flat side) with the response-form top-level LimitOrder (has variant: OrderSideVariant oneof) | See LimitOrder request vs response — read variant.buy / variant.sell on responses |
See also
- Swap API — direct cross-currency swaps and the auto-channel-setup
SimpleSwapflow - HTLC & Preimage API — on-chain HTLC settlement and the channel-vs-on-chain model
- Events API — full event-payload reference
- Streaming guide — subscribe-before-act, reconnection, dedup
- Common Patterns → Amounts & Decimals
/proto/orderbook.proto— authoritative schema