Skip to main content

Order Book Trading

Harbor's order book lets wallets offer limit-order trading on top of L1-secured deposits and withdrawals. Users deposit to Harbor, then authorize orders, cancels, account reads, and withdrawals with L1-signed JSON messages.

Harbor acts as a delegated executor: it holds per-user trading accounts, verifies each L1 signature, and places orders on the user's behalf.

Chain support

Trading signatures currently support ETH and BTC only. Signed messages on other chains are rejected with signature verification not supported for chain <chain>.

Lifecycle

1
Deposit
Send L1 assets to Harbor's vault — no memo required
2
Sign
Sign JSON messages (create / cancel / withdraw) with your L1 key
3
Trade
The order router validates your signature and places orders on the exchange
4
Withdraw
Sign a withdraw message at any time — funds go back to L1

Account Model

A trading account is a portfolio that can be funded from multiple L1 addresses.

FieldDescription
accountIdOpaque numeric identifier returned in account responses
balancesPer-asset breakdown: amount, amountInOrders, amountInWithdrawals (decimal strings)
signersOne controller plus zero or more deposit_only aliases

controller signers can read balances and sign orders, cancels, link requests, and withdrawals. deposit_only signers can receive deposits into the portfolio but cannot read or mutate it. A single (chain, l1Address) can be active on at most one account.

1. Deposit

Deposits use the standard Harbor Deposit Transactions rules. The memo decides which trading account receives the credit:

MemoResult
emptyCredits the sender's trading account. If this is the first deposit, the sender becomes the controller.
d:<CHAIN>:<L1_ADDRESS>Credits the account controlled by the named EOA. Use this when the depositor is a smart-contract wallet, router, or any address that cannot sign orders.

When a d: deposit targets an existing account, the sender is installed as a deposit_only alias on that account unless it is already bound elsewhere. Existing bindings are not silently rewired; use Linking Extra Addresses for explicit consolidation.

11:12● ● ●
Deposit to Trading
USDTEthereum
10,000.00USDT
0x1f8C6A8e8…1bE7a3D
Send from Wallet
Wallet deposits USDT from the user's L1 address.

1.1 Resolve the inbound address

curl https://api.harbor.xyz/xnode/inbound_addresses

1.2 Broadcast the L1 transfer

  • Native assets — direct L1 transfer to the inbound address, with the memo attached per chain.
  • ERC-20 tokens — call the Router Contract's depositWithExpiry. Do not send ERC-20s with a plain transfer; those tokens bypass deposit crediting.

Harbor returns the account state the next time the wallet calls POST /api/v1/trading/account.

Depositing from smart-contract wallets

The account controller is the only address that can sign trading actions. If the depositor cannot sign, set the deposit memo to d:<CHAIN>:<EOA> naming an EOA the user controls.

2. Auto-Trading With o:

An o: memo lets a user deposit once and immediately rest a limit order without a follow-up signed API call. On fill, Harbor auto-withdraws proceeds to ToAddress. On TTL expiry or cancel-and-withdraw, Harbor refunds to RefundAddress or the depositor.

Use GET /api/v1/limit/quote to build the memo. Do not hand-encode it unless you have to; the endpoint resolves the book, validates TTL and ticks, sizes the order, estimates fees, and returns the exact memo to attach to the deposit.

See Memos for the wire format.

Endpoint · GET /api/v1/limit/quote

fromAmount is always required. Supply exactly one of toAmount or price.

ParamDescription
fromAssetInbound asset, e.g. BTC.BTC or ETH.USDT.
toAssetDesired filled asset. fromAsset + toAsset must map to one Harbor CLOB book.
fromAmountSource amount in 1e8 chain precision.
toAmountDesired destination amount net of liquidity + affiliate fees. Excludes outbound gas.
priceLimit price in 1e8 chain precision. Requires priceAsset.
priceAssetAsset the price is quoted in; must be the resolved market's base or quote asset.
destinationL1 address for filled proceeds. Must be on toAsset's chain.
refundAddressOptional controller and refund destination. Must be on the source chain.
ttlSecondsOrder TTL. Default bounds are 60 seconds through 365 days.
affiliates / affiliatesBpsOptional repeated affiliate shortcodes and matching bps.

Example

curl "https://api.harbor.xyz/api/v1/limit/quote?\
fromAsset=BTC.BTC&toAsset=ETH.USDT&fromAmount=10000000&\
price=6700000000000&priceAsset=ETH.USDT&ttlSeconds=3600&\
destination=0x56781111...&refundAddress=bc1q..."

Response Shape

