agent402-client 0.8.17 → 0.8.19

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
@@ -5,7 +5,7 @@ instance) - the buy side of [Agentic Finance](https://agent402.tools/agentic-fin
5
5
  agents paying per request over x402 or MPP. **Resolve a task to a tool, then call it - with payment handled for
6
6
  you.** Free pure-CPU tools settle with a built-in proof-of-work (no wallet, zero
7
7
  dependencies); wallet-only tools settle via an x402- or MPP-wrapped fetch you
8
- provide, or through a prepaid credits key already issued. Results are cached, and every
8
+ provide, or through a prepaid credits key. Results are cached, and every
9
9
  send carries an `Idempotency-Key` that is stable per client and per operation, so
10
10
  a retried lost response replays the paid answer on the credits and proof-of-work
11
11
  paths (the wallet path is different: see "Retries and double charges").
@@ -71,11 +71,14 @@ const a = new Agent402({ fetch: payFetch });
71
71
  const article = await a.call("extract", { url: "https://agent402.tools/guides/x402-in-5-minutes" });
72
72
  ```
73
73
 
74
- ## Prepaid credits (keys already issued)
74
+ ## Paying with a credits key
75
75
 
76
- New credits are not on sale. A key already issued (`a402_...`) still works: pass it
76
+ A prepaid credits key (`a402_...`; agent402.tools/credits says whether packs are on
77
+ sale) pays for Agent402's own services: pass it
77
78
  as `creditsKey` and the SDK sends `Authorization: Bearer a402_...` on wallet-only
78
- calls (the wallet-scoped routes, memory and usage, refuse it and need a wallet); the server authorizes
79
+ calls (the wallet-scoped routes, memory and usage, refuse it and need a wallet, as do
80
+ the routes that buy from an outside seller: the route-execute tiers,
81
+ `seller-payability`, `decide-execute` and the flight routes); the server authorizes
79
82
  against the key's balance before the handler runs and debits the list price only
80
83
  on a successful (200) response (the `X-Credits-Balance` header carries what is
81
84
  left; credits never expire). A refused call throws with the balance; nothing is
@@ -116,7 +119,17 @@ What the server does with it depends on how the call was paid:
116
119
  dedupe). So on the wallet path a retry after a lost response can settle twice;
117
120
  the `Idempotency-Key` cannot prevent that on its own. If that matters for your
118
121
  workload, use a credits key for the tools you retry, or keep the signed
119
- request and resend it yourself.
122
+ request and resend it yourself. A resent signed request needs no key: the
123
+ server answers a request re-sent with the same signed EVM payment, to the same
124
+ route with the same body, from its first answer inside the replay window,
125
+ with no second settlement, and marks it `X-Replayed-Answer: 1` (identity-bound
126
+ 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.
120
133
 
121
134
  ## Workflows (skill packs)
122
135
 
@@ -352,8 +365,13 @@ caps hold at signing by default.
352
365
  `amount`, so each option is priced from a built-in table of known assets per
353
366
  network (`KNOWN_ASSETS`: USDC on each Agent402 rail, USDG on Robinhood Chain,
354
367
  with their on-chain decimals), never from a decimals value the seller sends. An
355
- option in an asset outside the table cannot be priced, and a capped wallet never
356
- signs it. When a cap is set, the client preflights the `402` and checks the
368
+ option in an asset outside the table cannot be priced. With the `x402Client`
369
+ passed, the signing guard drops such an option before anything is signed, so
370
+ it is skipped beside an option that can be priced and the call pays that one;
371
+ the call is refused only when no option can be priced. With a pre-wrapped
372
+ `fetch` and no `x402Client`, nothing stops the wallet from signing it, so a
373
+ capped call is refused before the wallet runs when any option in the `402`
374
+ cannot be priced. When a cap is set, the client preflights the `402` and checks the
357
375
  ceiling against the **larger** of the advertised price (from the seller's
358
376
  `/api/pricing`) and the amount the `402` quotes; if no `402` comes back
359
377
  (FREE_MODE) it checks the advertised price. Caps hold under **concurrency** too:
@@ -361,7 +379,7 @@ each call reserves its amount synchronously, so N simultaneous calls can't each
361
379
  pass against the same pre-commit total.
362
380
 
363
381
  - **Zero dependencies** for the free/proof-of-work path (uses `node:crypto`).
364
- - **Non-custodial:** paid settlement is your `@x402/fetch` / `mppx` fetch + wallet (or a prepaid credits key you bought); this client never sees a private key.
382
+ - **Non-custodial:** paid settlement is your `@x402/fetch` / `mppx` fetch + wallet (or a prepaid credits key); this client never sees a private key.
365
383
  - MIT licensed. Part of [Agent402](https://github.com/MikeyPetrillo/Agent402).
366
384
 
367
385
  ## Pick the settlement chain (`withNetworkPreference`)
@@ -385,7 +403,11 @@ const payFetch = wrapFetchWithPayment(fetch, client);
385
403
  Short names map to CAIP-2 (`base`, `solana`, `polygon`, `arbitrum`,
386
404
  `robinhood`); unknown entries pass through verbatim so future chains work
387
405
  without a package update. If the preference matches none of a seller's
388
- payment options it throws **before** any payment is signed.
406
+ payment options it throws **before** any payment is signed. Since 0.8.19 a
407
+ name resolves through the known-asset table (`NETWORK_NAMES`) to every id the
408
+ chain is spelled with on the wire, and an id to the ids of its chain, so
409
+ `["algorand"]`, or either spelling of Algorand mainnet, matches a seller that
410
+ uses the other.
389
411
 
390
412
 
391
413
  ## 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 } 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.17";
33
+ const VERSION = "0.8.19";
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
@@ -94,8 +94,8 @@ export class Agent402 {
94
94
  * @param {string[]} [opts.payNetworks] the chains your pre-wrapped `fetch` pays on (e.g. ["base"], the
95
95
  * names withNetworkPreference takes): without an x402Client the
96
96
  * caps count the dearest accept on these chains, not on every chain
97
- * @param {string} [opts.creditsKey] a prepaid card-credits key (a402_...) already issued (new credits are
98
- * not on sale) - pays wallet-only tools when no payFetch is given
97
+ * @param {string} [opts.creditsKey] a prepaid card-credits key (a402_...) - pays wallet-only tools on
98
+ * Agent402's own services when no payFetch is given
99
99
  * @param {number|null} [opts.maxResponseBytes=33554432]
100
100
  * Hard ceiling on a response body, enforced BEFORE it is parsed. null disables it.
101
101
  * @param {{id:string, validate:function}} [opts.outputValidator]
@@ -160,7 +160,7 @@ export class Agent402 {
160
160
  if (x402Client) this.guardX402Client(x402Client);
161
161
  // The chains a pre-wrapped fetch pays on, as CAIP-2 ids (null = unknown).
162
162
  this._payNetworks = Array.isArray(payNetworks) && payNetworks.length
163
- ? new Set(payNetworks.map((n) => String(NETWORK_CAIP2[String(n).trim().toLowerCase()] || String(n).trim()).toLowerCase()))
163
+ ? new Set(payNetworks.flatMap((n) => networkIdsOf(n)).map((id) => id.toLowerCase()))
164
164
  : null;
165
165
  // A response-size ceiling, enforced before parsing. See _readJson.
166
166
  this.maxResponseBytes = maxResponseBytes === null ? null : (numOrNull(maxResponseBytes) ?? DEFAULT_MAX_RESPONSE_BYTES);
@@ -581,7 +581,7 @@ export class Agent402 {
581
581
  }
582
582
  const r = await send(); // no wallet - succeeds only on a FREE_MODE instance
583
583
  if (r.ok) return this._deliverResponse(slug, r, cacheKey, cache, validator, { maxResponseBytes, paid: false });
584
- throw this._fail(slug, r, `call "${slug}" failed: HTTP ${r.status} - wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (a prepaid credits key already issued; new credits are not on sale)`);
584
+ throw this._fail(slug, r, `call "${slug}" failed: HTTP ${r.status} - wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (a prepaid credits key; routes that buy from an outside seller need a wallet)`);
585
585
  }
586
586
 
587
587
  // Free (compute-payable) tool: succeeds plainly on a FREE_MODE instance,
@@ -656,8 +656,9 @@ export class Agent402 {
656
656
  * "guarded" (the signing guard holds every signature to the caps, so the
657
657
  * cheapest known accept is what can be paid, and unknown ones are dropped
658
658
  * 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:
659
+ * one counts, among the chains in `payNetworks` when they are given, and an
660
+ * accept this client cannot price is refused here, since nothing stops the
661
+ * wallet from signing it), or "credits" (nothing is signed:
661
662
  * the credits gate debits the route's own quote, and no accept is below it,
662
663
  * since a chain premium or price floor only raises an accept, so the
663
664
  * cheapest known accept counts).
@@ -682,11 +683,19 @@ export class Agent402 {
682
683
  return listUsd; /* no readable 402: the advertised price */
683
684
  }
684
685
  if (!q) return listUsd;
685
- if (q.max == null || (mode === "wallet" && q.unknown > 0)) {
686
+ // An option in an asset this client cannot price: with no guard the wallet
687
+ // may sign it, for an amount no cap was checked against, so one such
688
+ // option refuses the call; the guard drops it before signing, so beside a
689
+ // priced option it is skipped there; a credits gate signs nothing, so it
690
+ // is ignored. An offer with nothing priceable is refused under a cap in
691
+ // either signing mode.
692
+ const unpriceable = q.max == null || (mode === "wallet" && q.unknown > 0);
693
+ if (unpriceable) {
686
694
  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 });
695
+ throw new SpendingLimitError(q.max == null
696
+ ? `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)" : ""}`
697
+ : `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)`,
698
+ { limit: "knownAsset", slug, priceUsd: null, unknownOptions: q.unknown });
690
699
  }
