Like the hundred-headed serpent of Greek myth, Ladon generates as many addresses as you need.
Fast, minimal, multi-chain HD wallet CLI, library, and address-pool daemon — EVM, Bitcoin, Solana.
cargo install ladon| Feature | Default | Description |
|---|---|---|
pool |
yes | Async runtime (tokio) + database support (sqlx) for the pool daemon |
sqlite |
yes | SQLite backend for pool mode (implies pool) |
postgres |
no | Postgres backend for pool mode (implies pool) |
pg |
no | Alias for postgres |
full |
no | Enables sqlite + postgres |
SQLite works by default. Enable Postgres with:
cargo install ladon --features pg
# or
cargo build --features postgresladon has three sub-commands. pool is the default — when no sub-command is given, it runs the address-pool daemon.
| Sub-command | Purpose |
|---|---|
derive |
Derive one or more addresses and print to stdout |
decrypt |
Decrypt an encrypted wallet file |
pool |
Run the address-pool daemon (requires [ladon] in config, default) |
All settings live in Config.toml (or any path passed with --config/-C).
Per-flag overrides are available on derive for ad-hoc use.
ladon --config /etc/ladon/Config.toml
# Equivalent to:
ladon --config /etc/ladon/Config.toml poolSee Config.toml for a fully annotated example.
Output goes to stdout. Redirect it to whatever format you need:
# JSON (default) — pipe or redirect
ladon derive --chain evm --num 5
ladon derive --chain evm --num 20 > wallet.json
# CSV
ladon derive --chain evm --num 20 --format csv > wallet.csv
# Plain text (one address per line)
ladon derive --chain solana --num 10 --format text > addresses.txt
# Encrypted output
ladon derive --chain evm --num 5 --encrypt --password "secret" > wallet.enc
# Bitcoin from existing mnemonic
ladon derive --chain btc --mnemonic "word1 ... word12"
# Specific indexes or ranges
ladon derive --chain evm --indexes "0,5,22-44,55,66-109"
# Watch-only from xpub
ladon derive --chain evm --xpub xpub6C...
# From xpriv
ladon derive --chain evm --xpriv xprv9s...| Flag | Default | Description |
|---|---|---|
--chain / -c |
evm |
evm/eip155[:id], btc/bip122[:id], solana[:id] |
--num / -n |
1 |
Number of addresses |
--index / -i |
— | Single specific index |
--indexes |
— | Comma-separated indexes/ranges |
--account |
0 |
BIP-44 account |
--change |
0 |
BIP-44 change |
--network |
bitcoin |
Bitcoin network |
--passphrase |
"" |
BIP-39 passphrase |
--strength / -s |
12 |
Mnemonic word count |
--solana-mode |
full |
full, cold-export, hsm-sim, pda |
--program-id |
— | Base58 program ID for PDA mode |
--xpub |
— | Watch-only from xpub |
--xpriv |
— | Derive from xpriv |
--format / -f |
json |
json, csv, text |
--encrypt |
false | Encrypt output |
--password |
— | Encryption password |
ladon decrypt wallet.enc --password "secret"
ladon decrypt wallet.enc --password "secret" > wallet.jsonThe pool sub-command runs a long-lived service that:
- Connects to a SQLite or Postgres database.
- Polls the pool table on a configurable interval.
- Derives and inserts new addresses when the count drops below
[ladon.pool].threshold. - Keeps the total at
[ladon.pool].targetaddresses per chain.
Available rows have is_used IS NULL. When assigning an address, your application
should retrieve the oldest available row first, ordered by ascending index per chain.
Then either set is_used = true or delete the assigned row.
Marking assigned rows with is_used = true is the safest mode: used rows no longer
count toward the available pool, but they still preserve the maximum generated index.
If your application deletes assigned rows instead, at least one highest-index row must
remain in the table for each chain. Otherwise Ladon cannot distinguish a never-filled
pool from a fully-consumed pool and will restart generation at the chain's configured
start_index value, which defaults to 0.
Set start_index on a [[ladon.derive.chains]] entry to control the first index
generated for a brand-new chain pool. Once any row exists for that chain, Ladon always
continues from MAX(index) + 1.
[ladon]
store = "ladon"
[ladon.derive.secret]
kind = "env"
var = "LADON_MNEMONIC"
[[ladon.derive.chains]]
name = "evm"
start_index = 1
[ladon.pool]
target = 1000
threshold = 200
batch = 100
interval_secs = 10
[ladon.table]
name = "derived_addresses"
[ladon.table.columns]
id = "id"
chain = "chain"
address = "address"
path = "path"
index = "index"
is_used = "is_used"
created_at = "created_at"
[stores.ladon]
driver = "sqlite"
url = "sqlite://data/ladon/addresses.db"Ladon can share a merged Config.toml with other packages (Pano, Bria, Oracles).
In that case the file contains shared root sections ([stores], [chains], …) plus
package-specific namespaces ([ladon], [pano], …).
ladon --config ../Config.toml poolLadon reads:
[stores.<id>]— resolved via[ladon].store(defaults to"ladon")[ladon]— all pool/derivation/table settings[chains.<id>]— optional shared chain metadata referenced viachain = "<id>"in[[ladon.derive.chains]]
Ladon ignores [pano], [bria], and [oracles] sections. Unknown fields inside
[ladon] are rejected with an actionable error.
Config values support ${VAR} and ${VAR:-default} syntax:
[ladon.derive.secret]
kind = "env"
var = "${LADON_MNEMONIC_VAR_NAME}"[stores.ladon]
driver = "postgres"
url = "${DATABASE_URL:-postgres://localhost/ladon}"A missing ${VAR} without a default fails with an error naming the variable.
| Key | Default | Description |
|---|---|---|
enabled |
true |
Whether deployment tooling should start Ladon |
store |
"ladon" |
Store id from [stores] used by pool mode |
| Key | Default | Description |
|---|---|---|
format |
"json" |
Default output format: json, csv, text |
strength |
12 |
Mnemonic word count: 12 or 24 |
account |
0 |
Default BIP-44 account |
change |
0 |
Default BIP-44 change branch |
num |
1 |
Default number of addresses |
encrypt |
false |
Whether to encrypt output by default |
| Key | Description |
|---|---|
kind |
"env" (mnemonic from env var), "xpriv_env" (raw hex xpriv), or "file" (mnemonic from file) |
var |
Environment variable name (for env and xpriv_env) |
passphrase_var |
Optional BIP-39 passphrase env var |
path |
File path (for file kind) |
One entry per blockchain.
| Key | Default | Description |
|---|---|---|
name |
— | "evm", "btc", or "solana" |
chain |
"" |
Optional shared chain id from [chains.<id>] |
account |
0 |
BIP-44 account |
change |
0 |
BIP-44 change branch |
start_index |
0 |
First pool index for a new chain |
network |
"bitcoin" |
BTC network: bitcoin, testnet, signet, regtest |
solana_mode |
"full" |
Solana mode: full, cold-export, hsm-sim, pda |
program_id |
"" |
Base58 program ID for PDA mode |
| Key | Default | Description |
|---|---|---|
target |
1000 |
Keep this many addresses in the pool |
threshold |
200 |
Refill when count drops below this |
batch |
100 |
Derive this many at a time |
interval_secs |
10 |
Poll database every N seconds |
| Key | Default | Description |
|---|---|---|
name |
"derived_addresses" |
Address pool table name |
| Key | Default | Description |
|---|---|---|
id |
"id" |
Primary key column (ULID text) |
chain |
"chain" |
Chain identifier column |
address |
"address" |
Address column |
path |
"path" |
Derivation path column |
index |
"index" |
HD index column |
is_used |
"is_used" |
Usage marker (NULL = available) |
created_at |
"created_at" |
ISO-8601 creation timestamp |
| Key | Default | Description |
|---|---|---|
driver |
— | "sqlite" or "postgres" |
url |
— | Connection URL. SQLite: sqlite://path; Postgres: postgres://… |
migrate |
true |
Whether to create/update schema automatically |
connect_timeout_secs |
10 |
Connection open timeout |
max_connections |
1 (SQLite), 5 (Postgres) |
Maximum connections |
No extra feature flags needed. SQLite is the default backend.
[stores.ladon]
driver = "sqlite"
url = "sqlite://data/ladon/addresses.db"
max_connections = 1Requires --features pg or --features postgres. Without the feature, a
Postgres store fails with a clear error:
Store `ladon` requires driver `postgres`, but the `pg`/`postgres` feature is
not enabled. Recompile with `--features pg` or `--features postgres`.
[stores.ladon]
driver = "postgres"
url = "${DATABASE_URL}"
max_connections = 5Published images are available from GitHub Container Registry. The image does
not include a config file. Mount your environment-specific Config.toml at
runtime and provide secrets through environment variables.
docker run --rm \
-e LADON_MNEMONIC="word1 word2 ... word12" \
-e DATABASE_URL="postgres://user:password@host:5432/ladon" \
-v "$PWD/Config.toml:/app/Config.toml:ro" \
ghcr.io/melonask/ladon:latestFor SQLite-backed pool mode, also mount a writable data directory:
docker run --rm \
-e LADON_MNEMONIC="word1 word2 ... word12" \
-v "$PWD/Config.toml:/app/Config.toml:ro" \
-v "$PWD/data:/app/data" \
ghcr.io/melonask/ladon:latestThe default container command is pool. Override it to run other commands:
docker run --rm ghcr.io/melonask/ladon:latest derive --chain evm --num 5
docker run --rm \
-v "$PWD/prod.toml:/config/Config.toml:ro" \
ghcr.io/melonask/ladon:latest --config /config/Config.toml# Start the pool daemon with Postgres using deploy/docker-compose.yml
export LADON_IMAGE="ghcr.io/melonask/ladon:latest"
docker compose -f deploy/docker-compose.yml up -dSet LADON_IMAGE and LADON_MNEMONIC in the environment or in deploy/.env.
The compose file points DATABASE_URL at its bundled Postgres service. Edit it
if you want to use an external database.
The container uses restart: unless-stopped so it recovers automatically.
sudo cp deploy/ladon.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ladonStore secrets in /etc/ladon/env:
LADON_MNEMONIC=word1 word2 ... word12
DATABASE_URL=postgres://ladon:secret@localhost/ladon
RUST_LOG=ladon=info
use ladon::{derive, decrypt_data, encrypt_data, Params, WalletOutput};
let wallet: WalletOutput = derive(Params {
chain: "evm".into(),
num: 5,
..Default::default()
})?;
for key in &wallet.keys {
println!("{}: {}", key.index, key.address);
}| Variable | Used by | Description |
|---|---|---|
LADON_MNEMONIC |
pool / config | BIP-39 mnemonic (when [ladon.derive.secret].kind = "env") |
LADON_PASSPHRASE |
pool / config | Optional BIP-39 passphrase |
DATABASE_URL |
pool / pg config | Postgres connection URL (fallback) |
RUST_LOG |
all | Tracing filter (e.g. ladon=info) |
Config values may reference any environment variable with ${VAR} or
${VAR:-default} syntax.
| Chain | Default path |
|---|---|
| EVM | m/44'/60'/0'/0/* |
| Bitcoin | m/44'/0'/0'/0/* |
| Solana | m/44'/501'/0'/0' (hardened, SLIP-0010) |
- Private keys are zeroized on drop.
- Use
--solana-mode cold-exportor--encryptwhen storing derive output. --passwordon the CLI is visible in shell history. Prefer environment-specific secret management (LADON_MNEMONIC, vault agents, etc.) in automation.- Ed25519 derivation follows SLIP-0010 (all path segments hardened).
- In the pool daemon, the mnemonic is never written to disk — it lives only in the process environment.
# Run all tests (default features: pool + sqlite)
cargo test
# Run with Postgres feature
cargo test --features pg
# Build the library only (no tokio/sqlx dependency)
cargo build --lib --no-default-features# Unit and integration tests with default SQLite-backed pool support
cargo test
# Include Postgres-specific config paths
cargo test --features pgEnd-to-end coverage is exercised through the Pano multi-service scenario in
../pano/tests/e2e/, which runs Ladon with a namespaced [ladon] config,
shared [chains], and a PostgreSQL [stores.ladon] profile.
MIT