Agent-First Pay: A Payment Tool Designed from the Agent Side
What a multi-chain payment tool should look like when the primary caller is an agent: human-set spend limits, stable settlement ids, one interface across chains, structured receive payloads, and durable redacted audit.
A human moving money has a moment of pause. They glance at the recipient, the chain, the amount, and the fee. If anything looks off — the address starts with the wrong prefix, the amount is one zero too many, the network is mainnet instead of testnet — they hesitate before clicking send.
An agent does not hesitate. Whatever the prompt produces is what runs. If the tool lets the agent express a send, the send happens. If the wallet’s balance is reachable, it is reachable to anything the agent decides to do with it.
afpay starts from that asymmetry:
What should a payment tool look like when the primary caller is an autonomous coding agent, and the action is irreversible?
The premise: payment is an irreversible action
The tools that move money for humans assume a human is in the loop. The recipient is read by a person. The amount is confirmed by a person. The chain is selected by a person. The wallet is unlocked by a person each time. When something looks wrong, a person stops.
Removing the person from that loop does not just remove a confirmation click. It removes every assumption built around having a confirming reader. A payment tool for an agent has to put those assumptions back as structure: limits the agent cannot raise, identifiers the agent can reconcile against, addresses the agent cannot mistype, and records that survive the agent crashing.
afpay is one binary that speaks Cashu, Lightning, Solana, EVM, and Bitcoin, with that structure built into every flow.
The limit rule: spend boundaries are set outside the agent
The first question to answer is: what stops the agent from sending everything? The answer cannot be “the prompt told it not to” — prompts drift. The answer has to live below the agent, in the tool itself.
afpay limit add --scope network --network cashu --window 1h --max-spend 10000
afpay limit add --scope wallet --wallet w_1a2b --window 24h --max-spend 50000
afpay limit add --scope global-usd-cents --window 24h --max-spend 500000
Limits are sliding windows. They can be scoped per wallet, per network,
or globally in USD across networks. Every send is checked against every
rule before the network is touched; any breach returns a structured
rejection with error_code: "limit_exceeded" and the rule that was hit.
In RPC and REST server modes, the operator runs the daemon with limits configured at startup. The agent talks to the daemon over gRPC or HTTP. The agent cannot modify the daemon’s limit config — there is no protocol verb for it.
The rule: the bounds on what an agent can spend are set by a human, in a place the agent cannot reach, and enforced before the network is touched.
The settlement rule: every send produces a reconcilable id
An agent that issues a send and then crashes — process killed, network dropped, host rebooted — must be able to come back and know what happened. The only reliable answer is a stable identifier the agent can look up on-chain or against the wallet provider.
afpay’s send flows return one. EVM and Solana return a transaction id the agent can fetch from the chain. Bitcoin returns a txid. Lightning returns a preimage. Cashu returns a token id. In every case, the result has a stable handle that survives the agent’s process.
afpay send --network sol --to 7xKX… --amount 1000000 --token usdc
# {"code":"ok","result":{"transaction_id":"5xYz…","status":"confirmed"},"trace":{"duration_ms":2100}}
receive --wait returns the same shape: the real on-chain id once the
funds arrive, not a synthetic identifier the agent has to translate
later. For the design behind that, see the v0.2 settlement-ids post.
The rule: every action against the network leaves a handle the agent can reconcile against without trusting its own in-memory record.
The chain rule: one interface, five networks
Multi-chain support, done as five separate CLIs, asks the agent to learn five wallet libraries and five output shapes. Each one has its own exit-code convention, its own way of saying “insufficient funds,” its own quirks for fees and confirmations. The agent ends up encoding most of that into a prompt that goes stale the first time a wallet author updates their CLI.
afpay collapses the interface. send, receive, history, balance
work the same across Cashu, Lightning, Solana, EVM, and Bitcoin. Chain
specifics — Solana reference keys, EVM gas estimation, Lightning
invoices versus Cashu tokens — live inside the binary as command
options the agent can use when it needs them, not as five different
mental models.
afpay send --network ln --to lnbc1pjk…
afpay send --network sol --to 7xKX… --amount 1000000 --token usdc
afpay send --wallet evm-base --to 0xAbc… --amount 1000000 --token usdc
afpay send --network btc --to tb1q… --amount 5000
The output is the same code / result / trace shape every time. An
agent that learns to handle one network learns to handle all five.
The rule: the agent should not have to know which wallet author last shipped a CLI to do its job correctly.
The receive rule: addresses are structured payloads
A receive address copy-pasted as a string invites disaster. The most expensive bugs in this category are not typos; they are right strings on the wrong chain. A USDC address on Ethereum looks indistinguishable from a USDC address on Polygon. A Solana mint and a Cashu mint share nothing but the word “mint.” A Lightning invoice and a BTC address are syntactically different but functionally confusable to a model that has seen too many examples.
afpay returns receive payloads as structured objects:
{"code":"ok","result":{"network":"sol","asset":"usdc","address":"7xKX…","reference":"ref_4f8e"}}
The chain is a field. The asset is a field. On Solana, the order-binding reference key is a field — see the v0.4.2 reference-keys post for why that matters when an agent is waiting for one of many concurrent payments. The receiving side gets a payload it can validate against, not a string it has to disambiguate by length.
receive --wait then blocks until the payment arrives, with a timeout
the caller sets:
afpay receive --network sol --wallet sol-main \
--wait --amount 1000000 --token usdc \
--wait-timeout-s 120 --wait-poll-interval-ms 2000
Agents do not poll well; afpay polls for them, and emits a single structured result when the funds land.
The rule: a receive flow returns a typed payload, not a string the agent has to interpret.
The audit rule: every action leaves a durable redacted record
An agent that moves money needs to be observable. Not because it will behave badly, but because when something downstream goes wrong, the question “what did the agent actually do” has to have an answer that survives the agent’s process.
afpay writes history into a local store (redb by default, PostgreSQL
optional). history update does an incremental sync against the chain;
history list and history status query the local store without
touching the network. Every send and every receive lands as a record
the agent (or a human) can query afterward.
The records use the afdata redaction conventions. Anything bearing a
_secret suffix — DSNs, private keys, signed mnemonics — is rendered
as "***" in every output mode and never written to disk in plain
form. For the security posture that runs underneath this, see the
v0.4 security-runtime post.
The rule: every action against the network produces a redacted record the agent can reconcile against, and the renderer cannot leak a secret by forgetting.
The shape of this release: afpay encodes the contract
The current afpay line carries each rule into a concrete primitive:
- Multi-tier spend limits. Per-wallet, per-network, and global USD windows; enforced before the network is touched; immutable from the agent in server modes.
- Real settlement identifiers. Every send and
receive --waitreturns the on-chain handle, not a synthetic id. - One interface across chains. Cashu, Lightning, Solana, EVM, and
Bitcoin behind the same
send/receive/history/balanceverbs and the same JSONL output shape. - Structured receive payloads. Network, asset, address, and reference fields, so an agent cannot confuse chains, assets, or intents.
- Local-first audit. History stored locally with incremental network sync; queryable without touching the chain.
- Redaction by default.
_secretsuffix is honored everywhere values are rendered or stored. - Cascading RPC deployment. A coordinator can forward to per-chain daemons over encrypted gRPC, so wallet processes can be isolated by network on different hosts; limits are enforced per daemon.
The change to internalize is not any one flag. It is that a payment tool for an agent puts the boundaries the agent cannot uphold itself into the tool.
The next direction: tighter guardrails, deeper observability
Payments are an unusually unforgiving design space. Some next steps are clear:
- Policy profiles. Named bundles of limits and approvals (e.g., “research budget”, “production payouts”) that the operator selects once and the agent inherits, rather than having to set rules one at a time.
- Human-in-the-loop checkpoints. Optional approval requirements above a threshold or for specific receivers, expressed as a structured pending event the agent waits on rather than as a free-form prompt.
- Cross-chain quotes as structured plans. Convert “send $50 worth of USDC to this address” into a chain choice, route, and fee estimate the agent can present before committing.
- Recoverable workflows. Send flows that survive process restart in more places (Lightning quote claim, EVM nonce recovery, on-chain rebroadcast), with all transitions visible as events.
- Tighter audit surfaces. Per-action receipts the operator can archive offline, with redaction provable from the record alone.
- Shared error vocabulary with the rest of the kit.
limit_exceeded,insufficient_balance,provider_unavailable— the same shapes afdata, afhttp, and afpsql use, so an agent that has learned the kit does not have to relearn it per tool.
The direction is not “make wallets prettier.” It is a payment tool that extends the agent’s competence without extending its blast radius — the agent gets to act, the operator gets to bound, and every action that crosses the network leaves a record that survives the agent.