wavecli exit batch --outpoint TXID1:0 --outpoint TXID2:1 submits one
cooperative batch. See cooperative exit batches
for partial admission and unknown-outcome handling.
This guide covers installing, configuring, and operating the Ark client
daemon (waved) and its CLI (wavecli).
Requires Go 1.26+ (see go.mod).
git clone https://github.com/lightninglabs/wavelength.git
cd wavelength
# Build both binaries into bin/
make build
# Or install to $GOPATH/bin
make installAfter building, two binaries are produced:
bin/waved-- the long-running daemon processbin/wavecli-- the CLI for controlling the daemon
The "user-facing" verbs in the CLI (balance, recv, send, activity,
create, unlock) route through the wavewalletrpc subserver. That code
is gated behind the wavewalletrpc build tag; the default make build does
not enable it. Without the tag, those verbs return:
daemon was not built with -tags wavewalletrpc;
rebuild with `make build-wavewalletrpc` or see docs/wavewalletrpc_build.md
Two options:
- Use the
ark *anddev *subtrees — same RPCs as the top-level verbs but exposed under power-user parents. Works with the default build. - Build with the tag enabled when you want the top-level verbs:
make build-wavewalletrpc # produces wavewalletrpc-enabled waved make install-wavewalletrpc
See wavewalletrpc_build.md for more.
Command results are written to stdout. Failures are written to stderr as one
JSON envelope with stable code, message, and retryable fields, plus
details and remediation when useful:
{
"error": {
"code": "UNAVAILABLE",
"message": "connection refused",
"remediation": "verify waved is running and --rpcserver points to it, then retry",
"retryable": true
}
}The process exits with 2 for invalid arguments, 3 for authentication or
authorization failures, 4 for missing resources, and 5 when a valid
fund-moving command needs explicit confirmation on non-interactive stdin.
Other failures exit with 1. A successful --dry-run is a normal result: it
prints a JSON preview containing "dry_run": true and exits with 0.
If send needs confirmation but stdin is not interactive, its prepared amount,
fee, rail, and destination preview is written to stderr before the
CONFIRMATION_REQUIRED envelope. Review it and rerun with --yes.
waved supports two wallet backends: lwwallet (standalone,
in-process) and lnd (uses an existing lnd node).
The lightweight wallet requires only an Esplora API endpoint for chain access. No external lnd node is needed.
waved \
--network=regtest \
--wallet.type=lwwallet \
--wallet.esploraurl=http://localhost:3000 \
--server.host=localhost:10010 \
--server.insecure \
--rpc.listenaddr=localhost:10029Uses an existing lnd node for signing and key derivation. Point the daemon at the lnd gRPC interface with the TLS cert and admin macaroon.
waved \
--network=regtest \
--wallet.type=lnd \
--lnd.host=localhost:10009 \
--lnd.tlspath=~/.lnd/tls.cert \
--lnd.macaroonpath=~/.lnd/data/chain/bitcoin/regtest/admin.macaroon \
--server.host=localhost:10010 \
--server.insecure \
--rpc.listenaddr=localhost:10029| Flag | Default | Description |
|---|---|---|
--datadir |
~/.waved |
Root data directory for all daemon state |
--network |
mainnet |
Bitcoin network: mainnet, testnet, testnet4, regtest, simnet, signet |
--debuglevel |
info |
Logging verbosity: trace, debug, info, warn, error, critical |
--logdir |
~/.waved/logs/<network> |
Directory for persistent daemon logs |
--allow-mainnet |
false |
Required to run on mainnet (safety guard) |
--wallet.type |
lwwallet |
Wallet backend: lwwallet, lnd, or btcwallet |
--wallet.esploraurl |
network default | Esplora REST API URL (lwwallet only); required on regtest/simnet, see docs/signet.md |
--wallet.feeurl |
network default | Fee-estimate JSON endpoint URL (btcwallet only); required on regtest/simnet, see docs/signet.md |
--wallet.btcwallet_blockheaderssource |
Block header import source for btcwallet fast sync | |
--wallet.btcwallet_filterheaderssource |
Filter header import source for btcwallet fast sync | |
--wallet.pollinterval |
30s |
Esplora poll interval (lwwallet only) |
--wallet.recoverywindow |
100 |
Address look-ahead window (lwwallet only) |
--wallet.dbbackend |
bolt |
Wallet database backend (lwwallet only): bolt or sqlite; fixed once the wallet is created |
--wallet.password_file |
Auto-unlock password file path (lwwallet/btcwallet) | |
--lnd.host |
localhost:10009 |
lnd gRPC address |
--lnd.tlspath |
Path to lnd TLS certificate | |
--lnd.macaroonpath |
Path to lnd admin macaroon | |
--lnd.rpctimeout |
30s |
Timeout for lnd RPC calls |
--lnd.account |
(empty) | lnd wallet account to spend from; empty uses lnd's default account. Set when sharing an lnd node with another daemon. The account must already exist and be taproot-scoped; a watch-only account is accepted only on a remote-signing lnd (one whose default account is itself watch-only) and must report an extended public key and derivation path |
--assets.backend |
none |
Taproot Assets backend: none refuses asset VTXOs, tapd verifies their proofs through a tapd node, adding issuance proofs of unknown assets to its universe |
--assets.tapd.host |
tapd gRPC address; required with --assets.backend=tapd |
|
--assets.tapd.tlspath |
Path to tapd TLS certificate | |
--assets.tapd.macaroonpath |
Path to tapd admin macaroon | |
--assets.tapd.rpctimeout |
30s |
Timeout for tapd RPC calls |
--server.host |
network default | Ark operator address override for the selected transport |
--server.transport |
grpc |
Ark operator transport: grpc or rest |
--server.insecure |
false |
Disable TLS for server connection |
--server.tlscertpath |
Operator TLS certificate path | |
--rpc.listenaddr |
localhost:10029 |
Daemon gRPC listen address |
--rpc.tlscertpath |
Custom TLS cert for daemon RPC | |
--rpc.tlskeypath |
Custom TLS key for daemon RPC | |
--swap.serveraddress |
network default | Swap server address override for swapruntime builds |
--swap.servertransport |
grpc |
Swap server transport: grpc or rest |
--maxpaymentcltv |
480 in swap-enabled builds |
Largest total Lightning payment CLTV reserved by automatic VTXO refresh; 0 disables the payment reserve |
Empty Ark and swap addresses resolve from the selected network and transport. See signet.md for the testnet3, testnet4, and signet endpoints and override examples.
There is no mailbox-ID flag: the client and compound server mailbox IDs are derived automatically from the client's identity key at connect time.
All flags can be set via environment variables with the WAVED_
prefix and dots replaced by underscores:
| Variable | Description |
|---|---|
WAVED_WALLET_PASSWORD |
Wallet password for create/unlock |
WAVED_LWWALLET_SEED |
Hex-encoded raw seed (dev/CI only). Without WAVED_WALLET_PASSWORD the wallet uses a public dev password, which mainnet refuses unless allow-insecure-mainnet is set |
WAVED_LWWALLET_SEED_BIRTHDAY |
Creation date of that seed, YYYY-MM-DD or RFC3339 |
WAVED_NETWORK |
Bitcoin network override |
WAVED_WALLET_TYPE |
Wallet backend type |
WAVED_WALLET_ESPLORAURL |
Esplora URL |
WAVED_SERVER_HOST |
Operator server address |
A raw seed carries no birthday, unlike an aezeed mnemonic, so a wallet
created from WAVED_LWWALLET_SEED without WAVED_LWWALLET_SEED_BIRTHDAY
starts scanning at genesis. That is slow but never misses funds. Set the
birthday only when the seed is known to have no history before that date:
too early only costs scan time, while too late silently hides earlier
funds. A malformed date is rejected rather than treated as unset.
The lwwallet backend usually resolves its own start height from the
Esplora address index and ignores this, so it matters most for the
neutrino-backed btcwallet backend, which has no index to consult.
After starting the daemon, the wallet must be created and unlocked before any operations can proceed.
The create and unlock CLI commands require a daemon built with
wavewalletrpc (see Installation above). For the default build, configure
auto-unlock via --wallet.password_file and skip the CLI step.
In lwwallet mode, wallet creation generates a new aezeed mnemonic and creates the wallet database with its key material encrypted under your password. Record the mnemonic offline: it is shown once and is the only backup.
# Via environment variable (recommended for automation)
WAVED_WALLET_PASSWORD=your_password wavecli create
# Via explicit stdin (never consumed unless requested)
printf '%s\n' 'your_password' | wavecli create --password-stdin
# Via password file
wavecli create \
--wallet-password-file=/path/to/password_file
# Interactive (prompts for password on TTY)
wavecli createImportant: The mnemonic is displayed on stderr during creation. Write it down and store it securely -- it is your only backup.
Each time the daemon restarts, the wallet must be unlocked:
WAVED_WALLET_PASSWORD=your_password wavecli unlockTo skip the manual unlock step, provide the password file at daemon startup:
waved \
--wallet.type=lwwallet \
--wallet.password_file=/path/to/password_file \
...The daemon reads the file, decrypts the seed, and starts the wallet automatically.
wavecli connects to the daemon's gRPC server. All output is JSON.
wavecli authenticates to the daemon over TLS with the daemon's admin
macaroon. By default it derives both from the daemon data directory:
- TLS cert:
<datadir>/data/<network>/tls.cert - Macaroon:
<datadir>/data/<network>/admin.macaroon
where --datadir defaults to ~/.waved and --network to mainnet. A
daemon run on a non-default datadir or network must be matched on the CLI,
otherwise wavecli looks under ~/.waved/data/mainnet/ and fails with
read macaroon: ... no such file. For a signet instance under
~/.waved-signet:
wavecli --network=signet --datadir=~/.waved-signet getinfoThe daemon also writes readonly.macaroon beside the admin credential. Use
the admin macaroon only for administration and provisioning. An admin client
can create a least-privilege credential with wavecli bakemacaroon; the
command accepts either a known entity:action permission or an exact
uri:/package.Service/Method grant. Exact URI grants are useful when one
method shares a broader write permission with unrelated operations.
For example, this payment credential can inspect balances and activity and spend through the two-phase send flow, including a full-balance on-chain send. It cannot call Deposit, SweepWallet, Exit, wallet-signing, recovery, or administrative RPCs:
wavecli bakemacaroon \
--save-to ~/.waved/payment.macaroon \
--valid-for 3600 \
uri:/wavewalletrpc.WalletService/Balance \
uri:/wavewalletrpc.WalletService/List \
uri:/wavewalletrpc.WalletService/PrepareSend \
uri:/wavewalletrpc.WalletService/Send \
uri:/wavewalletrpc.WalletInspectionService/InspectActivityThe saved file contains the binary macaroon with mode 0600. Point the
integration at it with --macaroonpath ~/.waved/payment.macaroon. Omitting
--save-to prints the hex-encoded macaroon as JSON. BakeMacaroon requires
the admin-only macaroon:generate permission. A credential carrying that
permission, or the exact BakeMacaroon URI, can mint any supported permission
and must also be treated as an admin credential. Root key ID 0 uses the
same default root key as admin.macaroon and readonly.macaroon.
ListPermissions exposes the active RPC-to-permission map to authenticated
clients.
A macaroon cannot ride an unencrypted connection, so --no-tls and the
macaroon are mutually exclusive. There are two working modes:
- TLS + macaroon (default). Point
--datadir/--networkat the daemon, or override--tlscertpath/--macaroonpathdirectly. - Plaintext (regtest / dev). Pass both
--no-tlsand--no-macaroons. Passing only--no-tlsfails withcredentials require transport level security, because the CLI still tries to attach the macaroon.
The command examples below omit these connection flags. Set them once via an alias:
# signet instance over TLS
alias wave='wavecli --network=signet --datadir=~/.waved-signet'
# local regtest over plaintext
alias wave='wavecli --no-tls --no-macaroons'The everyday wallet verbs and daemon introspection make up the default
--help face. The advanced ark, recovery, and dev subtrees are
hidden from --help (set WAVELENGTH_DEV=1 to reveal them under an "Advanced"
group) but stay fully runnable in every build — wavecli ark … works
with or without the env var. WAVELENGTH_DEV only changes visibility; it never
gates execution.
The swap subtree was retired — send/recv --offchain and activity
cover it, and a stale wavecli swap … fails with a hint toward
send/recv. The swapruntime daemon runtime that powers the offchain
verbs is unchanged.
wavecli
├── getinfo — daemon status (no wavewalletrpc)
├── bakemacaroon — provision a scoped daemon credential
├── balance — wallet balances (wavewalletrpc)
├── create / unlock — wallet bring-up (wavewalletrpc)
├── recv — boarding address / Lightning invoice (wavewalletrpc)
├── send — Lightning invoice / onchain leave (wavewalletrpc)
├── activity [inspect] — unified wallet activity feed (wavewalletrpc)
├── exit {status|summary|plan|export} — cooperative leave by default, forced unroll (wavewalletrpc)
├── wallet-sweep — preview a backing-wallet sweep; broadcast requires approval
├── mcp serve — MCP server for AI agents (wavewalletrpc)
├── schema — JSON dump of CLI methods
├── ark — power-user parent (hidden; no wavewalletrpc)
│ ├── board — board confirmed boarding UTXOs
│ ├── vtxos {list|refresh|leave}
│ ├── oor {receive|get|list}
│ ├── send {oor|inround} — real transfers require approval
│ ├── rounds {get|list|join|watch}
│ ├── sweep [list] — broadcast requires approval
│ ├── fees {estimate|history}
│ └── listtransactions
├── recovery {list|status|escalate|cancel} — daemon-owned vHTLC recovery rows (hidden)
└── dev — generated low-level RPC (hidden; no wavewalletrpc)
└── daemon <Method> — call any waverpc.DaemonService method
| Flag | Default | Description |
|---|---|---|
--rpcserver |
localhost:10029 |
Daemon gRPC address |
--datadir |
~/.waved |
Daemon data dir; TLS cert + macaroon are derived from <datadir>/data/<network>/ |
--network |
mainnet |
Network segment of the derived cert / macaroon paths |
--tlscertpath |
Explicit daemon TLS cert path (overrides --datadir) |
|
--macaroonpath |
Explicit admin macaroon path (overrides --datadir) |
|
--no-tls |
false |
Disable TLS (regtest / dev); requires --no-macaroons |
--no-macaroons |
false |
Disable macaroon auth (required alongside --no-tls) |
--timeout |
30s |
Maximum duration for each finite daemon RPC; 0 disables the deadline. Live watches use their stream-specific bounds. |
--json |
false |
Emit machine-readable JSON output |
--request-json |
Raw JSON request payload (overrides bespoke flags) |
Flag names are canonical kebab-case in help and examples. Equivalent snake_case spellings remain silent compatibility aliases; proto JSON and MCP argument field names remain snake_case.
wallet-sweep --broadcast, ark sweep --broadcast, and real
ark send inround|oor calls require a y/N confirmation on a terminal. Agents,
pipelines, --no-input, and CI=true must pass the command-local --yes
flag. Without it, the CLI describes the blocked action and returns
CONFIRMATION_REQUIRED with exit code 5 before dialing the daemon. Preview and
--dry-run paths never require approval.
Display daemon status information.
wavecli getinfoBake a daemon macaroon from one or more known entity:action or exact
uri:/package.Service/Method permissions. The caller must authenticate with
a credential authorized to call BakeMacaroon.
| Flag | Type | Description |
|---|---|---|
--save-to |
string | Write the binary macaroon to this path with mode 0600 |
--root-key-id |
uint64 | Numeric macaroon root key ID (default 0) |
--valid-for |
uint64 | Add an expiry caveat for this many seconds; 0 does not expire |
wavecli bakemacaroon --save-to ./info.macaroon --valid-for 3600 info:read
wavecli bakemacaroon uri:/waverpc.DaemonService/GetInfoDisplay wallet balance (boarding, VTXO, total, onchain) in satoshis.
wavecli balance # requires wavewalletrpc
wavecli dev daemon GetBalance # always availableAllocate an inbound payment surface.
| Flag | Type | Description |
|---|---|---|
--offchain |
bool | Returns a BOLT-11 invoice via the swap subsystem (default) |
--onchain |
bool | Returns a fresh boarding address |
--amt |
uint | Required for --offchain; ignored for --onchain |
--amt-hint |
uint | Optional expected amount for --onchain (accounting only) |
--memo |
string | Optional memo embedded in the offchain invoice |
wavecli recv --onchain # boarding address
wavecli recv --offchain --amt 5000 --memo coffee
# No-wavewalletrpc equivalent for the boarding-address case:
wavecli dev daemon NewAddressFund a boarding output holding a Taproot Asset from the tapd wallet, and list
those deposits. Requires --assets.backend=tapd and tapd v0.9.0 or later,
which signs its own asset inputs when it publishes. The units come from one issuance, and any
surplus returns to tapd as change. The daemon's own wallet pays and signs for
the network fee, bounded by --max-fee-sat, and the Bitcoin value of the
deposit output (value_sat) beyond what tapd's spent asset outputs carry.
tapd signs only those asset outputs, when it publishes. The daemon sizes the deposit value from the operator's terms: two
minimum VTXOs plus twice the quoted round fee. When the deposit boards, the value pays the round fee and
the asset VTXO's satoshis, and the rest becomes a BTC VTXO. Retrying with the
same --request-id resumes the deposit instead of funding it again. A
deposit is published once tapd broadcasts it and confirmed once it has the
confirmations the operator requires of a boarding input.
wavecli dev daemon deposit-asset --request-id alice-1 \
--asset-ref <asset id or group key> --asset-amount 500 \
--max-fee-sat 5000
wavecli dev daemon list-asset-depositsJoin the next round with a confirmed asset deposit. The round creates an asset
VTXO holding the deposited units and the operator's minimum VTXO amount, and a
BTC VTXO with the rest of the deposit's value after the round fee. Once the
round checkpoints, list-asset-deposits shows its round_id. A deposit that a
round already spends is refused; a failed round frees it to board again.
wavecli dev daemon board-asset-deposit --request-id alice-1Take back a confirmed asset deposit that no round holds. The claim spends the
deposit through the owner's exit path, so it is accepted only once the
boarding exit delay has passed since the deposit confirmed, and it moves the
units and the deposit's full Bitcoin value to a new tapd address. The claim
pays no fee itself; the daemon wallet pays it through a CPFP child and bumps it
until it confirms. list-asset-deposits shows the claim_txid, and claimed
once the claim is six blocks deep. A deposit that is claimed, or being
claimed, can no longer board, and a deposit that a round holds cannot be
claimed. Retrying returns the same claim. The daemon refuses claims unless
tapd confirms transfers spending assets it does not hold, first released in
v0.9.0.
wavecli dev daemon claim-asset-deposit --request-id alice-1Trigger the client to join the next round with any confirmed boarding UTXOs.
| Flag | Type | Description |
|---|---|---|
--target-vtxo-count |
uint32 | Fan boarded balance into N VTXOs (default 1) |
--no-persist |
bool | Skip restart-safe replay (testing only) |
wavecli ark board
wavecli ark board --target-vtxo-count 4Allocate an out-of-round receive script backed by a newly derived wallet key,
or replay the exact allocation for a previously used idempotency key. Returns
pk_script_hex, pubkey_xonly_hex, the wallet key locator, and the absolute
indexer registration expiry.
| Flag | Type | Description |
|---|---|---|
--label |
string | Optional indexer registration label |
--idempotency-key |
string | Return the same allocation on exact retry |
wavecli ark oor receive
wavecli ark oor receive --idempotency-key my-app:durable-operation-42Idempotency keys share one daemon-wide namespace. Prefix them with an application or tenant identity when several callers use the same daemon.
List VTXOs known to the wallet with optional filters.
| Flag | Type | Description |
|---|---|---|
--status |
string, repeatable | Filter by status: live, pending_forfeit, forfeiting, forfeited, spent, unilateral_exit, failed, spending, pending_round, expired. Default: every status except forfeited and spent |
--all |
bool | List every status; checkpoint PSBTs only with --fields oor_final_checkpoint_psbts |
--min-amount |
int64 | Minimum amount in sats |
--fields |
string | Comma-separated field names to include |
--ndjson |
bool | Emit one JSON object per VTXO (newline-delimited) |
Only live VTXOs are spendable; use balance for balances.
# VTXO inventory: every status except forfeited and spent
wavecli ark vtxos list
# Every VTXO the wallet has ever held
wavecli ark vtxos list --all
# Live VTXOs above 10k sats, only outpoint and amount
wavecli ark vtxos list --status live --min-amount 10000 \
--fields outpoint,amount_sat
# Consumed VTXOs only
wavecli ark vtxos list --status forfeited,spent
# Streaming NDJSON for piping to jq
wavecli ark vtxos list --status live --ndjson | jq '.amount_sat'Queue VTXOs for refresh in the next round and (by default) join that
round immediately. Pass --no-join to leave the intent queued in
PendingRoundAssembly so it can batch with subsequent refresh / leave
RPCs; commit the batch later with ark rounds join.
Automatic expiry handling delays to the advertised window boundary when doing
so preserves the wallet's dynamic critical threshold and minimum cooperative
retry buffer. If the operator configures a narrower, later window, the wallet
keeps its normal earlier threshold and pays the quoted fee instead of weakening
unilateral-exit safety. When one VTXO reaches its trigger, the wallet coalesces
eligible siblings from the same expiry batch into one automatic round
registration, even when their individual trigger heights are staggered. A
sibling remains deferred when it still has a safe future free-refresh window
worth preserving.
The daemon learns the window from its cached operator terms at bootstrap (and
on later terms refreshes). When the cached boundary fires, it fetches a fresh
GetInfo snapshot and rechecks the current window before reserving the VTXO.
An operator fee-schedule hot reload is not pushed proactively to
already-connected wallets.
A refresh is charged an operator fee (on-chain share + liquidity +
margin), set by the server-issued quote at seal time and auto-accepted
up to the daemon's maxoperatorfeesat cap. --dry-run previews an
itemized advisory estimate for the selected VTXOs — per outpoint:
amount, remaining lifetime, liquidity / on-chain / margin components —
resolved entirely from the daemon's own view, with no manual amount or
remaining-blocks input. Selections fully inside the operator's
advertised free-refresh window preview with a zero total
(free_refresh_eligible), while the per-outpoint rows keep the
ordinary paid quote to show what the waiver saves. The estimate is
best-effort: if the operator or chain tip is unreachable the preview
still returns with estimate_error set. The binding fee remains the
seal-time quote and may differ from any estimate.
Automatic maintenance waits for the advertised free-refresh window and only
accepts quotes with zero total fee. Every re-quote is checked as well. Paid
refresh remains available through an explicit CLI/RPC request, subject to
maxoperatorfeesat. The old autorefreshfeefloorsat and
autorefreshfeerateppm settings are deprecated and cannot authorize automatic
fees. Nonzero legacy values produce one startup notice and do not enforce an
automatic fee budget. If manual and automatic requests share a round, its
entire fee must be zero; otherwise the round is rejected and its reservations
are released.
Retry the manual request separately in that case.
A rejected automatic quote waits six blocks before retrying while the VTXOs are still safe. Critical and expired recovery bypass that cooldown, but still require a zero-fee quote. A zero advertised window disables automatic refresh before expiry. Operators must open the free window before wallets reach their critical-exit boundary, with enough time for retries. A late or disabled window does not postpone critical exits, which can incur on-chain mining fees.
For scheduled batches, plan against the usable maintenance time:
min(free window, wallet refresh threshold) - critical threshold.
That time must cover each attempted slot's wait through its cutoff, the
signing ceremony and confirmation, six blocks per retry cooldown, and an
additional margin. A missed slot can require waiting another full interval;
the registration window is part of that interval, not a separate interval.
For example, an 864-block window with a 738-block wallet target and a 186-block
critical threshold leaves 552 blocks for attempts before critical handling.
An offline wallet returning later has less time. Schedule intervals use wall
clock time, while expiry uses block height: convert delays conservatively for
planning; a ten-minute average block time is not a safety guarantee. Widening
the free window alone cannot fix a wallet target that starts too late.
While waiting outside the window, the wallet polls updated terms every six blocks rather than every block. Allow that discovery delay when changing a window. Entering an already-known window fetches fresh terms immediately, and critical-exit assessment is never postponed by this polling limit.
Swap-enabled builds also default maxpaymentcltv to 480 blocks. Automatic
maintenance adds that payment window above each VTXO's dynamic unilateral-exit
budget and 72-block refresh-retry buffer. For example, a VTXO with a 186-block
critical threshold targets 186 + 72 + 480 = 738 blocks remaining, but
waits if the free window has not opened. Set
the value to zero to keep only the ordinary exit-safety policy. This is a
liquidity-readiness target, not an override of send-time expiry validation: a
payment whose actual route needs more lifetime still fails safely. If a fresh,
round-direct VTXO cannot provide the configured reserve plus one 72-block
healthy window, the wallet logs the mismatch and keeps the ordinary refresh
threshold. Repeated refreshes cannot extend an operator's batch lifetime, so
they would only create unnecessary rounds. An OOR descendant keeps the reserve because one
refresh can replace its inherited partial lifetime with a new full batch.
An interactive real refresh shows the estimate and asks for
confirmation. On non-interactive stdin (agents, pipelines) the command
refuses to prompt: pass --yes (explicit consent) or --dry-run
(preview). This mirrors the leave --all consent gate.
| Flag | Type | Description |
|---|---|---|
--outpoint |
string[] | VTXO outpoint(s) to refresh (txid:index) |
--all |
bool | Refresh all live VTXOs |
--dry-run |
bool | Validate without queuing and preview the estimated fee |
--yes |
bool | Skip the interactive fee confirmation |
--no-join |
bool | Skip the implicit ark rounds join follow-up |
# Preview the itemized fee estimate without queuing anything
wavecli ark vtxos refresh --outpoint <txid:idx> --dry-run
# Explicit outpoints (auto-joins the next round; prompts with the
# estimate on a TTY, requires --yes when stdin is not interactive)
wavecli ark vtxos refresh --outpoint <txid:idx> --yes
# Batch with other intents — explicitly join later
wavecli ark vtxos refresh --outpoint <txid:idx> --yes --no-join
wavecli ark vtxos leave --outpoint <txid:idx> --no-join \
--address bcrt1p...
wavecli ark rounds joinSend via in-round refresh (waits for next round to commit).
| Flag | Type | Description |
|---|---|---|
--to |
string[] | Recipient address(es) (bech32m) |
--pubkey |
string[] | Recipient x-only pubkey hex(es); paired after --to entries |
--amount |
int64[] | Amount(s) in sats (one per recipient, --to then --pubkey order) |
--dry-run |
bool | Validate without submitting |
--yes |
bool | Approve submitting the real transfer without a prompt |
wavecli ark send inround --to bcrt1p... --amount 50000 --yes
# Multiple recipients
wavecli ark send inround \
--to bcrt1p...addr1 --amount 50000 \
--to bcrt1p...addr2 --amount 30000
# Via JSON input
wavecli ark send inround --request-json '{
"recipients": [
{"address":"bcrt1p...","amount_sat":50000},
{"address":"bcrt1p...","amount_sat":30000}
]
}'Send via out-of-round transfer (immediate, through operator).
| Flag | Type | Description |
|---|---|---|
--to |
string | Recipient address (one of --to / --pubkey) |
--pubkey |
string | Recipient 32-byte x-only pubkey hex |
--amount |
int64 | Amount in sats |
--idempotency-key |
string | Caller-provided key for retry-safe sends |
--dry-run |
bool | Validate without initiating |
--yes |
bool | Approve initiating the real transfer without a prompt |
wavecli ark send oor --pubkey <pubkey_xonly_hex> --amount 25000 --yes
wavecli ark send oor --pubkey <hex> --amount 25000 \
--idempotency-key my-attempt-1Unified send for Lightning invoice (--offchain, default) or onchain
send (--onchain). Onchain sends are atomic: the destination receives
exactly --amt sats and any residual lands back in the wallet as a
change VTXO. Use --sweep-all with --amt=0 to drain every live VTXO
to the destination instead.
By default send blocks until the payment reaches a terminal state
(printing the Lightning preimage on success); pass --no-wait to
return as soon as the send is dispatched. Non-interactive callers must
pass --force or --yes to skip the confirmation prompt.
| Flag | Type | Description |
|---|---|---|
--offchain |
bool | BOLT-11 dispatch via swap subsystem (default) |
--onchain |
bool | Atomic onchain send via SendOnChain |
--amt |
uint | Amount in sats (required for onchain unless --sweep-all) |
--max-fee |
uint | Max swap fee in sats (invoice sends only) |
--max-credit |
uint | Offchain only: max total server credit to reserve; 0 disables credit use |
--max-credit-topup |
uint | Offchain only: max new Ark value to move into server credit; 0 disables top-ups |
--note |
string | Caller-supplied label |
--sweep-all |
bool | Onchain only: drain wallet; --amt must be 0 |
--force / --yes |
bool | Skip the interactive confirmation prompt |
--no-wait |
bool | Return once dispatched instead of blocking to a terminal state |
--dry-run |
bool | Prepare and print the preview without dispatching |
wavecli send lnbcrt... --offchain --force
wavecli send lnbcrt... --offchain --max-credit 1000 \
--max-credit-topup 1000 --force
wavecli send bcrt1... --onchain --amt 1000 --force
wavecli send bcrt1... --onchain --sweep-all --forceCooperatively exit a VTXO by default; queues the outpoint for
cooperative leave and joins the next round. Unilateral (on-chain)
unroll only starts when --force-unroll-ack is set to the exact
string I_KNOW_WHAT_I_AM_DOING. exit replaces the legacy unroll
verb at the user surface.
| Flag | Type | Description |
|---|---|---|
--outpoint |
string | VTXO outpoint to exit (txid:vout) |
--onchain-address |
string | Cooperative leave destination; omitted generates a fresh wallet address |
--force-unroll-ack |
string | Must be exactly I_KNOW_WHAT_I_AM_DOING to force unilateral unroll |
--dry-run |
bool | Validate locally and print the preview without dispatching |
wavecli exit --outpoint <txid:vout>
wavecli exit --outpoint <txid:vout> --force-unroll-ack I_KNOW_WHAT_I_AM_DOING
wavecli exit status --outpoint <txid:vout>
wavecli exit summary
wavecli exit plan --outpoint <txid:vout>exit status reports progress for a forced unilateral unroll job (it
survives daemon restarts); exit summary aggregates all in-progress
exits; exit plan previews backing-wallet funding readiness before
forcing an exit. Status enum (UNROLL_JOB_STATUS_*) still uses the old
"unroll" naming.
Export one VTXO's signed transaction ancestry for an external service to broadcast and fund with child-pays-for-parent (CPFP) transactions:
wavecli exit export --outpoint <txid:vout> --output ancestry.json
# Or write the same JSON to stdout:
wavecli exit export --outpoint <txid:vout> > ancestry.json--output creates a new file with mode 0600 and refuses to overwrite an
existing file or symlink. Export requires an unlocked, ready wallet and
onchain:write permission because the bundle contains executable signed
transactions. It does not require confirmed fee inputs in the user's backing
wallet, allocate a funding address, contact the operator, broadcast anything,
or start a local unroll job. The external service provides and signs its own
confirmed Bitcoin inputs to fund the CPFP children.
The export does not reserve the VTXO or disable automatic refresh. Coordinate
handoff before broadcasting: spending, refreshing, or forfeiting the VTXO can
make an earlier bundle unusable. Sharing the file reveals its transaction
ancestry and allows the recipient to initiate unrolling. The file contains no
private keys. With --sweep-address, it also authorizes a final sweep to the
chosen address.
Add --sweep-address ADDRESS to include a presigned final sweep. The daemon
validates the address network and signs a version-3 timeout spend paying the
full VTXO value to that address, plus a zero-value P2A fee anchor. The service
waits for CSV maturity and funds its own CPFP child to relay the sweep. No
onchain fee inputs or fee estimate are needed from the user. An unavailable
signer, invalid payout, or asset-bearing VTXO fails a sweep export.
wavecli exit export --outpoint <txid:vout> \
--sweep-address <bitcoin-address> --output exit.jsonWithout --sweep-address, export contains ancestry only and a final sweep
still needs a later user signature. Both modes produce format version 2.
Export does not start an unroll or reserve the VTXO.
Exports also carry the saved batch expiry height and public sweep evidence
when available. Backup integrations can use WalletService.ExportExits to
export several VTXOs with shared ancestry included once and a separate sweep
destination per target.
See the bundle format and executor contract for field definitions, validation, timelock handling, and package submission.
List local transaction history from the daemon's persisted accounting and boarding-sweep records.
| Flag | Type | Description |
|---|---|---|
--from |
string | Include entries at or after this ISO 8601 timestamp |
--to |
string | Include entries at or before this ISO 8601 timestamp |
--limit |
uint32 | Maximum entries to return |
--offset |
uint32 | Number of filtered entries to skip |
--type |
string | Optional filter: boarding, round, oor, or sweep |
wavecli ark listtransactions --limit 25
wavecli ark listtransactions \
--type oor \
--from 2026-05-01T00:00:00Z \
--to 2026-05-08T23:59:59ZThe merged wallet activity feed: send / recv / deposit / exit history.
| Flag | Default | Purpose |
|---|---|---|
--pending |
Only entries still in flight | |
--kind |
Filter by kind (send,recv,deposit,exit); repeatable |
|
--limit |
daemon default | Page size |
--cursor |
Page token from a prior page's next_cursor |
|
--format |
table |
Output format (table, expanded/x, json) |
wavecli activity
wavecli activity --pending --kind send,recv
wavecli activity --format json
wavecli activity --json
wavecli activity --cursor <next_cursor>
wavecli activity inspect <id>The VTXO inventory and onchain history are not part of the activity feed.
Use the ark subtree for those:
wavecli ark vtxos list # VTXO inventory (all but forfeited/spent)
wavecli ark listtransactions # raw transaction / onchain history
wavecli ark sweep list # boarding-timeout sweep recordsIntrospect available CLI commands and their parameters.
wavecli schema
wavecli schema ark.vtxos.list
wavecli schema --allEvery schema entry includes a stable output_schema_id and
output_schema_version, plus a side_effect boolean and whether the same
method is exposed through MCP. The unit suite walks the real cobra command
tree and the live MCP tool list, so a declared flag or tool cannot silently
drift from the executable surface.
Start an MCP (Model Context Protocol) server on stdio for AI agent integration. Exposes daemon RPCs as typed tool calls.
wavecli mcp serveNote: Wallet management tools (create, unlock, genseed) are
intentionally excluded from MCP to prevent sensitive material from
transiting the protocol. Use the CLI directly for wallet operations.
ark.oor.receive is exposed because it only allocates a fresh
wallet-derived receive target and does not reveal seed material.
The everyday wallet send MCP tool is intentionally two-phase. First call
send.prepare with the destination, rail, amount, fee limit, and note. It
validates the payment and returns the exact preview plus a short-lived,
single-use send_intent_id without moving funds. After inspecting that
preview, call send with only that send_intent_id. The second call consumes
the prepared intent and may move funds; it cannot silently replace the
reviewed payment parameters. The raw ark.send.inround / ark.send.oor
advanced tools are not two-phase — they dispatch in one call, gated only by
their optional dry_run flag.
A complete end-to-end workflow on regtest using the default (no
wavewalletrpc) build. With a wavewalletrpc-enabled daemon, swap the
relevant commands per the references above.
# 1. Start a regtest bitcoind + Esplora (e.g., via Nigiri)
nigiri start
# 2. Start the daemon. Auto-unlock so you don't need wavewalletrpc to
# create/unlock from the CLI.
waved \
--network=regtest \
--wallet.type=lwwallet \
--wallet.esploraurl=http://localhost:3000 \
--wallet.password_file=/path/to/password_file \
--server.host=localhost:10010 \
--server.insecure \
--rpc.listenaddr=localhost:10029
# 2b. Alias the CLI for this regtest daemon: plaintext transport (no TLS,
# no macaroon) on the regtest network. See the Authentication section
# above for why both --no-tls and --no-macaroons are needed.
alias wave='wavecli --no-tls --no-macaroons --network=regtest'
# 3. Get a boarding address.
ADDR=$(wave dev daemon NewAddress | jq -r .address)
# 4. Fund it on-chain and confirm.
bitcoin-cli -regtest sendtoaddress "$ADDR" 0.01
bitcoin-cli -regtest -generate 6
# 5. Verify balance.
wave dev daemon GetBalance
# 6. Board into the next round.
wave ark board
# 7. After the round confirms, list the new VTXO.
wave ark vtxos list
# 8. Send funds (in-round to a peer's bech32m address).
wave ark send inround --to bcrt1p... --amount 5000For per-client manual testing under the arktest harness, see
MANUAL_TESTING.md at the server (lumos) repo root.
Wallet passwords are never accepted as CLI arguments. The priority order for password resolution:
- Environment variable --
WAVED_WALLET_PASSWORD=pass - Password file --
--wallet-password-file=/path/to/file - Explicit stdin --
printf '%s\n' 'pass' | wavecli unlock --password-stdin - Interactive prompt -- prompted on TTY if none of the above
Plain piped stdin is never consumed as a password. Pass global --no-input
when an invocation must never prompt or implicitly read input. CI=true also
suppresses prompts while leaving stdout and stderr output unchanged. Explicit
sources such as --password-stdin, a password file, or the environment remain
available under --no-input.
For production deployments, use the password file approach with
restrictive file permissions (chmod 600).
| Problem | Solution |
|---|---|
daemon was not built with -tags wavewalletrpc |
Rebuild with make build-wavewalletrpc, or use the ark * / dev * subtrees |
connection refused |
Daemon not running or wrong --rpcserver address |
wallet not ready |
Run wavecli unlock (requires wavewalletrpc) or restart the daemon with --wallet.password_file |
wallet already exists |
Wallet was already created; use unlock instead |
GenSeed is only available in lwwallet/btcwallet mode |
Switch daemon to --wallet.type=lwwallet or btcwallet |
read macaroon: ... no such file |
The CLI is looking under the wrong data dir/network. Pass --datadir / --network to match the daemon (see Authentication), or --macaroonpath directly |
credentials require transport level security |
A macaroon can't ride a plaintext connection. Use TLS (drop --no-tls), or add --no-macaroons alongside --no-tls |
| TLS certificate errors | Point --datadir / --network at the daemon's cert, set --tlscertpath, or use --no-tls --no-macaroons for regtest |
password must be at least 8 bytes |
Wallet password minimum is 8 characters |
decryption failed: wrong password |
Incorrect password or corrupted seed file |
Enable the dedicated health listener with --health.listen=127.0.0.1:10032.
It serves only GET /readyz and GET /livez, without authentication or wallet
RPCs. Use a private interface for a container supervisor. The listener starts
before wallet and operator initialization. An empty address disables it.
| State | /readyz |
/livez |
|---|---|---|
| Starting, locked wallet, or waiting for startup dependencies | 503 | 200 |
| Startup complete and both core actors responding | 200 | 200 |
| Actor probe fails or last successful sample is over 7 seconds old | 503 | 200 during grace |
| Either actor makes no progress for 2 minutes after startup | 503 | 503 |
| Shutdown | 503 or connection closed | 503 or connection closed |
Full startup includes wallet-dependent actors, mailbox ingress, authenticated operator terms, and wallet-ready hooks. Slow startup reconciliation does not start the liveness failure timer. A permanently stuck startup therefore remains unready and needs operator diagnosis; these probes do not impose a startup restart deadline.
After startup, the daemon probes the round actor and virtual transaction output (VTXO) manager every 5 seconds, with one shared 2-second deadline. Each probe traverses the actor's ordinary receive queue and acknowledges without invoking business logic, storage, or another actor. Admission never waits for queue room. Each actor retains at most one pending probe even after a timeout. HTTP requests read cached status and cannot create additional probe work.
Liveness also observes each actor's completed-turn counter. A full queue or a late acknowledgement can fail readiness while the actor continues to make progress. Such backlogs stay live. Each actor has its own activity deadline, so progress in one cannot hide a stuck peer.
These checks measure local receive-loop responsiveness. They do not measure block freshness, wallet balances, transaction progress, or operator/LND reachability. Idle wallets and long block intervals remain healthy. Temporary external calls that delay an actor withdraw readiness but have 2 minutes to recover before liveness fails. An external call that itself parks a core actor for longer than that is indistinguishable from a local wedge. Use dependency monitoring separately; restarting waved cannot repair a remote service.
For Kubernetes, use /readyz to remove an unresponsive pod from service and
/livez to permit a restart after sustained failure. Readiness alone cannot
recover a wedged background client. Poll the cached endpoints every 5–15 seconds
with a 2-second HTTP timeout. Install an image supporting health.listen before
enabling these probes. To roll back to an older image, first disable the health
flag and restore its previous probes. No database or RPC migration is required.