Api

Setup Guide

Install and run Hydra App with Docker Compose

This guide walks you through running Hydra App and connecting to it from a client. The app is the gRPC server that exposes every API documented in this section.

Prerequisites

  • Docker & Docker Compose
  • ~2 GB free disk for the data directory
  • A funded wallet for whatever networks you enable

Building a bot or trading agent? After completing the setup below, jump to the Bot Quickstart for a minimum runnable template.


Mainnet or staging?

Everything in this guide applies to both. They differ in three places — the image you pull, the config.yaml you write, and the peers you connect to — so pick a track now and stay on it.

MainnetStaging
FundsRealTestnet only
Imageghcr.io/offchain-dex/hydra-app-public:latestghcr.io/offchain-dex/hydra-app-tester:latest
NetworksBitcoin, Ethereum, Arbitrum OneBitcoin Signet, Ethereum Sepolia, Arbitrum Sepolia, Tron Shasta
EndpointsPublic — *.hydranet.app, shown in full belowAccess-controlled — request via Discord
Config templateMainnet config.yamlStaging config.yaml
PeersMainnet peersStaging peers

The two images are built from the same production code; hydra-app-public ships with reduced logging (release_max_level_info), hydra-app-tester keeps the verbose build used for testing. Either runs against either network — the image does not pin the network, config.yaml does — but pair them as above unless you have a reason not to.

⚠️ Mainnet access is invite-gated at launch

Running the software is not the same as being admitted. Before you write a config, make sure your identity key is either whitelisted or covered by an invite you can redeem — see Mainnet access is gated.

⚠️ Never point a mainnet config at a data directory that has held testnet state

Give mainnet its own settings.data_path_name and its own volume. Sharing a data directory across networks mixes wallets and channel state, and the failure mode on the mainnet side involves real funds.


Architecture

Your Application (gRPC client)
          ↓
    gRPC over HTTP/2  (or gRPC-Web)
          ↓
Hydra App  (your machine, default :5003)
          ↓
   ├─ authentication-service  (auth tokens)
   ├─ liquidity-service        (channel leases — see Lease API)
   ├─ orderbook                (markets & matching)
   ├─ hydra-proxy              (Electrum / Esplora / Web3 / Subgraph proxy)
   └─ Local data store         (fjall key-value DB on disk)

Hydra App brokers everything. You never talk to the orderbook, liquidity service, or chain RPCs directly — the app does it for you and exposes a single gRPC surface.

Authentication is handled by Hydra App, not by your client. The app obtains and refreshes tokens with the authentication service automatically, using the seed/MNEMONIC you set up below. Clients connect to the local app over plain gRPC with no API keys, no headers, no auth handshake. The auth_grpc_url / auth_ws_url and proxy_auth fields in config.yaml configure Hydra App's upstream connection — not your client. Your client never sees those URLs.


Mainnet access is gated

At launch, mainnet is invite-gated: the authentication service admits a known set of identities, and a node whose identity is not among them will not get past authentication. Two ways in:

  1. Redeem an invite code — the usual route. Someone already on mainnet mints one with CreateInvite and sends it to you privately. Redeem it in the web app, or from your own node with RedeemInvite.
  2. Ask the team to whitelist your identity keyopen a ticket in Discord with the key below.

An invite can be redeemed before your identity is authenticated. That is deliberate — the referral client connects without authentication, so redeeming is what gets you admitted rather than something you do once you are already in. Redeeming from your own node needs referral_config.referral_service_url in config.yaml; redeeming in the web app needs nothing configured.

Finding your identity key

The key to hand over is your Ed25519 identity public key, hex-encoded (32 bytes → 64 hex characters). Three ways to read it, all the same value:

On startup, the app logs it before anything else initialises:

Initializing HydraApp with signer 9f4c1e…  ← this is the key

Over the API, at any time — app.GetPublicKey returns it and takes no parameters:

curl -s http://localhost:5003 -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"app_getPublicKey","params":{}}'

On disk, as the data directory name — state lives under <data_dir>/<identity-hex>/.

⚠️ The identity follows the mnemonic, not the machine

The identity key is derived from your BIP-39 seed by HKDF-SHA512 under a fixed, versioned domain-separation tag — so the same mnemonic produces the same identity everywhere: CLI, desktop, and the web app alike. Move your node to a new host with the same mnemonic and you keep your access.

The converse is what catches people: a new mnemonic is a new identity, and a new identity is not whitelisted. Generating a fresh wallet — including by letting interactive mode create one on a machine where you meant to restore — means redeeming another invite or asking to be whitelisted again. Whitelist the key you actually intend to run with.

The identity is cryptographically independent of your BIP-32 / BIP-44 wallet keys; it authenticates you and signs for the backup, referral and liquidity services, and it is not an address that holds funds.


Step 1: Configuration files

Create a working directory and two files: config.yaml and .env.

mkdir hydra-app && cd hydra-app

Mainnet config.yaml

Every mainnet endpoint is public — nothing here is redacted, and you do not need to request access to run against mainnet. Copy this template as-is and adjust the ports, the data path and the token list to taste.

settings:
  data_path_name: hydra-app-mainnet   # keep mainnet state in its own directory
  critical_db: &fjall_hot             # fund-critical data (required)
    type: fjall
    compression:
      type: zstd
      preset: balanced                # fast | balanced | best (or `level: <i32>`)
    cache_size_mb: 96
    max_memtable_size_mb: 32
    max_write_buffer_size_mb: 192
    max_journaling_size_mb: 192
    worker_threads: 4
  db: *fjall_hot                      # non-critical data (required)
  archive_db:                         # cold settled history — rare reads,
    type: fjall                       # bursty writes
    compression:
      type: zstd
      preset: balanced
    cache_size_mb: 32
    max_memtable_size_mb: 128
    max_write_buffer_size_mb: 256
    max_journaling_size_mb: 512
    worker_threads: 2
  # Automatic archive retention (optional; omit to disable). The two filters
  # ARE the whole policy — pacing is derived at runtime. See auto_prune below.
  # auto_prune:
  #   max_age_secs: 7776000           # keep 90 days (minimum 172800 = 48h)
  #   max_entries: 100000             # and at most 100k newest rows per table
  server_port: 5003
  metrics_port: 9090

authentication_config:
  auth_grpc_url: https://authentication-service.hydranet.app
  auth_ws_url:   wss://authentication-service-ws.hydranet.app

proxy_url: https://hydra-proxy.hydranet.app

# Server-side wallet backups. NOT recommended for liquidity providers —
# adds latency on high-volume nodes. Comment out to disable.
backup_config:
  grpc_url: https://backup-service.hydranet.app

liquidity_config:
  liquidity_url: https://liquidity-service.hydranet.app

