agent402-client 0.8.11 → 0.8.13
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/index.js +67 -20
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
23
23
|
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
24
24
|
// network calls - so a seller can attribute traffic (and settled payments)
|
|
25
25
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
26
|
-
const VERSION = "0.8.
|
|
26
|
+
const VERSION = "0.8.13";
|
|
27
27
|
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
28
28
|
// 32MB: about a hundred times any realistic response from this catalog (the
|
|
29
29
|
// largest is a base64 image at a few MB), so it cannot break a legitimate
|
|
@@ -33,6 +33,33 @@ const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024 * 1024;
|
|
|
33
33
|
|
|
34
34
|
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; };
|
|
35
35
|
|
|
36
|
+
// Discovery reads (catalog, find, route, skill packs, leaderboard, a
|
|
37
|
+
// proof-of-work challenge) settle nothing, so a 503 the server sheds under
|
|
38
|
+
// load is safe to repeat. It is retried only when it names its own delay
|
|
39
|
+
// (a Retry-After header or a retryAfterSeconds body field), at most
|
|
40
|
+
// DISCOVERY_ATTEMPTS tries in all, each wait capped at DISCOVERY_MAX_WAIT_MS.
|
|
41
|
+
// A paid or priced call never goes through here: a repeat of one could settle
|
|
42
|
+
// twice.
|
|
43
|
+
const DISCOVERY_ATTEMPTS = 3;
|
|
44
|
+
const DISCOVERY_MAX_WAIT_MS = 5000;
|
|
45
|
+
|
|
46
|
+
/** The wait (ms) a shed 503 asks for, or null when `r` is not one. */
|
|
47
|
+
async function shedRetryWaitMs(r, maxWaitMs = DISCOVERY_MAX_WAIT_MS) {
|
|
48
|
+
if (!r || r.status !== 503) return null;
|
|
49
|
+
const header = r.headers && typeof r.headers.get === "function" ? r.headers.get("retry-after") : null;
|
|
50
|
+
let seconds = header != null && String(header).trim() !== "" ? Number(header) : NaN;
|
|
51
|
+
if (!Number.isFinite(seconds)) {
|
|
52
|
+
try {
|
|
53
|
+
const body = await (typeof r.clone === "function" ? r.clone() : r).json();
|
|
54
|
+
seconds = Number(body && body.retryAfterSeconds);
|
|
55
|
+
} catch { seconds = NaN; }
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
58
|
+
return Math.min(seconds * 1000, maxWaitMs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
|
|
36
63
|
export class Agent402 {
|
|
37
64
|
/**
|
|
38
65
|
* @param {object} [opts]
|
|
@@ -43,8 +70,8 @@ export class Agent402 {
|
|
|
43
70
|
* @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
|
|
44
71
|
* @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
|
|
45
72
|
* @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
|
|
46
|
-
* @param {string} [opts.creditsKey] a prepaid card-credits key (a402_...) issued
|
|
47
|
-
* pays wallet-only tools
|
|
73
|
+
* @param {string} [opts.creditsKey] a prepaid card-credits key (a402_...) already issued (new credits are
|
|
74
|
+
* not on sale) - pays wallet-only tools when no payFetch is given
|
|
48
75
|
* @param {number|null} [opts.maxResponseBytes=33554432]
|
|
49
76
|
* Hard ceiling on a response body, enforced BEFORE it is parsed. null disables it.
|
|
50
77
|
* @param {{id:string, validate:function}} [opts.outputValidator]
|
|
@@ -62,6 +89,8 @@ export class Agent402 {
|
|
|
62
89
|
// above). Caller-supplied init.headers still win on a key collision.
|
|
63
90
|
this.f = (url, init = {}) => fetchImpl(url, { ...init, headers: { "User-Agent": USER_AGENT, ...(init.headers || {}) } });
|
|
64
91
|
this._catalog = null;
|
|
92
|
+
// The wait between shed-503 retries of a discovery read (tests replace it).
|
|
93
|
+
this._sleep = sleepMs;
|
|
65
94
|
this._cache = cache ? new Map() : null;
|
|
66
95
|
// Salt for the default Idempotency-Key: one per client instance, so the key
|
|
67
96
|
// for (slug, params) is stable across call() invocations on THIS client
|
|
@@ -147,10 +176,35 @@ export class Agent402 {
|
|
|
147
176
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
148
177
|
}
|
|
149
178
|
|
|
179
|
+
/**
|
|
180
|
+
* GET a discovery surface, retrying a shed 503 (see DISCOVERY_ATTEMPTS).
|
|
181
|
+
* Returns the response once it is 2xx; otherwise throws an Error naming the
|
|
182
|
+
* operation, the surface and the last status, with `.status`, `.surface`
|
|
183
|
+
* and `.attempts` set.
|
|
184
|
+
*/
|
|
185
|
+
async _discoveryGet(label, path) {
|
|
186
|
+
let r;
|
|
187
|
+
let attempts = 0;
|
|
188
|
+
for (;;) {
|
|
189
|
+
attempts++;
|
|
190
|
+
r = await this.f(`${this.baseUrl}${path}`);
|
|
191
|
+
if (r.ok) return r;
|
|
192
|
+
if (attempts >= DISCOVERY_ATTEMPTS) break;
|
|
193
|
+
const wait = await shedRetryWaitMs(r);
|
|
194
|
+
if (wait == null) break;
|
|
195
|
+
await this._sleep(wait);
|
|
196
|
+
}
|
|
197
|
+
const surface = path.split("?")[0];
|
|
198
|
+
const err = new Error(`${label} failed: HTTP ${r.status} from ${surface}${attempts > 1 ? ` after ${attempts} attempts` : ""}`);
|
|
199
|
+
err.status = r.status;
|
|
200
|
+
err.surface = surface;
|
|
201
|
+
err.attempts = attempts;
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
|
|
150
205
|
async _loadCatalog() {
|
|
151
206
|
if (this._catalog) return this._catalog;
|
|
152
|
-
const r = await this.
|
|
153
|
-
if (!r.ok) throw new Error(`could not load catalog: HTTP ${r.status}`);
|
|
207
|
+
const r = await this._discoveryGet("could not load catalog", "/api/pricing");
|
|
154
208
|
const j = await r.json();
|
|
155
209
|
const m = new Map();
|
|
156
210
|
for (const e of j.endpoints || []) m.set(e.slug, { method: e.method, path: e.path, computePayable: e.computePayable, price: e.price });
|
|
@@ -160,8 +214,7 @@ export class Agent402 {
|
|
|
160
214
|
|
|
161
215
|
/** Resolve a plain-language task to the best-matching tools (route, price, schema, example). */
|
|
162
216
|
async find(task, { k = 5 } = {}) {
|
|
163
|
-
const r = await this.
|
|
164
|
-
if (!r.ok) throw new Error(`find failed: HTTP ${r.status}`);
|
|
217
|
+
const r = await this._discoveryGet("find", `/api/find?q=${encodeURIComponent(task)}&k=${k}`);
|
|
165
218
|
return (await r.json()).results || [];
|
|
166
219
|
}
|
|
167
220
|
|
|
@@ -190,8 +243,7 @@ export class Agent402 {
|
|
|
190
243
|
const includeParam = include === "external" || include === "local" ? include : "all";
|
|
191
244
|
const qs = new URLSearchParams({ q: String(task ?? ""), k: String(top), include: includeParam });
|
|
192
245
|
if (network) qs.set("network", String(network));
|
|
193
|
-
const r = await this.
|
|
194
|
-
if (!r.ok) throw new Error(`route failed: HTTP ${r.status}`);
|
|
246
|
+
const r = await this._discoveryGet("route", `/api/route?${qs}`);
|
|
195
247
|
return r.json();
|
|
196
248
|
}
|
|
197
249
|
|
|
@@ -204,8 +256,7 @@ export class Agent402 {
|
|
|
204
256
|
* to fetch the rendered prompt messages, or hand the slug to an MCP client.
|
|
205
257
|
*/
|
|
206
258
|
async findWorkflows(task, { k = 2 } = {}) {
|
|
207
|
-
const r = await this.
|
|
208
|
-
if (!r.ok) throw new Error(`findWorkflows failed: HTTP ${r.status}`);
|
|
259
|
+
const r = await this._discoveryGet("findWorkflows", `/api/find?q=${encodeURIComponent(task)}&k=${k}`);
|
|
209
260
|
return (await r.json()).packs || [];
|
|
210
261
|
}
|
|
211
262
|
|
|
@@ -216,8 +267,7 @@ export class Agent402 {
|
|
|
216
267
|
*/
|
|
217
268
|
async getWorkflowPrompt(slug, args = {}) {
|
|
218
269
|
const qs = new URLSearchParams(Object.entries(args).map(([k, v]) => [k, String(v)])).toString();
|
|
219
|
-
const r = await this.
|
|
220
|
-
if (!r.ok) throw new Error(`getWorkflowPrompt("${slug}") failed: HTTP ${r.status}`);
|
|
270
|
+
const r = await this._discoveryGet(`getWorkflowPrompt("${slug}")`, `/api/skill-packs/${encodeURIComponent(slug)}/prompt${qs ? `?${qs}` : ""}`);
|
|
221
271
|
return r.json();
|
|
222
272
|
}
|
|
223
273
|
|
|
@@ -238,9 +288,7 @@ export class Agent402 {
|
|
|
238
288
|
const top = Math.min(Math.max(parseInt(limit, 10) || 10, 1), 50);
|
|
239
289
|
const sortParam = sort === "calls" ? "calls" : "usd";
|
|
240
290
|
const includeParam = include === "all" ? "all" : "external";
|
|
241
|
-
const
|
|
242
|
-
const r = await this.f(url);
|
|
243
|
-
if (!r.ok) throw new Error(`topSellers failed: HTTP ${r.status}`);
|
|
291
|
+
const r = await this._discoveryGet("topSellers", `/api/leaderboard?top=${top}&sort=${sortParam}&include=${includeParam}`);
|
|
244
292
|
const snap = await r.json();
|
|
245
293
|
return {
|
|
246
294
|
window: snap.windowLabel || snap.windowServed || "24h",
|
|
@@ -387,7 +435,7 @@ export class Agent402 {
|
|
|
387
435
|
const r = await send({ Authorization: `Bearer ${this.creditsKey}` });
|
|
388
436
|
if (r.status === 402) {
|
|
389
437
|
const body = await r.json().catch(() => ({}));
|
|
390
|
-
throw this._fail(slug, r, `call "${slug}" refused by credits: ${body.error || "payment required"}${body.balanceUsd != null ? ` (balance $${body.balanceUsd})` : ""} - top up at ${body.topup
|
|
438
|
+
throw this._fail(slug, r, `call "${slug}" refused by credits: ${body.error || "payment required"}${body.balanceUsd != null ? ` (balance $${body.balanceUsd})` : ""} ${body.topup ? ` - top up at ${body.topup}` : body.topupNote ? ` - ${body.topupNote}` : ""}`);
|
|
391
439
|
}
|
|
392
440
|
if (!r.ok) throw this._fail(slug, r, await this._failureDetail(slug, r));
|
|
393
441
|
this._spendSettle(reservation);
|
|
@@ -397,7 +445,7 @@ export class Agent402 {
|
|
|
397
445
|
}
|
|
398
446
|
const r = await send(); // no wallet - succeeds only on a FREE_MODE instance
|
|
399
447
|
if (r.ok) return this._deliverResponse(slug, r, cacheKey, cache, validator, { maxResponseBytes, paid: false });
|
|
400
|
-
throw this._fail(slug, r, `call "${slug}" failed: HTTP ${r.status} - wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (prepaid
|
|
448
|
+
throw this._fail(slug, r, `call "${slug}" failed: HTTP ${r.status} - wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (a prepaid credits key already issued; new credits are not on sale)`);
|
|
401
449
|
}
|
|
402
450
|
|
|
403
451
|
// Free (compute-payable) tool: succeeds plainly on a FREE_MODE instance,
|
|
@@ -436,8 +484,7 @@ export class Agent402 {
|
|
|
436
484
|
}
|
|
437
485
|
|
|
438
486
|
async _powChallenge(slug) {
|
|
439
|
-
const r = await this.
|
|
440
|
-
if (!r.ok) throw new Error(`proof-of-work challenge for "${slug}" failed: HTTP ${r.status}`);
|
|
487
|
+
const r = await this._discoveryGet(`proof-of-work challenge for "${slug}"`, `/api/pow/challenge?slug=${encodeURIComponent(slug)}`);
|
|
441
488
|
return r.json();
|
|
442
489
|
}
|
|
443
490
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
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",
|