agent402-client 0.8.18 → 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.
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) |
@@ -365,8 +406,13 @@ caps hold at signing by default.
365
406
  `amount`, so each option is priced from a built-in table of known assets per
366
407
  network (`KNOWN_ASSETS`: USDC on each Agent402 rail, USDG on Robinhood Chain,
367
408
  with their on-chain decimals), never from a decimals value the seller sends. An
368
- option in an asset outside the table cannot be priced, and a capped wallet never
369
- signs it. When a cap is set, the client preflights the `402` and checks the
409
+ option in an asset outside the table cannot be priced. With the `x402Client`
410
+ passed, the signing guard drops such an option before anything is signed, so
411
+ it is skipped beside an option that can be priced and the call pays that one;
412
+ the call is refused only when no option can be priced. With a pre-wrapped
413
+ `fetch` and no `x402Client`, nothing stops the wallet from signing it, so a
414
+ capped call is refused before the wallet runs when any option in the `402`
415
+ cannot be priced. When a cap is set, the client preflights the `402` and checks the
370
416
  ceiling against the **larger** of the advertised price (from the seller's
371
417
  `/api/pricing`) and the amount the `402` quotes; if no `402` comes back
372
418
  (FREE_MODE) it checks the advertised price. Caps hold under **concurrency** too:
@@ -398,7 +444,11 @@ const payFetch = wrapFetchWithPayment(fetch, client);
398
444
  Short names map to CAIP-2 (`base`, `solana`, `polygon`, `arbitrum`,
399
445
  `robinhood`); unknown entries pass through verbatim so future chains work
400
446
  without a package update. If the preference matches none of a seller's
401
- payment options it throws **before** any payment is signed.
447
+ payment options it throws **before** any payment is signed. Since 0.8.19 a
448
+ name resolves through the known-asset table (`NETWORK_NAMES`) to every id the
449
+ chain is spelled with on the wire, and an id to the ids of its chain, so
450
+ `["algorand"]`, or either spelling of Algorand mainnet, matches a seller that
451
+ uses the other.
402
452
 
403
453
 
404
454
  ## Only pay who you meant to (`withPayeeAllowlist`)
