Agent-First Pay v0.9.0: Illegal Commands Stopped Existing
v0.9.0 compiles the whole afpay CLI from a closed-world registry: one root, 27 command groups, 89 leaves, generated from a table rather than written out five times. Rules that used to be runtime checks — 'this credential is required for that backend', 'this listener flag belongs to that mode' — are now shapes, so an agent reads the requirement out of the help instead of discovering it by failing. Discovery answers cli-help-v2 JSON, scoped per command and bounded in size; bare --version answers a version event. Arguments become command-local, several aliases are gone, and the container image finally catches up to phoenixd 0.9.0 and Bitcoin Core 31.1 — including a signet path that never actually started.
There is a particular kind of error message that reads fine to a human and is almost useless to an agent:
--endpoint is required for phoenixd backend
A person shrugs and adds the flag. An agent has just burned a turn to learn a
rule that was knowable before it ever ran the command — and the only place that
rule existed was inside an if statement, several hundred lines away from the
help text it contradicts. Multiply that by every backend, every mode, and every
listener flag, and the CLI becomes a surface an agent can only map by repeatedly
failing against it.
Agent-First Pay v0.9.0 removes that whole category. The CLI is no longer written; it is compiled — one root, 27 command groups, 89 leaves, roughly 118 legal combinations across 54 action ids, all generated from a single closed-world registry. What used to be a runtime check is now a shape, and a shape either exists or it does not.
A shape carries the rule the check used to enforce
The phoenixd example above is not a validation anymore. It is a variant, keyed on a fixed discriminator, and it shows up in the help for exactly the backend it belongs to:
ln-wallet-create-phoenixd phoenixd; authenticates with an endpoint and HTTP password
afpay ln wallet create [...] --backend phoenixd --endpoint-url <URL> --password-secret <PASSWORD> [--label <LABEL>] [...]
ln-wallet-create-lnbits LNbits; authenticates with an endpoint and admin API key
afpay ln wallet create [...] --backend lnbits --endpoint-url <URL> --admin-key-secret <KEY> [--label <LABEL>] [...]
ln-wallet-create-nwc Nostr Wallet Connect; authenticates with a connection URI
afpay ln wallet create [...] --backend nwc --nwc-uri-secret <URI> [--label <LABEL>] [...]
The same treatment applies wherever a rule used to live in an ok_or: the btc
backend selection and its per-backend chain source, the confirm/cancel pair on
limit reconcile, the release-versus-source split in container install, and the
wait-only knobs on every receive. There is nothing left to reject, because the
illegal request cannot be spelled.
Five session modes are five shapes
afpay also stopped guessing. Bare afpay used to mean --mode cli silently;
now the long-lived session modes are five distinct shapes, each an event stream,
and each listener flag is legal only in the shape that serves it:
{"id":"session-rpc","usage":"afpay --mode rpc [--data-dir <DIR>] [--rpc-secret <SECRET>] [--rpc-listen <HOST:PORT>] [--public-listen] [...]","about":"Encrypted gRPC daemon; only this shape accepts --rpc-listen"}
{"id":"session-rest","usage":"afpay --mode rest [--data-dir <DIR>] [--public-listen] [--rest-listen <HOST:PORT>] [--rest-api-key-secret <KEY>] [...]","about":"HTTP API server; only this shape accepts --rest-listen"}
Passing --rest-listen to the RPC daemon is no longer a misconfiguration that
starts anyway and confuses you later. It is not a command.
Discovery is the contract
Because the registry knows the whole surface, the CLI can hand it over. --help
answers a structured cli-help-v2 document, scoped to wherever you asked:
afpay --help # root: shapes, defaults, subcommands
afpay cashu --help # one group
afpay cashu send --help # one leaf, with per-flag notes
afpay --help --recursive # the whole tree, for a doc generator
afpay cashu send --help --output plain # the same thing, for a human
Scoping matters more than it sounds: the root document stays small enough to sit in a prompt, and an agent that needs one leaf pays for one leaf instead of pulling the entire tree. The plain rendering is the same data, printed:
afpay cashu send [--data-dir <DIR>] [--dry-run] --amount-sats <SATS> [--cashu-mint <URL>...] [--wallet <WALLET_ID>] [--idempotency-key <KEY>] [...]
--amount-sats Amount in sats
--cashu-mint Restrict to wallets on these mints, tried in order
--idempotency-key Opaque key (≤128 chars); a repeat with the same key and body replays the first response instead of re-broadcasting
defaults: --output=json --output-to=split
Bare --version answers a version event rather than a line of prose, so the same
parser reads it:
{"kind":"result","result":{"code":"version","name":"afpay","display_name":"Agent-First Pay","version":"0.9.0","build":"…"}}
Free-string arguments that had no business being free are closed enums now, too —
--with, --btc-network, --sol-cluster, --runtime, and the container
--mode. And the generic cli_error is gone from the spore: about 25 call sites
that all reported the same anonymous failure now name themselves.
What breaks
v0.9.0 is a breaking release, and most of the breakage is the same change viewed from different angles: arguments are command-local.
afpay --data-dir /w cashu send --amount-sats 1000 # before
afpay cashu send --data-dir /w --amount-sats 1000 # now
Beyond that: -f is gone (--follow remains — a generated surface cannot
express short flags), the --rest-api-key and --agent claude aliases are gone,
and the Lightning --endpoint is now --endpoint-url. global backup --output
is now --archive-out, and the sol/evm
config set --rpc-endpoint flags gained network prefixes, both because the old
names collide with reserved or command-local ones. Regenerate any hardcoded
invocations from afpay --help --recursive rather than porting them by hand; that
is the point of the document existing.
The container caught up, and signet started working
The image installs two pieces of third-party software, and both had quietly aged: phoenixd moves 0.7.2 → 0.9.0 and Bitcoin Core 28.1 → 31.1.
Verifying that bump turned up something worse than staleness. The
bitcoin.conf the container generated put rpcbind and rpcallowip in the
global section — and on any chain but mainnet, bitcoind does not ignore those, it
refuses to start:
Error: Config setting for -rpcbind only applied on signet network when in [signet] section.
So afpay container install --with bitcoind --btc-network signet had never come
up; supervisord just restarted a daemon that exited every time. The RPC block is
now written under the section for the selected chain, and --btc-rpc-port is
pinned there as well — previously it never reached bitcoin.conf at all, which
also meant signet’s 38332 default silently disagreed with the port the setup step
and the generated wallet were talking to.
The pins are no longer on the honor system either: a drift check compares them against upstream on every gate run, so the next phoenixd or Bitcoin Core release fails the build instead of aging in place.
Upgrading
Underneath, the crate moves onto Agent-First Data 0.29.2, taking write_raw for
raw process output and index-only addressing for config paths, and afpay now
resolves a single wincode again — the five exact Solana version pins that worked
around that conflict are deleted.
brew install agentfirstkit/tap/afpay # macOS / Linux
scoop install afpay # Windows (agentfirstkit bucket)
cargo install agent-first-pay # any platform, from crates.io
Then afpay skill install to refresh the behavior rules your agent runs on, and
afpay --help --recursive to regenerate whatever you had hardcoded. The rules
that used to live in error messages are in that document now.