Agent Skill
Use this skill when an agent needs to move money — pay for a service, settle a
bill, accept funds — across Cashu, Lightning (ln), Solana (sol), EVM chains
(evm), or on-chain Bitcoin (btc) without learning five separate wallets.
Prefer afpay over network-specific CLIs or parsing human wallet output.
For flag-level detail, ask the command itself: afpay <command> --help returns
every legal shape of that call at once, plus the ready-to-run --help line for
each subcommand. One call is enough — there is no recursive mode. Add
--output plain for a terminal-shaped rendering. This skill covers behavior,
decisions, and recovery only.
Arguments are command-local and always follow the whole command path:
afpay cashu send --amount-sats 100 --data-dir /srv/afpay, never
afpay --data-dir /srv/afpay cashu send …. The two long-lived server and
session modes are the exception: --mode and its listener flags belong to bare
afpay.
Core Rules
- Treat stdout as the protocol: parse Agent-First Data events. Successful
commands are
kind:"result"events whose businesscodeis insideresult; failures arekind:"error"events witherror.code,error.message,error.retryable, and oftenerror.hint. - A rejected invocation names its own rule in
error.code—cli_unknown_argument,cli_unknown_command,cli_unregistered_combination,cli_invalid_argument_value, and their siblings.cli_unregistered_combinationmeans the flags are each valid but not legal together: read the shapes in--helpand pick one, do not retry with more flags. - Every network exposes the same verbs:
wallet,send,receive,balance,limit,config,backup,restore. The subcommand is the network (afpay sol send ...); cross-network views areafpay balance,afpay wallet,afpay history,afpay limit. afpay ui …is not a second way to ask afpay anything. Each panel runs the same request as the command it mirrors, then opens a window on the person’s machine and does not return until they are done with it. Call it only when you want a human involved; when you need the answer yourself, call the command — the same request, the same numbers, and it exits.afpay ui sendis the panel that asks a person before moving money. It blocks until they answer, and only an approval sends: a closed window is a refusal. Readdecisionanddispatchedoff the terminal result rather than inferring either. A refusal is an answer — do not re-issue it as asendplus apay confirm.afpay ui receiveshows what to scan; it does not watch for the payment. When you need to know the money arrived, use<network> receive --wait.- A panel reaches the person three ways, and which are on offer differs by what
the panel does. The two watch panels offer all three, including the LAN link —
a receive code is for pointing a phone at.
afpay ui senddoes not: that link URL is a bearer capability, and what it would bear is the approval of money leaving. To answer a send from another device, deliver it assessionand open it throughafui session serve. Do not choose a delivery on the person’s behalf when they have not asked for one — leave--modeoff andAFUI_DELIVERYdecides, which is how an unattended machine avoids having a window opened on it. - Any panel can be replaced with
afui frontend(ui_api_version1; seedocs/architecture.md). After installing one, readui_frontend_idoff theui_readyprogress event: present means your override is serving, absent means afpay’s own page is — a workspace frontend nobody enabled is skipped in silence, and that field is the only way to tell it apart from a live one. A frontend afpay cannot load is aui_frontend_*error and no window at all; fix it or setAFUI_SAFE_MODE=1, never assume it fell back. - Writing a
send_confirmtemplate: restructure it freely, but declare both controls withdata-afpay-decision="approve"and="refuse"— afpay binds them, the page does not, and a page missing either one refuses to open. - Amounts are in the network’s base unit (sats, lamports, token base units), not human decimals — confirm the unit before sending. afpay never guesses decimals.
- Pick the network the payee actually accepts; do not assume. afpay rejects
cross-network mistakes (e.g. a
0xaddress passed tosol) with a typed error. - Keep secret-bearing flags/fields on the
_secretsuffix convention afpay already uses (e.g.--pg-url-secret); do not invent new sensitive-name lists.
Paying Is Two Commands
<network> senddoes not pay. It resolves the payment and returnspay_planned: the wallet afpay picked,amount_native,fee_estimate_native,fee_unit, thespend_debitsit would consume, and aplan_id. Read those numbers and every item inwarningsbefore going further — warnings are part of the result so log filters cannot hide them. This is the only point at which refusing is free.afpay pay confirm --plan-id <id>is what pays, and it is the only command that does. Pass--idempotency-keyon every confirm.- A plan is single-use and expires (
expires_at_epoch_ms). Confirming a spent one answersplan_not_found; that is not a reason to re-plan and pay again unless you know the first confirm never ran — checkafpay historyfirst. plan_stalemeans the workspace, daemon configuration, wallet, or spend rules changed after the plan was resolved.error.hintnames which. Resolve a new plan and read it again; never treat this as a transient failure to retry through.- Do not carry a
plan_idbetween machines or workspaces. It is bound to the one that issued it and will not be found anywhere else. - Use
--dry-runto preview a command without executing it; the response echoes what would happen. For a payment the plan already does this, so--dry-runis for the rest of the surface.
Spend Limits Hold
- Per-wallet, per-network, and global limits are checked before every send; any
breach rejects the transaction. This is a guardrail, not an error to route
around — never split an amount into smaller sends to slip under a cap. Surface
the rejection to the user so they can raise the limit (
<network> limit add). - What a limit holds is the planned amount plus an estimated fee, not the final charge. Network fees are estimated when the plan is made and the network charges what it charges at execution — a gas price that moves, a different UTXO set, a fresh melt quote, an account that has to be created. Treat the reviewed total as close, not as a ceiling, and do not design a workflow that depends on a limit being exact to the last unit.
- A
--mode restdaemon enforces limits server-side, and every node in a chain enforces its own. To make caps unbreakable by the agent, run the daemon on a machine the agent cannot reach and talk to it over--peer-urlwith that node’s--peer-api-key-secret. - The peer withholds what it withholds. Seeds, spend-limit rule writes,
reservation reconcile, and wallet-config writes are refused over
--peer-urlexactly as they are over HTTP; aforbiddenanswer there means “run it on the daemon’s host”, never “find another route”. - A
peer_unreachable,peer_not_afpay,peer_route_unsupported,peer_unauthorized, orpeer_mismatcherror is a configuration fault, not a transient one. Report it as-is; onlypeer_unreachableis worth retrying. afpay container installstands that daemon up in a container (Docker, Podman, or Apple — the supported isolated deployment). It refuses to expose a listener without an operator allowlist, so installation needs at least one--allow <category>=<url>(mint,esplora,ln, …) — without it the daemon will not start. That allowlist is an operator boundary; do not treat its absence as something to work around.
Receiving Funds
receiveproduces an invoice/address; add--wait(with--wait-sync-limit) to block for incoming funds. For Cashu-over-Lightning, claiming may be a separate step — follow the returned code rather than assuming one round-trip.receiveandwallet createtake--idempotency-keytoo, and for the same reason a payment does: a repeat mints a second invoice a payer may not be looking at, or a second wallet you cannot tell from the first. Pass one and re-send it verbatim on retry.
Secrets Are Redacted — Keep Them That Way
- Wallet seeds and provider credentials are redacted in normal output. The only command that reveals a seed is the explicit show-seed, whose output is unredacted by design — never echo, log, or pass that value anywhere else.
- A
backuparchive is not redacted. It contains wallet seed secrets in the clear, wallet data, and any database dump — it is the wallet, in a file. It carries no encryption and no signature of its own, so treat it exactly like a seed: encrypt it before it leaves the host that made it, and neverrestorean archive whose origin you cannot vouch for.
Recovery
- A
pay confirmthat times out or returns an uncertain result is not a signal to plan again — that risks double-paying. Re-send the same--idempotency-keywith the same--plan-id: if the first one ran, you get its original result back. Only if that answersplan_not_founddo you checkafpay history/history statusandlimit reconcilebefore deciding anything. idempotency_conflictmeans that key was already used for a different plan. Pick a new key; do not retry with the same one.accounting_inconsistentmeans the network side effect happened but one or more spend-ledger reservations did not commit. It is the sole terminal outcome, names the transaction and reservations, and is replayed by the same idempotency key. Never retry or re-plan; runlimit reconcilelocally after verifying the transaction.errorwithretryable:trueis safe to retry after anyretry_after_msfor anything before apay confirm. After a confirm has been sent, a network error is not a statement that nothing happened: the transaction may have been broadcast, the mint may have advanced, the backend may have paid, and only the reply was lost. Re-send the same--idempotency-keywith the same--plan-idas above, and if that cannot answer, checkafpay historyand the chain or backend before doing anything else. Never plan again on the strength ofretryable:truealone.retryable:falsemeans fix the inputs — do not loop.restoreis destructive and requires--dangerously-overwrite; confirm the target data dir before running it, and prefer a fresh--data-dirwhen unsure.
Setup Checklist
afpay --version || brew install agentfirstkit/tap/afpay
afpay skill install # installs this skill for codex, claude-code, opencode, hermes