orderbook_config:
  orderbook_url: https://orderbook.hydranet.app

pricing_config:
  price_oracle_url: https://price-oracle.hydranet.app
  fiat_currencies:
    - USD

# Needed only to mint or redeem invites FROM THIS NODE (CreateInvite /
# RedeemInvite / ListInvites / GetInviteEligibility). Redeeming an invite in
# the web app instead needs nothing here. See "Mainnet access is gated".
referral_config:
  referral_service_url: https://referral-service.hydranet.app

networks:
  - protocol: bitcoin
    network: bitcoin                  # mainnet — NOT "bitcoin-mainnet"
    blockchain:
      type: electrum
      urls:
        - wss://hydra-proxy.hydranet.app/electrum/mainnet/ws
      esplora_url:    https://hydra-proxy.hydranet.app/esplora/mainnet
      waterfalls_url: https://hydra-proxy.hydranet.app/waterfalls/mainnet
      proxy_auth: true
    lightning_tcp_port: 19735
    lightning_ws_port: 19736
    gossip_sync:
      type: rgs
      rgs_server_url: https://rapidsync.lightningdevkit.org   # public RGS
    # Channel terms, in blocks (144/day). Mirrors the hub: propose a
    # three-day window to punish a stale commitment, accept at most five
    # days before we can claim our own balance.
    lightning:
      channel_handshake:
        our_to_self_delay_blocks: 432
      channel_handshake_limits:
        their_to_self_delay_blocks: 720

  - protocol: evm
    network: ethereum                 # mainnet — NOT "ethereum-mainnet"
    web3_provider:
      url: wss://hydra-proxy.hydranet.app/web3/ws/1
      proxy_auth: true
    # Enriched endpoints, proxy-fronted. The Hydranet mainnet node runs with
    # these; the plain web3_provider alone works but answers history and
    # log queries less efficiently.
    alchemy_rpc:
      custom_url: https://hydra-proxy.hydranet.app/alchemy/rpc/1
      proxy_auth: true
    alchemy_ws:
      custom_url: wss://hydra-proxy.hydranet.app/alchemy/ws/1
      proxy_auth: true
    lithium:
      subgraph:
        http_url: https://index-api.onfinality.io/sq/7446948102396997632/ethereum
      contract_address: "0xcc4c47cf0175AA8108e78679c2586A0a6abb242a"
      contract_deploy_block: 25667649
      tcp_quic_port: 29980
      ws_port: 29981
      # Channel terms, in seconds. A channel opens with the LONGER of the
      # two peers' dispute_period proposals, within both ceilings: propose
      # two days, accept at most five.
      channel_handshake:
        dispute_period_secs: 172800
      channel_handshake_limits:
        max_dispute_period_secs: 432000
    # Third-party watchtower this node hands its channel evidence to. The
    # node dials it itself after its initial sync and keeps it attached.
    watchtowers:
      - "0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://ethereum-ws.hydranet.app:443"
    tokens:
      - "erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"   # USDC
      - "erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7"   # USDT (Tether)

  - protocol: evm
    network: arbitrum                 # Arbitrum One — NOT "arbitrum-one"
    web3_provider:
      url: wss://hydra-proxy.hydranet.app/web3/ws/42161
      proxy_auth: true
    alchemy_rpc:
      custom_url: https://hydra-proxy.hydranet.app/alchemy/rpc/42161
      proxy_auth: true
    alchemy_ws:
      custom_url: wss://hydra-proxy.hydranet.app/alchemy/ws/42161
      proxy_auth: true
    lithium:
      subgraph:
        http_url: https://index-api.onfinality.io/sq/7446948102396997632/arbitrum
      contract_address: "0x895090D931660fbFc5F6bDEdC24A87dD3D64202D"
      contract_deploy_block: 490348527
      tcp_quic_port: 29982
      ws_port: 29983
      channel_handshake:
        dispute_period_secs: 172800
      channel_handshake_limits:
        max_dispute_period_secs: 432000
    watchtowers:
      - "0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://arbitrum-ws.hydranet.app:443"
    tokens:
      - "erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831"   # USDC
      # This contract reports "USD₮0" as both symbol and name — it is Tether
      # bridged to Arbitrum by LayerZero. Listed under the asset's own
      # identity instead. See "Token display overrides" below.
      - id: "erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
        symbol: USDT
        name: "Tether USD"
      - "erc20:0xB0F66Bdb39acBb043308eb9Dbe78F5bB47ea5430"   # HDN

What differs from staging

MainnetStaging
Network namesbitcoin, ethereum, arbitrumbitcoin-signet, ethereum-sepolia, arbitrum-sepolia, tron-shasta
EndpointsPublic *.hydranet.appAccess-controlled
gossip_sync.rgs_server_urlPublic LDK RGS, no proxy_authProxy-fronted, proxy_auth: true
alchemy_rpc / alchemy_wsProxy-fronted, custom_urlProxy-fronted, custom_url
htlc_factory_addressAbsent — see belowPresent on Tron
TronNot part of mainnet launchtron-shasta available
watchtowersDeclared per EVM networkAttach at runtime

On-chain HTLC settlement is off at mainnet launch

Neither mainnet EVM network sets htlc_factory_address, so mainnet settles through channels only. The on-chain half of the HTLC & Preimage APICreateHtlc, SettleHtlcPreimage, the OrderSettlement on-chain leg options — is inactive there. This is deliberate for launch, not an omission to fix in your own config: pointing the field at an unaudited factory of your own would put real funds behind it.

Channel settlement is the default path and needs nothing configured. When factories are deployed, adding htlc_factory_address to the network block is the only change required.

pricing_config is optional. Omit the block and the Pricing API simply has no oracle to answer from; nothing else degrades. Fiat conversion is a display concern.

Staging config.yaml

Endpoints redacted. The staging service endpoints, subgraph URLs, and Lithium contract addresses below are placeholders. The real values are distributed privately to approved testers — open a ticket in our Discord to request access. The structure of this file (every field name, every nesting level) is accurate; only the access-controlled values are masked. On-chain values (token and HTLC-factory contracts, deploy blocks) and the staging peer strings are public and shown in full.

The block below is the staging template. Copy it, fill in the placeholders (anywhere you see <...>) with the values you receive, and adjust ports / tokens to taste.

