Api

Orderbook API

Markets, orders, trades, streaming market and DEX events

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

Fees & discounts

Orders

Trades (public + per-account)

Streams


Shared types

OrderbookCurrency

Identifies an asset for orderbook RPCs (note: distinct from Network — flat shape).

FieldTypeNotes
protocolProtocol enumPROTOCOL_BITCOIN or PROTOCOL_EVM
network_idstringMagic bytes (Bitcoin) or decimal chain ID (EVM)
asset_idstringSee asset_id format below

asset_id format

Verified live against a running Hydra App; do not use the ticker.

AssetForm
Native asset (BTC, ETH, …)Zero-padded 32-byte hex: 0x0000000000000000000000000000000000000000000000000000000000000000
ERC-20 tokenerc20:<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:

VariantPayloadMeaning
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.

⚠️ OrderAmount echoes the denomination you placed the order in

OrderAmount is a oneof precisely so it can carry either denomination. The side of an order (buy/sell) does not determine it. When you create a LimitOrder or MarketOrder, you choose base or quote freely — a buy can be sized in base or quote, and so can a sell. On response orders, remaining_amount, unmatched_amount, and failed_amount come 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 oneof variant actually set — never on the side:

  • Python: WhichOneof('amount') (hasattr is useless on oneofs — 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.amount in the public orderbook (GetOrderbook). That field is a single scalar reporting offered liquidity, so it genuinely is base-on-sell / quote-on-buy. The remaining_amount / unmatched_amount / failed_amount on 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:

EnumValues
OrderSideORDER_SIDE_UNSPECIFIED (0), ORDER_SIDE_BUY (1), ORDER_SIDE_SELL (2)
OrderTypeORDER_TYPE_UNSPECIFIED (0), ORDER_TYPE_LIMIT (1), ORDER_TYPE_LIQUIDITY (2), ORDER_TYPE_MARKET (3), ORDER_TYPE_SWAP (4)
AmountSideAMOUNT_SIDE_UNSPECIFIED (0), AMOUNT_SIDE_BASE (1), AMOUNT_SIDE_QUOTE (2)
SwapRoleSWAP_ROLE_UNSPECIFIED (0), SWAP_ROLE_LAST_MAKER (1), SWAP_ROLE_INTERMEDIATE_MAKER (2), SWAP_ROLE_TAKER (3)
SwapStatusSWAP_STATUS_UNSPECIFIED (0) → SWAP_STATUS_SWAP_FAILED (9). See proto for the full list.
CandlestickIntervalCANDLESTICK_INTERVAL_UNSPECIFIED (0), ..._ONE_MINUTE (1), ..._THREE_MINUTES (2), … ..._ONE_MONTH (15)

SWAP_ROLE_TAKER was 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

ParamTypeDescription
first_currencyOrderbookCurrencyOne side of the pair
other_currencyOrderbookCurrencyThe 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:

FieldTypeDescription
baseCurrencyInfoBase currency + on-chain decimals
quoteCurrencyInfoQuote currency + on-chain decimals
taker_base_feeDecimalStringList taker fee ratio on the base side — see the note below
taker_quote_feeDecimalStringList taker fee ratio on the quote side
maker_base_feeDecimalStringList maker fee ratio on the base side. Negative = a rebate paid to the maker
maker_quote_feeDecimalStringList maker fee ratio on the quote side
base_precisionuint32Decimal places of precision for base amounts
quote_precisionuint32Decimal places of precision for quote amounts
min_base_amountDecimalStringStructural minimum, base side — see below
min_quote_amountDecimalStringStructural minimum, quote side
min_place_base_amountDecimalString?(2026-08-15) Advisory placement-floor snapshot, base side — see below
min_place_quote_amountDecimalString?(2026-08-15) Advisory placement-floor snapshot, quote side
order_min_notional_usdDecimalString?(2026-08-15) The pair's USD placement floor. Absent when disabled.
max_taker_discountDecimalString?(2026-09-04) Deepest discount a taker's fee on this market is struck at. 0 = takers pay list here
max_maker_discountDecimalString?(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

MarketInfo advertises 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 — call GetMarketFeeRates, 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 naive listed × (1 − discount) gets the rebate case backwards.

The three optional fields 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 refetch GetMarketInfo — see Errors → Placement rejections. Sizing purely off a cached min_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:

FieldTypeDescription
taker_base_feeDecimalStringCharged on the base this node receives when it takes liquidity
taker_quote_feeDecimalStringCharged on the quote this node receives when it takes liquidity
maker_base_feeDecimalStringCharged on the base this node receives when one of its resting orders fills. Negative = a rebate paid to this node
maker_quote_feeDecimalStringCharged on the quote this node receives when one of its resting orders fills
taker_discountDecimalStringThe 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_discountDecimalStringThe 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_update event 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:

FieldTypeDescription
currencyOrderbookCurrencyThe currency itself
tickerstring?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
classCurrencyClassCURRENCY_CLASS_STANDARD (1) or CURRENCY_CLASS_STABLECOIN (2)
stateCurrencyState..._ENABLED (1), ..._CANCEL_ONLY (2), ..._FROZEN (3)
min_notional_usdDecimalString?This currency's own USD placement floor. Policy input, not the floor an order is checked against
min_native_amountDecimalString?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 HDN counts your HDN wherever it lives.

Lifecycle states:

StateNew ordersCancels & in-flight settlementSwap 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 0 would be indistinguishable from an absent one — treat CURRENCY_CLASS_UNSPECIFIED / CURRENCY_STATE_UNSPECIFIED as a conversion error, not a default. A currency with no ticker is always CANCEL_ONLY and carries STANDARD (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:

RPCAnswers
GetFeeDiscountProgramsWhat ladders exist, as configured
GetFeeDiscountWhere you stand on them
GetMarketFeeRatesWhat one market costs you, both roles, already priced

The arithmetic

A 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:

FieldTypeDescription
volumeVolumeDiscountProgram?The volume ladder
holdingsHoldingDiscountProgram[]One entry per holding ladder
max_taker_discountDecimalString?Operator's default ceiling on a market's taker discount. Absent = only the ladders' own maxima bound it
max_maker_discountDecimalString?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:

VariantMeaning
no_programThe operator runs no discount program: every fill is list price
excludedThis node is excluded: list price in both roles, and its settled volume earns nothing
activeActiveFeeDiscount — your standing on the programs that run

ActiveFeeDiscount:

FieldTypeDescription
taker_discountDecimalStringThe 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_discountDecimalStringThe same for your maker fills
volumeVolumeDiscountStanding?Absent when the operator runs no volume ladder
holdingsHoldingDiscountStanding[]One per configured holding ladder; empty when none run

taker_discount / maker_discount here are figures to show, not to price with. Every market bounds them by its own maximum. To price an order, call GetMarketFeeRates — 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. Call GetFeeDiscount to 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:

FieldTypeDescription
infoMarketInfoMarket configuration
ordersmap<string, LiquidityPosition>Active liquidity positions, keyed by order_id

LiquidityPosition:

FieldTypeDescription
client_pubkeybytesEd25519 public key of the position owner
sideOrderSideBuy or sell
priceDecimalStringPrice (quote per base)
amountDecimalStringOffered — base amount on sell orders, quote on buy orders
matched_amountDecimalStringAmount already matched
pending_cancelboolTrue 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

ParamTypeRequiredDescription
order_variantOrderVariantYESExactly one of LimitOrder / MarketOrder / SwapOrder
client_order_idstringNOAdded 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_id and it's unique among your open orders, the orderbook stores it on the order and returns the original order_id on any retry with the same client_order_id. A network blip or DEADLINE_EXCEEDED retry won't double-place the order. See Errors → Idempotency.

⚠️ AddLiquidity was removed from CreateOrder (2026-06-11)

The add_liquidity request variant — the range-based min_buy_price / mid_price / max_sell_price / remove_on_fill form — no longer exists. OrderVariant now creates only limit_order, market_order, or swap_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. The LiquidityOrder message and ORDER_TYPE_LIQUIDITY still 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)

FieldTypeDescription
base / quoteOrderbookCurrencyPair
sideOrderSideORDER_SIDE_BUY or ORDER_SIDE_SELL
priceDecimalStringQuote per base
amountOrderAmountBase or quote denomination

MarketOrder — fill at the best available price

FieldTypeDescription
base / quoteOrderbookCurrencyPair
amountOrderAmountBase or quote denomination
sideOrderSideORDER_SIDE_BUY or ORDER_SIDE_SELL

SwapOrder — multi-hop cross-currency swap

FieldTypeDescription
from_currencyOrderbookCurrencySource currency
currency_pathOrderbookCurrency[]Intermediate hop currencies (may be empty)
to_currencyOrderbookCurrencyDestination currency
amountSwapAmount{ 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):

FieldTypeDescription
sendingLegSettlementHow you send to the hub: CHANNEL (0, default), ONCHAIN (1), or CHANNEL_OR_ONCHAIN (2, orderbook picks — prefers channel)
receivingLegSettlementHow you receive from the hub (same values)
route_filteruint32 (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: false is 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;
}
  • PairOrder is a oneof of LiquidityOrder (passive provision), LimitOrder (fixed price), or MarketOrder (best-available).
  • SwapOrder carries from_currency / currency_path[] / to_currency and 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 response

The proto reuses the names LimitOrder / MarketOrder / SwapOrder for two distinct messages:

  • The request form is OrderVariant.LimitOrder (nested under OrderVariant) — it carries the order spec you submit to CreateOrder / EstimateOrder.
  • The response / persisted form is the top-level LimitOrder — it carries the order as it lives in the orderbook, with created_at, remaining_amount, and a variant: OrderSideVariant field instead of a flat OrderSide side.

The two have non-trivially different fields. In particular, the response form's variant is itself a oneof (OrderSideVariant) of Buy { bought_base_amount, sold_quote_amount, paid_base_fee } or Sell { sold_base_amount, bought_quote_amount, paid_quote_fee }. A response LimitOrder does not carry a flat side: OrderSide fieldWhichOneof('side') on variant is 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 MarketOrder and SwapOrder.

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, not trades — only the caller's own trade calls (GetPairMarketTrades, GetPairSwapTrades) use trades.

Trade:

FieldTypeDescription
taker_order_idstringThe order that crossed the book
base_amountDecimalStringBase amount of the fill
quote_amountDecimalStringQuote amount of the fill
priceDecimalStringPre-fee execution price
final_priceDecimalStringAfter-fee effective price
timestampTimestampFill time
maker_order_sideOrderSideThe 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:

FieldTypeDescription
swap_idstringThe DEX swap ID backing the fill
order_idstringCaller's order that produced the fill
base_amount, quote_amountDecimalStringFilled volumes
base_fee, quote_feeDecimalStringFees paid
price, final_priceDecimalStringPre- and after-fee prices
timestampTimestampFill time
order_sideOrderSideSide of the caller's order
order_typeOrderTypeORDER_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:

FieldTypeDescription
swap_idstringDEX swap ID
order_idstringCaller's order that produced the swap
from_currency_amountDecimalStringAmount sent in the source currency
to_currency_amountDecimalStringAmount received in the destination currency
to_currency_feeDecimalStringFee paid (in destination currency)
timestampTimestampFill 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 the orderbook_subscribeDexEvents WebSocket subscription.


Common pitfalls

SymptomCauseFix
Invalid sending currency: Conversion errorUsed assetId: "BTC" or mixed-case ERC20:0xAbC…Use the canonical forms — see asset_id format
Order placed, no fill eventsSubscribed to SubscribeMarketEvents (or SubscribeDexEvents) after placing the orderSubscribe at startup, then act
Two orders placed after a DEADLINE_EXCEEDED retryCreateOrder is not idempotent without client_order_idAlways set client_order_id on retry-prone paths
Streaming call returns -32603 Internal error via JSON-RPCSubscribed over a one-shot HTTP POST, which cannot carry a streamOpen a WebSocket to the same URL and subscribe there — see Subscriptions over WebSocket — or use gRPC
Bot decodes SwapRole integer 0 as takerSwapRole 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 computedDerived your fee from MarketInfo's list rates, or applied listed × (1 − discount) to a maker rebateCall GetMarketFeeRates. The formula is listed − discount × |listed| — signed, so a rebate deepens
A maker rebate came out smaller after earning a tierSame sign bug, in the arithmeticSee the arithmetic
Bot reads a fill amount as zero or as the wrong currencyRead OrderAmount.base.amount (or .quote.amount) without checking which variant is setOrderAmount 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 responseMixed 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


Copyright © 2025