agent402-client 0.8.23 → 0.8.25
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 +4 -1
- package/index.js +17 -12
- package/known-assets.js +86 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -398,7 +398,10 @@ can still redeem until its `validBefore`, so the error carries `paid: "unknown"`
|
|
|
398
398
|
and `heldUsd`, and `spendingSummary()` reports it under `heldUsd`. A held
|
|
399
399
|
amount counts for the rolling 24 hours, and longer when the authorization
|
|
400
400
|
stays valid longer (a seven-day Circle Gateway authorization is held until its
|
|
401
|
-
`validBefore` passes).
|
|
401
|
+
`validBefore` passes). With an `x402Client`, an accept asking to stay valid for
|
|
402
|
+
more than a day (`MAX_VALIDITY_SECONDS`) is refused before anything is signed,
|
|
403
|
+
and a payload signed for longer is refused before it is sent, so no hold
|
|
404
|
+
outlives a day after the send (seven days and an hour for a batched payment). Pass your `x402Client` so the client sees exactly which
|
|
402
405
|
payments were signed; through a pre-wrapped `fetch` alone it cannot tell, so any
|
|
403
406
|
failed paid call is held. A paid HTTP-success response that later fails byte,
|
|
404
407
|
JSON, or output validation remains settled spend. Free proof-of-work calls are
|
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, networkIdsOf, acceptUsd } from "./known-assets.js";
|
|
23
|
-
export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf } from "./known-assets.js";
|
|
22
|
+
import { quoteOf, guardSigning, networkIdsOf, acceptUsd, signedExpirySeconds, MAX_VALIDITY_SECONDS } from "./known-assets.js";
|
|
23
|
+
export { KNOWN_ASSETS, NETWORK_NAMES, knownAsset, acceptUsd, networkIdsOf, MAX_VALIDITY_SECONDS } 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
|
export { resetRetryFetch, isConnectionReset } from "./reset-retry.js";
|
|
@@ -32,7 +32,7 @@ export { AsyncJobPendingError } from "./async-job.js";
|
|
|
32
32
|
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
33
33
|
// network calls - so a seller can attribute traffic (and settled payments)
|
|
34
34
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
35
|
-
const VERSION = "0.8.
|
|
35
|
+
const VERSION = "0.8.25";
|
|
36
36
|
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
37
37
|
// A package built on this SDK (an adapter) names itself first: its own
|
|
38
38
|
// product token, then this one. Anything that is not one well-formed token
|
|
@@ -119,13 +119,15 @@ export const HOLD_SLACK_MS = 10 * 60 * 1000;
|
|
|
119
119
|
|
|
120
120
|
/** When a signed payment payload can no longer settle, in ms since the epoch
|
|
121
121
|
* plus HOLD_SLACK_MS: an EIP-3009 `authorization.validBefore` or a Permit2
|
|
122
|
-
* `deadline`, in seconds
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
* `deadline`, in seconds, never later than `maxSeconds` after `nowMs` (the
|
|
123
|
+
* longest window this client signs: MAX_VALIDITY_SECONDS, or
|
|
124
|
+
* GATEWAY_MAX_TIMEOUT_SECONDS for a batched payment), so no hold outlives
|
|
125
|
+
* the ceiling a payload must pass to be sent. null when the payload names no
|
|
126
|
+
* expiry this reads (a Solana transaction, an unknown scheme). */
|
|
127
|
+
export function signedValidUntilMs(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
|
|
128
|
+
const v = signedExpirySeconds(paymentPayload);
|
|
129
|
+
if (v == null || !Number.isFinite(v)) return null;
|
|
130
|
+
return Math.min(v * 1000, nowMs + maxSeconds * 1000) + HOLD_SLACK_MS;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/** A spend-log entry the caps still count at `now`: anything recorded inside
|
|
@@ -688,8 +690,10 @@ export class Agent402 {
|
|
|
688
690
|
const d = ctx.refusal;
|
|
689
691
|
throw new SpendingLimitError(d.reason === "unknown-asset"
|
|
690
692
|
? `refusing to pay for "${slug}" - no payment option it asked to sign is in a known asset, so none can be priced`
|
|
691
|
-
: d.reason === "signed-amount"
|
|
693
|
+
: d.reason === "signed-amount" || d.reason === "signed-validity"
|
|
692
694
|
? `refusing to pay for "${slug}" - ${d.detail}, so the payment was not sent`
|
|
695
|
+
: d.reason === "validity"
|
|
696
|
+
? `refusing to pay for "${slug}" - ${d.detail}, so nothing was signed`
|
|
693
697
|
: `refusing to pay $${d.priceUsd} for "${slug}" - the payment it asked to sign is over ${d.limit || "the cap"} ($${d.allowanceUsd} left to sign)`,
|
|
694
698
|
{ limit: d.limit || "signing", slug, priceUsd: d.priceUsd, cap: d.allowanceUsd, atSigning: true, cause: e, ...(e?.paid === "unknown" ? { paid: "unknown", heldUsd: e.heldUsd } : {}) });
|
|
695
699
|
}
|
|
@@ -801,7 +805,7 @@ export class Agent402 {
|
|
|
801
805
|
// claim on the deposit until its validBefore whatever the seller answers
|
|
802
806
|
// (a refusal, a failure, a redirect, a job still running), so its signed
|
|
803
807
|
// amount is held against the caps from the first send.
|
|
804
|
-
const sent = [{ usd: acceptedUsd, until: signedValidUntilMs(payload) }];
|
|
808
|
+
const sent = [{ usd: acceptedUsd, until: signedValidUntilMs(payload, { maxSeconds: GATEWAY_MAX_TIMEOUT_SECONDS }) }];
|
|
805
809
|
const heldUsd = this._spendHold(reservation, sent);
|
|
806
810
|
const paidHeaders = { "PAYMENT-SIGNATURE": header, ...asyncHeaders };
|
|
807
811
|
let r;
|
|
@@ -1361,6 +1365,7 @@ export const NETWORK_CAIP2 = {
|
|
|
1361
1365
|
"base-sepolia": "eip155:84532",
|
|
1362
1366
|
robinhood: "eip155:4663",
|
|
1363
1367
|
arc: "eip155:5042",
|
|
1368
|
+
ink: "eip155:57073",
|
|
1364
1369
|
monad: "eip155:143",
|
|
1365
1370
|
celo: "eip155:42220",
|
|
1366
1371
|
avalanche: "eip155:43114",
|
package/known-assets.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
// and a buyer with a cap does not sign it. The table is keyed by CAIP-2 ids
|
|
13
13
|
// only: a bare v1 network name (for example "base") is not priced.
|
|
14
14
|
//
|
|
15
|
+
// An entry may also name its token's EIP-712 domain (`name` and `version`,
|
|
16
|
+
// read from the token's own name() and version()). An EVM "exact" signer
|
|
17
|
+
// signs under the domain the accept's `extra` carries, so for such an entry
|
|
18
|
+
// an accept whose `extra.name` or `extra.version` is not exactly the token's
|
|
19
|
+
// cannot be priced either, and a buyer with a cap does not sign it.
|
|
20
|
+
//
|
|
15
21
|
// The same file ships in agent402-client, agent402-mcp and agent402-openclaw;
|
|
16
22
|
// the parent repo's scripts/test-known-assets.js keeps the copies identical and
|
|
17
23
|
// checks the table against the assets the server offers.
|
|
@@ -29,6 +35,7 @@ export const KNOWN_ASSETS = Object.freeze({
|
|
|
29
35
|
"eip155:1329": [{ asset: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392", decimals: 6, symbol: "USDC" }], // Sei
|
|
30
36
|
"eip155:10": [{ asset: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", decimals: 6, symbol: "USDC" }], // Optimism
|
|
31
37
|
"eip155:5042": [{ asset: "0x3600000000000000000000000000000000000000", decimals: 6, symbol: "USDC" }], // Arc
|
|
38
|
+
"eip155:57073": [{ asset: "0x2D270e6886d130D724215A266106e6832161EAEd", decimals: 6, symbol: "USDC", name: "USDC", version: "2" }], // Ink
|
|
32
39
|
"eip155:4663": [{ asset: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", decimals: 6, symbol: "USDG" }], // Robinhood Chain
|
|
33
40
|
// Solana: the USDC mint.
|
|
34
41
|
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": [{ asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6, symbol: "USDC" }],
|
|
@@ -56,6 +63,7 @@ export const NETWORK_NAMES = Object.freeze({
|
|
|
56
63
|
sei: ["eip155:1329"],
|
|
57
64
|
optimism: ["eip155:10"],
|
|
58
65
|
arc: ["eip155:5042"],
|
|
66
|
+
ink: ["eip155:57073"],
|
|
59
67
|
robinhood: ["eip155:4663"],
|
|
60
68
|
solana: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
|
|
61
69
|
"solana-devnet": ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
|
|
@@ -88,6 +96,15 @@ export function knownAsset(network, asset) {
|
|
|
88
96
|
return list.find((e) => sameAsset(e.asset, asset)) || null;
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
/** True when the accept carries the EIP-712 domain the table names for its
|
|
100
|
+
* asset (`extra.name` and `extra.version`, exactly), or when the entry names
|
|
101
|
+
* none. */
|
|
102
|
+
export function acceptDomainMatches(entry, accept) {
|
|
103
|
+
if (!entry || (entry.name == null && entry.version == null)) return true;
|
|
104
|
+
const extra = accept && typeof accept.extra === "object" && accept.extra ? accept.extra : {};
|
|
105
|
+
return extra.name === entry.name && extra.version === entry.version;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
/** The base-unit amount the signer of `x402Version` signs for one accept, as
|
|
92
109
|
* a string of digits: `maxAmountRequired` under v1, `amount` under v2. With
|
|
93
110
|
* no version, the one field present (or both, when they agree). null when
|
|
@@ -111,12 +128,13 @@ export function acceptAmount(accept, x402Version = undefined) {
|
|
|
111
128
|
|
|
112
129
|
/** The USD value of one accept: the base-unit amount its version's signer
|
|
113
130
|
* signs (acceptAmount) over the KNOWN decimals of its asset. null when the
|
|
114
|
-
* asset is not known
|
|
131
|
+
* asset is not known, when the accept names a different EIP-712 domain than
|
|
132
|
+
* the table's for that asset, or when the amount cannot be read. */
|
|
115
133
|
export function acceptUsd(accept, x402Version = undefined) {
|
|
116
134
|
const raw = acceptAmount(accept, x402Version);
|
|
117
135
|
if (raw == null) return null;
|
|
118
136
|
const entry = knownAsset(accept.network, accept.asset);
|
|
119
|
-
if (!entry) return null;
|
|
137
|
+
if (!entry || !acceptDomainMatches(entry, accept)) return null;
|
|
120
138
|
return Number(raw) / 10 ** entry.decimals;
|
|
121
139
|
}
|
|
122
140
|
|
|
@@ -134,6 +152,51 @@ export function quoteOf(accepts, x402Version = undefined) {
|
|
|
134
152
|
return { max, min, unknown, total: accepts.length };
|
|
135
153
|
}
|
|
136
154
|
|
|
155
|
+
// The longest a signed payment may stay redeemable. An EVM "exact" signer
|
|
156
|
+
// sets validBefore (EIP-3009) or deadline (Permit2) to the moment of signing
|
|
157
|
+
// plus the accept's maxTimeoutSeconds, and the signed payload is a bearer
|
|
158
|
+
// claim until then, so a seller that quotes years gets an authorization it
|
|
159
|
+
// could redeem for years and a held amount that never stops counting. Honest
|
|
160
|
+
// accepts ask for minutes (Agent402's own ask 300 s); a day admits every
|
|
161
|
+
// one of them. An accept that asks for longer is dropped before selection,
|
|
162
|
+
// and a payload signed past it is refused before it is sent.
|
|
163
|
+
export const MAX_VALIDITY_SECONDS = 24 * 3600;
|
|
164
|
+
// Signing reads the clock a moment before the check after it does.
|
|
165
|
+
const VALIDITY_CLOCK_SLACK_SECONDS = 60;
|
|
166
|
+
|
|
167
|
+
/** True when an accept asks for a validity window past MAX_VALIDITY_SECONDS.
|
|
168
|
+
* An accept with no readable maxTimeoutSeconds is left to the check of what
|
|
169
|
+
* was signed (signedValidityProblem). */
|
|
170
|
+
export function acceptValidityTooLong(accept, maxSeconds = MAX_VALIDITY_SECONDS) {
|
|
171
|
+
const raw = accept?.maxTimeoutSeconds;
|
|
172
|
+
if (raw == null || raw === "") return false;
|
|
173
|
+
const n = Number(raw);
|
|
174
|
+
return !Number.isFinite(n) || n > maxSeconds;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The expiry a signed payload carries, in unix seconds: an EIP-3009
|
|
178
|
+
* `authorization.validBefore` or a Permit2 `deadline`. null when the payload
|
|
179
|
+
* names none (a Solana transaction, an unknown scheme). NaN when it names
|
|
180
|
+
* one that is not a whole number of seconds. */
|
|
181
|
+
export function signedExpirySeconds(paymentPayload) {
|
|
182
|
+
const p = paymentPayload?.payload;
|
|
183
|
+
const v = p?.authorization?.validBefore ?? p?.permit2Authorization?.deadline;
|
|
184
|
+
if (v == null) return null;
|
|
185
|
+
return /^\d{1,15}$/.test(String(v)) ? Number(v) : NaN;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Why a created payment payload stays redeemable for too long, or null: its
|
|
189
|
+
* expiry is past `nowMs` plus `maxSeconds` (and a minute of clock slack), or
|
|
190
|
+
* it names an expiry that cannot be read. */
|
|
191
|
+
export function signedValidityProblem(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
|
|
192
|
+
const exp = signedExpirySeconds(paymentPayload);
|
|
193
|
+
if (exp == null) return null;
|
|
194
|
+
if (!Number.isFinite(exp)) return "the signed authorization's expiry cannot be read";
|
|
195
|
+
const left = exp - Math.floor(nowMs / 1000);
|
|
196
|
+
if (left > maxSeconds + VALIDITY_CLOCK_SLACK_SECONDS) return `the signed authorization stays valid for ${left} seconds, past the ${maxSeconds}-second ceiling`;
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
137
200
|
/** Enforce a USD allowance at signing on an @x402/core x402Client.
|
|
138
201
|
*
|
|
139
202
|
* `allowance()` is the most this buyer may sign for the payment being built
|
|
@@ -141,8 +204,10 @@ export function quoteOf(accepts, x402Version = undefined) {
|
|
|
141
204
|
* for none). A registered policy drops every accept that cannot be priced or
|
|
142
205
|
* is over the allowance, so the client selects among the ones that fit, and a
|
|
143
206
|
* hook before payload creation checks the selected accept again and passes
|
|
144
|
-
* its USD value to `onSign`.
|
|
145
|
-
*
|
|
207
|
+
* its USD value to `onSign`. An accept asking for a validity window past
|
|
208
|
+
* MAX_VALIDITY_SECONDS is dropped and refused the same way. A refusal throws
|
|
209
|
+
* (from the policy) or aborts (from the hook) before any signature exists;
|
|
210
|
+
* `onRefuse` hears why first. */
|
|
146
211
|
export function guardSigning(client, { allowance, onSign = null, onRefuse = null }) {
|
|
147
212
|
if (!client || typeof client.registerPolicy !== "function" || typeof client.onBeforePaymentCreation !== "function") {
|
|
148
213
|
throw new TypeError("guardSigning needs an @x402/core x402Client (registerPolicy and onBeforePaymentCreation)");
|
|
@@ -151,7 +216,12 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
151
216
|
const refuse = (detail) => { try { onRefuse?.(detail); } catch { /* the refusal stands */ } return detail; };
|
|
152
217
|
client.registerPolicy((version, accepts) => {
|
|
153
218
|
const cap = allowance();
|
|
154
|
-
const
|
|
219
|
+
const priced = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
|
|
220
|
+
const kept = priced.filter((a) => !acceptValidityTooLong(a));
|
|
221
|
+
if (!kept.length && priced.length) {
|
|
222
|
+
const d = refuse({ reason: "validity", priceUsd: null, allowanceUsd: cap, detail: `every payment option it may sign asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds` });
|
|
223
|
+
throw new Error(`spending guard: ${d.detail}; nothing was signed`);
|
|
224
|
+
}
|
|
155
225
|
if (!kept.length) {
|
|
156
226
|
const q = quoteOf(accepts, version);
|
|
157
227
|
const d = refuse({ reason: q && q.min != null ? "over-allowance" : "unknown-asset", priceUsd: q ? q.min : null, allowanceUsd: cap });
|
|
@@ -172,6 +242,10 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
172
242
|
refuse({ reason: "over-allowance", priceUsd: usd, allowanceUsd: cap });
|
|
173
243
|
return { abort: true, reason: `spending guard: the selected payment option asks $${usd}, above the $${cap} this buyer may sign; nothing was signed` };
|
|
174
244
|
}
|
|
245
|
+
if (acceptValidityTooLong(r)) {
|
|
246
|
+
const d = refuse({ reason: "validity", priceUsd: usd, allowanceUsd: cap, detail: `the selected payment option asks to stay valid for ${r.maxTimeoutSeconds} seconds, past the ${MAX_VALIDITY_SECONDS}-second ceiling` });
|
|
247
|
+
return { abort: true, reason: `spending guard: ${d.detail}; nothing was signed` };
|
|
248
|
+
}
|
|
175
249
|
onSign?.(usd, r);
|
|
176
250
|
return undefined;
|
|
177
251
|
});
|
|
@@ -252,9 +326,10 @@ export async function signedPaymentProblem(paymentPayload, requirements, x402Ver
|
|
|
252
326
|
}
|
|
253
327
|
|
|
254
328
|
/** Refuse to send a payment payload whose signed amount is not the one the
|
|
255
|
-
* allowance passed
|
|
256
|
-
*
|
|
257
|
-
*
|
|
329
|
+
* allowance passed, or whose expiry is past MAX_VALIDITY_SECONDS from now
|
|
330
|
+
* (signedValidityProblem): registered after payload creation, so the check
|
|
331
|
+
* reads what was actually signed, and a throw here stops the payment-aware
|
|
332
|
+
* fetch before the payload leaves the process. guardSigning registers it; a
|
|
258
333
|
* client with its own selector (and no guardSigning) can register it alone. */
|
|
259
334
|
export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse = null } = {}) {
|
|
260
335
|
if (!client || typeof client.onAfterPaymentCreation !== "function") {
|
|
@@ -262,9 +337,10 @@ export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse
|
|
|
262
337
|
}
|
|
263
338
|
client.onAfterPaymentCreation(async ({ paymentRequired, selectedRequirements: r, paymentPayload }) => {
|
|
264
339
|
const cap = allowance();
|
|
265
|
-
const
|
|
340
|
+
const amountProblem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
|
|
341
|
+
const problem = amountProblem || signedValidityProblem(paymentPayload);
|
|
266
342
|
if (!problem) return;
|
|
267
|
-
try { onRefuse?.({ reason: "signed-amount", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
|
|
343
|
+
try { onRefuse?.({ reason: amountProblem ? "signed-amount" : "signed-validity", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
|
|
268
344
|
throw new Error(`spending guard: ${problem}; the payment was not sent`);
|
|
269
345
|
});
|
|
270
346
|
return client;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.25",
|
|
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",
|