agent402-client 0.8.19 → 0.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +48 -7
  2. package/index.js +222 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -94,6 +94,48 @@ credits call reserves its price like a wallet call). A payment `fetch` still win
94
94
  when both are given, and free pure-CPU tools keep settling with proof-of-work.
95
95
  The same Bearer header also reads the balance directly, e.g. `curl -H "Authorization: Bearer a402_..." https://agent402.tools/api/credits/balance`.
96
96
 
97
+ ## Paying from a Circle Gateway deposit (batched, no gas per call)
98
+
99
+ Where a server offers it (Agent402 does on Arc and Base when its operator turns
100
+ it on; `/api/pricing` says under `payment.gatewayNanopayments`), the 402 carries
101
+ a second `exact` accept whose `extra.name` is `GatewayWalletBatched`. A buyer who
102
+ deposited USDC into Circle Gateway once (an on-chain transfer through Circle's
103
+ GatewayWallet contract; it pays gas once, and this SDK never deposits or
104
+ withdraws) pays each call by signing the same EIP-3009 authorization under the
105
+ Gateway domain the accept carries instead of the USDC contract's; Gateway
106
+ verifies it against the deposit, settles it off chain and lands the batch on
107
+ chain later. Pass the signer as the `gateway` option. Off by default: without it
108
+ nothing changes. The SDK has no dependencies: install `viem` for
109
+ `privateKeyToAccount`, or pass any EIP-712 signer with `address` and
110
+ `signTypedData`.
111
+
112
+ ```js
113
+ import { Agent402 } from "agent402-client";
114
+ import { privateKeyToAccount } from "viem/accounts";
115
+
116
+ const signer = privateKeyToAccount(process.env.AGENT_KEY); // its address holds a Gateway deposit on Base or Arc
117
+ const a = new Agent402({ gateway: { signer }, maxPerCallUsd: 0.05 });
118
+ const out = await a.call("extract", { url: "https://agent402.tools/guides/x402-in-5-minutes" });
119
+ ```
120
+
121
+ `signer` is any EIP-712 signer with `address` and `signTypedData` (a viem
122
+ account). `gateway.networks` (chain names, as `withNetworkPreference` takes, for
123
+ example `["arc"]`) limits which chains' batched accepts are signed; the default
124
+ is any EVM chain the 402 offers one on. The caps are checked against the
125
+ accept's own amount before anything is signed. A 402 with no batched accept this
126
+ signer may pay (another seller, an identity-bound route, a route with a lasting
127
+ effect) falls back to your payment `fetch` or `creditsKey` exactly as without the
128
+ option, and with neither fails with a 402 that says so. A refusal for a short
129
+ Gateway balance relays the server's hint and says to deposit first. A batched
130
+ accept asking for a validity window past seven days and an hour
131
+ (`GATEWAY_MAX_TIMEOUT_SECONDS`) is never signed: the signed authorization is a
132
+ bearer claim on the deposit until its `validBefore`. For the same reason a
133
+ batched authorization that was sent and refused stays valid until its
134
+ `validBefore` and keeps counting against the caps; only a signer that throws
135
+ before anything is sent releases the amount. The mechanism in full: [wiki: Circle Gateway](https://github.com/MikeyPetrillo/Agent402/wiki/Circle-Gateway).
136
+ Two helpers are exported for a client of your own: `gatewayAcceptOf(accepts,
137
+ networks?)` and `signGatewayPayment(accept, { signer, resource })`.
138
+
97
139
  ## Retries and double charges
98
140
 
99
141
  Every send carries an `Idempotency-Key`. By default it is derived from the
@@ -124,12 +166,11 @@ What the server does with it depends on how the call was paid:
124
166
  route with the same body, from its first answer inside the replay window,
125
167
  with no second settlement, and marks it `X-Replayed-Answer: 1` (identity-bound
126
168
  routes, routes with a lasting effect and streamed calls excepted).
127
- - **Circle Gateway batched option:** where the server offers it on Arc or Base,
128
- the 402 carries a second accept whose `extra.name` is `GatewayWalletBatched`.
129
- This package pays the plain accept on those networks (the stock x402 client
130
- takes the first accept it can pay, and the plain one is first); the batched
131
- one needs a client that signs under the Gateway domain against a Circle
132
- Gateway deposit.
169
+ - **Circle Gateway batched option:** a payment the server has not had confirmed
170
+ by Gateway yet is answered `402` with reason `settlement-pending`; the SDK
171
+ resends the same signed header after the server's `Retry-After` (never a new
172
+ signature), and past three resends throws `GatewayPendingError` with
173
+ `paymentHeader`, the header to resend later. See the section above.
133
174
 
