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 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/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.


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

BlockPurpose
settings.server_portThe gRPC port your client connects to (here 5003). Map this in Docker.
settings.metrics_portPrometheus-style metrics. Optional — expose if you want monitoring.
settings.data_path_nameOn-disk directory name for the wallet/DB. Bump this (e.g. hydra-app-3hydra-app-4) to start fresh.
settings.db_option / critical_db_optionStorage backend. fjall is the current default.
settings.compressionOn-disk compression. zstd + balanced is sensible.
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.
networks[]Per-network blockchain config. Each entry is a network the app will activate.

Per-network blocks

For Bitcoin networks:

FieldDescription
networkOne of bitcoin-mainnet, bitcoin-signet, bitcoin-testnet, bitcoin-regtest
blockchain.typeelectrum is currently the only supported type
blockchain.urlsElectrum WebSocket endpoints
blockchain.esplora_urlEsplora HTTP endpoint for richer queries
blockchain.waterfalls_urlWaterfalls (block explorer) endpoint
blockchain.proxy_authIf true, Hydra App authenticates with the proxy automatically
lightning_tcp_portLightning peer TCP port
lightning_ws_portLightning peer WebSocket port (for browser clients)
gossip_syncLightning gossip source. rgs (Rapid Gossip Sync) is the default.

For EVM networks:

FieldDescription
networke.g. ethereum-mainnet, ethereum-sepolia, arbitrum-one, arbitrum-sepolia
web3_provider.urlWebSocket Web3 RPC endpoint
web3_provider.proxy_authSame as Bitcoin — proxy auth handshake
lithium.subgraph.http_urlSubgraph endpoint Hydra App uses to index Lithium events
lithium.contract_addressLithium contract on the network
lithium.tcp_quic_port / ws_portLithium peer ports
tokensList 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.AddToken for 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=
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.

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:
    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

.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-3/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.


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.


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 networkproto id (hex magic bytes)
bitcoin-mainnetf9beb4d9
bitcoin-testnet0b110907
bitcoin-signet0a03cf40
bitcoin-regtestfabfb5da

EVM (protocol: 2 / PROTOCOL_EVM)

config.yaml networkproto id (decimal chain ID)
ethereum-mainnet1
ethereum-sepolia11155111
arbitrum-one42161
arbitrum-sepolia421614
polygon-mainnet137
optimism-mainnet10

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
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-signet activated but no balanceWallet is brand newSend testnet sats to the address returned by wallet_getDepositAddress
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


Copyright © 2025