@x402.kit/buyer
v0.2.0
Published
Buyer-side x402 client — wrapFetch catches 402s, signs within a hard spending cap, retries once. Includes low-level signPayment for QR/POS flows.
Maintainers
Readme
@x402.kit/buyer
Buyer-side x402 client. Wrap fetch once; 402s get paid and retried, with a hard spending cap the wrapper will never sign past.
import { wrapFetch } from "@x402.kit/buyer";
const paidFetch = wrapFetch(fetch, {
signer, // viem LocalAccount, or any PaymentSigner (passkey wallets welcome)
maxAmount: "1000000", // atomic units — terms above this are never signed
assets: [USDC], // token allowlist — required (see below)
});
const res = await paidFetch("https://api.example.com/premium");maxAmountis required by design — an agent without a spending cap is an incident waiting to happen. Refusals do not throw; the original 402 comes back andonSkippedtells you why.assets(a token-address allowlist) is also required —maxAmountis a bare atomic-unit number with no notion of which token or how many decimals, so without it a hostile 402 could name an expensive token (WBTC) at the same integer. PassallowAnyAsset: trueto consciously opt out. Filter further withnetworks/schemes; observe withonPaid.maxAmountbounds ONE payment.maxTotalAmountbounds the SUM of every payment a wrapper signs — set it for unattended agents, or a seller that answers 402 to every request is limited only by your balance.uptoterms (a cap the seller settles at or below) are paid by default:maxAmountandmaxTotalAmountboth count the cap — the worst case you signed. The seller-reported actual charge inPAYMENT-RESPONSEis exposed throughonPaid(terms, settlement)for your own accounting but never restores the budget (a seller could claim anything). Restrict withschemes: [exactScheme]to refuse caps.- For QR/POS flows without fetch,
signPayment(requirements, { signer })signs chosen terms directly. - Paying with a token that lacks EIP-3009 (the permit2 path)? Run the one-time
approvePermit2({ walletClient, publicClient, token })first — the only gas-spending call in this package; it no-ops when already approved and refuses to approve an address with no contract code. The allowance is unlimited by convention and outlives this kit —revokePermit2sets it back to zero. After that, payments are signature-only as usual. - Subscriptions/installments:
signPaymentSchedule(terms, { signer, periods, maxTotalAmount, assets })signs one standard payment per billing period in a single ceremony. Exposure is exactly n × amount — signing refuses pastmaxTotalAmount— and each installment only settles inside its own window.assetsis required here for the same reason as onwrapFetch(the terms usually come from the seller). Undo:revokePermit2for permit2 schedules; EIP-3009 installments expire with their windows (orcancelAuthorizationon the token). - Axios?
import { attachX402 } from "@x402.kit/buyer/axios"; attachX402(axios, { signer, maxAmount, assets })— same safety model, handling axios's 402-as-rejection and retrying once. - MCP?
import { wrapMcpClient } from "@x402.kit/buyer/mcp"wraps any MCP client'scallToolwith the same caps: a payment-required tool result is signed undermaxAmount/maxTotalAmount/assetsand retried once with the payment in_meta["x402/payment"]; the receipt arrives viaonPaid, and a refusal hands the seller's result back untouched (onSkippedsays why). The SDK is not a dependency — anything withcallTool(params)wraps. - Retry safely by re-sending the SAME signed payload (same nonce) — the facilitator dedupes on the signed on-chain nonce, so a resend settles once. A fresh signature is a genuinely new payment (there is no unsigned "idempotency key" that could be abused to replay one payment as many).
- Schemes whose terms outlive one request declare
requiresConsent; the wrapper refuses to sign them unless their name appears inconsentTo. - A signed authorization is a bearer instrument, so the wrapper defends it:
the on-chain validity window is clamped to
maxValiditySeconds(default 300s) regardless of what the server proposes; a 402 that arrived via a redirect to another origin is never signed; and the paid retry is sentredirect: "manual"to the original origin only — thePAYMENT-SIGNATUREheader is never followed to a redirect target. (The axios adapter pinsmaxRedirects: 0/fetchOptions.redirect: "manual"; the browser XHR adapter cannot suppress redirects, so in browsers useadapter: "fetch".)