{
"inboundAddress": "bc1q6cs...gp8",
"memo": "o:ETH.USDT:0x56781111.../bc1q...:6700000000000/3600::",
"expiry": "1700000120",
"fees": {
"asset": "ETH.USDT",
"affiliate": "0",
"outbound": "2500000",
"liquidity": "3350000",
"total": "5850000"
},
"symbol": "BTC-USDT",
"side": "sell",
"price": "6700000000000",
"qty": "10000000",
"amountHeld": "10000000",
"assetHeld": "BTC.BTC"
}

Important fields:

  • memo is the exact string to attach to the L1 deposit.
  • qty is the tick-aligned base quantity that will rest on the book.
  • amountHeld / assetHeld describe the reserved balance. Any excess stays spendable in the trading account.
  • fees.affiliate and fees.liquidity are realized on fill. fees.outbound is charged later by the withdraw processor.
  • expiry is Unix seconds. Broadcast before this timestamp.

If an o: order cannot be placed, Harbor queues an automatic refund to the depositor or refundAddress. If refund queueing fails, the balance remains in the trading account for signed withdrawal.

3. Signed Message Protocol

All mutating calls and private reads carry a signed envelope inside a message wrapper:

{
"message": {
"action": "create_order",
"payloadJson": "{\"market\":\"BTC-USDT\",\"price\":\"67000.00\",\"quantity\":\"0.1\",\"side\":\"buy\",\"type\":\"limit\"}",
"nonce": 12346,
"expiry": 1700000120,
"l1Address": "0x742d35Cc6634C0532925a3b844Bc9e7595f8FbDc",
"chain": "ETH",
"signature": "0xabcd..."
}
}
FieldDescription
actionOne of create_order, cancel_order, cancel_and_withdraw, withdraw, get_account, list_orders, link_alias_initiate, revoke_alias, list_link_challenges.
payloadJsonCanonical JSON string for the action payload. It must be a string, not a nested object.
noncePositive int64. Writes are monotonic per controller. Reads require a positive nonce but do not persist it.
expiryUnix seconds. The server rejects expiry <= now; use a short window such as now + 60..120s.
l1AddressController signer address. deposit_only signers cannot call private reads or writes.
chainETH or BTC.
signatureETH EIP-712 signature or BTC BIP-137 signature over the action envelope.

Signing rules:

  • ETH — use eth_signTypedData_v4. Domain: { name: "harbor.orderbook.trading", version: "1" }; chainId and verifyingContract are omitted. Each action has its own primary type, e.g. CreateOrder, CancelOrder, Withdraw.
  • BTC — sign canonical JSON with the Bitcoin Signed Message convention. The BTC preimage includes action, chain, domain, expiry, l1_address, nonce, and payload, with keys sorted recursively and no whitespace.
  • payloadJson is the inner payload canonicalized on its own.

Nonce rules:

  • Writes consume the nonce after signature / role checks and action-specific validation. Some failed writes still burn the nonce; after any failed write, re-sign with a larger nonce.
  • Reads do not consume nonces. Freshness comes from expiry.

Signing Examples

Both examples place the same order. signature is the wallet output over the shown signing input.

ETH eth_signTypedData_v4

Sign this typed data:

{
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" }
],
"CreateOrder": [
{ "name": "action", "type": "string" },
{ "name": "chain", "type": "string" },
{ "name": "l1Address", "type": "string" },
{ "name": "nonce", "type": "uint256" },
{ "name": "expiry", "type": "uint256" },
{ "name": "payload", "type": "CreateOrderPayload" }
],
"CreateOrderPayload": [
{ "name": "market", "type": "string" },
{ "name": "side", "type": "string" },
{ "name": "type", "type": "string" },
{ "name": "timeInForce", "type": "string" },
{ "name": "price", "type": "string" },
{ "name": "quantity", "type": "string" },
{ "name": "replacesClientOrderId", "type": "string" }
]
},
"primaryType": "CreateOrder",
"domain": {
"name": "harbor.orderbook.trading",
"version": "1"
},
"message": {
"action": "create_order",
"chain": "ETH",
"l1Address": "0x742d35Cc6634C0532925a3b844Bc9e7595f8FbDc",
"nonce": 12346,
"expiry": 1700000120,
"payload": {
"market": "BTC-USDT",
"side": "buy",
"type": "limit",
"timeInForce": "aon",
"price": "67000.00",
"quantity": "0.1",
"replacesClientOrderId": ""
}
}
}

Submit the signed envelope:

{
"message": {
"action": "create_order",
"payloadJson": "{\"market\":\"BTC-USDT\",\"price\":\"67000.00\",\"quantity\":\"0.1\",\"replacesClientOrderId\":\"\",\"side\":\"buy\",\"timeInForce\":\"aon\",\"type\":\"limit\"}",
"nonce": 12346,
"expiry": 1700000120,
"l1Address": "0x742d35Cc6634C0532925a3b844Bc9e7595f8FbDc",
"chain": "ETH",
"signature": "0x..."
}
}

