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, checkafpay <network> receive --help: not every network can wait for receipt.- 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 — refresh withafpay history update, inspectafpay history list, and check the provider. 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 (Apple, Podman, or Docker — 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
- An invoice or address is a request for payment, not proof of receipt. Not
every network can wait, and the available matching conditions differ; read
afpay <network> receive --helpbefore choosing a waiting shape. Cashu token redemption is a separate operation from requesting a Lightning deposit and claiming its proofs; follow the returned code. - Pass an idempotency key to wallet creation and invoice creation wherever their help offers one, then preserve the exact arguments and key on retry. A repeat without a key can create a second wallet or invoice. Do not assume token redemption or quote claiming exposes the same retry controls.
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
- After a
pay confirmtimeout, preserve the same--idempotency-key,--plan-id, arguments, and workspace. One identical retry can recover a stored terminal receipt; it cannot prove that an unfinished submission failed.idempotency_in_progressmeans the key is still Pending: stop submitting and inspect provider or chain history. Do not poll the key indefinitely, change keys, or create a fresh plan to retry that payment.plan_not_foundalso requires checking history; a consumed plan does not prove whether payment succeeded. idempotency_conflictmeans that key was already used for a different plan. Stop and inspect the original payment and the intended plan; changing the key automatically can duplicate a payment.accounting_inconsistentcovers two outcomes. A known successful payment can have failed spend-ledger confirmation; an unknown submission means the provider may have completed the payment but afpay has no confirmed receipt. Readconfirm_errorsandhint, then verify against provider or chain history before reporting payment status. Atransaction_idmay be present for either outcome or absent for an unknown submission, andreservation_idsmay be empty. Neither field alone proves success. Useafpay history statuswhen a recovery ID is available, and refresh withafpay history updatebefore trustingafpay history list. Missing local history alone does not prove that no payment happened.- A known success with an accounting failure is replayable if its terminal
receipt was stored. An unknown submission consumes the plan but keeps the
key Pending, so the same key answers
idempotency_in_progress, not a replay. Neither outcome authorizes another payment. Runlimit reconcilelocally only after verifying whether funds moved. Reconcile repairs spend-budget reservations; it does not clear a Pending idempotency key or create a receipt. sentorcashu_sentwithidempotency_finalize_failed: trueis a successful payment whose replay receipt could not be stored at one or more nodes. Keep that receipt and repair the affected node’s local record; do not repeat the payment even if an identical retry cannot replay it.errorwithretryable:trueis safe to retry after anyretry_after_ms. That holds for apay confirmtoo, because of what afpay answers there: anetwork_errorreturned by a confirm means the payment was not submitted — nothing was signed, broadcast, or handed to a backend, the plan was given back, and the key was cleared. Retry with the same--plan-idand the same--idempotency-key; do not plan again or change the key.retryable:falsemeans fix the inputs — do not loop.- An outcome afpay cannot establish is never reported as
network_error. Once submission may have begun, a lost or unreadable provider reply — including a success status whose body does not identify the payment — surfaces asaccounting_inconsistentwith the plan consumed and the key Pending. That is the unknown-submission case above: verify against provider or chain history and never retry it blindly. The same applies when the confirm itself got no answer at all (a timeout or dropped connection on your side): that is not anetwork_errorfrom afpay, so follow the timeout rule above. - On-chain Bitcoin asks the wallet’s chain source for a fee rate at a
roughly six-block target, at plan time and again at confirm. If the source
has no estimate, or returns one above afpay’s fixed safety ceiling
(1000 sat/vB — a guard against a faulty or hijacked source, not a fee
policy), the payment fails with a retryable
network_errorbefore signing and nothing is broadcast. Retry later or point the wallet at a healthy chain source; there is no flag to override the ceiling. 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