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: Operation timed out
When it occurs:
- Request took too long (>10 seconds for most operations)
- Network latency issues
Action:
- Retry with exponential backoff
- Check network connectivity
- For long operations, use streaming endpoints
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
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
- Rental 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: rental capacity
{
"code": 8,
"message": "rental 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": "rental 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
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, {})
const available = balanceResp.getBalance()?.getOffchain()?.getLocal()
if (BigInt(available || '0') < BigInt(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 |
Swap Service
| Error | Cause | Solution |
|---|---|---|
RESOURCE_EXHAUSTED: insufficient balance | Not enough offchain funds | Use SimpleSwap instead |
FAILED_PRECONDITION: price exceeded tolerance | Price moved too much | Increase tolerance or retry |
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 |
FAILED_PRECONDITION: funding allowance exceeded | Peer-initiated deposit/payment exceeds set allowance | Raise via node.IncreaseFundingAllowance |
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 on Release or Lease Extension | Switch to onchain_fee_payment or offchain_fee_payment |
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 |
Pricing Service
| Error | Cause | Solution |
|---|---|---|
NOT_FOUND: price unavailable | Provider has no quote for that asset/currency | Response field price is empty, not an error — only some asset/symbol combos return a price. Treat empty as "unknown." |
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 |
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.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 | Query orderbook.GetAllOwnOrders and look for the same (base, quote, side, amount, price) shape — note: not perfectly unique, see below |
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
Hydra's RPCs don't accept idempotency tokens. To make your bot truly idempotent across retries, you need to track 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 specifically: include a unique client tag in your in-memory mapping of
action_id → submitted_order_request. After a retry, ifGetAllOwnOrdersshows two orders matching the request shape, you've double-submitted — cancel one withCancelOrder.
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