BTC Bitcoin Signed Message

Sign this canonical JSON string with the Bitcoin Signed Message convention:

{
"action": "create_order",
"chain": "BTC",
"domain": "harbor.orderbook.trading",
"expiry": 1700000120,
"l1_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"nonce": 12346,
"payload": {
"market": "BTC-USDT",
"price": "67000.00",
"quantity": "0.1",
"side": "buy",
"timeInForce": "aon",
"type": "limit"
}
}

Submit the signed envelope:

{
"message": {
"action": "create_order",
"payloadJson": "{\"market\":\"BTC-USDT\",\"price\":\"67000.00\",\"quantity\":\"0.1\",\"side\":\"buy\",\"timeInForce\":\"aon\",\"type\":\"limit\"}",
"nonce": 12346,
"expiry": 1700000120,
"l1Address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"chain": "BTC",
"signature": "H..."
}
}

4. Create Order

11:12● ● ●
Place Limit Order
Market
BTC-USDT
Mid: $67,890.50
Buy
Limit · AON
67,000.00USDT
0.10000000BTC
Order total6,700.00 USDT
Available (trading)12,000.00 USDT
Nonce12346
Review & Sign
1. User builds the order in-wallet.
11:12● ● ●
Sign Order
6,700 USDT
0.1 BTC
ActionCreate Order
MarketBTC-USDT
Side / TypeBuy · Limit
Price67,000.00
Quantity0.10000000 BTC
Nonce12346
Sign & Submit
2. Wallet shows the labelled envelope fields.
11:12● ● ●
Submitting Order
Placing order…
Harbor is validating your signature
Signed message built
POST /api/v1/trading/orders
Signature verified · nonce accepted
Funds held · order placed on exchange
Order resting on book
3. Signature verified → funds locked → resting on book.

Endpoint · POST /api/v1/trading/orders

Payload inside payloadJson:

{
"market": "BTC-USDT",
"side": "buy",
"type": "limit",
"timeInForce": "aon",
"price": "67000.00",
"quantity": "0.1"
}

Only type: "limit" and timeInForce: "aon" are accepted. quantity is base-asset quantity; price is quote-per-base. On success, Harbor reserves quote for buys or base for sells and returns a TradingOrder.

Use clientOrderId (trd-<uuid>) for cancels. The numeric id is opaque.

{
"order": {
"id": "42",
"clientOrderId": "trd-7f3ab912-9d01-4cb9-bb1a-9e0e4eeaacbb",
"symbol": "BTC-USDT",
"side": "buy",
"type": "limit",
"price": "67000.0",
"qty": "0.1",
"filledQty": "0.0",
"status": "open"
}
}

5. Cancel And Withdraw

11:12● ● ●
Main Wallet
$26,392.67
+$142.18 (0.54%)
PortfolioOrdersHistory
BTC-USDTBUYlimit · AON
+0.10000000 BTC@$67,000.00
Total: 6,700.00 USDT · Open
11:12:04
BTC-USDTSELLlimit · AON
−0.02500000 BTC@$68,500.00
Total: 1,712.50 USDT · Open
10:58:41
Open orders in the wallet.
11:12● ● ●
Cancel Order
BTC-USDTBUYlimit · AON
+0.10000000 BTC@$67,000.00
order_id trd-7f3a…b912
ActionCancel Order
order_idtrd-7f3a…b912
Nonce12347
Unlocks6,700.00 USDT
Sign & Cancel
Cancel signs against the clientOrderId.

Cancel Order

Endpoint · POST /api/v1/trading/orders/cancel

Payload:

{ "orderId": "trd-7f3ab912-9d01-4cb9-bb1a-9e0e4eeaacbb" }

Response: { "cancelled": true }

orderId is the clientOrderId, not the numeric id. Cancelled funds return to the trading account's withdrawable balance. To move them back to L1, call withdraw.

Cancel And Withdraw

Endpoint · POST /api/v1/trading/orders/cancel_and_withdraw

For o: orders only. Cancels the order and queues a refund outbound to the order's RefundAddress.

Payload:

{ "orderId": "trd-7f3ab912-9d01-4cb9-bb1a-9e0e4eeaacbb" }

Response:

{
"cancelled": true,
"withdrawId": "42",
"clientIdPrefix": "trade-refund-100-1700000000000000000"
}

Plain signed orders are rejected with order was not opened via o: memo; use cancel_order + withdraw.

6. Withdraw