settings:
  data_path_name: hydra-app
  # Per-role storage backends. Each of critical_db / db / archive_db takes a
  # `type` (fjall | redb | indexed_db) plus its own compression. Optional
  # MiB tuning fields (cache_size_mb, max_memtable_size_mb, …) may be added;
  # omitted means the backend default. Full reference + environment profiles:
  # hydra-app/docs/database-tuning.md.
  critical_db:                    # fund-critical data (required)
    type: fjall
    compression:
      type: zstd
      preset: balanced            # fast | balanced | best (or explicit `level: <i32>`)
  db:                             # non-critical data (required)
    type: fjall
    compression:
      type: zstd
      preset: balanced
  archive_db:                     # cold, append-only settled history (optional)
    type: fjall
    compression:
      type: zstd
      preset: best                # cold storage — favour ratio over speed
  # Automatic archive retention (optional; omit to disable). These two
  # filters ARE the whole policy — pacing is derived at runtime (2026-08-26).
  # At least one filter is required. Pruning a settled payment also deletes
  # its stored preimage, so payments prune by max_age_secs only.
  # auto_prune:
  #   max_age_secs: 7776000       # keep 90 days (minimum 172800 = 48h)
  #   max_entries: 100000         # and at most 100k newest rows per table
  server_port: 5003
  metrics_port: 9090

authentication_config:
  auth_grpc_url: <auth-grpc-url>             # provided by the team
  auth_ws_url:   <auth-ws-url>               # provided by the team

proxy_url: <hydra-proxy-url>                  # provided by the team

# backup_config:
#   Uncomment if you want server-side wallet backups. NOT recommended for
#   liquidity providers — adds latency on high-volume nodes.
#   grpc_url: <backup-service-url>

liquidity_config:
  liquidity_url: <liquidity-service-url>     # provided by the team

orderbook_config:
  orderbook_url: <orderbook-url>             # provided by the team

pricing_config:
  price_oracle_url: <price-oracle-url>       # provided by the team
  fiat_currencies:
    - USD

# referral_config:                           # optional — enables invites / referral
#   referral_service_url: <referral-service-url>   # provided by the team

networks:
  - protocol: bitcoin
    network: bitcoin-signet
    blockchain:
      type: electrum
      urls:
        - <electrum-ws-url>                   # provided by the team
      esplora_url:    <esplora-url>           # provided by the team
      waterfalls_url: <waterfalls-url>        # provided by the team
      proxy_auth: true
    lightning_tcp_port: 19735
    lightning_ws_port: 19736
    gossip_sync:
      type: rgs
      rgs_server_url: <rgs-url>               # provided by the team (proxy-fronted)
      proxy_auth: true

  - protocol: evm
    network: ethereum-sepolia
    web3_provider:
      url: <ethereum-sepolia-web3-ws-url>     # provided by the team
      proxy_auth: true
    alchemy_rpc:                              # optional enriched RPC
      custom_url: <ethereum-alchemy-rpc-url>  # provided by the team
      proxy_auth: true
    alchemy_ws:
      custom_url: <ethereum-alchemy-ws-url>   # provided by the team
      proxy_auth: true
    lithium:
      # logs (default): contract event logs + verified contract reads are
      # authoritative; the subgraph below only bootstraps the routing graph.
      # subgraph: the indexer becomes the sole, unverified source — needed
      # only for a contract predating the channel lifecycle events.
      state_backend: logs
      subgraph:
        http_url: <ethereum-subgraph-url>     # provided by the team
      contract_address: <lithium-eth-contract>  # provided by the team
      contract_deploy_block: 11262882         # first block to index from
      tcp_quic_port: 29980
      ws_port: 29981
    tokens:
      - "erc20:0x8cd0dA3d001b013336918b8Bc4e56D9DDa1347E0"  # USDC on Sepolia (Circle public faucet)

  - protocol: evm
    network: arbitrum-sepolia
    web3_provider:
      url: <arbitrum-sepolia-web3-ws-url>     # provided by the team
      proxy_auth: true
    alchemy_rpc:
      custom_url: <arbitrum-alchemy-rpc-url>  # provided by the team
      proxy_auth: true
    alchemy_ws:
      custom_url: <arbitrum-alchemy-ws-url>   # provided by the team
      proxy_auth: true
    lithium:
      subgraph:
        http_url: <arbitrum-subgraph-url>     # provided by the team
      contract_address: <lithium-arb-contract>  # provided by the team
      contract_deploy_block: 287009472
      tcp_quic_port: 29982
      ws_port: 29983
    tokens:
      - "erc20:0x67E6a7eaE40107FF676908B28C3FC632A38f1499"  # HDN
      - "erc20:0x1baAbB04529D43a73232B713C0FE471f7c7334d5"  # USDC2 (Arbitrum Sepolia)

  - protocol: tron
    network: tron-shasta                      # mainnet | shasta | nile
    provider:
      url: <tron-provider-url>                # Tron node HTTP API base (/wallet, /walletsolidity)
      proxy_auth: true
    indexer:                                  # optional enriched indexer
      url: <tron-indexer-url>
      proxy_auth: true
    rpc:                                      # eth-compatible JSON-RPC endpoint
      url: <tron-jsonrpc-url>
      proxy_auth: true
    # Required for on-chain HTLC settlement on Tron; omit to disable it.
    htlc_factory_address: "TFuhf6EWZs2suo1V2mnR5fpqpijbLtvnoy"
    lithium:
      state_backend: logs
      contract_deploy_block: 66576623
      subgraph:
        http_url: <tron-subgraph-url>         # provided by the team
      contract_address: <lithium-tron-contract>  # base58check (T...)
      tcp_quic_port: 29984
      ws_port: 29985
    tokens:
      - "trc20:TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs"  # USDT

Why redacted? During the alpha, the staging endpoints are not public. Every URL marked <...> and every <lithium-*-contract> is access-controlled. Public values (chain IDs, ports, deploy blocks, well-known testnet token contracts) are kept inline because they're already publicly known.

What each block does

BlockPurpose
settings.server_portThe gRPC and JSON-RPC port your client connects to (here 5003) — one port, both protocols, routed by content type. Optional: omit it and no server starts at all, which is how you run a headless node with no API. The process binds 0.0.0.0, so map it in Docker on loopback — the API is unauthenticated.
settings.metrics_portPrometheus-style metrics. Optional — omit for no metrics server. Also binds 0.0.0.0.
settings.data_path_nameOn-disk directory name for the wallet/DB. Bump this (e.g. hydra-apphydra-app-2) to start fresh.
settings.critical_db / db / archive_dbPer-role storage backends (as of 2026-06). Each takes a type (fjall | redb | indexed_db), its own compression, and optional tuning — see Storage backends. critical_db/db are required; archive_db (cold settled history) defaults to fjall. Replaces the old flat critical_db_option / db_option / top-level compression. See database-tuning.md.
settings.critical_db_migrate_from / db_migrate_from / archive_db_migrate_from(optional) A previous backend to copy data from at startup, same schema as the backend it feeds. The migration is idempotent — it runs only when the destination is empty — so leaving it set is safe but logs a warning on every boot. Remove it once the migration is confirmed.
settings.auto_prune(2026-07-08, optional) Archive retention per network — see Archive Prune and below. At least one of max_age_secs / max_entries required. interval_secs was removed on 2026-08-26 — delete it if your config still has it.
settings.blacklist(optional) { path: <file> } — a runtime peer-connection blacklist. Loaded at startup and watched for live changes, so edits apply without a restart; its entries plus any runtime bans are pushed into every node. Each non-empty, non-comment line is <network-name> <node-id>. The parent directory must exist for the watch to attach. See Node API → Peer Blacklist.
authentication_configHydranet auth service. Hydra App handles tokens for you.
proxy_urlSingle endpoint for the proxy that fronts Electrum, Esplora, Web3, and Subgraph requests.
liquidity_configProvider for service-backed channel liquidity (used by the Lease API).
orderbook_configThe matching engine.
pricing_configPrice oracle for fiat conversion, plus the fiat_currencies you want quoted (used by the Pricing API).
referral_config(2026-07-08, optional) Referral service URL enabling CreateInvite / RedeemInvite / GetReferral.
networks[]Per-network blockchain config. Each entry is a network the app will activate.

