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 +60 -10
- package/index.js +249 -21
- package/known-assets.js +37 -1
- 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:**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
|
369
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
660
|
-
*
|
|
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
|
-
|
|
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}" -
|
|
689
|
-
|
|
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 {
|
|
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
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
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:
|
|
1210
|
+
algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k",
|
|
984
1211
|
};
|
|
985
1212
|
|
|
986
1213
|
export function withNetworkPreference(client, networks) {
|
|
987
|
-
|
|
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((
|
|
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.
|
|
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",
|