cashctl
A wallet CLI for Cash and Circle wallets.
Features
Section titled “Features”cashctl transfer— Send a held cash tokencashctl decode— Inspect any cash token, Circle Hub connection, or NWC URI locally, no network callcashctl receive— “Cash-in” a token: verify and add it to your walletcashctl redeem— Redeem a held cash token into a Lightning walletcashctl consolidate— Merge several held cash tokens into onecashctl cash list-recipients— Check your allocation and co-recipients of a held token (network)cashctl join— Join a circle to get a personal Lightning walletcashctl init— Set up your identity and, optionally, a walletcashctl wallet show/history/use— Show your identity, registered wallets, and local action historycashctl wallet balance— Sums every wallet’s balance plus unredeemed held tokens into one figurecashctl wallet protect— Re-key a still-shared bearer holding so the original secret can no longer spend itcashctl wallet <op>— Run ordinary NWC operations against whichever wallet is currentcashctl connect add/list/use/rm— Register an NWC connection: a plain Lightning wallet you already havecashctl version— Print the cashctl version
cashctl mints nothing itself — minting is the Cash Hub operator’s own tooling.
Installation
Section titled “Installation”AI agents:
Fetch https://ohstr.github.io/cashctl/PROMPT.mdSee Agent skills below for what that does.
macOS / Linux:
curl -fsSL https://ohstr.github.io/cashctl/install.sh | shDetects your OS and CPU (amd64/arm64) and installs to /usr/local/bin.
Falls back to ~/.local/bin if that’s not writable.
Homebrew (macOS/Linux):
brew install ohstr/tap/cashctlWindows (PowerShell):
irm https://ohstr.github.io/cashctl/install.ps1 | iexgo install:
go install github.com/ohstr/cashctl@latestDocker — no toolchain required:
# :latest tracks the newest release, :edge tracks main — see Docker section below.docker run --rm ghcr.io/ohstr/cashctl:latest --helpFrom source — see Development.
cashctl transfer
Section titled “cashctl transfer”Send a held cash token. The destination needs no prefix for
the common case — a hex pubkey, npub1..., a NIP-05 identifier
(name@domain, resolved live), or an nconnection1... are all recognized
by shape:
cashctl transfer 5 # no recipient — get a cash string to hand anyonecashctl transfer npub1w0lxfr9... # transfer it allcashctl transfer 3 alice@example.com # split off 3, keep the rest as a new tokencashctl transfer cashcashctl transfer connection:<platform>:<external-id>:<ia-pubkey>cashctl transfer nconnection1... --ia ia@example.comEquivalent, explicit-flag form for scripted/agentic use:
cashctl transfer --to npub1w0lxfr9... --amount 3.
An nconnection1... never carries an Identity Authority itself — resolving
one asks for one, via --ia <identity> (hex or NIP-05) or an interactive
prompt. Whenever a target gets resolved to something not obvious from what
you typed (a NIP-05 lookup, or an nconnection1...’s IA), cashctl shows it
back before using it.
Cash selection: when an amount is given and --token isn’t, cashctl
picks which held token(s) reach it exactly, rather than just resolving
which one token to act on:
- one held token’s amount matches exactly → a full transfer of it.
- one held token covers it → split-transfers the smallest one that does (best-fit, not the largest available).
- no single token covers it, but several from the same minter, summed, do → consolidates that subset into one token first, then transfers from the result — two chained calls shown as one confirmation.
- nothing covers it → refuses, naming exactly how much you hold and why it can’t be reached (funds fragmented across separate Hubs), rather than silently sending as several transfers to different minters.
cashctl decode
Section titled “cashctl decode”Inspect any cash token, Circle Hub connection (circlehub1...), or NWC
URI locally — no network call, and no wallet needed.
cashctl decode lokicash1... # local-only, includes mint-signature verification if presentcashctl decode lokicash1... --check # also cross-checks against the HubWithout --check, an interactive session asks whether to run it (default
no — no network call at all unless you opt in); --json never prompts
and just skips it.
cashctl receive
Section titled “cashctl receive”“Cash-in” a token.
cashctl receive lokicash1...cashctl receive lokicash1...#deadbeef # bearer-mode: the combined "<token>#<bearer_secret>" presentationDecodes the token, prints its details, then cross-checks it against the Cash Hub before saving — a token with no matching recipient, or one the Hub can’t be reached to confirm at all, is refused outright.
If you paste a Circle Hub or Cash Hub connection here instead of a token,
cashctl gives you a specific error pointing you to the right command.
A bearer-mode token is two values, not one. lokicash1... alone only
decodes it — redeeming or transferring it needs the secret embedded,
<token>#<bearer_secret>. There’s no --secret flag: a bearer token
pasted without it just gets inspected and checked, never saved.
A saved bearer-mode receipt gets protected automatically — re-keyed
under a fresh secret (defaults to yes; always proceeds under
--yes/--json) and merged with any other cash you hold from the same
issuer. A failure here doesn’t fail the receive — retry later with
cashctl consolidate --to cash.
cashctl redeem
Section titled “cashctl redeem”Redeem a held cash token into a Lightning wallet.
cashctl redeem # auto-picks your one held token and default walletcashctl redeem work # into wallet "work" — or: --token tok-a1b2 --into workcashctl redeem --invoice lnbc1... # bypass both — redeem into any invoice, no cashctl wallet neededIf you hold more than one token and don’t pass --token, redeem
(and transfer/consolidate too) shows a numbered list and asks which
one — --token/--yes/--json skip straight past it for scripted use.
Before confirming, redeem shows the expected fee (only when it’s
actually non-zero — a same-node redeem is routinely free) and warns if
the token’s redemption deadline is close or already passed. Every
money-moving confirmation (redeem/transfer/consolidate) defaults to
no on a bare Enter.
| Flag | Meaning |
|---|---|
--token |
which held token (auto-picked if you only hold one) |
(positional), or --into |
destination wallet (default: your default wallet) |
--invoice |
redeem straight into this external invoice |
--as |
override credential — required for a connection-key-bound token |
cashctl consolidate
Section titled “cashctl consolidate”Merge several held cash tokens into one.
cashctl consolidate # auto-detects which held tokens share a minter and merges each groupcashctl consolidate tok-a1b2 tok-c3d4 # just these twocashctl consolidate --sources tok-a1b2,lokicash1...:5:pubkey:<privkey> --to pubkey:<hex>With no IDs/--sources given, cashctl can’t just merge everything —
only tokens sharing a minter can actually be combined — so it groups your
held tokens by minter and consolidates each group that has 2+ tokens
(a lone token from a minter needs nothing merged, and is left alone).
One group: it just proceeds. More than one: an interactive session asks
which group(s) to process (Enter for all); --json/--yes processes
every qualifying group, since there’s no terminal to ask from.
Positional IDs (or --sources, comma-separated) skip all of that for
exact control — amount and credential are already known for each held
entry. Use the verbose <token>:<amount-loki>:<credential> form
(--sources only) for a source that isn’t in your local ledger; the IDs
themselves are visible via cashctl wallet show --json; plain-text
wallet show never prints them (see cashctl consolidate --help).
--to defaults to your own identity; --to cash merges into a
fresh, anonymous bearer note instead (needs Hub support). An
nconnection1... --to target needs --ia <identity> (hex or NIP-05) to
resolve its Identity Authority, same as transfer.
cashctl cash list-recipients
Section titled “cashctl cash list-recipients”Check your allocation and co-recipients of a held token — the same call
cashctl receive makes internally to check a token before saving it.
cashctl cash list-recipients # your allocation + co-recipients of a held tokencashctl join
Section titled “cashctl join”Join a circle to get a personal Lightning wallet.
cashctl join <circlehub1... or NWC URI> 100The self-service entry point into a circle. Give it a Circle Hub’s
connection and the spend cap you want — both positional, in either order,
or via --hub/--max-amount. A cap is required — there’s no “unlimited”
option.
join calls create_circle_wallet on your behalf and saves the resulting
wallet. If it’s your first wallet, it also becomes your default.
cashctl join circlehub1... --max-amount 100 --budget-renewal monthly| Flag | Meaning |
|---|---|
(positional), or --hub |
the Circle Hub connection (required) |
(positional), or --max-amount |
requested spend cap, in loki (required) |
--expiry |
requested expiry duration (default: the Hub’s own) |
--budget-renewal |
daily|weekly|monthly|yearly|never (default: the Hub’s own) |
--as |
override credential (defaults to your local identity) |
join is a top-level shortcut for cashctl circle join.
cashctl init
Section titled “cashctl init”Set up your identity and, optionally, a wallet.
cashctl initReuses your ncli vault identity if you have
one. Otherwise it generates a new identity just for cashctl.
If you already have a Lightning wallet connection (NWC), init offers to
register it as your default. Run init again any time — it’s idempotent,
and just reports where things stand.
Note: under --json, init generates a fresh local identity and
skips the wallet offer — there’s no way to paste a connection string in
that mode.
cashctl init --json# {# "npub": "npub1...",# "identity_source": "cashctl-local",# "default_wallet": ""# }cashctl wallet show
Section titled “cashctl wallet show”Your identity, wallets, and history.
cashctl wallet show # identity, registered wallets, held tokenscashctl wallet history # local action log (receive/redeem/transfer/...)cashctl wallet use <name> # switch your default wallet (also: cashctl connect use)cashctl wallet balance
Section titled “cashctl wallet balance”Your unified balance.
cashctl wallet balance # one number: every wallet + every held token, summedcashctl wallet balance --breakdown # itemized, per-wallet/per-token (short: -v)cashctl wallet balance --from work # just one wallet or held tokenAn expired wallet can’t be queried live. balance falls back to the
last-known figure from your most recent successful check, marked
stranded ([expired] in text mode).
cashctl balance is also available as a top-level shortcut for wallet balance.
cashctl wallet protect
Section titled “cashctl wallet protect”Re-key a bearer holding that’s still shared, so the original secret can no
longer spend it. receive does this automatically; use this if that was
declined or failed.
cashctl wallet protect # picks the holding for youcashctl wallet protect <id> # a specific held token (see `wallet show --json`)cashctl wallet protect --token <id> # same, by flagcashctl wallet <op>
Section titled “cashctl wallet <op>”Ordinary NWC wallet operations. Plain NIP-47
calls against whichever wallet is current (-c/--connection overrides it
for one call):
cashctl wallet get-infocashctl wallet budgetcashctl wallet invoice 5000 --desc "coffee"cashctl wallet pay lnbc1...cashctl wallet list-txcashctl wallet sign-message "hello"invoice and pay are also available as top-level shortcuts: cashctl invoice 5000 / cashctl pay lnbc1....
cashctl connect add/list/use/rm
Section titled “cashctl connect add/list/use/rm”Register a Lightning wallet you already have, over NWC — your own, or one handed to you from another device:
cashctl connect add work nostr+walletconnect://...cashctl connect listcashctl connect use workcashctl connect rm workcashctl version
Section titled “cashctl version”Print the cashctl version.
cashctl versionAgent skills
Section titled “Agent skills”For coding agents: AGENTS.md points to the matching skill
under skills/ — one per command group. Each skill works
standalone with just the cashctl binary on PATH.
npx skills add ohstr/cashctl --all -ycashctl --help prints the complete command tree.
Configuration
Section titled “Configuration”State lives under $XDG_CONFIG_HOME/cashctl (or ~/.config/cashctl on
Linux/macOS) in a single SQLite database, cashctl.db (0600 — it can hold
a plaintext identity key and bearer-mode spending secrets). Override the
location with --config-dir. Set NO_COLOR to disable ANSI color on
stderr.
Breaking, if you used a pre-release build: cashctl.db replaces the
three flat JSON files (identity.json, connections.json, ledger.json)
earlier builds used — no migration path, no dual-read. If you have
existing state in those files, back them up before upgrading; cashctl
won’t see them anymore.
If you point init at an ncli vault,
cashctl only reads it. Your vault stays at its own usual path, unaffected.
Set NCLI_VAULT_PASSWORD to unlock it non-interactively — needed for
init and other vault-backed commands run without a TTY (scripted/agentic
use).
Docker
Section titled “Docker”docker run --rm -v ~/.config/cashctl:/root/.config/cashctl ghcr.io/ohstr/cashctl:latest wallet showThe :edge tag tracks main; a versioned tag tracks that release.
Development
Section titled “Development”Build from source with just:
just build # go build -o cashctl .License
Section titled “License”Unlicense — public domain.