134
175
  ## Workflows (skill packs)
135
176
 
@@ -228,7 +269,7 @@ that runs as one.
228
269
 
229
270
  | Method | What |
230
271
  |---|---|
231
- | `new Agent402({ baseUrl?, fetch?, x402Client?, creditsKey?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd?, payNetworks?, maxResponseBytes?, outputValidator?, respondAsync?, asyncTimeoutMs?, onProgress? })` | `fetch` is your x402- or MPP-wrapped fetch for paid tools (optional); `x402Client` is the `@x402/core` client that `fetch` wraps, so the caps are enforced on the payment that is signed (see below); `payNetworks` (for example `["base"]`, the names `withNetworkPreference` takes) tells the caps which chains a pre-wrapped `fetch` without an `x402Client` pays on, so a call is counted at the dearest option on those chains (when none of a 402's options is on them, the dearest of all counts); `creditsKey` is a prepaid card-credits key (`a402_...`) used for paid tools when no `fetch` is given; `cache` (default `true`) memoizes deterministic results; the three USD caps set optional spending limits (see below); `maxResponseBytes` (default 32MB, `null` to disable) refuses an oversized response body before it is parsed; `outputValidator` binds a caller-supplied delivery check; `respondAsync` (default `"auto"`), `asyncTimeoutMs` (default 15 minutes) and `onProgress` control slow routes run as jobs (see above) |
272
+ | `new Agent402({ baseUrl?, fetch?, x402Client?, creditsKey?, gateway?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd?, payNetworks?, maxResponseBytes?, outputValidator?, respondAsync?, asyncTimeoutMs?, onProgress? })` | `fetch` is your x402- or MPP-wrapped fetch for paid tools (optional); `x402Client` is the `@x402/core` client that `fetch` wraps, so the caps are enforced on the payment that is signed (see below); `payNetworks` (for example `["base"]`, the names `withNetworkPreference` takes) tells the caps which chains a pre-wrapped `fetch` without an `x402Client` pays on, so a call is counted at the dearest option on those chains (when none of a 402's options is on them, the dearest of all counts); `creditsKey` is a prepaid credits key (`a402_...`) used for paid tools when no `fetch` is given; `gateway` (`{ signer, networks? }`) pays the Circle Gateway batched accept where a 402 offers one, from the signer's Gateway deposit (see above); `cache` (default `true`) memoizes deterministic results; the three USD caps set optional spending limits (see below); `maxResponseBytes` (default 32MB, `null` to disable) refuses an oversized response body before it is parsed; `outputValidator` binds a caller-supplied delivery check; `respondAsync` (default `"auto"`), `asyncTimeoutMs` (default 15 minutes) and `onProgress` control slow routes run as jobs (see above) |
232
273
  | `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools on **this host** (route, price, schema, example) |
233
274
  | `await a.route(task, { k?, include?, network? })` | Cross-seller Smart Order Router: rank eligible/routable seller rows from the host's current index (free, read-only; opaque results may include server-reported `executeVia`) |
234
275
  | `await a.findWorkflows(task, { k = 2 })` | Resolve a task to matching multi-tool workflow templates (skill packs) |
package/index.js CHANGED
@@ -19,7 +19,7 @@
19
19
  // const a = new Agent402({ fetch: payFetch });
20
20
  import { createHash, randomBytes } from "node:crypto";
21
21
  import { AsyncLocalStorage } from "node:async_hooks";
22
- import { quoteOf, guardSigning, networkIdsOf } from "./known-assets.js";
22
+ import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.js";
23
23
  export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
24
24
  import { distrustFetch } from "./tls-distrust.js";
25
25
  export { distrustFetch, distrustDispatcher, distrustCheckServerIdentity, DISTRUSTED_SPKI_SHA256 } from "./tls-distrust.js";
@@ -30,7 +30,7 @@ export { AsyncJobPendingError } from "./async-job.js";
30
30
  // `User-Agent: agent402-client/<version>` - a standard header, no extra
31
31
  // network calls - so a seller can attribute traffic (and settled payments)
32
32
  // to this SDK. Product token only; nothing about the caller rides along.
33
- const VERSION = "0.8.19";
33
+ const VERSION = "0.8.20";
34
34
  const USER_AGENT = `agent402-client/${VERSION}`;
35
35
  // 32MB: about a hundred times any realistic response from this catalog (the
36
36
  // largest is a base64 image at a few MB), so it cannot break a legitimate
@@ -67,6 +67,42 @@ async function shedRetryWaitMs(r, maxWaitMs = DISCOVERY_MAX_WAIT_MS) {
67
67
 
68
68
  const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
69
69
 
70
+ // The Circle Gateway batched option: a second `exact` accept a seller may
71
+ // offer on an EVM chain, named by `extra.name`, that a buyer pays from a USDC
72
+ // balance deposited into Circle Gateway once (an on-chain transfer the buyer
73
+ // pays gas for) by signing the same EIP-3009 TransferWithAuthorization under
74
+ // the GatewayWalletBatched EIP-712 domain the accept carries (its name,
75
+ // version and verifyingContract, with the chain's own id) instead of the
76
+ // asset's. Gateway verifies and settles it off chain, so no gas rides on a
77
+ // call. A stock x402 client signs under the asset's domain, so this is paid
78
+ // here, by the signer the `gateway` option names, and only when that option
79
+ // is set. A payment Gateway has not confirmed inside the seller's wait is
80
+ // answered 402 with reason settlement-pending: the SAME header is resent
81
+ // after the seller's Retry-After (never a new signature, which could be a
82
+ // second charge), at most GATEWAY_PENDING_RESENDS times, each wait capped at
83
+ // GATEWAY_PENDING_MAX_WAIT_MS; past that GatewayPendingError carries the
84
+ // header to resend later.
85
+ export const GATEWAY_KIND_NAME = "GatewayWalletBatched";
86
+ const GATEWAY_TYPES = Object.freeze({
87
+ TransferWithAuthorization: [
88
+ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
89
+ { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
90
+ ],
91
+ });
92
+ const GATEWAY_PENDING_RESENDS = 3;
93
+ const GATEWAY_PENDING_MAX_WAIT_MS = 30_000;
94
+ // The longest validity window a batched accept may ask for. A seller's
95
+ // accept sets validBefore (now plus its maxTimeoutSeconds), and the signed
96
+ // authorization is a bearer claim on the deposit until then, so an accept
97
+ // asking for years is refused unsigned. Gateway's kinds publish a minimum
98
+ // validity of seven days, and a seller derives its window from that plus a
99
+ // small buffer, so seven days and an hour admits every honest accept.
100
+ // Agent402's own accept adds VALIDITY_BUFFER_SECONDS (src/gateway-nanopayments.js)
101
+ // to that minimum; scripts/test-gateway-nanopayments.js keeps it under this ceiling.
102
+ export const GATEWAY_MAX_TIMEOUT_SECONDS = 7 * 24 * 3600 + 3600;
103
+ const GATEWAY_DOCS_URL = "https://github.com/MikeyPetrillo/Agent402/wiki/Circle-Gateway";
104
+ const EVM_ADDR = /^0x[0-9a-fA-F]{40}$/;
105
+
70
106
  // A pre-wrapped payment fetch with no x402Client is the one mode where the caps
71
107
  // are checked on an unpaid preflight only, not on the payment that is signed.
72
108
  // Said once per process, on the first capped paid call in that mode.
@@ -94,8 +130,14 @@ export class Agent402 {
94
130
  * @param {string[]} [opts.payNetworks] the chains your pre-wrapped `fetch` pays on (e.g. ["base"], the
95
131
  * names withNetworkPreference takes): without an x402Client the
96
132
  * caps count the dearest accept on these chains, not on every chain
97
- * @param {string} [opts.creditsKey] a prepaid card-credits key (a402_...) - pays wallet-only tools on
133
+ * @param {string} [opts.creditsKey] a prepaid credits key (a402_...) - pays wallet-only tools on
98
134
  * Agent402's own services when no payFetch is given
135
+ * @param {{signer: {address: string, signTypedData: function}, networks?: string[]}} [opts.gateway]
136
+ * Pay the Circle Gateway batched accept where a 402 offers one: `signer` is an
137
+ * EIP-712 signer (a viem account: `address` and `signTypedData`) that holds a USDC
138
+ * deposit in Circle Gateway; `networks` (chain names, as withNetworkPreference takes)
139
+ * limits which chains' batched accepts it signs (default: any EVM chain offered). Off
140
+ * by default. A 402 with no batched accept falls back to `fetch` or `creditsKey`.
99
141
  * @param {number|null} [opts.maxResponseBytes=33554432]
100
142
  * Hard ceiling on a response body, enforced BEFORE it is parsed. null disables it.
101
143
  * @param {{id:string, validate:function}} [opts.outputValidator]
@@ -114,8 +156,9 @@ export class Agent402 {
114
156
  constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = undefined, tlsDistrust = true,
115
157
  maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null, x402Client = null, payNetworks = null,
116
158
  maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, outputValidator = null,
117
- respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null } = {}) {
159
+ respondAsync = "auto", asyncTimeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, onProgress = null, gateway = null } = {}) {
118
160
  this.respondAsync = respondAsync;
161
+ this.gateway = normalizeGateway(gateway);
119
162
  this.asyncTimeoutMs = numOrNull(asyncTimeoutMs) ?? DEFAULT_ASYNC_TIMEOUT_MS;
120
163
  this.onProgress = typeof onProgress === "function" ? onProgress : null;
121
164
  this.creditsKey = typeof creditsKey === "string" && /^a402_[A-Za-z0-9_-]{32,64}$/.test(creditsKey) ? creditsKey : null;
@@ -500,6 +543,15 @@ export class Agent402 {
500
543
 
501
544
  // Wallet-only tool → settle in USDC via the provided x402 fetch.
502
545
  if (!tool.computePayable) {
546
+ // The Circle Gateway batched accept, when this client signs for it and
547
+ // the 402 offers one; otherwise the paths below, as without the option.
548
+ if (this.gateway) {
549
+ const paid = await this._gatewayCall(slug, tool, { paidSend, asyncHeaders, collect, cacheKey, cache, validator, maxResponseBytes });
550
+ if (paid !== NO_GATEWAY_ACCEPT) return paid;
551
+ if (!this.payFetch && !this.creditsKey) {
552
+ throw Object.assign(new Error(`call "${slug}" failed: HTTP 402 - the seller offers no Circle Gateway batched accept${this.gateway.networks ? ` on ${this.gateway.networks.join(", ")}` : ""} for this route, so the gateway signer cannot pay it; construct with { fetch: payFetch } to pay a plain accept`), { status: 402, slug, paid: false });
553
+ }
554
+ }
503
555
  if (this.payFetch) {
504
556
  // Spending policy: refuse to pay BEFORE signing if the price breaks a
505
557
  // configured ceiling (per-call / rolling-24h / per-host).
@@ -551,7 +603,7 @@ export class Agent402 {
551
603
  }
552
604
  }
553
605
  if (this.creditsKey) {
554
- // Prepaid card credits: the server authorizes against the key's balance
606
+ // A prepaid credits key: the server authorizes against the key's balance
555
607
  // before the handler and debits only on a 200 (X-Credits-Balance tells
556
608
  // what is left). The same spend caps apply as on the wallet path, and
557
609
  // against the same number: a credits gate holds and debits the amount
@@ -612,6 +664,64 @@ export class Agent402 {
612
664
  return this._readJson(r, { maxBytes: maxResponseBytes, slug: "job", paid: true });
613
665
  }
614
666
 
667
+ /**
668
+ * Pay a wallet-only route on the Circle Gateway batched accept its 402
669
+ * offers. Reads the unpaid 402 (one round trip), picks the batched accept
670
+ * (gatewayAcceptOf), holds its amount to the caps BEFORE anything is
671
+ * signed, signs the authorization under the Gateway domain and sends it as
672
+ * the payment header on the plain fetch (never the payment-wrapped one,
673
+ * which would sign the plain accept). Returns NO_GATEWAY_ACCEPT, with
674
+ * nothing reserved or signed, when the answer is not a 402 or offers no
675
+ * batched accept this signer may pay, so the caller falls back. Once the
676
+ * signed header has been sent the amount is spent for the caps whatever
677
+ * comes back; only a signer that throws releases the reservation.
678
+ */
679
+ async _gatewayCall(slug, tool, { paidSend, asyncHeaders, collect, cacheKey, cache, validator, maxResponseBytes }) {
680
+ const pre = await paidSend();
681
+ if (pre?.status !== 402) return NO_GATEWAY_ACCEPT;
682
+ const env = await challengeEnvelope(pre);
683
+ const accept = gatewayAcceptOf(acceptsOf(env), this.gateway.networks);
684
+ if (!accept) return NO_GATEWAY_ACCEPT;
685
+ const host = hostOf(this.baseUrl);
686
+ const listUsd = parseUsd(tool.price);
687
+ // The amount the accept asks for, priced from the known-asset table
688
+ // (gatewayAcceptOf keeps only an accept that table can price); the
689
+ // catalog's figure never lowers what the caps are checked against.
690
+ const usd = Math.max(listUsd, acceptUsd(accept));
691
+ const reservation = this._spendReserve(host, usd, slug, listUsd);
692
+ let header;
693
+ try {
694
+ ({ header } = await signGatewayPayment(accept, { signer: this.gateway.signer, resource: env?.resource }));
695
+ } catch (e) {
696
+ // Nothing signed: the reservation is the only thing to undo.
697
+ this._spendRelease(reservation);
698
+ throw e;
699
+ }
700
+ // From here the signed authorization leaves this process. It is a bearer
701
+ // claim on the deposit until its validBefore whatever the seller answers
702
+ // (a refusal, a failure, a redirect, a job still running), so it counts
703
+ // as spent against the caps from the first send.
704
+ this._spendSettle(reservation);
705
+ const paidHeaders = { "PAYMENT-SIGNATURE": header, ...asyncHeaders };
706
+ let r = await collect(await paidSend(paidHeaders));
707
+ let body = null;
708
+ for (let resend = 0; ; resend++) {
709
+ if (r.status !== 402) break;
710
+ body = await readJsonQuietly(r);
711
+ const pending = body && typeof body === "object" && (body.reason === "settlement-pending" || body.retry === "same-payment");
712
+ if (!pending) break;
713
+ // Gateway may yet settle it: the header is handed back so the same
714
+ // payment can be resent later.
715
+ if (resend >= GATEWAY_PENDING_RESENDS) throw new GatewayPendingError(slug, header, body);
716
+ const retryAfterSec = Number(r.headers?.get?.("retry-after")) || Number(body.retryAfterSeconds) || 10;
717
+ await this._sleep(Math.min(Math.max(0, retryAfterSec) * 1000, GATEWAY_PENDING_MAX_WAIT_MS));
718
+ r = await collect(await paidSend(paidHeaders));
719
+ }
720
+ if (r.status === 402) throw this._fail(slug, r, gatewayRefusalMessage(slug, body, accept, this.gateway.signer.address));
721
+ if (!r.ok) throw this._fail(slug, r, await this._failureDetail(slug, r));
722
+ return this._deliverResponse(slug, r, cacheKey, cache, validator, { maxResponseBytes, paid: true });
723
+ }
724
+
615
725
  async _assertOutput(slug, body, validator, { paid = false, cacheHit = false } = {}) {
616
726
  if (!validator) return body;
617
727
  try {
@@ -859,6 +969,105 @@ export class SpendingLimitError extends Error {
859
969
  }
860
970
  }
861
971
 
972
+ /** A Circle Gateway batched payment the seller had not confirmed after the
973
+ * resends. `paymentHeader` is the signed payment: resend the same request
974
+ * with it as PAYMENT-SIGNATURE to be served once Gateway confirms it, with
975
+ * no new charge. Never sign a new authorization for the same request. The
976
+ * amount counts against the caps (Gateway may settle it). */
977
+ export class GatewayPendingError extends Error {
978
+ constructor(slug, paymentHeader, body = null) {
979
+ super(`"${slug}" is paid on the Circle Gateway batched option and Gateway had not confirmed the payment after ${GATEWAY_PENDING_RESENDS} resends; resend the same request with the same PAYMENT-SIGNATURE header (paymentHeader) after a short wait, never a new signature${body?.hint ? ` - ${body.hint}` : ""}`);
980
+ this.name = "GatewayPendingError";
981
+ this.slug = slug;
982
+ this.paymentHeader = paymentHeader;
983
+ this.status = 402;
984
+ this.paid = null;
985
+ this.retryAfterSeconds = Number(body?.retryAfterSeconds) || null;
986
+ }
987
+ }
988
+
989
+ /** Sentinel: the 402 offered no batched accept this client signs for. */
990
+ const NO_GATEWAY_ACCEPT = Symbol("no-gateway-accept");
991
+
992
+ async function readJsonQuietly(r) {
993
+ try { return await (typeof r?.clone === "function" ? r.clone() : r).json(); } catch { return null; }
994
+ }
995
+
996
+ function normalizeGateway(value) {
997
+ if (value == null || value === false) return null;
998
+ if (typeof value !== "object") throw new TypeError("gateway must be { signer, networks? }");
999
+ const signer = value.signer;
1000
+ if (!signer || typeof signer.signTypedData !== "function" || !EVM_ADDR.test(String(signer.address || ""))) {
1001
+ throw new TypeError("gateway.signer must be an EIP-712 signer with an EVM address and signTypedData (a viem account)");
1002
+ }
1003
+ const networks = Array.isArray(value.networks) && value.networks.length ? value.networks.map(String) : null;
1004
+ return Object.freeze({ signer, networks });
1005
+ }
1006
+
1007
+ /** The first Circle Gateway batched accept a buyer may pay: scheme exact,
1008
+ * `extra.name` the Gateway kind, on an EVM chain (among `networks` when
1009
+ * given), with a payTo, a verifyingContract, a domain version, a validity
1010
+ * window of at most GATEWAY_MAX_TIMEOUT_SECONDS and an amount in a known
1011
+ * asset (so the caps can price it; a batched accept in an asset the table
1012
+ * does not know is never signed). null when there is none. */
1013
+ export function gatewayAcceptOf(accepts, networks = null) {
1014
+ const list = Array.isArray(accepts) ? accepts : [];
1015
+ const wanted = networks ? new Set(networks.flatMap((n) => networkIdsOf(n)).map((id) => id.toLowerCase())) : null;
1016
+ return list.find((a) => a && typeof a === "object" && a.scheme === "exact"
1017
+ && a.extra && typeof a.extra === "object" && a.extra.name === GATEWAY_KIND_NAME
1018
+ && /^eip155:[0-9]{1,12}$/.test(String(a.network || "")) && (!wanted || wanted.has(String(a.network).toLowerCase()))
1019
+ && EVM_ADDR.test(String(a.payTo || "")) && EVM_ADDR.test(String(a.extra.verifyingContract || ""))
1020
+ && typeof a.extra.version === "string" && /^[0-9]{1,4}$/.test(a.extra.version)
1021
+ && /^[0-9]{1,78}$/.test(String(a.amount ?? "")) && Number(a.maxTimeoutSeconds) > 0 && Number(a.maxTimeoutSeconds) <= GATEWAY_MAX_TIMEOUT_SECONDS
1022
+ && acceptUsd(a) != null) || null;
1023
+ }
1024
+
1025
+ /**
1026
+ * Sign a Circle Gateway batched payment for one accept: an EIP-3009
1027
+ * TransferWithAuthorization from the signer to the accept's payTo for exactly
1028
+ * its amount, valid from now (validAfter 0) for the accept's
1029
+ * maxTimeoutSeconds, under the Gateway EIP-712 domain the accept carries
1030
+ * (name, version, verifyingContract) with the accept's chain id. Returns the
1031
+ * x402 v2 payment payload and its base64 header value.
1032
+ */
1033
+ export async function signGatewayPayment(accept, { signer, resource = undefined, now = Date.now, nonce = undefined } = {}) {
1034
+ if (!signer || typeof signer.signTypedData !== "function" || !EVM_ADDR.test(String(signer.address || ""))) throw new TypeError("signGatewayPayment needs a signer with an EVM address and signTypedData");
1035
+ if (!gatewayAcceptOf([accept])) throw new TypeError("signGatewayPayment needs a Circle Gateway batched accept (scheme exact, extra.name GatewayWalletBatched, an EVM network, payTo, verifyingContract, version, amount, maxTimeoutSeconds)");
1036
+ const nowSec = Math.floor(now() / 1000);
1037
+ const authorization = {
1038
+ from: signer.address, to: accept.payTo, value: String(accept.amount),
1039
+ validAfter: "0", validBefore: String(nowSec + Math.floor(Number(accept.maxTimeoutSeconds))),
1040
+ nonce: nonce || `0x${randomBytes(32).toString("hex")}`,
1041
+ };
1042
+ const signature = await signer.signTypedData({
1043
+ domain: { name: accept.extra.name, version: accept.extra.version, chainId: Number(String(accept.network).split(":")[1]), verifyingContract: accept.extra.verifyingContract },
1044
+ types: GATEWAY_TYPES,
1045
+ primaryType: "TransferWithAuthorization",
1046
+ message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
1047
+ });
1048
+ const payload = { x402Version: 2, ...(resource && typeof resource === "object" ? { resource } : {}), accepted: accept, payload: { signature, authorization } };
1049
+ return { payload, header: Buffer.from(JSON.stringify(payload)).toString("base64") };
1050
+ }
1051
+
1052
+ /** The message for a 402 that refused a batched payment: the seller's own
1053
+ * explanation first, and when it says the Gateway balance came up short,
1054
+ * what to do about it (deposit, never a resend of the same authorization).
1055
+ * A short balance is read from the machine fields the seller sends (reason
1056
+ * insufficient-funds, or retry fund-gateway-deposit); the prose is read only
1057
+ * when a body carries neither field. */
1058
+ function gatewayRefusalMessage(slug, body, accept, address) {
1059
+ const said = body && typeof body === "object" ? (body.hint ?? body.detail ?? body.error ?? body.message) : null;
1060
+ const reason = typeof body?.reason === "string" ? body.reason : null;
1061
+ const retry = typeof body?.retry === "string" ? body.retry : null;
1062
+ const short = reason != null || retry != null
1063
+ ? reason === "insufficient-funds" || retry === "fund-gateway-deposit"
1064
+ : /insufficient|balance|underflow|not enough/i.test(typeof said === "string" ? said : "");
1065
+ const chain = String(accept.network);
1066
+ return `call "${slug}" refused on the Circle Gateway batched option (${chain}): HTTP 402${said != null ? ` - ${typeof said === "string" ? said : JSON.stringify(said)}` : ""}${short
1067
+ ? ` The signer ${address} needs a USDC balance in Circle Gateway on that chain: deposit through Circle's GatewayWallet contract first (this client never deposits), then call again; see ${GATEWAY_DOCS_URL}.`
1068
+ : ""} The refused authorization was sent and stays valid until its validBefore, so it counts against this client's spending caps.`;
1069
+ }
1070
+
862
1071
  /** The dearest amount a 402 asks for, in USD, or null when it cannot be read.
863
1072
  *
864
1073
  * Each accept is priced from the known-asset table (./known-assets.js): the
@@ -878,16 +1087,22 @@ export async function quoted402Usd(res) {
878
1087
 
879
1088
  /** The accepts array of a 402, from the PAYMENT-REQUIRED header first, then the body. */
880
1089
  async function challengeAccepts(res) {
1090
+ return acceptsOf(await challengeEnvelope(res));
1091
+ }
1092
+
1093
+ /** The decoded PaymentRequired object of a 402 (the header first, then the
1094
+ * body), or null when neither carries accepts. */
1095
+ async function challengeEnvelope(res) {
881
1096
  const fromHeader = decodeChallengeHeader(res?.headers?.get?.("payment-required"));
882
1097
  if (fromHeader) return fromHeader;
883
1098
  let body = null;
884
1099
  try { body = await res.json(); } catch { body = null; }
885
- return acceptsOf(body);
1100
+ return acceptsOf(body) ? body : null;
886
1101
  }
887
1102
 
888
1103
  function decodeChallengeHeader(header) {
889
1104
  if (typeof header !== "string" || !header.trim()) return null;
890
- try { return acceptsOf(JSON.parse(Buffer.from(header.trim(), "base64").toString("utf8"))); } catch { return null; }
1105
+ try { const obj = JSON.parse(Buffer.from(header.trim(), "base64").toString("utf8")); return acceptsOf(obj) ? obj : null; } catch { return null; }
891
1106
  }
892
1107
 
893
1108
  /** The accepts array, at the top level or under the envelope keys sellers wrap it in. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-client",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "description": "Agent402 buyer SDK: resolve a task to a tool across 500+ pay-per-call endpoints and call it with payment handled - free via proof-of-work, or over x402 or MPP (a stock mppx fetch works) with a wallet you keep; the buy side of Agentic Finance. Caching, idempotent retries (Idempotency-Key), non-custodial, zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "index.js",