Setup Guide
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.
| Mainnet | Staging | |
|---|---|---|
| Funds | Real | Testnet only |
| Image | ghcr.io/offchain-dex/hydra-app-public:latest | ghcr.io/offchain-dex/hydra-app-tester:latest |
| Networks | Bitcoin, Ethereum, Arbitrum One | Bitcoin Signet, Ethereum Sepolia, Arbitrum Sepolia, Tron Shasta |
| Endpoints | Public — *.hydranet.app, shown in full below | Access-controlled — request via Discord |
| Config template | Mainnet config.yaml | Staging config.yaml |
| Peers | Mainnet peers | Staging 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 launchRunning 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 stateGive mainnet its own
settings.data_path_nameand 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/
MNEMONICyou set up below. Clients connect to the local app over plain gRPC with no API keys, no headers, no auth handshake. Theauth_grpc_url/auth_ws_urlandproxy_authfields inconfig.yamlconfigure 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:
- Redeem an invite code — the usual route. Someone already on mainnet mints one with
CreateInviteand sends it to you privately. Redeem it in the web app, or from your own node withRedeemInvite. - Ask the team to whitelist your identity key — open 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_urlinconfig.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 machineThe 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
| Mainnet | Staging | |
|---|---|---|
| Network names | bitcoin, ethereum, arbitrum | bitcoin-signet, ethereum-sepolia, arbitrum-sepolia, tron-shasta |
| Endpoints | Public *.hydranet.app | Access-controlled |
gossip_sync.rgs_server_url | Public LDK RGS, no proxy_auth | Proxy-fronted, proxy_auth: true |
alchemy_rpc / alchemy_ws | Proxy-fronted, custom_url | Proxy-fronted, custom_url |
htlc_factory_address | Absent — see below | Present on Tron |
| Tron | Not part of mainnet launch | tron-shasta available |
watchtowers | Declared per EVM network | Attach at runtime |
On-chain HTLC settlement is off at mainnet launchNeither mainnet EVM network sets
htlc_factory_address, so mainnet settles through channels only. The on-chain half of the HTLC & Preimage API —CreateHtlc,SettleHtlcPreimage, theOrderSettlementon-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_addressto the network block is the only change required.
pricing_configis 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
| Block | Purpose |
|---|---|
settings.server_port | The 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_port | Prometheus-style metrics. Optional — omit for no metrics server. Also binds 0.0.0.0. |
settings.data_path_name | On-disk directory name for the wallet/DB. Bump this (e.g. hydra-app → hydra-app-2) to start fresh. |
settings.critical_db / db / archive_db | Per-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_config | Hydranet auth service. Hydra App handles tokens for you. |
proxy_url | Single endpoint for the proxy that fronts Electrum, Esplora, Web3, and Subgraph requests. |
liquidity_config | Provider for service-backed channel liquidity (used by the Lease API). |
orderbook_config | The matching engine. |
pricing_config | Price 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:
| Field | Description |
|---|---|
network | bitcoin (mainnet), bitcoin-signet, bitcoin-testnet (testnet4), bitcoin-testnet3, bitcoin-regtest — see Network identifiers |
blockchain.type | Tagged: 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_port | Lightning peer TCP port |
lightning_ws_port | Lightning 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_sync | Lightning 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:
| Field | Description |
|---|---|
network | ethereum / arbitrum (mainnets), ethereum-sepolia, arbitrum-sepolia, … — see Network identifiers. No -mainnet suffix exists. |
web3_provider.url | WebSocket Web3 RPC endpoint |
web3_provider.proxy_auth | Same 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_url | Subgraph 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_address | Lithium contract on the network |
lithium.contract_deploy_block | Block 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_port | Lithium 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. |
tokens | Tokens to activate: erc20:<contract> strings, or maps with display overrides — see Token display overrides. |
For Tron networks:
| Field | Description |
|---|---|
network | tron (mainnet), tron-shasta, or tron-nile. Not part of mainnet launch. |
provider.url | Tron node HTTP API base URL (serves /wallet, /walletsolidity) |
indexer.url | (optional) Enriched indexer (TronGrid / Alchemy-Tron) for richer history queries |
rpc.url | eth-compatible JSON-RPC endpoint |
htlc_factory_address | base58check 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 / watchtowers | Same as EVM |
tokens | trc20:<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.AddTokenfor it.
On-chain HTLC settlement (trading a currency you hold on-chain without a channel — see the HTLC & Preimage API) requires an
htlc_factory_addresson 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
decimalsoverride, 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 viaasset.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:
type | Keys | Trade-off |
|---|---|---|
rgs | rgs_server_url, proxy_auth | Downloads a compact snapshot from a trusted server. Fast, low bandwidth, and you trust the server's view |
p2p | bootstrap_nodes[] | Live gossip from the peers you name, as <node_id>@<host>:<port>. No trusted third party, much more bandwidth, slow to bootstrap |
hybrid | rgs_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):
| Block | Keys (with defaults) |
|---|---|
channel_handshake — what we propose | our_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 accept | their_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_update | cltv_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:
| Block | Keys (with defaults) |
|---|---|
channel_handshake | dispute_period_secs: 86400 (protocol floor 3600), safety.reserve_proportional_millionths: 10000, safety.max_pending_payments: 100 |
channel_handshake_limits | max_dispute_period_secs: 1209600 (14 days), min_counterparty_reserve_millionths: 1000, max_self_reserve_millionths: 100000, min_max_pending_payments: 1 |
channel_policy | cltv_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_asset | A 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 winsA channel opens with the longer of the two peers'
dispute_period_secsproposals, 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_delaypair 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 defendA 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):
| Key | Default | Meaning |
|---|---|---|
cache_size_mb | 96 | Block cache of decompressed SST blocks |
max_memtable_size_mb | 32 | Per-keyspace memtable before it flushes to an L0 SST. Smaller ⇒ more flushes ⇒ more compaction work |
max_write_buffer_size_mb | 192 | Global cap on active memtables for this DB. 0 disables the cap (not recommended) |
max_journaling_size_mb | 192 | WAL on disk. fjall requires ≥ 64 MiB. Too small blocks writes under sustained load |
worker_threads | min(cpus, 4) | Background flushes and compactions. Pin to your CPU limit minus one |
durability | immediate | immediate 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: bufferedtrades crash-safety for latency
immediateis 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.
buffereddrops 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-criticaldbrole, whose contents (gossip graph, sync cursors, watchtower replicas) are re-acquired from peers on restart. Never set it oncritical_db.
type: redb (copy-on-write B-tree):
| Key | Default | Meaning |
|---|---|---|
cache_size_mb | 256 | Cache covering both reads and writes |
durability | immediate | immediate flushes on every commit; none does not, and can lose data on a crash |
two_phase_commit | true | Cross-database consistency |
quick_repair | true | Faster 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_configrequiresauthentication_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_entriesmust 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_entries — and 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_secswas removed. If your config still sets it, delete it. At least one filter is required, andmax_age_secshas 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=
| Variable | Purpose |
|---|---|
RUST_LOG | tracing-subscriber filter. Bump any =debug to =trace for deeper diagnostics on that crate. |
MNEMONIC | Optional BIP-39 seed. Empty → interactive mode (CLI prompt). Set → daemon mode. |
PASSWORD | Optional 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_PORTandDATA_PATH_NAME— those do nothing. The gRPC/JSON-RPC port comes fromsettings.server_portinconfig.yaml; the data directory name fromsettings.data_path_name. Setting them in.envis a no-op.
⚠️ Mnemonic & password securityPutting
MNEMONICandPASSWORDin plain.envis convenient for testnet but never do this on mainnet without protections. At minimum:
- Mount the
.envfrom 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_configcovers the wallet, not your seed.
Step 2: Choose a mode
| Mode | When to use | How |
|---|---|---|
| Interactive | Local dev, exploration, first run | Leave MNEMONIC="". The app prompts you on stdin to create or unlock a wallet. Requires tty: true and stdin_open: true in compose. |
| Daemon | Bots, servers, CI | Set 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
Daemon mode (recommended for bots, servers, CI)
.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.txtinside the container) so the CLI prompt isn't trampled by log lines.docker compose logswill look mostly empty. To follow the log file:docker compose exec hydra-app tail -F /root/.local/share/hydra-app/log.txtIn daemon mode, logs go to stdout normally and
docker compose logs -fworks 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: truein 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 defaultDropcleanup gets killed by Docker's hard timeout.
⚠️ The API port is unauthenticated — keep it on loopbackHydra 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. Theauthentication_configinconfig.yamlis 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"binds0.0.0.0and, 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.
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
.protois also athttps://docs.hydranet.ai/proto/<name>.proto— handy forimportpaths 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/hydraand 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.
| Network | Peer string |
|---|---|
| Bitcoin | 03eaace825811aee04ea8e56d31a8927104d5f301ff2aa8998cc431903ec26289a@wss://lightning-ws.hydranet.app:443 |
| Ethereum | 0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://ethereum-ws.hydranet.app:443 |
| Arbitrum One | 0x53f1067c4d85000f24d8019b8d676fbf7f321ca3@wss://arbitrum-ws.hydranet.app:443 |
⚠️ The port is always443, on every networkHydranet's mainnet endpoints are served through Cloudflare, which cannot proxy the Lithium / Lightning ports. The hub does still listen on
19736/29981/29983at 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. Use443.
The same address is also the watchtower. Declare it in the network block's
watchtowerslist and the node attaches on its own after the initial sync — that is what the mainnet template does. Passing the identical string toConnectToWatchtowerattaches 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.
| Network | Peer string |
|---|---|
| Bitcoin Signet | 03726edb9778282abf3b08cbac5114fe45e8b0d302ad278bd1dc7af5d3bb134083@wss://lightning-ws.staging.hydranet.ai:443 |
| Ethereum Sepolia | 0xd78f26fdc770e4041960d3e06a58fb9b91fa5ff6@wss://ethereum-ws.staging.hydranet.ai:443 |
| Arbitrum Sepolia | 0xd78f26fdc770e4041960d3e06a58fb9b91fa5ff6@wss://arbitrum-ws.staging.hydranet.ai:443 |
| Tron Shasta | TUQi2SbHaqQWYkgNwvsDRRVwUtXjwKhYwW@wss://tron-ws.staging.hydranet.ai:443 |
The port is always
443, on all four networks. Staging keeps origin listeners on19736/29981/29983/29985for clients still migrating, but those are not the way in — and they are the same numbers as your ownlightning_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
ConnectToWatchtowerto 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-mainnetsuffixThe mainnet network name is the bare chain name —
bitcoin,ethereum,arbitrum,tron. Only testnets carry a suffix. There are no aliases:bitcoin-mainnet,ethereum-mainnet,arbitrum-oneandtron-mainnetare not accepted and fail at boot withInvalidNetworkName.Earlier revisions of this page listed the
-mainnetforms. They were wrong and never parsed — if you copied a network block from this page before 2026-09-06, fix thenetwork:line.
Bitcoin (protocol: 1 / PROTOCOL_BITCOIN)
config.yaml network | proto id (hex magic bytes) | |
|---|---|---|
bitcoin | f9beb4d9 | mainnet |
bitcoin-testnet | 1c163f28 | testnet4 |
bitcoin-testnet3 | 0b110907 | testnet3 |
bitcoin-signet | 0a03cf40 | |
bitcoin-regtest | fabfb5da |
bitcoin-testnetis testnet4, magic1c163f28. Testnet3 is a separate name,bitcoin-testnet3(0b110907). This page previously mappedbitcoin-testnetto the testnet3 magic bytes — a node configured for one and addressed with the other'sidwill not match.
EVM (protocol: 2 / PROTOCOL_EVM)
config.yaml network | proto id (decimal chain ID) | |
|---|---|---|
ethereum | 1 | mainnet |
arbitrum | 42161 | mainnet (Arbitrum One) |
optimism | 10 | mainnet |
polygon | 137 | mainnet |
bsc | 56 | mainnet |
ethereum-sepolia | 11155111 | |
arbitrum-sepolia | 421614 | |
ethereum-holesky | 17000 |
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 network | proto id (decimal chain ID) | |
|---|---|---|
tron | 728126428 | mainnet |
tron-shasta | 2494104990 | |
tron-nile | 3448148188 | |
tron-local | 3360022319 | TRE container |
When passing a
Networkto any RPC, use{ protocol, id }. Thenameandchain_idfields you may see in older client code no longer exist.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Nodes initialized never appears | Config parse error or missing required field | Check docker compose logs for the YAML parse line |
Boot fails with InvalidNetworkName | Used a -mainnet suffix — ethereum-mainnet, bitcoin-mainnet, arbitrum-one, tron-mainnet | Mainnet names are bare: bitcoin, ethereum, arbitrum, tron. See Network identifiers |
ConnectToPeer hangs, then times out | Peer string carries a Lithium/Lightning port (19736, 29981, 29983) instead of 443 | Cloudflare cannot proxy those ports, so the public hostname answers only on 443. See Mainnet peers |
Boot fails naming an auto_prune key | Config still sets interval_secs, removed 2026-08-26 | Delete it — the filters are the whole policy. See auto_prune |
Boot fails naming a token's symbol or name | A declared symbol contains whitespace, or a name is blank / space-padded | A symbol must be one word; a name non-blank and untrimmed. See Token display overrides |
| Boot fails naming a channel-terms key | A value is outside the protocol floors or what the node accepts | The message names the key — see Channel terms |
GetConnectedWatchtowers returns empty | The node attaches declared watchtowers only after its initial chain sync | Normal early in a boot. Persisting on a synced node means your channels are undefended while it is offline |
| A payment fails despite a sufficient balance | Sized off free_local rather than max_sendable — a per-payment ceiling binds below the balance | See max_sendable |
| Order rejected for precision | Amount carries more decimals than the market side allows; the orderbook rejects rather than rounds | Quantise to base_precision / quote_precision first — see SwapAmount |
docker compose logs is empty in interactive mode | Interactive mode writes logs to <data_dir>/log.txt, not stdout | docker compose exec hydra-app tail -F /root/.local/share/<data_path_name>/log.txt |
| Wallet wiped after restart | Older 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 electrum | Proxy auth failing | Confirm your auth credentials and network connectivity to proxy_url |
| Wallet prompts loop in daemon mode | MNEMONIC set but the mnemonic was created with a password and PASSWORD is blank | Set both, or unset both for interactive |
| Container restart-loops on first run | Port collision on host | Check lsof -i :5003 (gRPC) and the Lightning/Lithium ports listed in compose |
Container takes ~10s to stop on docker compose down | init: true missing — Docker hard-kills before Rust's cleanup completes | Add init: true to the service (already in the compose template above) |
bitcoin activated / bitcoin-signet activated but no balance | Wallet is brand new | Send 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 nothing | They aren't read by the binary | Remove 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.protofiles themselves; withrpc.discoveron a running app, the authoritative schema