Per-network blocks

For Bitcoin networks:

FieldDescription
networkbitcoin (mainnet), bitcoin-signet, bitcoin-testnet (testnet4), bitcoin-testnet3, bitcoin-regtest — see Network identifiers
blockchain.typeTagged: electrum (takes urls) or esplora (takes url). Required — there is no default
blockchain.urls(electrum) Electrum endpoints. All of them receive header-subscribe pushes; the first reachable one serves RPC, with failover to the next healthy server
blockchain.validate_tls(electrum, default true) Validate the Electrum server's TLS certificate. Turn off only against a server you control
blockchain.url(esplora) Esplora-compatible HTTP endpoint
blockchain.esplora_url(electrum) Esplora endpoint used as a fallback for Electrum RPCs that aren't supported, or when no Electrum server is reachable
blockchain.waterfalls_url(both variants) Waterfalls (QuickSync) endpoint
blockchain.proxy_auth(both variants) If true, Hydra App authenticates with the proxy automatically
lightning_tcp_portLightning peer TCP port
lightning_ws_portLightning peer WebSocket port (for browser clients)
lightning_relay_url(optional) WebSocket relay used to tunnel TCP peers through the proxy. For browser/wasm builds; a native node does not need it
gossip_syncLightning gossip source. Required, tagged, no default — see Gossip sync
lightning(2026-09-03, optional) Channel terms this node proposes, accepts and advertises — see Channel terms.

For EVM networks:

FieldDescription
networkethereum / arbitrum (mainnets), ethereum-sepolia, arbitrum-sepolia, … — see Network identifiers. No -mainnet suffix exists.
web3_provider.urlWebSocket Web3 RPC endpoint
web3_provider.proxy_authSame as Bitcoin — proxy auth handshake
alchemy_rpc / alchemy_ws(optional) Enriched Alchemy HTTP / WebSocket endpoints used for richer queries than the plain web3_provider. Each takes either custom_url (any URL — this is how the proxy-fronted mainnet endpoints are set) or api_key (Hydra builds the *.g.alchemy.com URL itself), plus proxy_auth. An api_key on a network Alchemy does not serve fails at boot. Omit both to rely on web3_provider alone.
lithium.state_backend(2026-07-30) Where Lithium channel state is read from. logs (the default) — contract event logs plus verified contract reads are authoritative, and the subgraph only bootstraps the routing graph. subgraph — the indexer becomes the sole, unverified source; needed only for a contract that predates the channel lifecycle events. Prefer logs.
lithium.subgraph.http_urlSubgraph endpoint Hydra App uses to index Lithium events (optional under state_backend: logs)
lithium.subgraph.ws_url(optional) WebSocket subgraph endpoint, for live subscription rather than polling
lithium.subgraph.proxy_auth(optional) Authenticate the subgraph client with the proxy
lithium.relay_url(optional) WebSocket relay for Lithium peer connections. For browser/wasm builds
lithium.contract_addressLithium contract on the network
lithium.contract_deploy_blockBlock the Lithium contract was deployed at — indexing starts here instead of from genesis. Set it to avoid a full-chain scan on first sync.
lithium.tcp_quic_port / ws_portLithium peer ports
lithium.channel_handshake etc.(2026-09-03, optional) Channel terms — see Channel terms.
htlc_factory_address(optional) HtlcFactory contract for on-chain HTLCs. Omit and that network settles through channels only. Not set on mainnet — see the mainnet template.
watchtower(optional, default false) Whether this node runs a watchtower service for others.
watchtowers(2026-09-04, optional) Watchtowers this node hands its own evidence to — see Watchtowers.
tokensTokens to activate: erc20:<contract> strings, or maps with display overrides — see Token display overrides.

For Tron networks:

FieldDescription
networktron (mainnet), tron-shasta, or tron-nile. Not part of mainnet launch.
provider.urlTron node HTTP API base URL (serves /wallet, /walletsolidity)
indexer.url(optional) Enriched indexer (TronGrid / Alchemy-Tron) for richer history queries
rpc.urleth-compatible JSON-RPC endpoint
htlc_factory_addressbase58check HTLC factory. Required for on-chain HTLC settlement on Tron — omit and that network settles through channels only.
lithium.*Same shape as EVM; contract_address is base58check (T...)
address_poll_interval_secs(optional) How often to poll the indexer for native TRX / TRC-10 activity
watchtower / watchtowersSame as EVM
tokenstrc20:<address> strings, or maps with display overrides

The token list determines which assets you can hold balances of and trade. To add a token after first run, add it here, restart the app, then call asset.AddToken for it.

On-chain HTLC settlement (trading a currency you hold on-chain without a channel — see the HTLC & Preimage API) requires an htlc_factory_address on EVM and Tron networks; Bitcoin uses native P2WSH/Taproot and needs no factory. Without it, that network settles through channels only.

Token display overrides

(2026-09-03) A tokens[] entry is either the bare id string it has always been, or a map declaring what to show for it:

tokens:
  - "erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831"    # USDC — chain's own symbol
  - id: "erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
    symbol: USDT
    name: "Tether USD"

A contract's symbol() is its deployer's choice — LayerZero's bridged Tether on Arbitrum reports USD₮0, with a non-ASCII — while every consumer of your node expects the asset's own identity. A declared symbol / name replaces the chain's wherever the node hands the token out, and is re-applied on every registration.

Rules, enforced at boot: a symbol must be a single word with no whitespace; a name must be non-blank with no leading or trailing whitespace; an id must not be listed twice on one network. Any violation names the offending token and stops the node.

There is no decimals override, by design. Decimals define the token's units and are only ever read from the chain — a declared wrong value would silently misprice every amount. The same overrides are available at runtime via asset.AddToken.