691
700
  const quoted = mode === "wallet" ? q.max : q.min;
692
701
  // Larger of the two: an understated catalog must not lower the ceiling
@@ -961,9 +970,12 @@ function resultCacheKey(slug, params, validator) {
961
970
  * withNetworkPreference(x402client, ["robinhood"]); // or ["eip155:4663"]
962
971
  * const payFetch = wrapFetchWithPayment(fetch, x402client);
963
972
  *
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.
973
+ * A name resolves through the known-asset table to every id that chain is
974
+ * spelled with on the wire (./known-assets.js NETWORK_NAMES), and an id to
975
+ * the ids of its chain, so either spelling of a chain matches the other;
976
+ * unknown entries pass through verbatim so future chains work without a
977
+ * package update. Throws (before paying) when the preference matches none of
978
+ * a seller's payment options.
967
979
  */
968
980
  export const NETWORK_CAIP2 = {
969
981
  base: "eip155:8453",
@@ -980,16 +992,17 @@ export const NETWORK_CAIP2 = {
980
992
  solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
981
993
  "solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
982
994
  stellar: "stellar:pubnet",
983
- algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
995
+ algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k",
984
996
  };
985
997
 
986
998
  export function withNetworkPreference(client, networks) {
987
- const prefs = (networks || []).map((n) => NETWORK_CAIP2[String(n).trim().toLowerCase()] || String(n).trim());
999
+ // Every id each preference names, in preference order, once each.
1000
+ const prefs = [...new Set((networks || []).flatMap((n) => networkIdsOf(n)))];
988
1001
  if (!prefs.length) return client;
989
1002
  const orig = client.createPaymentPayload.bind(client);
990
1003
  client.createPaymentPayload = (paymentRequired) => {
991
1004
  const list = Array.isArray(paymentRequired?.accepts) ? paymentRequired.accepts : [];
992
- const picked = prefs.flatMap((caip2) => list.filter((a) => String(a?.network || "").toLowerCase() === caip2.toLowerCase()));
1005
+ const picked = prefs.flatMap((id) => list.filter((a) => String(a?.network || "").toLowerCase() === id.toLowerCase()));
993
1006
  if (!picked.length) {
994
1007
  const offered = [...new Set(list.map((a) => a?.network).filter(Boolean))];
995
1008
  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.17",
3
+ "version": "0.8.19",
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",