Agent-First Pay v0.9.0: Illegal Commands Stopped Existing

by Agent-First Kit Contributors

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.