Gossip sync

Lightning pathfinding needs the gossip graph. gossip_sync is required on a Bitcoin network and is tagged by type — there is no default:

typeKeysTrade-off
rgsrgs_server_url, proxy_authDownloads a compact snapshot from a trusted server. Fast, low bandwidth, and you trust the server's view
p2pbootstrap_nodes[]Live gossip from the peers you name, as <node_id>@<host>:<port>. No trusted third party, much more bandwidth, slow to bootstrap
hybridrgs_server_url, proxy_auth, bootstrap_nodes[]An RGS snapshot to bootstrap, then live P2P gossip to stay current

On mainnet, point rgs_server_url at the public LDK server (https://rapidsync.lightningdevkit.org) with no proxy_auth — it is large and reliable enough not to need proxying. On staging it is proxy-fronted, so set proxy_auth: true alongside it.

Channel terms (optional)

(2026-09-03) What this node proposes, accepts and advertises on its channels. Every key is optional and defaults to the value the node used before the block existed, so an existing config keeps its behaviour — set only what you mean to change. A value outside the protocol floors, or one the underlying node would refuse, fails at boot naming the key.

Bitcoin networks take a top-level lightning block. Units are blocks (144/day):

BlockKeys (with defaults)
channel_handshake — what we proposeour_to_self_delay_blocks: 144 (counterparty's wait after a unilateral close; LDK floor 144), their_channel_reserve_millionths: 10000 (1%), our_max_accepted_htlcs: 483 (1..=483), minimum_depth_blocks (default = the network's own), announce_for_forwarding: true
channel_handshake_limits — what we accepttheir_to_self_delay_blocks: 2016 (longest wait we accept on our own balance; LDK ceiling 2016), max_self_reserve_millionths: 10000, min_funding_satoshis: 1000, max_minimum_depth_blocks: 144
channel_policy — ours alone, advertised via channel_updatecltv_expiry_delta_blocks: 72 (LDK floor 48), forwarding_fee_base_msat: 1000, forwarding_fee_proportional_millionths: 0

EVM / Tron networks take the same three blocks inside lithium. Units are whole seconds:

BlockKeys (with defaults)
channel_handshakedispute_period_secs: 86400 (protocol floor 3600), safety.reserve_proportional_millionths: 10000, safety.max_pending_payments: 100
channel_handshake_limitsmax_dispute_period_secs: 1209600 (14 days), min_counterparty_reserve_millionths: 1000, max_self_reserve_millionths: 100000, min_max_pending_payments: 1
channel_policycltv_expiry_delta_secs: 28800 (protocol floor 28800), routing.fee_proportional_millionths: 1000 (0.1%), routing.min_payment_amount_millionths: 0, routing.max_payment_amount_millionths: 1000000
per_assetA map of overrides keyed by canonical asset id (lowercase erc20:0x…, or trc20:T… as the base58 address is spelled). Any subset of the safety / routing keys; the rest stays node-wide.

The dispute window is a negotiation, and longer wins

A channel opens with the longer of the two peers' dispute_period_secs proposals, provided it is within both peers' max_dispute_period_secs. So:

  • Your proposal is the shortest window you can end up with.
  • Your limit is the longest — and it is how long your own funds stay locked behind a unilateral close.

Raising the proposal buys you more time to catch a peer publishing a stale state; raising the limit is what lets you open a channel with a more conservative peer at all. The mainnet template proposes two days and accepts five, mirroring the hub. The Bitcoin to_self_delay pair works the same way in blocks.

safety is bilateral: the merged reserve is the maximum of the two peers' proposals and the merged max_pending_payments is the minimum, so each side's caution is respected.

Watchtowers (optional)

(2026-09-04) An EVM / Tron network block can declare the watchtowers this node hands its channel evidence to:

watchtowers:
  - "0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://arbitrum-ws.hydranet.app:443"

Each entry takes the <node_id>@<host>:<port> form ConnectToPeer uses. The node dials them itself once its initial chain sync completes and keeps them attached from then on, so coverage survives a restart without an operator re-issuing ConnectToWatchtower. Verify with GetConnectedWatchtowers.

On mainnet the Hydranet node serves both roles: the peer string and the watchtower string are the same, and the two are reached as independent sessions on the same address. Declaring it under watchtowers opens the watchtower session; ConnectToPeer opens the channel session. Connecting one does not connect the other, so a node that wants both makes both.

⚠️ What a watchtower can and cannot defend

A watchtower acts on evidence against a channel's counterparty. It therefore cannot defend a channel whose counterparty is the watchtower — there is nobody for it to act against. With the Hydranet node in both roles, it defends your channels with other peers, and your channels with Hydranet itself rest on your own node being available to respond to a stale-state publication.

This matters most if Hydranet is your only peer, which it will be for most nodes at launch. Keeping your node online and its state backed up is the protection there; the watchtower is what extends coverage as you peer more widely.

Point watchtowers at a node that is not the counterparty of the channels you most want defended, once such a node is available to you.

Distinct from watchtower: true, which makes this node run a watchtower service for others.

Storage backends

Each of critical_db / db / archive_db is a tagged block: type selects the backend and the rest of the keys belong to it. Every tuning key is optional and falls back to the backend's own default.

type: fjall (LSM-tree, write-optimised — the native default):

KeyDefaultMeaning
cache_size_mb96Block cache of decompressed SST blocks
max_memtable_size_mb32Per-keyspace memtable before it flushes to an L0 SST. Smaller ⇒ more flushes ⇒ more compaction work
max_write_buffer_size_mb192Global cap on active memtables for this DB. 0 disables the cap (not recommended)
max_journaling_size_mb192WAL on disk. fjall requires ≥ 64 MiB. Too small blocks writes under sustained load
worker_threadsmin(cpus, 4)Background flushes and compactions. Pin to your CPU limit minus one
durabilityimmediateimmediate fsyncs on every commit; buffered returns as soon as the journal has the data

Worst-case RAM per opened fjall DB is roughly cache_size_mb + max_write_buffer_size_mb. Multiply by the number of active DBs — one per role per network — when sizing a container.

durability: buffered trades crash-safety for latency

immediate is the default and is what fund-critical state needs: LDK monitors, payment shards, the settled-payment archive. A commit does not return until it is on disk.

buffered drops the per-commit fsync. It survives a clean shutdown but can lose the most recent commits on a hard crash — so use it only for the non-critical db role, whose contents (gossip graph, sync cursors, watchtower replicas) are re-acquired from peers on restart. Never set it on critical_db.

type: redb (copy-on-write B-tree):

KeyDefaultMeaning
cache_size_mb256Cache covering both reads and writes
durabilityimmediateimmediate flushes on every commit; none does not, and can lose data on a crash
two_phase_committrueCross-database consistency
quick_repairtrueFaster recovery from a partial commit, at some write amplification

type: indexed_db is the browser backend (wasm builds only) and takes no tuning — the browser owns the storage policy.

compression

Per-DB, and also tagged. The default is type: none — the templates above set zstd explicitly because they want it:

compression:
  type: zstd
  preset: balanced     # fast (level 1) | balanced (3) | best (9)
  # level: 7           # explicit level overrides preset; valid range -7..=22

A level outside -7..=22 is rejected at boot.

Storage-backend gotchas

  • backup_config requires authentication_config. The backup service is reached with the authenticated session, so the combination is rejected at boot rather than silently dropping backups.
  • auto_prune.max_entries must be ≥ 1. Zero is not "unlimited" — omit the key for that.

auto_prune: the filters are the whole policy

(2026-08-26) settings.auto_prune takes max_age_secs and/or max_entriesand nothing else. Everything about how pruning runs (how often each network is visited, how long a chunk may hold the archive write lock, what share of the node's time goes to pruning) is derived at runtime from what the node measures about itself, and escalates automatically when retention falls behind.

interval_secs was removed. If your config still sets it, delete it. At least one filter is required, and max_age_secs has a 48-hour floor (172800): pruning a settled payment deletes its stored preimage, which an in-flight swap may still need.

Settled wallet transactions prune by both filters; settled payments prune by max_age_secs only — never by entry count, since a count-based cap could delete the preimage of a payment settled moments ago. With no max_age_secs, payments are not auto-pruned at all.

When the archive stops keeping up, the node emits a retention_lagging event with how far past the deadline it is, and prunes harder at the cost of foreground latency.

.env

# Log filter. Quiet by default, debug only the Hydra crates.
# Mirrors the .env.sample shipped with hydra-app.
RUST_LOG="none,hydra_app_bin=debug,hydra_app=debug,hydra_core=debug,hydra_evm=debug,hydra_bitcoin=debug,hydra_lithium=debug"

# Wallet credentials. Leave both empty for interactive mode; set both to
# run in daemon mode. See "Choose a mode" below + the security note.
MNEMONIC=
PASSWORD=
VariablePurpose
RUST_LOGtracing-subscriber filter. Bump any =debug to =trace for deeper diagnostics on that crate.
MNEMONICOptional BIP-39 seed. Empty → interactive mode (CLI prompt). Set → daemon mode.
PASSWORDOptional mnemonic encryption password. Required in daemon mode if the mnemonic was created with one; pass an empty string otherwise.

Those are the only env vars the binary reads. Older versions of these docs listed GRPC_PORT and DATA_PATH_NAME — those do nothing. The gRPC/JSON-RPC port comes from settings.server_port in config.yaml; the data directory name from settings.data_path_name. Setting them in .env is a no-op.

⚠️ Mnemonic & password security

Putting MNEMONIC and PASSWORD in plain .env is convenient for testnet but never do this on mainnet without protections. At minimum:

  • Mount the .env from a secret manager (Docker secrets, Kubernetes secrets, HashiCorp Vault, AWS Secrets Manager).
  • Restrict file permissions (chmod 600 .env).
  • Never commit .env — add it to .gitignore.
  • Prefer interactive mode for development; reserve daemon mode for hardened production hosts.
  • Back the mnemonic up offline before you fund the wallet. It is the only thing that recovers the funds — the on-disk channel state is not a backup, and backup_config covers the wallet, not your seed.

Step 2: Choose a mode

ModeWhen to useHow
InteractiveLocal dev, exploration, first runLeave MNEMONIC="". The app prompts you on stdin to create or unlock a wallet. Requires tty: true and stdin_open: true in compose.
DaemonBots, servers, CISet MNEMONIC and PASSWORD. The app boots the wallet automatically, no terminal needed.

For a trading bot, you almost always want daemon mode. For your first run on a new machine, use interactive mode once to confirm the wallet generates and the networks initialize cleanly.


Step 3: docker-compose.yml

services:
  hydra-app:
    # Mainnet: hydra-app-public — production code, reduced logs.
    # Staging:  ghcr.io/offchain-dex/hydra-app-tester:latest
    image: ghcr.io/offchain-dex/hydra-app-public:latest
    # tini reaps the interactive-mode child cleanly on Ctrl-C / SIGTERM.
    init: true
    env_file:
      - .env
    volumes:
      # Config: read-only mount so the container can't accidentally modify it.
      - ./config.yaml:/app/config.yaml:ro

      # Wallet + DB persistence. The binary writes to
      #   $XDG_DATA_HOME (= /root/.local/share on the official image)
      #   joined with `settings.data_path_name` from config.yaml.
      # If `data_path_name: hydra-app` (the staging default), state lives at
      #   /root/.local/share/hydra-app/
      # Mount the whole `/root/.local/share` directory so the path stays
      # correct even if you change `data_path_name` later.
      - hydra-data:/root/.local/share
    ports:
      # gRPC / JSON-RPC — must match settings.server_port in config.yaml.
      # Bound to the loopback interface ON PURPOSE: the API is unauthenticated.
      - "127.0.0.1:5003:5003"

      # Prometheus metrics — optional. Drop the line if metrics_port is unset.
      - "9090:9090"

      # Per-network peer ports. Required only for nodes that should ACCEPT
      # inbound connections (channel openings to you, liquidity providers).
      # If you only OPEN channels outbound, you can remove these entirely.
      # Match these to your config.yaml's per-network lightning_*/lithium_* ports.
      - "19735:19735"    # Bitcoin Signet — Lightning TCP
      - "19736:19736"    # Bitcoin Signet — Lightning WS (for browser peers)
      - "29980:29980"    # Ethereum Sepolia — Lithium TCP/QUIC
      - "29981:29981"    # Ethereum Sepolia — Lithium WS
      - "29982:29982"    # Arbitrum Sepolia — Lithium TCP/QUIC
      - "29983:29983"    # Arbitrum Sepolia — Lithium WS

    # Required ONLY for interactive mode (so the CLI prompt can read stdin).
    # In daemon mode (MNEMONIC + PASSWORD set in .env), drop these — the
    # container will run quietly and `docker compose logs` works as expected.
    tty: true
    stdin_open: true

    restart: unless-stopped

volumes:
  hydra-data:
    # Named volume. Survives `docker compose down`; gone after `down -v`.
    # For host-bind persistence instead, replace with:
    #   volumes:
    #     - ./hydra-data:/root/.local/share

Why this volume path matters

The Hydra App binary stores all persistent state — wallet, encrypted seed, channel state, the embedded fjall DB, interactive-mode log.txt, etc. — under:

${XDG_DATA_HOME:-$HOME/.local/share}/<settings.data_path_name>/

On the official rust:latest-based image, $HOME is /root and $XDG_DATA_HOME is unset, so the path resolves to /root/.local/share/<data_path_name>/. Mounting the parent (/root/.local/share) as a volume keeps the wallet across container restarts and image updates.

Older versions of these docs mounted /app/data — the binary never writes there, so the wallet was silently being wiped on every container recreate. If you set up using an older guide, copy your data folder out before recreating with the new mount.

Per-network peer ports

Listed ports above match the staging template (Bitcoin Signet + Ethereum Sepolia + Arbitrum Sepolia). If you enable other networks in config.yaml, look up each entry's lightning_tcp_port / lightning_ws_port / lithium.tcp_quic_port / lithium.ws_port and add a matching host:container line.

If you don't intend to receive inbound channel openings (most bots), you can drop these port lines entirely — outbound channels work without them.


Step 4: Start the app

.env has both MNEMONIC and PASSWORD set:

docker compose up -d
docker compose logs -f hydra-app

You're ready when the logs include lines like:

Nodes initialized
DEX initialized

…and the gRPC port is open. Sanity-check it from another shell:

curl -s -X POST http://127.0.0.1:5003 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"app_getNetworks"}'

You should get back the list of active networks.

Interactive mode (first run, exploration, key creation)

.env has MNEMONIC and PASSWORD empty. The container will start an interactive prompt asking for wallet type and mnemonic.

# Foreground — see the prompt directly:
docker compose up

# OR background then attach:
docker compose up -d
docker attach hydra-app    # use Ctrl-P Ctrl-Q to detach without stopping

Important: interactive mode logs to a file, not stdout. In interactive mode the binary redirects logs to <data_dir>/log.txt (i.e. /root/.local/share/<data_path_name>/log.txt inside the container) so the CLI prompt isn't trampled by log lines. docker compose logs will look mostly empty. To follow the log file:

docker compose exec hydra-app tail -F /root/.local/share/hydra-app/log.txt

In daemon mode, logs go to stdout normally and docker compose logs -f works as expected.

Stopping cleanly

docker compose stop                # gracefully stops; state preserved in the named volume
docker compose down                # stops and removes the container; state still preserved
docker compose down -v             # removes the data volume too — WIPES the wallet

init: true in the compose file ensures interactive-mode prompts shut down quickly on Ctrl-C / SIGTERM. Without it the container can take up to 10 seconds to stop because Rust's default Drop cleanup gets killed by Docker's hard timeout.

⚠️ The API port is unauthenticated — keep it on loopback

Hydra App has no authentication layer of its own. Every RPC — signing, sending, force-closing, setting allowances, cancelling orders — is reachable by anyone who can open a TCP connection to settings.server_port. The authentication_config in config.yaml is how the app authenticates to the Hydranet services; it does nothing for the port your clients connect to.

Publish the port as 127.0.0.1:5003:5003, as the compose file above does. A bare "5003:5003" binds 0.0.0.0 and, on a host without a firewall, hands the wallet to the internet. If you need remote access, use an SSH tunnel or put your own authenticating reverse proxy in front — and note that a release build sends no permissive CORS headers, so a browser page on another origin cannot reach it directly either (see Browser & gRPC-Web Specifics).


Step 5: Generate a client from the proto files

Hydra App speaks raw gRPC. To call it from your language, you need generated client stubs from the .proto schema files. The schema lives on this docs site as a downloadable bundle, plus individually browsable .proto files.

⬇ Download hydra-protos.zip

Or grab a single file:

curl -O https://docs.hydranet.ai/proto/hydra-protos.zip && unzip hydra-protos.zip -d hydra-protos
# → hydra-protos/{wallet,liquidity,orderbook,...}.proto

Each individual .proto is also at https://docs.hydranet.ai/proto/<name>.proto — handy for import paths or quick browsing.

Generate client stubs

Pick your language. All examples assume the protos are unpacked at ./hydra-protos/.

# Buf is the modern polyglot codegen tool. Install: https://buf.build/docs/installation
# Add a buf.gen.yaml describing your target languages, then:
buf generate hydra-protos
# → produces stubs for every plugin you configured

After running the relevant command, you'll have generated source files (one per .proto) you can import in your project. The exact import path depends on your language — see your generator's output.

Already have a Python client? If you've built one for your own bot, drop it in your repo and skip codegen. Want to share it back? Open a Discord ticket — we may be interested in publishing community SDKs.

Future: post-launch the protos will move to Buf Schema Registry at buf.build/offchain-dex/hydra and codegen will collapse to a one-liner: buf generate buf.build/offchain-dex/hydra. Until then, the static download above is the canonical source.


Step 6: Sanity-check from the client side

Once the app is running and you have generated stubs, hit it from your language. Examples below assume your generated code lives where the imports show.

import { AppServiceClient } from './proto/AppServiceClientPb'
import { GetNetworksRequest } from './proto/app_pb'

const client = new AppServiceClient('http://localhost:5003')

const response = await client.getNetworks(new GetNetworksRequest(), {})
for (const n of response.getNetworksList()) {
  console.log(`Network: protocol=${n.getProtocol()} id=${n.getId()}`)
}

You should see one entry per network you enabled in config.yaml.


Mainnet peers & watchtowers

Once the app is running, connect to the Hydranet node on each mainnet network you enabled. The peer string format is <node_id>@<host>:<port> — the same form ConnectToPeer and ConnectToWatchtower both take.

NetworkPeer string
Bitcoin03eaace825811aee04ea8e56d31a8927104d5f301ff2aa8998cc431903ec26289a@wss://lightning-ws.hydranet.app:443
Ethereum0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://ethereum-ws.hydranet.app:443
Arbitrum One0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://arbitrum-ws.hydranet.app:443

⚠️ The port is always 443, on every network

Hydranet's mainnet endpoints are served through Cloudflare, which cannot proxy the Lithium / Lightning ports. The hub does still listen on 19736 / 29981 / 29983 at its origin — those listeners exist so older clients can migrate — but they are not reachable through the public hostname, and they are being retired. A peer string carrying one of them times out rather than failing fast, which is the single most common way to get a silent hang instead of a connection.

Confusing matters, those are also the ports your own node listens on (lightning_ws_port, lithium.ws_port). Same numbers, different machine — yours are for peers dialling you, and neither belongs in the string you dial out with. Use 443.

The same address is also the watchtower. Declare it in the network block's watchtowers list and the node attaches on its own after the initial sync — that is what the mainnet template does. Passing the identical string to ConnectToWatchtower attaches one at runtime instead. The watchtower session is independent of the peer session, so connecting one does not connect the other.

Both EVM networks share one node identity (0x53f106…), since a single EVM node serves Ethereum and Arbitrum.


Staging peers & watchtowers

Once the app is running, connect to the Hydranet staging node on each network you enabled. The peer string format is <node_id>@<host>:<port> — the same form ConnectToPeer and ConnectToWatchtower both take.

NetworkPeer string
Bitcoin Signet03726edb9778282abf3b08cbac5114fe45e8b0d302ad278bd1dc7af5d3bb134083@wss://lightning-ws.staging.hydranet.ai:443
Ethereum Sepolia0xd78f26fdc770e4041960d3e06a58fb9b91fa5ff6@wss://ethereum-ws.staging.hydranet.ai:443
Arbitrum Sepolia0xd78f26fdc770e4041960d3e06a58fb9b91fa5ff6@wss://arbitrum-ws.staging.hydranet.ai:443
Tron ShastaTUQi2SbHaqQWYkgNwvsDRRVwUtXjwKhYwW@wss://tron-ws.staging.hydranet.ai:443

The port is always 443, on all four networks. Staging keeps origin listeners on 19736 / 29981 / 29983 / 29985 for clients still migrating, but those are not the way in — and they are the same numbers as your own lightning_ws_port / ws_port, which are for peers dialling you. Don't copy a config port into a peer string.

The same address is also the watchtower. Pass the identical string to ConnectToWatchtower to have staging hold your channel states and revocations. That session is independent of the peer session, so connecting one does not connect the other — make both calls if you want both.

Both EVM networks share one node identity (0xd78f26…), since a single EVM node serves Ethereum and Arbitrum.


Network identifiers (config string ↔ proto fields)

config.yaml uses human-readable network names. The gRPC API uses { protocol, id } integers and hex strings. Map between them:

⚠️ Mainnet names have no -mainnet suffix

The mainnet network name is the bare chain namebitcoin, ethereum, arbitrum, tron. Only testnets carry a suffix. There are no aliases: bitcoin-mainnet, ethereum-mainnet, arbitrum-one and tron-mainnet are not accepted and fail at boot with InvalidNetworkName.

Earlier revisions of this page listed the -mainnet forms. They were wrong and never parsed — if you copied a network block from this page before 2026-09-06, fix the network: line.

Bitcoin (protocol: 1 / PROTOCOL_BITCOIN)

config.yaml networkproto id (hex magic bytes)
bitcoinf9beb4d9mainnet
bitcoin-testnet1c163f28testnet4
bitcoin-testnet30b110907testnet3
bitcoin-signet0a03cf40
bitcoin-regtestfabfb5da

bitcoin-testnet is testnet4, magic 1c163f28. Testnet3 is a separate name, bitcoin-testnet3 (0b110907). This page previously mapped bitcoin-testnet to the testnet3 magic bytes — a node configured for one and addressed with the other's id will not match.

EVM (protocol: 2 / PROTOCOL_EVM)

config.yaml networkproto id (decimal chain ID)
ethereum1mainnet
arbitrum42161mainnet (Arbitrum One)
optimism10mainnet
polygon137mainnet
bsc56mainnet
ethereum-sepolia11155111
arbitrum-sepolia421614
ethereum-holesky17000

Also accepted: avalanche, fantom, gnosis, optimism-sepolia, bsc-testnet, polygon-amoy, avalanche-fuji, fantom-testnet, gnosis-chiado, ethereum-local.

Tron (protocol: 3 / PROTOCOL_TRON)

config.yaml networkproto id (decimal chain ID)
tron728126428mainnet
tron-shasta2494104990
tron-nile3448148188
tron-local3360022319TRE container

When passing a Network to any RPC, use { protocol, id }. The name and chain_id fields you may see in older client code no longer exist.


Troubleshooting

SymptomLikely causeFix
Nodes initialized never appearsConfig parse error or missing required fieldCheck docker compose logs for the YAML parse line
Boot fails with InvalidNetworkNameUsed a -mainnet suffix — ethereum-mainnet, bitcoin-mainnet, arbitrum-one, tron-mainnetMainnet names are bare: bitcoin, ethereum, arbitrum, tron. See Network identifiers
ConnectToPeer hangs, then times outPeer string carries a Lithium/Lightning port (19736, 29981, 29983) instead of 443Cloudflare cannot proxy those ports, so the public hostname answers only on 443. See Mainnet peers
Boot fails naming an auto_prune keyConfig still sets interval_secs, removed 2026-08-26Delete it — the filters are the whole policy. See auto_prune
Boot fails naming a token's symbol or nameA declared symbol contains whitespace, or a name is blank / space-paddedA symbol must be one word; a name non-blank and untrimmed. See Token display overrides
Boot fails naming a channel-terms keyA value is outside the protocol floors or what the node acceptsThe message names the key — see Channel terms
GetConnectedWatchtowers returns emptyThe node attaches declared watchtowers only after its initial chain syncNormal early in a boot. Persisting on a synced node means your channels are undefended while it is offline
A payment fails despite a sufficient balanceSized off free_local rather than max_sendable — a per-payment ceiling binds below the balanceSee max_sendable
Order rejected for precisionAmount carries more decimals than the market side allows; the orderbook rejects rather than roundsQuantise to base_precision / quote_precision first — see SwapAmount
docker compose logs is empty in interactive modeInteractive mode writes logs to <data_dir>/log.txt, not stdoutdocker compose exec hydra-app tail -F /root/.local/share/<data_path_name>/log.txt
Wallet wiped after restartOlder docs used the wrong volume mount (/app/data)Use hydra-data:/root/.local/share — the binary writes to $HOME/.local/share/<data_path_name>
failed to connect to electrumProxy auth failingConfirm your auth credentials and network connectivity to proxy_url
Wallet prompts loop in daemon modeMNEMONIC set but the mnemonic was created with a password and PASSWORD is blankSet both, or unset both for interactive
Container restart-loops on first runPort collision on hostCheck lsof -i :5003 (gRPC) and the Lightning/Lithium ports listed in compose
Container takes ~10s to stop on docker compose downinit: true missing — Docker hard-kills before Rust's cleanup completesAdd init: true to the service (already in the compose template above)
bitcoin activated / bitcoin-signet activated but no balanceWallet is brand newSend funds to the address returned by wallet_getDepositAddress. On mainnet, send a small amount first and confirm it lands before committing more.
GRPC_PORT / DATA_PATH_NAME env vars seem to do nothingThey aren't read by the binaryRemove them from .env; the values come from config.yaml

Next steps

  • Bot Quickstart — minimum runnable trading-bot template
  • Getting Started — first API calls explained
  • Common Patterns — fee structures, amounts, error handling
  • /proto — the .proto files themselves; with rpc.discover on a running app, the authoritative schema

Copyright © 2025