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 aLimitOrder,MarketOrder, orAddLiquidity, 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 | Minimum order amount in base |
min_quote_amount | DecimalString | Minimum 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:
| 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 AddLiquidity / 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.
OrderVariant — the four variants
Exactly one variant must be set.
AddLiquidity — maker / passive liquidity provision
| Field | Type | Description |
|---|---|---|
base / quote | OrderbookCurrency | Pair |
min_buy_price | DecimalString | Lower bound — won't buy below this |
mid_price | DecimalString | Centre of the range |
max_sell_price | DecimalString | Upper bound — won't sell above this |
amount | OrderAmount | Total volume to provide (base or quote denomination) |
remove_on_fill | bool | If true, remove the entire position as soon as any part fills |
LimitOrder — fixed price, take it or wait
| 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 } } |
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 - Events API — full event-payload reference
- Streaming guide — subscribe-before-act, reconnection, dedup
- Common Patterns → Amounts & Decimals
/proto/orderbook.proto— authoritative schema