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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -3
  3. package/index.js +89 -13
  4. 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. This is a
121
- buyer-side circuit breaker against a malicious or misconfigured `402` that quotes an
122
- inflated price.
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
- this.f = fetchImpl;
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
- const headers = { "Idempotency-Key": idem, ...extraHeaders };
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 quoted price
187
- // breaks a configured ceiling (per-call / rolling-24h / per-host).
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
- this._spendCheck(host, usd, slug);
191
- const r = await send({}, this.payFetch);
192
- if (!r.ok) throw new Error(`call "${slug}" failed: HTTP ${r.status}`);
193
- this._spendCommit(host, usd); // record spend only once the call actually settled
194
- return this._store(cacheKey, await r.json(), cache);
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
- /** Record a settled paid call against the rolling budget. */
253
- _spendCommit(host, usd) { if (usd > 0) this._spend.log.push({ ts: Date.now(), host, usd }); }
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.5.1",
4
- "description": "Buyer-side SDK for Agent402.Tools (x402): resolve a task to a tool across 1,407 pay-per-call endpoints 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/Stellar or USDG on Robinhood Chain). Caching, idempotent retries, cross-seller Smart Order Router.",
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": "Mike Petrillo <mike@agent402.tools> (https://github.com/MikeyPetrillo)",
46
+ "author": "Havok Holdings LLC <mike@agent402.tools> (https://agent402.tools)",
45
47
  "license": "MIT",
46
48
  "repository": {
47
49
  "type": "git",