agent402-client 0.5.1 → 0.6.1
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/LICENSE +21 -0
- package/README.md +14 -3
- package/index.js +89 -13
- package/package.json +5 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Havok Holdings LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ never double-charges.
|
|
|
11
11
|
npm install agent402-client
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
Runnable copy of the free-tier quickstart below: [`examples/hello-agent402.js`](https://github.com/MikeyPetrillo/Agent402/blob/main/examples/hello-agent402.js) — discover a tool and call it in ~15 lines, no wallet.
|
|
15
|
+
|
|
14
16
|
## Free tier (proof-of-work, no wallet)
|
|
15
17
|
|
|
16
18
|
```js
|
|
@@ -117,9 +119,18 @@ try {
|
|
|
117
119
|
|
|
118
120
|
Only **settled** paid calls count against the rolling window — a blocked or failed
|
|
119
121
|
call never consumes budget. Free proof-of-work calls are never counted. Omit a cap
|
|
120
|
-
(or leave it `null`) for no limit; with none set, behavior is unchanged.
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
(or leave it `null`) for no limit; with none set, behavior is unchanged.
|
|
123
|
+
|
|
124
|
+
**What the caps check.** When a cap is set, the client preflights the `402` and
|
|
125
|
+
checks the ceiling against the **larger** of the advertised price (from the
|
|
126
|
+
seller's `/api/pricing`) and the amount the `402` challenge actually quotes — so a
|
|
127
|
+
server that under-advertises and then quotes more in the `402` is refused *before*
|
|
128
|
+
your wallet fetch signs anything. If the `402` can't be read (FREE_MODE, or an
|
|
129
|
+
unrecognized challenge shape) it falls back to the advertised price rather than
|
|
130
|
+
block a legitimate payment. Caps hold under **concurrency** too: each call reserves
|
|
131
|
+
its amount synchronously, so N simultaneous calls can't each pass against the same
|
|
132
|
+
pre-commit total. (The `402` amount is derived assuming stablecoin settlement —
|
|
133
|
+
`atomic / 10^decimals ≈ USD` — which matches x402's USDC/USDG rails.)
|
|
123
134
|
|
|
124
135
|
- **Zero dependencies** for the free/proof-of-work path (uses `node:crypto`).
|
|
125
136
|
- **Non-custodial:** paid settlement is your `@x402/fetch` + wallet; this client never sees your key.
|
package/index.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
// const a = new Agent402({ fetch: payFetch });
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
|
|
17
|
+
// Keep in lockstep with package.json. Every request the SDK issues carries
|
|
18
|
+
// `User-Agent: agent402-client/<version>` — a standard header, no extra
|
|
19
|
+
// network calls — so a seller can attribute traffic (and settled payments)
|
|
20
|
+
// to this SDK. Product token only; nothing about the caller rides along.
|
|
21
|
+
const VERSION = "0.6.1";
|
|
22
|
+
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
23
|
+
|
|
17
24
|
const leadingZeroBits = (buf) => { let n = 0; for (const b of buf) { if (b === 0) { n += 8; continue; } n += Math.clz32(b) - 24; break; } return n; };
|
|
18
25
|
|
|
19
26
|
export class Agent402 {
|
|
@@ -32,7 +39,9 @@ export class Agent402 {
|
|
|
32
39
|
if (typeof fetchImpl !== "function") throw new Error("No fetch available — pass { fetchImpl } on Node < 18");
|
|
33
40
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
34
41
|
this.payFetch = payFetch || null;
|
|
35
|
-
|
|
42
|
+
// Wrap the plain fetch so every request identifies the SDK (see USER_AGENT
|
|
43
|
+
// above). Caller-supplied init.headers still win on a key collision.
|
|
44
|
+
this.f = (url, init = {}) => fetchImpl(url, { ...init, headers: { "User-Agent": USER_AGENT, ...(init.headers || {}) } });
|
|
36
45
|
this._catalog = null;
|
|
37
46
|
this._cache = cache ? new Map() : null;
|
|
38
47
|
// Spending policy (defends the x402 "wallet drain via uncapped spending"
|
|
@@ -167,7 +176,9 @@ export class Agent402 {
|
|
|
167
176
|
|
|
168
177
|
const idem = idempotencyKey || `a402-${createHash("sha256").update(`${cacheKey}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 24)}`;
|
|
169
178
|
const send = (extraHeaders = {}, useFetch = this.f) => {
|
|
170
|
-
|
|
179
|
+
// UA set here too (not only in the this.f wrapper) so the x402 payFetch
|
|
180
|
+
// path — the one that settles real payments — always carries it.
|
|
181
|
+
const headers = { "User-Agent": USER_AGENT, "Idempotency-Key": idem, ...extraHeaders };
|
|
171
182
|
let url = `${this.baseUrl}${tool.path}`;
|
|
172
183
|
const init = { method: tool.method, headers };
|
|
173
184
|
if (tool.method === "GET") {
|
|
@@ -183,15 +194,38 @@ export class Agent402 {
|
|
|
183
194
|
// Wallet-only tool → settle in USDC via the provided x402 fetch.
|
|
184
195
|
if (!tool.computePayable) {
|
|
185
196
|
if (this.payFetch) {
|
|
186
|
-
// Spending policy: refuse to pay BEFORE signing if the
|
|
187
|
-
//
|
|
188
|
-
const usd = parseUsd(tool.price);
|
|
197
|
+
// Spending policy: refuse to pay BEFORE signing if the price breaks a
|
|
198
|
+
// configured ceiling (per-call / rolling-24h / per-host).
|
|
189
199
|
const host = hostOf(this.baseUrl);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
let usd = parseUsd(tool.price);
|
|
201
|
+
// The catalog price is seller-ADVERTISED — a hostile server could under-
|
|
202
|
+
// state it and then quote more in the 402. When a cap is set, preflight
|
|
203
|
+
// the 402 to learn the price the wallet will actually be asked to sign and
|
|
204
|
+
// check the cap against the larger of the two. Fail-open: if the 402 can't
|
|
205
|
+
// be read (FREE_MODE / non-402 / unparseable), fall back to the advertised
|
|
206
|
+
// price — never block a legitimate payment on a parse miss.
|
|
207
|
+
if (this._spendCapsConfigured()) {
|
|
208
|
+
try {
|
|
209
|
+
const pre = await send();
|
|
210
|
+
if (pre.status === 402) {
|
|
211
|
+
const quoted = parse402Usd(await pre.json().catch(() => null));
|
|
212
|
+
if (quoted != null) usd = Math.max(usd, quoted);
|
|
213
|
+
}
|
|
214
|
+
} catch { /* fail-open to the advertised price */ }
|
|
215
|
+
}
|
|
216
|
+
// Reserve the amount synchronously (before the await) so concurrent calls
|
|
217
|
+
// can't each observe the pre-commit total and collectively blow a rolling
|
|
218
|
+
// cap; release the reservation if the call doesn't settle.
|
|
219
|
+
const reservation = this._spendReserve(host, usd, slug);
|
|
220
|
+
try {
|
|
221
|
+
const r = await send({}, this.payFetch);
|
|
222
|
+
if (!r.ok) throw new Error(`call "${slug}" failed: HTTP ${r.status}`);
|
|
223
|
+
this._spendSettle(reservation); // confirm the reservation as settled spend
|
|
224
|
+
return this._store(cacheKey, await r.json(), cache);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
this._spendRelease(reservation); // roll back — nothing settled
|
|
227
|
+
throw e;
|
|
228
|
+
}
|
|
195
229
|
}
|
|
196
230
|
const r = await send(); // no wallet — succeeds only on a FREE_MODE instance
|
|
197
231
|
if (r.ok) return this._store(cacheKey, await r.json(), cache);
|
|
@@ -249,13 +283,36 @@ export class Agent402 {
|
|
|
249
283
|
}
|
|
250
284
|
}
|
|
251
285
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
286
|
+
/** True if any spending ceiling is configured (worth preflighting the 402). */
|
|
287
|
+
_spendCapsConfigured() {
|
|
288
|
+
const s = this._spend;
|
|
289
|
+
return s.maxPerCall != null || s.daily != null || s.perHost != null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Check caps AND reserve the amount atomically (no await in between), so
|
|
293
|
+
* concurrent calls account for each other's in-flight reservations instead of
|
|
294
|
+
* all passing against the same pre-commit total. Returns a reservation handle
|
|
295
|
+
* (or null for a $0 call). Throws SpendingLimitError before reserving if over. */
|
|
296
|
+
_spendReserve(host, usd, slug) {
|
|
297
|
+
this._spendCheck(host, usd, slug);
|
|
298
|
+
if (!(usd > 0)) return null;
|
|
299
|
+
const entry = { ts: Date.now(), host, usd, pending: true };
|
|
300
|
+
this._spend.log.push(entry);
|
|
301
|
+
return entry;
|
|
302
|
+
}
|
|
303
|
+
/** Confirm a reservation as settled spend. */
|
|
304
|
+
_spendSettle(entry) { if (entry) entry.pending = false; }
|
|
305
|
+
/** Roll back a reservation whose call did not settle (failed / errored). */
|
|
306
|
+
_spendRelease(entry) {
|
|
307
|
+
if (!entry) return;
|
|
308
|
+
const i = this._spend.log.indexOf(entry);
|
|
309
|
+
if (i >= 0) this._spend.log.splice(i, 1);
|
|
310
|
+
}
|
|
254
311
|
|
|
255
312
|
/** Rolling-24h spend summary (settled paid calls only) — for observability. */
|
|
256
313
|
spendingSummary() {
|
|
257
314
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
258
|
-
const log = this._spend.log.filter((e) => e.ts >= cutoff);
|
|
315
|
+
const log = this._spend.log.filter((e) => e.ts >= cutoff && !e.pending);
|
|
259
316
|
const byHost = {};
|
|
260
317
|
for (const e of log) byHost[e.host] = Number(((byHost[e.host] || 0) + e.usd).toFixed(6));
|
|
261
318
|
return {
|
|
@@ -277,6 +334,25 @@ export class SpendingLimitError extends Error {
|
|
|
277
334
|
}
|
|
278
335
|
}
|
|
279
336
|
|
|
337
|
+
// Parse the USD amount an x402 `402` challenge actually requires — the max across
|
|
338
|
+
// the offered rails. x402 is stablecoin-settled (USDC/USDG), so
|
|
339
|
+
// atomic / 10^decimals ≈ USD. Returns null if the body isn't a parseable 402
|
|
340
|
+
// challenge, so the caller fails open to the advertised catalog price.
|
|
341
|
+
function parse402Usd(body) {
|
|
342
|
+
const accepts = body && body.accepts;
|
|
343
|
+
if (!Array.isArray(accepts) || !accepts.length) return null;
|
|
344
|
+
let maxUsd = 0;
|
|
345
|
+
for (const a of accepts) {
|
|
346
|
+
const atomic = Number(a && a.maxAmountRequired);
|
|
347
|
+
if (!Number.isFinite(atomic) || atomic < 0) return null;
|
|
348
|
+
const decimals = Number((a && a.extra && a.extra.decimals) ?? (a && a.decimals) ?? 6);
|
|
349
|
+
if (!Number.isFinite(decimals) || decimals < 0 || decimals > 30) return null;
|
|
350
|
+
const usd = atomic / 10 ** decimals;
|
|
351
|
+
if (usd > maxUsd) maxUsd = usd;
|
|
352
|
+
}
|
|
353
|
+
return maxUsd;
|
|
354
|
+
}
|
|
355
|
+
|
|
280
356
|
function numOrNull(v) { if (v == null) return null; const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : null; }
|
|
281
357
|
function parseUsd(price) {
|
|
282
358
|
if (typeof price === "number") return Number.isFinite(price) ? price : 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Buyer-side SDK for Agent402.Tools (x402): resolve a task to a tool across
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Buyer-side SDK for Agent402.Tools (x402): resolve a task to a tool across a catalog of 500 pay-per-call endpoints (400 tools + 100 skill packs) and call it with payment handled for you — free tools settle via built-in proof-of-work (no wallet), wallet-only tools via your x402 fetch — with a withNetworkPreference helper to pick the settlement chain (USDC on Base/Solana/Polygon/Arbitrum/Monad/Stellar/Algorand or USDG on Robinhood Chain — 8 chains). Caching, idempotent retries, cross-seller Smart Order Router.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"solana",
|
|
28
28
|
"polygon",
|
|
29
29
|
"arbitrum",
|
|
30
|
+
"monad",
|
|
30
31
|
"stellar",
|
|
32
|
+
"algorand",
|
|
31
33
|
"proof-of-work",
|
|
32
34
|
"http-402",
|
|
33
35
|
"micropayments",
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
"usdg",
|
|
42
44
|
"multi-chain"
|
|
43
45
|
],
|
|
44
|
-
"author": "
|
|
46
|
+
"author": "Havok Holdings LLC <mike@agent402.tools> (https://agent402.tools)",
|
|
45
47
|
"license": "MIT",
|
|
46
48
|
"repository": {
|
|
47
49
|
"type": "git",
|