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

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_feeDecimalStringTaker fee ratio on the base side
taker_quote_feeDecimalStringTaker fee ratio on the quote side
maker_base_feeDecimalStringMaker fee ratio on the base side
maker_quote_feeDecimalStringMaker 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.

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 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: empty.


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: { trades: Trade[], pagination: PaginationResponse }.

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.

Streaming methods are not available over JSON-RPC — use the gRPC interface.


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;
  }
}

MarketTradeUpdate carries a ClientMarketTrade (same shape as GetPairMarketTrades); SwapTradeUpdate carries a ClientSwapTrade. Use these to populate a live "my trades" view.

Same gRPC-only caveat as SubscribeMarketEvents.


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-RPCStreams not supported on JSON-RPCUse gRPC for Subscribe*
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
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