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 testnet wallet for whatever networks you enable (Bitcoin Signet, Ethereum Sepolia, Arbitrum Sepolia)
Building a bot or trading agent? After completing the setup below, jump to the Bot Quickstart for a minimum runnable template.
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.
Step 1: Configuration files
Create a working directory and two files: config.yaml and .env.
mkdir hydra-app && cd hydra-app
config.yaml
Endpoints redacted. The staging 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 and do not redistribute. The structure of this file (every field name, every nesting level) is accurate; only the access-controlled values are masked.
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-3
critical_db_option: fjall
db_option: fjall
compression:
type: zstd
preset: balanced
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
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: https://rapidsync.lightningdevkit.org
- protocol: evm
network: ethereum-sepolia
web3_provider:
url: <ethereum-sepolia-web3-ws-url> # provided by the team
proxy_auth: true
lithium:
subgraph:
http_url: <ethereum-subgraph-url> # provided by the team
contract_address: <lithium-eth-contract> # provided by the team
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
lithium:
subgraph:
http_url: <arbitrum-subgraph-url> # provided by the team
contract_address: <lithium-arb-contract> # provided by the team
tcp_quic_port: 29982
ws_port: 29983
tokens:
# Add the asset contracts you want active on this network.
# The team provides a list alongside the endpoint values.
- "ERC20:<arbitrum-asset-1>"
- "ERC20:<arbitrum-asset-2>"
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, RGS gossip server, 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 port your client connects to (here 5003). Map this in Docker. |
settings.metrics_port | Prometheus-style metrics. Optional — expose if you want monitoring. |
settings.data_path_name | On-disk directory name for the wallet/DB. Bump this (e.g. hydra-app-3 → hydra-app-4) to start fresh. |
settings.db_option / critical_db_option | Storage backend. fjall is the current default. |
settings.compression | On-disk compression. zstd + balanced is sensible. |
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. |
networks[] | Per-network blockchain config. Each entry is a network the app will activate. |
Per-network blocks
For Bitcoin networks:
| Field | Description |
|---|---|
network | One of bitcoin-mainnet, bitcoin-signet, bitcoin-testnet, bitcoin-regtest |
blockchain.type | electrum is currently the only supported type |
blockchain.urls | Electrum WebSocket endpoints |
blockchain.esplora_url | Esplora HTTP endpoint for richer queries |
blockchain.waterfalls_url | Waterfalls (block explorer) endpoint |
blockchain.proxy_auth | 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) |
gossip_sync | Lightning gossip source. rgs (Rapid Gossip Sync) is the default. |
For EVM networks:
| Field | Description |
|---|---|
network | e.g. ethereum-mainnet, ethereum-sepolia, arbitrum-one, arbitrum-sepolia |
web3_provider.url | WebSocket Web3 RPC endpoint |
web3_provider.proxy_auth | Same as Bitcoin — proxy auth handshake |
lithium.subgraph.http_url | Subgraph endpoint Hydra App uses to index Lithium events |
lithium.contract_address | Lithium contract on the network |
lithium.tcp_quic_port / ws_port | Lithium peer ports |
tokens | List of ERC20:<contract> strings for tokens you want active |
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.
.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.
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:
image: ghcr.io/offchain-dex/hydra-app-tester: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-3` (the staging default), state lives at
# /root/.local/share/hydra-app-3/
# 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.
- "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-3/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.
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.
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:
Bitcoin (protocol: 1 / PROTOCOL_BITCOIN)
config.yaml network | proto id (hex magic bytes) |
|---|---|
bitcoin-mainnet | f9beb4d9 |
bitcoin-testnet | 0b110907 |
bitcoin-signet | 0a03cf40 |
bitcoin-regtest | fabfb5da |
EVM (protocol: 2 / PROTOCOL_EVM)
config.yaml network | proto id (decimal chain ID) |
|---|---|
ethereum-mainnet | 1 |
ethereum-sepolia | 11155111 |
arbitrum-one | 42161 |
arbitrum-sepolia | 421614 |
polygon-mainnet | 137 |
optimism-mainnet | 10 |
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 |
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-signet activated but no balance | Wallet is brand new | Send testnet sats to the address returned by wallet_getDepositAddress |
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
- API Reference — complete proto reference