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 Orderbook Balances
- Get Orderbook
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 | Taker fee ratio on the base side |
taker_quote_fee | DecimalString | Taker fee ratio on the quote side |
maker_base_fee | DecimalString | Maker fee ratio on the base side |
maker_quote_fee | DecimalString | 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. |
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 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: 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;
}
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: { trades: Trade[], pagination: PaginationResponse }.
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.
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
| 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 | Streams not supported on JSON-RPC | Use gRPC for Subscribe* |
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 |
| 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