Error Codes
This document provides a comprehensive reference for all error codes in the Hydra API.
gRPC Status Codes
Hydra uses standard gRPC status codes. All errors follow this format:
{
"code": 3,
"message": "invalid asset id",
"details": []
}
Common Status Codes
OK (0)
Description: Success - no error
When it occurs: Successful operation
Action: None required
CANCELLED (1)
Description: Operation was cancelled
When it occurs:
- Client cancelled request
- Stream was closed
Action: Retry if needed
UNKNOWN (2)
Description: Unknown error
When it occurs:
- Unexpected server error
- Unhandled exception
Action: Check request parameters, contact support if persists
INVALID_ARGUMENT (3)
Description: Client specified an invalid argument
Common causes:
- Invalid asset ID format
- Invalid network parameters
- Malformed request data
- Out-of-range values
Examples:
// Error: invalid asset id
{
"code": 3,
"message": "invalid asset id"
}
// Error: invalid network
{
"code": 3,
"message": "unsupported network protocol"
}
// Error: invalid amount
{
"code": 3,
"message": "amount must be positive"
}
Action:
- Verify asset ID format (zero-padded hash for native assets, contract address for ERC-20s — see
asset.GetNativeAsset/asset.GetAssets) - Check
Networkshape:{ protocol, id }only — notchain_id/name - Validate all numeric values (
DecimalStringis human-readable, not base units) - Ensure required fields and one-of variants are present (e.g. exactly one
fee_paymenton Lease requests)
DEADLINE_EXCEEDED (4)
Description: The request ran out of time, and the operation may still have completed.
Don't read it as a failure. A read can simply be retried; for a write — a payment, an order, a channel operation — check whether it took effect before sending it again. A request that ran out of time without doing anything answers UNAVAILABLE instead, which is safe to retry.
Three LiquidityService RPCs are exempt from that cap — RequestChannelLiquidity, RequestChannelRelease and RequestChannelLeaseExtension. Each waits for an on-chain channel operation to be observed, which outlives 30 s on every supported network; they rely on their own internal deadlines instead. Set a client deadline generous enough for a confirmation on those three, or you will abandon a request whose fee has already been paid.
Action:
- Retry with exponential backoff — only if the RPC is idempotent; see Idempotency & Retry Safety
- Give chain-bound calls a client deadline measured in minutes, not seconds
- For anything you want to watch rather than wait on, use the streaming endpoints
A timeout is not a rollback. The operation may have completed server-side after your deadline fired. For a write, verify before retrying.
NOT_FOUND (5)
Description: Requested resource not found
Common causes:
- Transaction doesn't exist
- Order not found
- Channel doesn't exist
- Payment not found
Examples:
// Error: transaction not found
{
"code": 5,
"message": "transaction not found: abc123..."
}
// Error: order not found
{
"code": 5,
"message": "order not found: order_xyz789"
}
Action:
- Verify resource ID is correct
- Check that resource exists on the specified network
- Ensure resource hasn't been deleted
ALREADY_EXISTS (6)
Description: Resource already exists
When it occurs:
- Attempting to create duplicate resource
- Market already initialized
- Channel already opened
orderbook.CreateOrderof a different order under aclient_order_idthat already names one (prefixclient_order_id_taken)
Examples:
{
"code": 6,
"message": "market already initialized"
}
Action:
- Check if resource already exists
- Use update/modify operations instead of create
PERMISSION_DENIED (7)
Description: Caller doesn't have permission
When it occurs:
- Authentication failure
- Insufficient permissions
- Attempting to access another user's resource
Action:
- Verify authentication credentials
- Check API key permissions
- Ensure you own the resource
RESOURCE_EXHAUSTED (8)
Description: Resource has been exhausted
Common causes:
- Insufficient balance
- No liquidity available
- Lease capacity exceeded
- Channel capacity full
Examples:
// Error: insufficient balance
{
"code": 8,
"message": "insufficient onchain balance"
}
// Error: no liquidity
{
"code": 8,
"message": "no liquidity available in orderbook"
}
// Error: lease capacity
{
"code": 8,
"message": "lease amount exceeds available liquidity"
}
Action:
- Check balance before operations
- Add funds to wallet
- Try smaller amount
- Wait for liquidity to become available
FAILED_PRECONDITION (9)
Description: Operation rejected due to system state
Common causes:
- Channel not active yet
- Market not initialized
- Asset not supported
- Duration out of allowed range
Examples:
// Error: channel not ready
{
"code": 9,
"message": "channel not active for sending"
}
// Error: market not initialized
{
"code": 9,
"message": "market not initialized for this pair"
}
// Error: duration invalid
{
"code": 9,
"message": "lease duration below minimum"
}
Action:
- Wait for prerequisite conditions
- Initialize required resources first
- Check state before operations
- Verify parameter ranges
ABORTED (10)
Description: Operation was aborted
When it occurs:
- Concurrent modification conflict
- Transaction conflict
- Order already cancelled
Action: Retry operation
OUT_OF_RANGE (11)
Description: Operation outside valid range
Common causes:
- Amount too large or too small
- Fee rate invalid
- Duration out of range
Action:
- Check minimum/maximum values
- Adjust parameters to valid range
UNIMPLEMENTED (12)
Description: Operation not implemented
When it occurs:
- Feature not yet available
- Endpoint deprecated
- Network not supported
Action:
- Check API documentation for supported features
- Use alternative endpoint
INTERNAL (13)
Description: Internal server error
When it occurs:
- Database error
- Backend service failure
- Unexpected server condition
Action:
- Retry with exponential backoff
- Contact support if persists
UNAVAILABLE (14)
Description: Service temporarily unavailable
Common causes:
- Service maintenance
- Network issues
- Backend overload
- A wait inside Hydra App ran out before the operation was sent — for a peer, a chain provider, a lock. The message may say "timeout" or "deadline"; nothing took effect, and the request may be sent again
Action:
- Retry with exponential backoff
- Check status page
- Wait and retry
DATA_LOSS (15)
Description: Unrecoverable data loss
When it occurs: Rare - serious server issue
Action: Contact support immediately
UNAUTHENTICATED (16)
Description: Request lacks valid authentication
When it occurs:
- Missing authentication
- Invalid credentials
- Expired session
Action:
- Provide authentication credentials
- Refresh authentication token
- Check API key is valid
Error Handling Best Practices
1. Implement Retry Logic — for idempotent RPCs only
⚠️ Use this helper only for
Get*,Estimate*, and stream-reconnect calls. WrappingSendTransaction,OpenChannel,CreateOrder, etc. in a blind retry can cause double-execution. See Idempotency & Retry Safety for the full rules.
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation()
} catch (error: any) {
const code = error.code
// Don't retry client errors (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED,
// OUT_OF_RANGE, UNIMPLEMENTED, UNAUTHENTICATED, FAILED_PRECONDITION)
if ([3, 5, 7, 9, 11, 12, 16].includes(code)) {
throw error
}
// Retry on transient errors (DEADLINE_EXCEEDED, UNAVAILABLE) and INTERNAL
if ([4, 13, 14].includes(code)) {
const delay = baseDelay * Math.pow(2, i) + Math.floor(Math.random() * 500)
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`)
await new Promise(resolve => setTimeout(resolve, delay))
continue
}
throw error
}
}
throw new Error('Max retries exceeded')
}
// ✅ Safe — Get* is idempotent
const balance = await withRetry(() => walletClient.getBalance(request, {}))
// ❌ DANGEROUS — SendTransaction is NOT idempotent.
// A DEADLINE_EXCEEDED retry can broadcast twice.
// Use the verify-before-retry pattern from the Idempotency section.
// const tx = await withRetry(() => clientClient.sendTransaction(request, {}))
2. Graceful Error Handling
async function safeGetBalance(
client: WalletServiceClient,
network: Network,
assetId: string
): Promise<Balance | null> {
try {
const request = new GetBalanceRequest()
request.setNetwork(network)
request.setAssetId(assetId)
const response = await client.getBalance(request, {})
return response.getBalance()
} catch (error) {
switch (error.code) {
case 3:
console.error('Invalid asset ID:', assetId)
return null
case 5:
console.error('Asset not found:', assetId)
return null
case 14:
console.warn('Service unavailable, retrying...')
// Implement retry logic
return null
default:
console.error('Unexpected error:', error)
throw error
}
}
}
3. Check Preconditions
async function sendPaymentSafely(
walletClient: WalletServiceClient,
nodeClient: NodeServiceClient,
network: Network,
assetId: string,
amount: string
) {
// Check balance first
const balanceReq = new GetBalanceRequest()
balanceReq.setNetwork(network)
balanceReq.setAssetId(assetId)
const balanceResp = await walletClient.getBalance(balanceReq, {})
// Size a single payment off max_sendable, not free_local — a per-payment
// ceiling can bind below the balance.
const available = balanceResp.getBalance()?.getOffchain()?.getMaxSendable()?.getValue()
// Amounts are decimal strings in whole units — compare as decimals, not BigInt.
if (new Big(available || '0').lt(new Big(amount))) {
throw new Error('Insufficient balance')
}
// Proceed with payment
const paymentReq = new SendPaymentRequest()
// ... configure payment
return await nodeClient.sendPayment(paymentReq, {})
}
4. User-Friendly Messages
function getUserFriendlyError(error: any): string {
switch (error.code) {
case 3:
return 'Invalid input. Please check your entries.'
case 5:
return 'Resource not found. Please verify the ID.'
case 8:
if (error.message.includes('balance')) {
return 'Insufficient funds. Please add more to your wallet.'
}
if (error.message.includes('liquidity')) {
return 'Not enough liquidity available. Try a smaller amount.'
}
return 'Resource limit exceeded.'
case 9:
return 'Operation cannot be completed right now. Please try again later.'
case 14:
return 'Service temporarily unavailable. Please try again in a moment.'
default:
return 'An unexpected error occurred. Please try again.'
}
}
// Usage in UI
try {
await client.someOperation(request, {})
} catch (error) {
alert(getUserFriendlyError(error))
}
Service-Specific Errors
Wallet Service
| Error | Cause | Solution |
|---|---|---|
INVALID_ARGUMENT: invalid asset id | Wrong asset ID format | Check asset ID for network type |
NOT_FOUND: transaction not found | Transaction doesn't exist | Verify txid is correct |
Orderbook Service
| Error | Cause | Solution |
|---|---|---|
RESOURCE_EXHAUSTED: no liquidity | Empty orderbook | Wait or provide liquidity |
FAILED_PRECONDITION: market not initialized | Market doesn't exist | Call InitMarket first |
INVALID_ARGUMENT: amount below minimum | Amount too small | Increase to min_base_amount |
INVALID_ARGUMENT: ... out of range | A price, an amount, or an amount taken at its price, past what the market can hold: the book counts both in whole units of the market's precision, up to 2⁶³−1 | Send a price and amount within the market's range |
-32602 unknown field 'addLiquidity' | Used the removed add_liquidity order variant (removed 2026-06-11) | Provide liquidity with limit_orders instead — see Orderbook → Create Order |
INVALID_ARGUMENT: on-chain settlement not allowed on maker order | Set settlement/route_filter on a resting maker (limit) order | On-chain settlement is taker-only; omit it on maker orders |
Swap Service
| Error | Cause | Solution |
|---|---|---|
RESOURCE_EXHAUSTED: insufficient balance | Not enough offchain funds for a plain Swap | Use SimpleSwap, which sets the channels up for you |
FAILED_PRECONDITION: price exceeded tolerance | Price moved past price_change_tolerance between estimate and execute | Re-estimate and retry; widen the tolerance on volatile pairs |
Most simple-swap refusals are not errors at all.
EstimateSimpleSwapsucceeds and returns aSimpleSwapEstimatevariant —no_liquidity,fees_higher_than_amount,lease_too_big,insufficient_sending_balance,insufficient_native_balance. Branch on the one-of; aswitchthat only handles the three success arms will misreport every one of these.
Node Service
| Error | Cause | Solution |
|---|---|---|
FAILED_PRECONDITION: channel not active | Channel still opening / confirming | Wait for the Active event from SubscribeNodeEvents |
NOT_FOUND: channel not found | Invalid channel_id | Verify with watchOnlyNode.GetChannels |
RESOURCE_EXHAUSTED: insufficient capacity | Channel too small | Deposit more funds via node.DepositChannel or liquidity.RequestChannelLiquidity (deposit op) |
INVALID_ARGUMENT: peer not in zero-conf whitelist | Tried zero-conf with non-whitelisted peer | Add peer via node.AddPeerToZeroConfWhitelist first |
INVALID_ARGUMENT from node.UpdateChannel, contributions cancel out | The chain refuses an update that leaves the channel holding what it already held | Contributions that cancel are a payment, not an update — send it as one. See Update Channel |
INVALID_ARGUMENT from node.UpdateChannel on peer_contribution | Used deposit: { all: {} } on the peer's side, where All would resolve against a wallet your node cannot see | Name an exact amount for a peer deposit |
FAILED_PRECONDITION: funding allowance exceeded | Peer-initiated deposit/payment exceeds set allowance | Raise via node.IncreaseFundingAllowance |
Placement rejections (Orderbook)
Added 2026-08-13.
When the hub refuses an order, it encodes the reason in a stable, parseable message so an SDK can react without scraping prose:
<prefix>: key=value key=value — human tail
Match on the prefix and the key=value tokens only — never on the human tail, which is free text and may change. The gRPC status code is deliberately not part of the contract; the prefix is.
| Prefix | Keys | Meaning |
|---|---|---|
below_minimum | market, side, got, and exactly one of min_place / min_fill | Order size is under a placement minimum. The key present names the larger, binding one: min_place = the hub's USD placement floor at live prices; min_fill = the structural minimum. |
below_best_fill | side, got, min_fill, price, market | Strict price priority: the best order this one would have to take first rests at price, and its smallest fill, min_fill, is more than the order has (got; both in the unit side names). A taker takes nothing past that order, and a limit order would rest across it. Nothing was created: re-place at least min_fill, or at a price that does not reach price — see Price priority. Answered with FAILED_PRECONDITION |
asset_not_listed | currency | That currency isn't listed on the hub |
market_state | market, state | The market does not take this order in its current state: post_only (refuses market and swap orders, and limit orders that only take — IOC, FOK), cancel_only or frozen |
post_only_would_cross | price, best, market | A post-only limit order at price — or any limit order on a MARKET_STATE_POST_ONLY market — would have taken an order on offer, the best at best. Nothing was created. Re-price so it does not cross. Answered with FAILED_PRECONDITION |
nothing_to_take | market, and price / best when there is one | An order that only takes — a TIME_IN_FORCE_IOC limit order, or a market order — found nothing it may take within price (absent for a market order without limit_price). best is the best price an order it may take rests at on the other side, absent when there is none. Nothing was created. Answered with FAILED_PRECONDITION. A market order over an empty side used to be answered with an unprefixed INVALID_ARGUMENT |
fill_or_kill_unfilled | side, got, fillable, price, market | A TIME_IN_FORCE_FOK limit order for got could take only fillable at price or better (both in the unit side names). Nothing was created, and the book was left as it was. Answered with FAILED_PRECONDITION |
self_trade_prevented | resting_order_id, market | The order's self_trade_prevention stopped it at resting_order_id, a resting order of yours, before it filled anything. Nothing was created. Answered with FAILED_PRECONDITION |
venue_stopping | — | The hub is restarting and turns no new match into a swap until it is back: the order ended with nothing filled. Not a fault of the order: send it again once the hub is back. Answered with UNAVAILABLE |
sub_economic_client_limit | force_close_usd, added_usd, allowance_usd | (2026-09-16) Force-closing this client's in-flight sub-economic swaps already costs force_close_usd, and this match would add added_usd on top of an allowance of allowance_usd |
sub_economic_htlc_cap | network, node, current, addition, max | The peer already holds its maximum concurrent sub-economic HTLC legs |
sub_economic_venue_limit | force_close_usd, added_usd, admissible_usd, standing | (2026-09-16) The same figure venue-wide has no room for this match. admissible_usd rises with standing (settled volume, [0, 1]) |
matcher_overloaded | state, class | The matcher is shedding load |
client_order_id_taken | order_id | The client_order_id already names a different order of yours — live, or ended within the hub's retention period (24 hours by default). order_id is that order: resend it unchanged to get it back, or place this one under a new id. Hydra App answers it with ALREADY_EXISTS |
maker_unavailable | — | None of the makers the order matched can settle with it right now, so no swap was made and nothing of the order filled. The order may already have been announced on the event stream, and ended there. Not a fault of the order: retry, and it matches other liquidity. Hydra App answers it with UNAVAILABLE |
Anything not matching this closed set — including every message from a pre-2026-08-13 hub — is unclassified and should be treated as an opaque error, exactly as before.
The three sub-economic prefixes were renamed on 2026-09-16, and their keys with them. They were
below_floor_budgetandbelow_floor_countwhen this page was first written. A parser matching the old names now falls through to unclassified — which is safe (the order is still refused, and the refusal is still retryable) but loses the numbers that say what to do about it. The amounts are force-closure costs, not trade notionals: see Get Sub-Economic Policy for what they are measured in and how to predict a refusal instead of discovering it.All three are retryable and none of them invalidates cached
MarketInfo.sub_economic_client_limitandsub_economic_venue_limitclear as your in-flight small swaps settle;sub_economic_htlc_capclears as their HTLCs resolve.
Only
below_minimuminvalidates your cachedMarketInfo. It means the hub's live minimums moved past your cached view, so refetchGetMarketInfoand resize from themin_place=in the rejection — that value is authoritative, themin_place_*snapshot inMarketInfois only advisory.below_best_filldoes not: no minimum moved, the order met a resting order it is too small for.
market_statedoes not qualify: the market'sstateis what changed, and themarket_infoevent on the market stream carries the new one the moment it changes — no refetch needed.
HTLC & Preimage Service
| Error | Cause | Solution |
|---|---|---|
-32601 method not found: node_registerPreimage | Called the removed node.RegisterPreimage (removed 2026-06-11) | Call preimage.SettlePreimage — same request fields |
FAILED_PRECONDITION from htlc.CreateHtlcSettlementTx / DeriveHtlcAddress / VerifyHtlcByLockTxid | Called a UTXO-model-only RPC on an account-model (EVM / Tron) network | These pre-sign / fund-by-address flows apply to UTXO protocols only; account-model HTLCs settle permissionlessly |
FAILED_PRECONDITION: on-chain HTLCs unavailable | Network has no htlc_factory_address (EVM / Tron) configured | Set htlc_factory_address for that network in config.yaml, or settle via channel |
INVALID_ARGUMENT: lock_type (rejected/unsupported) | Passed a lock_type a protocol with a single canonical lock construction doesn't accept, or an HtlcExpectation.lock_type the lock doesn't match | Leave lock_type empty for the node default, or pin the type the counterparty actually used |
FAILED_PRECONDITION from preimage.SettleHtlcPreimage | Called it on a network with no on-chain HTLC facet | Use preimage.SettlePreimage alone there — that network settles through channels only |
On-chain HTLCs not swept after preimage.SettlePreimage | Since 2026-08-01 SettlePreimage covers channel legs only | Also call preimage.SettleHtlcPreimage; on a network with both facets you drive both |
SettleHtlcPreimage returns an empty txids list | The preimage unlocks no Locked HTLC on that network (or they were already claimed) | Not an error — it is idempotent and a no-op is a success. A partial list is also normal: a claim that can't be built is skipped, not failed |
Liquidity Service (Lease API)
| Error | Cause | Solution |
|---|---|---|
INVALID_ARGUMENT: lease_duration_seconds required | server_amount > 0 without a duration | Set lease_duration_seconds, or set server_amount = "0" |
INVALID_ARGUMENT: dual_fund_fee_payment not allowed | Used dual_fund_fee_payment or sponsored_deposit_fee_payment outside RequestChannelLiquidity, or sponsored_withdrawal_fee_payment outside RequestChannelRelease | Each sponsored method belongs to one operation — see the per-operation table |
| Channel holds a balance but no gas, and every fee method is refused | On-chain needs a funded wallet; off-chain needs the very balance the release is about to take away | (2026-09-11) Use sponsored_withdrawal_fee_payment — the only method that works from that state |
FAILED_PRECONDITION naming a quote_id | (2026-09-11) The quote expired, or the request no longer matches the one it was issued for | Re-run the matching Estimate*Fee, show the user the new fee, and send the fresh quote_id — see Quotes |
-32602 unknown field 'assetIds' on a withdraw release | ChannelReleaseOperation.Withdraw replaced asset_ids[] with asset_amounts{} (2026-07-23) | Send a map of asset_id → { server_amount?, client_amount? }; at least one side per asset — see ChannelReleaseOperation |
INVALID_ARGUMENT on a withdraw with no channel_id | channel_id may be absent only on EstimateRequestChannelReleaseFee, and every side must then be an exact amount | Supply channel_id for the real operation |
| A release quote comes back for a channel you don't own, or the exit falls to the local rail | (2026-09-13) Sent channel_id: "" where the field should be absent. The service's ownership check rejects an empty string | Omit the field on an estimate — see ChannelReleaseOperation |
INVALID_ARGUMENT: exactly one fee_payment required | None or multiple fee_payment variants set | Set exactly one |
RESOURCE_EXHAUSTED: insufficient liquidity | Provider has no inventory for that asset | Reduce server_amount, try a different asset, or self-fund via node.OpenChannel |
FAILED_PRECONDITION: channel does not exist | Bad channel_id on Release / Lease Extension | Verify with watchOnlyNode.GetChannel |
Watch-Only Node Service
| Error | Cause | Solution |
|---|---|---|
NOT_FOUND: channel not found | Bad channel_id | Use GetChannels to enumerate |
NOT_FOUND: payment not found | Bad payment_id | Use GetPayments / GetPaymentsByHash to look up |
Client Service
| Error | Cause | Solution |
|---|---|---|
INVALID_ARGUMENT: invalid amount | Amount oneof unset, or DecimalString not a number | Set exact or all; ensure value parses |
RESOURCE_EXHAUSTED: insufficient onchain balance | Wallet under-funded for the send + fee | Wait for incoming deposits or reduce amount |
FAILED_PRECONDITION: token allowance too low | EVM token send needs higher allowance | Call client.SetTokenAllowance first, or submit a signed token permit |
RESOURCE_EXHAUSTED on a token operation with a funded token balance | The native asset cannot cover the gas — a separate resource from the token being moved | Fund the native asset, or use a rail that pays it for you: SponsoredDepositFeePayment / DEPOSIT_RAIL_LIQUIDITY_SERVICE |
| The permit submission reverted on-chain | The deadline passed, the signature does not match owner, or the token already consumed that nonce | Re-read blockchain.GetTokenPermitTerms, re-sign, and submit promptly. The fee is spent either way |
Pricing Service
| Error | Cause | Solution |
|---|---|---|
| No price returned | The oracle does not list that deployment or its symbol, or the price it holds is stale | The response field price is simply absent — a success with an empty optional, not an error. Treat it as "unknown" and render without a fiat figure |
Idempotency & Retry Safety
A bot must know which RPCs are safe to retry blindly after a DEADLINE_EXCEEDED or transient UNAVAILABLE.
Safe to retry (read-only, idempotent)
| RPC family | Safe to retry? |
|---|---|
All Get* queries on every service | ✅ Always |
Estimate* RPCs (e.g. EstimateOpenChannelFee, EstimateRequestChannelLiquidityFee) | ✅ Always |
Subscribe* (re-establishing after a drop) | ✅ Always — reconnect with backoff |
preimage.SettlePreimage | ✅ Idempotent — settling an already-settled preimage is a no-op |
preimage.SettleHtlcPreimage | ✅ Idempotent — an HTLC already claimed is no longer Locked, so a retry simply claims nothing (empty txids) |
node.AddPeerToBlacklist / RemovePeerFromBlacklist | ✅ Idempotent |
htlc.UnwatchHtlc | ✅ Idempotent |
Retry only after checking server state first
These RPCs are not idempotent. After a DEADLINE_EXCEEDED you may have triggered the action without seeing the response. Verify before retrying:
| RPC | How to verify before retry |
|---|---|
client.SendTransaction, client.SendTokenTransaction, client.BumpTransaction | Query wallet.GetTransactions for a recent tx with the same recipient / amount |
client.SetTokenAllowance | Query blockchain.GetTokenAllowance for the new value |
client.FinalizeAndBroadcastTransaction | Query wallet.GetTransaction for the txid |
node.OpenChannel, node.DepositChannel, node.WithdrawChannel, node.UpdateChannel, node.CloseChannel, node.ForceCloseChannel, node.RedeemClosedChannel | Watch SubscribeNodeEvents — events for the channel will fire if the operation went through |
node.SendChannelPayment, node.SendPayment, node.PayInvoice, node.PayEmptyInvoice | Query watchOnlyNode.GetPaymentsByHash (you must know the payment hash) |
liquidity.RequestChannelLiquidity | Watch SubscribeNodeEvents for new channel creation |
liquidity.RequestChannelRelease | Watch SubscribeNodeEvents for the channel state change |
liquidity.RequestChannelLeaseExtension | Query watchOnlyNode.GetChannel and check the new expiry |
orderbook.CreateOrder | Don't — set client_order_id instead and the retry is idempotent by construction. Without one, query orderbook.GetAllOwnOrders for the same (base, quote, side, amount, price) shape, which is not perfectly unique |
orderbook.CancelOrder | Query orderbook.GetOrder — if status is cancelled, the request succeeded |
swap.Swap, swap.SimpleSwap | swap.SubscribeSimpleSwaps will emit progress; otherwise orderbook.GetAllOwnOrders |
Never auto-retry
| Status | Why |
|---|---|
INVALID_ARGUMENT | Your request is wrong — fix the code, don't retry |
FAILED_PRECONDITION | State doesn't allow this — wait for state change, then re-evaluate (don't blindly retry) |
PERMISSION_DENIED / UNAUTHENTICATED | Auth issue — re-authenticate first |
OUT_OF_RANGE | Numerical input outside permitted range — fix |
UNIMPLEMENTED | RPC doesn't exist on this server version |
Application-level idempotency keys
One RPC does take an idempotency token: orderbook.CreateOrder. Set client_order_id (your own string, max 64 chars) and it names that one order, while the order lives and for a retention period after it ends (24 hours by default). A retry of the same order under it gets the original answer — its order_id, and what its placement released — even when the order has ended meanwhile, instead of placing a second order; a different order under it is refused with client_order_id_taken. Read an open order back with GetOrderByClientId. Use it on every retry-prone order path — it is strictly better than any verify-then-retry dance.
Everything else needs an action ID at the application level:
- Generate a UUID for each high-level action (e.g. "place 0.1 BTC sell at $67k").
- Persist
(action_id, status: pending|done|failed)on disk before calling the RPC. - After the RPC returns or after a verification check, update the status.
- On startup, replay any
pendingactions: query the relevantGet*RPC, decide if the action completed, and update.
For order placement, the simplest correct scheme is to make
client_order_idbe youraction_id: replaying a pending action then converges on the same order rather than creating a second one.
Debugging Tips
- Enable verbose logging - Log all requests and responses
- Check error.message - Contains detailed error information
- Verify request format - Use protobuf debugging tools
- Test with small amounts - Use minimal values when debugging
- Check network status - Ensure blockchain is synced
- Monitor rate limits - Avoid excessive requests