11:12● ● ●
Withdraw
Asset
USDT
Available: 12,000.00
Trading
0x742d35Cc…95f8FbDcYour wallet
1,000.00USDT
ActionWithdraw
Network fee~5.00 USDT
Nonce12348
You receive995.00 USDT
Sign & Withdraw
User selects asset, destination, and amount.
11:12● ● ●
Withdrawing
Exchange withdrawal
Queued for L1 broadcast via vault signing pipeline
Signed message submitted
POST /api/v1/trading/withdraw
Balance debited · exchange withdraw queued
Vault signing L1 transaction
L1 broadcast · destination credited
Signed envelope queued → exchange withdrawal → L1 broadcast.

Endpoint · POST /api/v1/trading/withdraw

Payload:

{
"asset": "ETH.USDT",
"amount": "1000.00",
"destination": "0x56781111..."
}

Response:

{
"withdrawId": "128",
"clientIdPrefix": "trade-17-1744848724000000000"
}

The destination can differ from the signer, but it must be valid for the withdrawn asset's chain. Status, fee, and ETA are not returned; watch amountInWithdrawals via get_account.

7. Account And Orders

11:12● ● ●
Main Wallet
$26,392.67
+$142.18 (0.54%)
Send
Receive
Trade
PortfolioOrdersHistory
Holdings (Wallet)
BTC
Bitcoin
0.15000000
$10,183.58
USDT
Ethereum
4,250.00
$4,250.00
Trading Account (on Harbor)
USDT (trading)
8,000.00 in orders
12,000.00
$12,000.00
Trading account view returned by /api/v1/trading/account.

Private reads use the same signed envelope format as writes. The server verifies expiry, requires a positive nonce, and does not persist read nonces.

ActionPathReturns
get_accountPOST /api/v1/trading/accountBalances, pending / open orders, active signers
list_ordersPOST /api/v1/trading/orders/listRecent orders for the account

get_account payload: {}

{
"accountId": 17,
"balances": [
{
"asset": "ETH.USDT",
"amount": "20000.0",
"amountInOrders": "8000.0",
"amountInWithdrawals": "0.0"
}
],
"openOrders": [],
"signers": [
{
"id": 1,
"chain": "ETH",
"l1Address": "0x742d...F8FbDc",
"role": "controller",
"linkedVia": "first_deposit",
"linkedAt": 1744891200
}
]
}

list_orders payload: { "limit": 50 }. Status values are pending, open, partially_filled, filled, cancelled, and failed.

8. Linking Extra Addresses

Use linking when a controller wants to pre-authorize another deposit address or consolidate an existing per-address account.

  1. Controller signs link_alias_initiate:
{
"newChain": "BTC",
"newL1Address": "bc1q...",
"role": "deposit_only"
}

Response:

{
"challengeId": "e6b7...9f",
"digest": "AQID...base64",
"expiresAt": 1700001234
}
  1. The new address signs the decoded digest, then anyone can relay:
{
"challengeId": "e6b7...9f",
"signature": "..."
}

Endpoint · POST /api/v1/trading/link/complete

Response:

{
"accountId": 42,
"signerId": 7,
"mergedFromAccountId": 13
}

role is restricted to deposit_only. Merging is rejected if the source account has open orders (pending | open) or pending withdrawals (queued | processing | submitted).

Other link endpoints:

ActionPathPayload
list_link_challengesPOST /api/v1/trading/link/challenges{ "limit": 50 }
revoke_aliasPOST /api/v1/trading/link/revoke{ "signerId": 7 }

Controllers cannot be revoked through revoke_alias.

9. Public Order Feed

GET /api/v1/trading/limitOrders is public and unauthenticated. It returns limit orders across accounts, newest first.

ParamDescription
statusOptional. One of open, partially_filled, cancelled, filled, pending, failed.
addressOptional. Matches deposit sender, bound account addresses, refund address, or destination address.
limitOptional. Defaults to 50; capped at 100.
offsetOptional. 0-based pagination offset.
curl "https://api.harbor.xyz/api/v1/trading/limitOrders?status=open&address=0x742d...F8FbDc&limit=20"

Response fields use exchange precision for decimal amounts and Unix nanoseconds for createdAt / updatedAt. deposit is populated for o: auto-deposit orders when the inbound can be resolved. fees contains fill-time order fees and outbound-time withdraw fees.

Safety Notes

  • Store a monotonic write nonce per controller account. UnixNano() is a safe default.
  • After any failed write, re-sign with a larger nonce.
  • Keep expiry short, usually 60-120 seconds.
  • Signed orders, cancels, and withdrawals have no on-chain transaction cost. Fills and withdrawals still incur normal trading and outbound fees.