agent402-client 0.8.19 → 0.8.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -7
- package/index.js +222 -7
- 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) |
|
package/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// const a = new Agent402({ fetch: payFetch });
|
|
20
20
|
import { createHash, randomBytes } from "node:crypto";
|
|
21
21
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
22
|
-
import { quoteOf, guardSigning, networkIdsOf } from "./known-assets.js";
|
|
22
|
+
import { quoteOf, guardSigning, networkIdsOf, acceptUsd } from "./known-assets.js";
|
|
23
23
|
export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
|
|
24
24
|
import { distrustFetch } from "./tls-distrust.js";
|
|
25
25
|
export { distrustFetch, distrustDispatcher, distrustCheckServerIdentity, DISTRUSTED_SPKI_SHA256 } from "./tls-distrust.js";
|
|
@@ -30,7 +30,7 @@ export { AsyncJobPendingError } from "./async-job.js";
|
|
|
30
30
|
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
31
31
|
// network calls - so a seller can attribute traffic (and settled payments)
|
|
32
32
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
33
|
-
const VERSION = "0.8.
|
|
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;
|
|
@@ -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 {
|
|
@@ -859,6 +969,105 @@ export class SpendingLimitError extends Error {
|
|
|
859
969
|
}
|
|
860
970
|
}
|
|
861
971
|
|
|
972
|
+
/** A Circle Gateway batched payment the seller had not confirmed after the
|
|
973
|
+
* resends. `paymentHeader` is the signed payment: resend the same request
|
|
974
|
+
* with it as PAYMENT-SIGNATURE to be served once Gateway confirms it, with
|
|
975
|
+
* no new charge. Never sign a new authorization for the same request. The
|
|
976
|
+
* amount counts against the caps (Gateway may settle it). */
|
|
977
|
+
export class GatewayPendingError extends Error {
|
|
978
|
+
constructor(slug, paymentHeader, body = null) {
|
|
979
|
+
super(`"${slug}" is paid on the Circle Gateway batched option and Gateway had not confirmed the payment after ${GATEWAY_PENDING_RESENDS} resends; resend the same request with the same PAYMENT-SIGNATURE header (paymentHeader) after a short wait, never a new signature${body?.hint ? ` - ${body.hint}` : ""}`);
|
|
980
|
+
this.name = "GatewayPendingError";
|
|
981
|
+
this.slug = slug;
|
|
982
|
+
this.paymentHeader = paymentHeader;
|
|
983
|
+
this.status = 402;
|
|
984
|
+
this.paid = null;
|
|
985
|
+
this.retryAfterSeconds = Number(body?.retryAfterSeconds) || null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Sentinel: the 402 offered no batched accept this client signs for. */
|
|
990
|
+
const NO_GATEWAY_ACCEPT = Symbol("no-gateway-accept");
|
|
991
|
+
|
|
992
|
+
async function readJsonQuietly(r) {
|
|
993
|
+
try { return await (typeof r?.clone === "function" ? r.clone() : r).json(); } catch { return null; }
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function normalizeGateway(value) {
|
|
997
|
+
if (value == null || value === false) return null;
|
|
998
|
+
if (typeof value !== "object") throw new TypeError("gateway must be { signer, networks? }");
|
|
999
|
+
const signer = value.signer;
|
|
1000
|
+
if (!signer || typeof signer.signTypedData !== "function" || !EVM_ADDR.test(String(signer.address || ""))) {
|
|
1001
|
+
throw new TypeError("gateway.signer must be an EIP-712 signer with an EVM address and signTypedData (a viem account)");
|
|
1002
|
+
}
|
|
1003
|
+
const networks = Array.isArray(value.networks) && value.networks.length ? value.networks.map(String) : null;
|
|
1004
|
+
return Object.freeze({ signer, networks });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** The first Circle Gateway batched accept a buyer may pay: scheme exact,
|
|
1008
|
+
* `extra.name` the Gateway kind, on an EVM chain (among `networks` when
|
|
1009
|
+
* given), with a payTo, a verifyingContract, a domain version, a validity
|
|
1010
|
+
* window of at most GATEWAY_MAX_TIMEOUT_SECONDS and an amount in a known
|
|
1011
|
+
* asset (so the caps can price it; a batched accept in an asset the table
|
|
1012
|
+
* does not know is never signed). null when there is none. */
|
|
1013
|
+
export function gatewayAcceptOf(accepts, networks = null) {
|
|
1014
|
+
const list = Array.isArray(accepts) ? accepts : [];
|
|
1015
|
+
const wanted = networks ? new Set(networks.flatMap((n) => networkIdsOf(n)).map((id) => id.toLowerCase())) : null;
|
|
1016
|
+
return list.find((a) => a && typeof a === "object" && a.scheme === "exact"
|
|
1017
|
+
&& a.extra && typeof a.extra === "object" && a.extra.name === GATEWAY_KIND_NAME
|
|
1018
|
+
&& /^eip155:[0-9]{1,12}$/.test(String(a.network || "")) && (!wanted || wanted.has(String(a.network).toLowerCase()))
|
|
1019
|
+
&& EVM_ADDR.test(String(a.payTo || "")) && EVM_ADDR.test(String(a.extra.verifyingContract || ""))
|
|
1020
|
+
&& typeof a.extra.version === "string" && /^[0-9]{1,4}$/.test(a.extra.version)
|
|
1021
|
+
&& /^[0-9]{1,78}$/.test(String(a.amount ?? "")) && Number(a.maxTimeoutSeconds) > 0 && Number(a.maxTimeoutSeconds) <= GATEWAY_MAX_TIMEOUT_SECONDS
|
|
1022
|
+
&& acceptUsd(a) != null) || null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Sign a Circle Gateway batched payment for one accept: an EIP-3009
|
|
1027
|
+
* TransferWithAuthorization from the signer to the accept's payTo for exactly
|
|
1028
|
+
* its amount, valid from now (validAfter 0) for the accept's
|
|
1029
|
+
* maxTimeoutSeconds, under the Gateway EIP-712 domain the accept carries
|
|
1030
|
+
* (name, version, verifyingContract) with the accept's chain id. Returns the
|
|
1031
|
+
* x402 v2 payment payload and its base64 header value.
|
|
1032
|
+
*/
|
|
1033
|
+
export async function signGatewayPayment(accept, { signer, resource = undefined, now = Date.now, nonce = undefined } = {}) {
|
|
1034
|
+
if (!signer || typeof signer.signTypedData !== "function" || !EVM_ADDR.test(String(signer.address || ""))) throw new TypeError("signGatewayPayment needs a signer with an EVM address and signTypedData");
|
|
1035
|
+
if (!gatewayAcceptOf([accept])) throw new TypeError("signGatewayPayment needs a Circle Gateway batched accept (scheme exact, extra.name GatewayWalletBatched, an EVM network, payTo, verifyingContract, version, amount, maxTimeoutSeconds)");
|
|
1036
|
+
const nowSec = Math.floor(now() / 1000);
|
|
1037
|
+
const authorization = {
|
|
1038
|
+
from: signer.address, to: accept.payTo, value: String(accept.amount),
|
|
1039
|
+
validAfter: "0", validBefore: String(nowSec + Math.floor(Number(accept.maxTimeoutSeconds))),
|
|
1040
|
+
nonce: nonce || `0x${randomBytes(32).toString("hex")}`,
|
|
1041
|
+
};
|
|
1042
|
+
const signature = await signer.signTypedData({
|
|
1043
|
+
domain: { name: accept.extra.name, version: accept.extra.version, chainId: Number(String(accept.network).split(":")[1]), verifyingContract: accept.extra.verifyingContract },
|
|
1044
|
+
types: GATEWAY_TYPES,
|
|
1045
|
+
primaryType: "TransferWithAuthorization",
|
|
1046
|
+
message: { ...authorization, value: BigInt(authorization.value), validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore) },
|
|
1047
|
+
});
|
|
1048
|
+
const payload = { x402Version: 2, ...(resource && typeof resource === "object" ? { resource } : {}), accepted: accept, payload: { signature, authorization } };
|
|
1049
|
+
return { payload, header: Buffer.from(JSON.stringify(payload)).toString("base64") };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** The message for a 402 that refused a batched payment: the seller's own
|
|
1053
|
+
* explanation first, and when it says the Gateway balance came up short,
|
|
1054
|
+
* what to do about it (deposit, never a resend of the same authorization).
|
|
1055
|
+
* A short balance is read from the machine fields the seller sends (reason
|
|
1056
|
+
* insufficient-funds, or retry fund-gateway-deposit); the prose is read only
|
|
1057
|
+
* when a body carries neither field. */
|
|
1058
|
+
function gatewayRefusalMessage(slug, body, accept, address) {
|
|
1059
|
+
const said = body && typeof body === "object" ? (body.hint ?? body.detail ?? body.error ?? body.message) : null;
|
|
1060
|
+
const reason = typeof body?.reason === "string" ? body.reason : null;
|
|
1061
|
+
const retry = typeof body?.retry === "string" ? body.retry : null;
|
|
1062
|
+
const short = reason != null || retry != null
|
|
1063
|
+
? reason === "insufficient-funds" || retry === "fund-gateway-deposit"
|
|
1064
|
+
: /insufficient|balance|underflow|not enough/i.test(typeof said === "string" ? said : "");
|
|
1065
|
+
const chain = String(accept.network);
|
|
1066
|
+
return `call "${slug}" refused on the Circle Gateway batched option (${chain}): HTTP 402${said != null ? ` - ${typeof said === "string" ? said : JSON.stringify(said)}` : ""}${short
|
|
1067
|
+
? ` The signer ${address} needs a USDC balance in Circle Gateway on that chain: deposit through Circle's GatewayWallet contract first (this client never deposits), then call again; see ${GATEWAY_DOCS_URL}.`
|
|
1068
|
+
: ""} The refused authorization was sent and stays valid until its validBefore, so it counts against this client's spending caps.`;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
862
1071
|
/** The dearest amount a 402 asks for, in USD, or null when it cannot be read.
|
|
863
1072
|
*
|
|
864
1073
|
* Each accept is priced from the known-asset table (./known-assets.js): the
|
|
@@ -878,16 +1087,22 @@ export async function quoted402Usd(res) {
|
|
|
878
1087
|
|
|
879
1088
|
/** The accepts array of a 402, from the PAYMENT-REQUIRED header first, then the body. */
|
|
880
1089
|
async function challengeAccepts(res) {
|
|
1090
|
+
return acceptsOf(await challengeEnvelope(res));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** The decoded PaymentRequired object of a 402 (the header first, then the
|
|
1094
|
+
* body), or null when neither carries accepts. */
|
|
1095
|
+
async function challengeEnvelope(res) {
|
|
881
1096
|
const fromHeader = decodeChallengeHeader(res?.headers?.get?.("payment-required"));
|
|
882
1097
|
if (fromHeader) return fromHeader;
|
|
883
1098
|
let body = null;
|
|
884
1099
|
try { body = await res.json(); } catch { body = null; }
|
|
885
|
-
return acceptsOf(body);
|
|
1100
|
+
return acceptsOf(body) ? body : null;
|
|
886
1101
|
}
|
|
887
1102
|
|
|
888
1103
|
function decodeChallengeHeader(header) {
|
|
889
1104
|
if (typeof header !== "string" || !header.trim()) return null;
|
|
890
|
-
try {
|
|
1105
|
+
try { const obj = JSON.parse(Buffer.from(header.trim(), "base64").toString("utf8")); return acceptsOf(obj) ? obj : null; } catch { return null; }
|
|
891
1106
|
}
|
|
892
1107
|
|
|
893
1108
|
/** The accepts array, at the top level or under the envelope keys sellers wrap it in. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
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",
|