package/index.js CHANGED
@@ -19,8 +19,8 @@
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 } from "./known-assets.js";
23
- export { KNOWN_ASSETS, knownAsset, acceptUsd } from "./known-assets.js";
22
+ import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.js";
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";
26
26
  import { awaitJob, shouldRequestAsync, AsyncJobPendingError, DEFAULT_ASYNC_TIMEOUT_MS } from "./async-job.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.18";
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;
@@ -160,7 +203,7 @@ export class Agent402 {
160
203
  if (x402Client) this.guardX402Client(x402Client);
161
204
  // The chains a pre-wrapped fetch pays on, as CAIP-2 ids (null = unknown).
162
205
  this._payNetworks = Array.isArray(payNetworks) && payNetworks.length
163
- ? new Set(payNetworks.map((n) => String(NETWORK_CAIP2[String(n).trim().toLowerCase()] || String(n).trim()).toLowerCase()))
206
+ ? new Set(payNetworks.flatMap((n) => networkIdsOf(n)).map((id) => id.toLowerCase()))
164
207
  : null;
165
208
  // A response-size ceiling, enforced before parsing. See _readJson.
166
209
  this.maxResponseBytes = maxResponseBytes === null ? null : (numOrNull(maxResponseBytes) ?? DEFAULT_MAX_RESPONSE_BYTES);
@@ -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 {
@@ -656,8 +766,9 @@ export class Agent402 {
656
766
  * "guarded" (the signing guard holds every signature to the caps, so the
657
767
  * cheapest known accept is what can be paid, and unknown ones are dropped
658
768
  * there), "wallet" (no guard: the wallet may sign any accept, so the dearest
659
- * one counts, among the chains in `payNetworks` when they are given, and one
660
- * that cannot be priced is refused here), or "credits" (nothing is signed:
769
+ * one counts, among the chains in `payNetworks` when they are given, and an
770
+ * accept this client cannot price is refused here, since nothing stops the
771
+ * wallet from signing it), or "credits" (nothing is signed:
661
772
  * the credits gate debits the route's own quote, and no accept is below it,
662
773
  * since a chain premium or price floor only raises an accept, so the
663
774
  * cheapest known accept counts).
@@ -682,11 +793,19 @@ export class Agent402 {
682
793
  return listUsd; /* no readable 402: the advertised price */
683
794
  }
684
795
  if (!q) return listUsd;
685
- if (q.max == null || (mode === "wallet" && q.unknown > 0)) {
796
+ // An option in an asset this client cannot price: with no guard the wallet
797
+ // may sign it, for an amount no cap was checked against, so one such
798
+ // option refuses the call; the guard drops it before signing, so beside a
799
+ // priced option it is skipped there; a credits gate signs nothing, so it
800
+ // is ignored. An offer with nothing priceable is refused under a cap in
801
+ // either signing mode.
802
+ const unpriceable = q.max == null || (mode === "wallet" && q.unknown > 0);
803
+ if (unpriceable) {
686
804
  if (mode === "credits") return listUsd;
687
- throw new SpendingLimitError(
688
- `refusing to pay for "${slug}" - ${q.max == null ? "no" : "a"} payment option it offers is in an asset this client cannot price, and a capped wallet signs only what it can price${mode === "wallet" ? " (pass your x402Client to guard signing and pay the options it can price)" : ""}`,
689
- { limit: "knownAsset", slug, priceUsd: null, unknownOptions: q.unknown });
805
+ throw new SpendingLimitError(q.max == null
806
+ ? `refusing to pay for "${slug}" - no payment option it offers is in an asset this client can price, and a capped wallet signs only what it can price${mode === "wallet" ? " (pass your x402Client to guard signing and pay the options it can price)" : ""}`
807
+ : `refusing to pay for "${slug}" - ${q.unknown} of its ${q.total} payment options ${q.unknown === 1 ? "is" : "are"} in an asset this client cannot price, and without the signing guard the wallet may sign any of them (pass your x402Client to guard signing and pay the options it can price)`,
808
+ { limit: "knownAsset", slug, priceUsd: null, unknownOptions: q.unknown });
690
809
  }
691
810
  const quoted = mode === "wallet" ? q.max : q.min;
692
811
  // Larger of the two: an understated catalog must not lower the ceiling
@@ -850,6 +969,105 @@ export class SpendingLimitError extends Error {
850
969
  }
851
970
  }
852
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
+
853
1071
  /** The dearest amount a 402 asks for, in USD, or null when it cannot be read.
854
1072
  *
855
1073
  * Each accept is priced from the known-asset table (./known-assets.js): the
@@ -869,16 +1087,22 @@ export async function quoted402Usd(res) {
869
1087
 
870
1088
  /** The accepts array of a 402, from the PAYMENT-REQUIRED header first, then the body. */
871
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) {
872
1096
  const fromHeader = decodeChallengeHeader(res?.headers?.get?.("payment-required"));
873
1097
  if (fromHeader) return fromHeader;
874
1098
  let body = null;
875
1099
  try { body = await res.json(); } catch { body = null; }
876
- return acceptsOf(body);
1100
+ return acceptsOf(body) ? body : null;
877
1101
  }
878
1102
 
879
1103
  function decodeChallengeHeader(header) {
880
1104
  if (typeof header !== "string" || !header.trim()) return null;
881
- 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; }
882
1106
  }
883
1107
 
884
1108
  /** The accepts array, at the top level or under the envelope keys sellers wrap it in. */
@@ -961,9 +1185,12 @@ function resultCacheKey(slug, params, validator) {
961
1185
  * withNetworkPreference(x402client, ["robinhood"]); // or ["eip155:4663"]
962
1186
  * const payFetch = wrapFetchWithPayment(fetch, x402client);
963
1187
  *
964
- * Short names map to CAIP-2; unknown entries pass through verbatim so future
965
- * chains work without a package update. Throws (before paying) when the
966
- * preference matches none of a seller's payment options.
1188
+ * A name resolves through the known-asset table to every id that chain is
1189
+ * spelled with on the wire (./known-assets.js NETWORK_NAMES), and an id to
1190
+ * the ids of its chain, so either spelling of a chain matches the other;
1191
+ * unknown entries pass through verbatim so future chains work without a
1192
+ * package update. Throws (before paying) when the preference matches none of
1193
+ * a seller's payment options.
967
1194
  */
968
1195
  export const NETWORK_CAIP2 = {
969
1196
  base: "eip155:8453",
@@ -980,16 +1207,17 @@ export const NETWORK_CAIP2 = {
980
1207
  solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
981
1208
  "solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
982
1209
  stellar: "stellar:pubnet",
983
- algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
1210
+ algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k",
984
1211
  };
985
1212
 
986
1213
  export function withNetworkPreference(client, networks) {
987
- const prefs = (networks || []).map((n) => NETWORK_CAIP2[String(n).trim().toLowerCase()] || String(n).trim());
1214
+ // Every id each preference names, in preference order, once each.
1215
+ const prefs = [...new Set((networks || []).flatMap((n) => networkIdsOf(n)))];
988
1216
  if (!prefs.length) return client;
989
1217
  const orig = client.createPaymentPayload.bind(client);
990
1218
  client.createPaymentPayload = (paymentRequired) => {
991
1219
  const list = Array.isArray(paymentRequired?.accepts) ? paymentRequired.accepts : [];
992
- const picked = prefs.flatMap((caip2) => list.filter((a) => String(a?.network || "").toLowerCase() === caip2.toLowerCase()));
1220
+ const picked = prefs.flatMap((id) => list.filter((a) => String(a?.network || "").toLowerCase() === id.toLowerCase()));
993
1221
  if (!picked.length) {
994
1222
  const offered = [...new Set(list.map((a) => a?.network).filter(Boolean))];
995
1223
  throw new Error(`network preference [${prefs.join(", ")}] matched none of the seller's payment options [${offered.join(", ")}]`);
package/known-assets.js CHANGED
@@ -32,10 +32,46 @@ export const KNOWN_ASSETS = Object.freeze({
32
32
  "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": [{ asset: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", decimals: 6, symbol: "USDC" }], // devnet
33
33
  // Stellar: the USDC Stellar Asset Contract (7 decimals on Stellar).
34
34
  "stellar:pubnet": [{ asset: "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", decimals: 7, symbol: "USDC" }],
35
- // Algorand: USDC is ASA 31566704.
35
+ // Algorand: USDC is ASA 31566704. Mainnet is named by its CAIP-2 id (the
36
+ // first 32 characters of the base64 genesis hash) and, by sellers that
37
+ // predate it, by the full hash.
38
+ "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k": [{ asset: "31566704", decimals: 6, symbol: "USDC" }],
36
39
  "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=": [{ asset: "31566704", decimals: 6, symbol: "USDC" }],
37
40
  });
38
41
 
42
+ /** Chain name -> every network id in the table that names that chain. A
43
+ * chain spelled two ways on the wire lists both, so a buyer's preference by
44
+ * name, or by either spelling, matches a seller's accept in the other. */
45
+ export const NETWORK_NAMES = Object.freeze({
46
+ base: ["eip155:8453"],
47
+ "base-sepolia": ["eip155:84532"],
48
+ polygon: ["eip155:137"],
49
+ arbitrum: ["eip155:42161"],
50
+ monad: ["eip155:143"],
51
+ celo: ["eip155:42220"],
52
+ avalanche: ["eip155:43114"],
53
+ sei: ["eip155:1329"],
54
+ optimism: ["eip155:10"],
55
+ arc: ["eip155:5042"],
56
+ robinhood: ["eip155:4663"],
57
+ solana: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
58
+ "solana-devnet": ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
59
+ stellar: ["stellar:pubnet"],
60
+ algorand: ["algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k", "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="],
61
+ });
62
+
63
+ /** The network ids a preference names: a chain name's ids; for an id, every
64
+ * id of the chain it belongs to (so either spelling matches the other); an
65
+ * id the table does not know passes through as itself. */
66
+ export function networkIdsOf(pref) {
67
+ const p = String(pref || "").trim();
68
+ if (!p) return [];
69
+ const byName = NETWORK_NAMES[p.toLowerCase()];
70
+ if (byName) return [...byName];
71
+ for (const ids of Object.values(NETWORK_NAMES)) if (ids.some((id) => id.toLowerCase() === p.toLowerCase())) return [...ids];
72
+ return [p];
73
+ }
74
+
39
75
  const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
40
76
  // EVM addresses compare without case (checksum casing is presentation);
41
77
  // base58, Stellar and Algorand identifiers compare exactly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-client",
3
- "version": "0.8.18",
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",