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, MarketOrder, or AddLiquidity, 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_amountDecimalStringMinimum order amount in base
min_quote_amountDecimalStringMinimum order amount in quote

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 AddLiquidity / 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.

OrderVariant — the four variants

Exactly one variant must be set.

AddLiquidity — maker / passive liquidity provision

FieldTypeDescription
base / quoteOrderbookCurrencyPair
min_buy_priceDecimalStringLower bound — won't buy below this
mid_priceDecimalStringCentre of the range
max_sell_priceDecimalStringUpper bound — won't sell above this
amountOrderAmountTotal volume to provide (base or quote denomination)
remove_on_fillboolIf true, remove the entire position as soon as any part fills

LimitOrder — fixed price, take it or wait

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

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