Autonomous Agent Payer
dero-pay/agent is the agent side of DeroPay's x402 receipt rail. Everything here pays the invoices minted by createX402RouteGuard / createPaymentHandlers — a plain DERO transfer to an integrated address, redeemed for an HMAC-signed receipt token. No extra smart contract, no facilitator service.
This is the counterpart to the x402 Payment Guard. The guard mints the 402 challenge; the agent payer settles it. They share the same receipt rail, so an agent can pay any DeroPay-guarded route or MCP tool.
agent merchant (dero-pay/next)
│ GET /api/protected/report │
├────────────────────────────────────▶ createX402RouteGuard
│ 402 { error, payment:{invoiceId, │ └─ engine.createInvoice()
│ integratedAddress, ... } } │
◀────────────────────────────────────┤
│ │
├─ SpendPolicy.reserve(origin, amt) │ deny-by-default; throws on breach
├─ payer.transfer(integratedAddress) │ DERO moves on-chain
│ │ └─ PaymentMonitor matches the
│ GET /api/pay/status?invoiceId= │ payment ID, tracks confirmations
├────────────── poll ───────────────▶│
│ ... completed │
│ POST /api/pay/receipts/issue │
├────────────────────────────────────▶ └─ issueReceiptFromInvoice()
│ { receipt, claims } │
◀────────────────────────────────────┤
│ GET /api/protected/report │
│ X-DeroPay-Receipt: <token> │
├────────────────────────────────────▶ └─ verifyPaymentReceipt() → 200Quick Start
import {
createPayingFetch,
createWalletRpcPayer,
SpendPolicy,
} from "dero-pay/agent";
const payingFetch = createPayingFetch({
payer: createWalletRpcPayer(), // http://127.0.0.1:10103/json_rpc
policy: new SpendPolicy({
allowOrigins: ["https://api.example.com"],
maxAtomicPerRequest: 100_000n, // 1 DERO
maxAtomicPerWindow: { amountAtomic: 500_000n, windowSeconds: 3600 },
}),
onPayment: (evidence) => auditLog.write(evidence),
});
const res = await payingFetch("https://api.example.com/api/protected/report");payingFetch is fetch-compatible: non-402 responses pass through untouched. On an x402-deropay-draft challenge it reserves spending authority, pays, waits for settlement, redeems the receipt, and retries the original request (buffered body included) with X-DeroPay-Receipt.
Endpoint Discovery
The 402 challenge does not carry the payment API's location, so the payer assumes the upstream convention ${origin}/api/pay (status at /api/pay/status, redemption at /api/pay/receipts/issue). Override per deployment:
createPayingFetch({
// ...
paymentApi: "https://pay.example.com/api/pay",
// or a resolver:
// paymentApi: (challenge, resourceUrl) => "https://pay.example.com/api/pay",
});Safety Properties
The payer is built for autonomous operation, where a mistaken payment is real funds moving without a human in the loop. Its defaults are conservative:
- Deny-by-default. An empty
allowOriginsauthorizes nothing. Every payment is reserved against the policy before the wallet is touched; denials throwSpendPolicyError/CredentialError. - Loopback-only wallet.
createWalletRpcPayerrefuses non-loopback wallet URLs unlessallowNonLoopback: true— an autonomous payer pointed at a remote wallet is a key-exfiltration hazard. createPayingFetchnever pays the same demand twice. Concurrent calls through the auto-fetch that hit the same(origin, resource, amount)challenge share one in-flight payment. A call that times out while settling throwsX402SettlementTimeoutError("deadline") and leaves a pending record — the next call resumes polling that invoice instead of paying again. A 402 that persists after the receipt was issued throwsX402PaymentRejectedErrorrather than paying again.- Origins bound who, not where. The allowlist and spend caps constrain which servers you pay and how much — but the recipient (the integrated address the challenge names) is server-supplied, and there is no destination allowlist. An allowlisted server can direct the deposit anywhere within the cap; do not treat an allowlisted origin as a trusted recipient.
- Loud unpayables. A 402 the payer cannot honor (foreign protocol, wrong network, malformed body) throws
X402UnpayableErrorby default, so agent code cannot mistake an unpaid body for a paid one. Setunpayable: "passthrough"to hand the 402 back instead. - Evidence. Every settled payment invokes
onPaymentwith{ at, origin, resource, network, invoiceId, integratedAddress, amountAtomic, txid, receiptJti, receiptExpiresAt }.
Receipt Reuse
Receipts are cached per (origin, path) and attached proactively until claims.expiresAt (server default TTL: 600s), so repeat calls don't re-pay. Against a guard with enforceSingleUseReceipts the cached receipt earns a 409 once burned; the payer evicts it and recovers through a fresh challenge automatically. Set reuseReceipts: false to skip caching (and that recovery round-trip) entirely.
Settlement Timing
requiredConfirmations (typically 3) means real block time — roughly 18s per confirmation on mainnet. Defaults: settleTimeoutMs: 180_000, settlePollIntervalMs: 2_500. On timeout the thrown X402SettlementTimeoutError carries { invoiceId, txid, integratedAddress, resource, origin, paymentApi, reason }:
reason | Meaning | Recovery |
|---|---|---|
"deadline" | Confirmations didn't land in time | Resumable — re-run the call, the same invoice is polled |
"invoice_expired" | The transfer left the wallet but the invoice lapsed | Reconcile with the merchant |
Spending Policies
Both policy classes implement the same SpendGuard interface (reserve(origin, amountAtomic, context) → { commit, release }), so they drop into createPayingFetch and createPayingToolCaller interchangeably.
SpendPolicy— origin allowlist, per-request cap, optional rolling-window cap. Reservation-based: two concurrent payments cannot both slip under a nearly exhausted window.CredentialPolicy— macaroon-style attenuable capabilities built from HMAC chains (mintSpendCredential,attenuate,verifyCredentialSignature). Attenuation tightens the spend cap (lowest wins), shortens expiry (earliest wins), and narrows theresource-prefix(every required prefix must match the challenge'sresource, e.g./api/). Hand an attenuated credential to a sub-agent instead of a wallet.
A credential only restricts on the caveats it actually carries — one minted with no spend cap, no origin, or no expiry is unrestricted on that axis. Always set the caveats you depend on. Note the origin caveat currently matches any-of (adding an origin widens the allowed set rather than narrowing it), so do not rely on origin caveats alone to confine a sub-agent — pair them with a resource-prefix and a spend cap. For strict deny-by-default origin control, use SpendPolicy.
MCP Paid Tools
createPaidToolGuard (server) and createPayingToolCaller (client) put per-call payment gating on MCP tools over the same rail — transport-agnostic, no MCP SDK dependency.
- The challenge is a
payment_requiredtool result whose JSON matches the HTTP 402 body byte for byte (plussettling: truewhile a referenced invoice confirms). - The payment travels as the reserved tool argument
x402Payment, carrying either the paid invoice's id (fresh redemption) or a receipt token (reuse, when the guard runssingleUse: false). - The guard mints invoices through the merchant's own
InvoiceEngineand verifies receipts locally with the sharedreceiptSecret. Replaying the paid call is the settlement poll: the guard re-issues the SAME invoice's challenge until confirmations land, and the caller refuses to pay a second invoice for the same call (X402ToolPaymentRejectedError). - With
singleUse: true(default) every redemption and receiptjtiis burned in the store's replay ledger, so one payment unlocks exactly one call.
Unlike createPayingFetch, the createPayingToolCaller path does not currently de-duplicate concurrent identical tool calls (there is no shared in-flight guard). Two paid calls fired in parallel for the same tool can each mint and pay an invoice. Serialize paid tool calls if a double-pay would matter, until the in-flight guard lands.
// server
const { guard } = createPaidToolGuard({
getEngine: paymentHandlers.getEngine,
receiptSecret: process.env.DEROPAY_RECEIPT_SECRET!,
pricing: { amountAtomic: 50_000n }, // 0.5 DERO per call
});
server.tool("summarize", guard("summarize", async (args) => run(args)));
// client
const callTool = createPayingToolCaller({
callTool: mcpClient.callTool,
payer: createWalletRpcPayer(),
policy,
serverOrigin: "https://tools.example.com",
});Demo
A runnable end-to-end demo pays the x402 example app against a local chain. The agent needs its own wallet — paying the merchant wallet from itself won't register as an incoming transfer.
See Also
- x402 Payment Guard — the merchant side that mints the challenges this payer settles
- API Reference — full
dero-pay/agentexport list