agent402-client 0.8.0 → 0.8.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/README.md +37 -4
- package/index.js +55 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ const a = new Agent402(); // → https://agent402.tools
|
|
|
23
23
|
|
|
24
24
|
// Don't know the slug? Resolve a task in one call.
|
|
25
25
|
const matches = await a.find("extract the article from a url");
|
|
26
|
-
// → [{ slug: "extract", route, price, inputSchema, example,
|
|
26
|
+
// → [{ slug: "extract", route, price, inputSchema, example, ... }]
|
|
27
27
|
|
|
28
28
|
// Call it - proof-of-work is solved automatically for free tools.
|
|
29
29
|
const out = await a.call("hash", { text: "hello world", algo: "sha256" });
|
|
@@ -101,7 +101,7 @@ same id is a new payment).
|
|
|
101
101
|
## Workflows (skill packs)
|
|
102
102
|
|
|
103
103
|
For jobs that no single tool covers - e.g. "audit a domain", "build a stock
|
|
104
|
-
brief" - Agent402 ships curated multi-tool **skill packs**: 5
|
|
104
|
+
brief" - Agent402 ships curated multi-tool **skill packs**: 5-7 catalog tools
|
|
105
105
|
composed into a Claude-ready task template. Discover them the same way you'd
|
|
106
106
|
discover a tool:
|
|
107
107
|
|
|
@@ -114,6 +114,38 @@ const { messages } = await a.getWorkflowPrompt("security-audit", { domain: "stri
|
|
|
114
114
|
// → feed messages straight to any LLM
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Cross-seller routing (neutral Smart Order Router)
|
|
118
|
+
|
|
119
|
+
`find()` searches **this host's catalog only**. To rank tools across eligible
|
|
120
|
+
routable seller rows in the host's **current index** (not complete ecosystem
|
|
121
|
+
coverage), use `route()` - free, read-only, no wallet:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const task = "screenshot webpage";
|
|
125
|
+
const { query, include, results } = await a.route(task, {
|
|
126
|
+
k: 5,
|
|
127
|
+
include: "external", // all | external | local
|
|
128
|
+
network: "robinhood", // optional chain filter (short name or CAIP-2)
|
|
129
|
+
});
|
|
130
|
+
// results[]: opaque server rows - { seller, slug, url, priceUsd, executeVia?, ... }
|
|
131
|
+
|
|
132
|
+
// executeVia quotes which route-execute* tier fits a row's underlying price.
|
|
133
|
+
// It does NOT bind that row: the tier re-resolves an eligible match under its
|
|
134
|
+
// cap at execution time (url, route, seller, and identity can differ).
|
|
135
|
+
const hint = results[0]?.executeVia;
|
|
136
|
+
if (hint?.tool) {
|
|
137
|
+
const { result, receipt } = await a.call(hint.tool, {
|
|
138
|
+
task: query,
|
|
139
|
+
include,
|
|
140
|
+
params: { url: "https://example.com" }, // inputs the chosen operation needs
|
|
141
|
+
}, { cache: false }); // routed externals can be dynamic; client cache is on by default
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`executeVia` is server-reported metadata only. The SDK passes discovery rows
|
|
146
|
+
through unchanged, does not validate seller output, and does not guarantee
|
|
147
|
+
which seller or tool the execution tier will pick.
|
|
148
|
+
|
|
117
149
|
## Discover the live x402 economy
|
|
118
150
|
|
|
119
151
|
Want to see who's actually getting paid on x402 right now - not just what tools
|
|
@@ -134,7 +166,8 @@ await a.topSellers({ sort: "calls", include: "all" });
|
|
|
134
166
|
| Method | What |
|
|
135
167
|
|---|---|
|
|
136
168
|
| `new Agent402({ baseUrl?, fetch?, creditsKey?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd?, maxResponseBytes? })` | `fetch` is your x402- or MPP-wrapped fetch for paid tools (optional); `creditsKey` is a prepaid card-credits key (`a402_...`) used for paid tools when no `fetch` is given; `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 |
|
|
137
|
-
| `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools (route, price, schema, example) |
|
|
169
|
+
| `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools on **this host** (route, price, schema, example) |
|
|
170
|
+
| `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`) |
|
|
138
171
|
| `await a.findWorkflows(task, { k = 2 })` | Resolve a task to matching multi-tool workflow templates (skill packs) |
|
|
139
172
|
| `await a.getWorkflowPrompt(slug, args)` | Fetch the rendered prompt messages for a skill pack with arguments substituted in |
|
|
140
173
|
| `await a.topSellers({ limit?, sort?, include? })` | Live x402 leaderboard: which sellers are settling the most USDC (primarily on Base) in the last ~24h (free, no payment) |
|
|
@@ -180,7 +213,7 @@ const a = new Agent402({
|
|
|
180
213
|
});
|
|
181
214
|
|
|
182
215
|
try {
|
|
183
|
-
await a.call("some-expensive-tool", {
|
|
216
|
+
await a.call("some-expensive-tool", { ... });
|
|
184
217
|
} catch (e) {
|
|
185
218
|
if (e instanceof SpendingLimitError) console.log(e.limit, e.priceUsd, e.cap);
|
|
186
219
|
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// agent402-client
|
|
1
|
+
// agent402-client - a tiny buyer-side client for agent402.tools (or any Agent402
|
|
2
2
|
// instance). Resolve a task to a tool, then call it with payment handled for you:
|
|
3
3
|
// - free pure-CPU tools settle with a built-in proof-of-work (no wallet, zero deps),
|
|
4
4
|
// - wallet-only tools settle via an x402-wrapped fetch you provide (@x402/fetch),
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
|
|
17
17
|
// Keep in lockstep with package.json. Every request the SDK issues carries
|
|
18
|
-
// `User-Agent: agent402-client/<version>`
|
|
19
|
-
// network calls
|
|
18
|
+
// `User-Agent: agent402-client/<version>` - a standard header, no extra
|
|
19
|
+
// network calls - so a seller can attribute traffic (and settled payments)
|
|
20
20
|
// to this SDK. Product token only; nothing about the caller rides along.
|
|
21
21
|
const VERSION = "0.7.0";
|
|
22
22
|
const USER_AGENT = `agent402-client/${VERSION}`;
|
|
@@ -47,7 +47,7 @@ export class Agent402 {
|
|
|
47
47
|
maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null, creditsKey = null,
|
|
48
48
|
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES } = {}) {
|
|
49
49
|
this.creditsKey = typeof creditsKey === "string" && /^a402_[A-Za-z0-9_-]{32,64}$/.test(creditsKey) ? creditsKey : null;
|
|
50
|
-
if (typeof fetchImpl !== "function") throw new Error("No fetch available
|
|
50
|
+
if (typeof fetchImpl !== "function") throw new Error("No fetch available - pass { fetchImpl } on Node < 18");
|
|
51
51
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
52
52
|
this.payFetch = payFetch || null;
|
|
53
53
|
// Wrap the plain fetch so every request identifies the SDK (see USER_AGENT
|
|
@@ -58,14 +58,14 @@ export class Agent402 {
|
|
|
58
58
|
// Spending policy (defends the x402 "wallet drain via uncapped spending"
|
|
59
59
|
// failure mode): optional hard ceilings enforced BEFORE any payment is
|
|
60
60
|
// signed. A malicious or misconfigured 402 that quotes an inflated price is
|
|
61
|
-
// refused instead of paid. null = no limit (default
|
|
61
|
+
// refused instead of paid. null = no limit (default - behavior unchanged).
|
|
62
62
|
// Amounts commit to the rolling window only on a settled paid call, so a
|
|
63
63
|
// failed/blocked call never counts against the budget.
|
|
64
64
|
this._spend = {
|
|
65
65
|
maxPerCall: numOrNull(maxPerCallUsd),
|
|
66
66
|
daily: numOrNull(dailyLimitUsd),
|
|
67
67
|
perHost: numOrNull(maxPerHostUsd),
|
|
68
|
-
log: [], // [{ ts, host, usd }]
|
|
68
|
+
log: [], // [{ ts, host, usd }] - settled paid calls in the last 24h
|
|
69
69
|
};
|
|
70
70
|
// A response-size ceiling, enforced before parsing. See _readJson.
|
|
71
71
|
this.maxResponseBytes = maxResponseBytes === null ? null : (numOrNull(maxResponseBytes) ?? DEFAULT_MAX_RESPONSE_BYTES);
|
|
@@ -146,9 +146,39 @@ export class Agent402 {
|
|
|
146
146
|
return (await r.json()).results || [];
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Cross-seller Smart Order Router - rank tools across eligible/routable seller
|
|
151
|
+
* rows in the host's current index (local catalog plus crawled x402/MPP sellers
|
|
152
|
+
* the router considers healthy enough to route). Free; no payment, no wallet, no
|
|
153
|
+
* proof-of-work. Coverage is whatever the host's index holds at call time, not
|
|
154
|
+
* every seller on the internet.
|
|
155
|
+
*
|
|
156
|
+
* Returns the server JSON as-is (reporting only - no validation or authorization).
|
|
157
|
+
* Each result may carry server-reported `executeVia` naming a `route-execute*`
|
|
158
|
+
* tier whose underlying cap covers that row's price. `call(executeVia.tool, ...)`
|
|
159
|
+
* re-resolves an eligible match under that tier at execution time; it does not
|
|
160
|
+
* bind the discovery row's url, route, seller, or identity.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} task
|
|
163
|
+
* @param {object} [opts]
|
|
164
|
+
* @param {number} [opts.k=5] max results (1-25, server-bounded)
|
|
165
|
+
* @param {"all"|"external"|"local"} [opts.include="all"]
|
|
166
|
+
* @param {string} [opts.network] chain filter (e.g. "robinhood", "eip155:4663")
|
|
167
|
+
* @returns {Promise<{query:string, include:string, count:number, sellers:number, results:Array<object>}>}
|
|
168
|
+
*/
|
|
169
|
+
async route(task, { k = 5, include = "all", network } = {}) {
|
|
170
|
+
const top = Math.min(Math.max(parseInt(k, 10) || 5, 1), 25);
|
|
171
|
+
const includeParam = include === "external" || include === "local" ? include : "all";
|
|
172
|
+
const qs = new URLSearchParams({ q: String(task ?? ""), k: String(top), include: includeParam });
|
|
173
|
+
if (network) qs.set("network", String(network));
|
|
174
|
+
const r = await this.f(`${this.baseUrl}/api/route?${qs}`);
|
|
175
|
+
if (!r.ok) throw new Error(`route failed: HTTP ${r.status}`);
|
|
176
|
+
return r.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
149
179
|
/**
|
|
150
180
|
* Resolve a task to matching multi-tool workflow templates (skill packs).
|
|
151
|
-
* Each pack composes 5
|
|
181
|
+
* Each pack composes 5-7 catalog tools into a Claude-ready task template
|
|
152
182
|
* for jobs that no single tool covers (e.g. audit a domain). Returns
|
|
153
183
|
* `[{slug, title, tagline, toolSlugs, score, url, promptName}]` (possibly
|
|
154
184
|
* empty when the lexical signal is weak). Use `getWorkflowPrompt(slug, args)`
|
|
@@ -162,7 +192,7 @@ export class Agent402 {
|
|
|
162
192
|
|
|
163
193
|
/**
|
|
164
194
|
* Fetch the rendered prompt messages for a skill pack with arguments
|
|
165
|
-
* substituted in. Same output as MCP `prompts/get`
|
|
195
|
+
* substituted in. Same output as MCP `prompts/get` - usable directly with
|
|
166
196
|
* any LLM. `args` are passed by promptArg name (see /api/skill-packs.json).
|
|
167
197
|
*/
|
|
168
198
|
async getWorkflowPrompt(slug, args = {}) {
|
|
@@ -173,11 +203,11 @@ export class Agent402 {
|
|
|
173
203
|
}
|
|
174
204
|
|
|
175
205
|
/**
|
|
176
|
-
* Live x402 leaderboard
|
|
206
|
+
* Live x402 leaderboard - the sellers earning the most USDC (or serving the
|
|
177
207
|
* most calls) on Base in the last ~24h, derived from on-chain USDC
|
|
178
208
|
* transfers. Free; no payment, no wallet, no proof-of-work. Useful when
|
|
179
209
|
* building agents that want to discover the live x402 economy beyond a
|
|
180
|
-
* single service's catalog. Hourly snapshot
|
|
210
|
+
* single service's catalog. Hourly snapshot - safe to call freely.
|
|
181
211
|
*
|
|
182
212
|
* @param {object} [opts]
|
|
183
213
|
* @param {number} [opts.limit=10] max rows (1-50)
|
|
@@ -209,7 +239,7 @@ export class Agent402 {
|
|
|
209
239
|
* Register a wallet address for Base builder code attribution. Idempotent:
|
|
210
240
|
* the same wallet always returns the same code. No authentication required.
|
|
211
241
|
*
|
|
212
|
-
* @param {string} walletAddress the caller's wallet address (e.g. "0x
|
|
242
|
+
* @param {string} walletAddress the caller's wallet address (e.g. "0x...")
|
|
213
243
|
* @param {object} [opts]
|
|
214
244
|
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch)
|
|
215
245
|
* @returns {Promise<{builderCode:string, walletAddress:string}>}
|
|
@@ -239,7 +269,7 @@ export class Agent402 {
|
|
|
239
269
|
async call(slug, params = {}, { idempotencyKey, cache = true, maxResponseBytes } = {}) {
|
|
240
270
|
const cat = await this._loadCatalog();
|
|
241
271
|
const tool = cat.get(slug);
|
|
242
|
-
if (!tool) throw new Error(`unknown tool "${slug}"
|
|
272
|
+
if (!tool) throw new Error(`unknown tool "${slug}" - use client.find(task) to discover one`);
|
|
243
273
|
|
|
244
274
|
const cacheKey = `${slug}:${JSON.stringify(params)}`;
|
|
245
275
|
if (this._cache && cache && this._cache.has(cacheKey)) return this._cache.get(cacheKey);
|
|
@@ -247,7 +277,7 @@ export class Agent402 {
|
|
|
247
277
|
const idem = idempotencyKey || `a402-${createHash("sha256").update(`${cacheKey}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 24)}`;
|
|
248
278
|
const send = (extraHeaders = {}, useFetch = this.f) => {
|
|
249
279
|
// UA set here too (not only in the this.f wrapper) so the x402 payFetch
|
|
250
|
-
// path
|
|
280
|
+
// path - the one that settles real payments - always carries it.
|
|
251
281
|
const headers = { "User-Agent": USER_AGENT, "Idempotency-Key": idem, ...extraHeaders };
|
|
252
282
|
let url = `${this.baseUrl}${tool.path}`;
|
|
253
283
|
const init = { method: tool.method, headers };
|
|
@@ -268,12 +298,12 @@ export class Agent402 {
|
|
|
268
298
|
// configured ceiling (per-call / rolling-24h / per-host).
|
|
269
299
|
const host = hostOf(this.baseUrl);
|
|
270
300
|
let usd = parseUsd(tool.price);
|
|
271
|
-
// The catalog price is seller-ADVERTISED
|
|
301
|
+
// The catalog price is seller-ADVERTISED - a hostile server could under-
|
|
272
302
|
// state it and then quote more in the 402. When a cap is set, preflight
|
|
273
303
|
// the 402 to learn the price the wallet will actually be asked to sign and
|
|
274
304
|
// check the cap against the larger of the two. Fail-open: if the 402 can't
|
|
275
305
|
// be read (FREE_MODE / non-402 / unparseable), fall back to the advertised
|
|
276
|
-
// price
|
|
306
|
+
// price - never block a legitimate payment on a parse miss.
|
|
277
307
|
if (this._spendCapsConfigured()) {
|
|
278
308
|
try {
|
|
279
309
|
const pre = await send();
|
|
@@ -293,7 +323,7 @@ export class Agent402 {
|
|
|
293
323
|
this._spendSettle(reservation); // confirm the reservation as settled spend
|
|
294
324
|
return this._store(cacheKey, await this._readJson(r, { maxBytes: maxResponseBytes, slug, paid: true }), cache);
|
|
295
325
|
} catch (e) {
|
|
296
|
-
this._spendRelease(reservation); // roll back
|
|
326
|
+
this._spendRelease(reservation); // roll back - nothing settled
|
|
297
327
|
throw e;
|
|
298
328
|
}
|
|
299
329
|
}
|
|
@@ -315,13 +345,13 @@ export class Agent402 {
|
|
|
315
345
|
return this._store(cacheKey, await this._readJson(r, { maxBytes: maxResponseBytes, slug, paid: true }), cache);
|
|
316
346
|
} catch (e) { this._spendRelease(reservation); throw e; }
|
|
317
347
|
}
|
|
318
|
-
const r = await send(); // no wallet
|
|
348
|
+
const r = await send(); // no wallet - succeeds only on a FREE_MODE instance
|
|
319
349
|
if (r.ok) return this._store(cacheKey, await this._readJson(r, { maxBytes: maxResponseBytes, slug, paid: false }), cache);
|
|
320
|
-
throw new Error(`call "${slug}" failed: HTTP ${r.status}
|
|
350
|
+
throw new Error(`call "${slug}" failed: HTTP ${r.status} - wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (prepaid card credits from ${this.baseUrl}/credits)`);
|
|
321
351
|
}
|
|
322
352
|
|
|
323
353
|
// Free (compute-payable) tool: succeeds plainly on a FREE_MODE instance,
|
|
324
|
-
// otherwise pay with a proof-of-work (fetched from /api/pow/challenge
|
|
354
|
+
// otherwise pay with a proof-of-work (fetched from /api/pow/challenge - the
|
|
325
355
|
// Agent402 server signals it via the X-Pow-Challenge header, not the 402 body).
|
|
326
356
|
let r = await send();
|
|
327
357
|
if (!r.ok) {
|
|
@@ -348,7 +378,7 @@ export class Agent402 {
|
|
|
348
378
|
if (s.maxPerCall == null && s.daily == null && s.perHost == null) return;
|
|
349
379
|
if (s.maxPerCall != null && usd > s.maxPerCall) {
|
|
350
380
|
throw new SpendingLimitError(
|
|
351
|
-
`refusing to pay $${usd} for "${slug}"
|
|
381
|
+
`refusing to pay $${usd} for "${slug}" - exceeds maxPerCallUsd $${s.maxPerCall}`,
|
|
352
382
|
{ limit: "maxPerCallUsd", slug, priceUsd: usd, cap: s.maxPerCall });
|
|
353
383
|
}
|
|
354
384
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
@@ -357,7 +387,7 @@ export class Agent402 {
|
|
|
357
387
|
const spent = s.log.reduce((a, e) => a + e.usd, 0);
|
|
358
388
|
if (spent + usd > s.daily) {
|
|
359
389
|
throw new SpendingLimitError(
|
|
360
|
-
`refusing to pay $${usd} for "${slug}"
|
|
390
|
+
`refusing to pay $${usd} for "${slug}" - would bring 24h spend to $${(spent + usd).toFixed(6)}, over dailyLimitUsd $${s.daily}`,
|
|
361
391
|
{ limit: "dailyLimitUsd", slug, priceUsd: usd, spent, cap: s.daily });
|
|
362
392
|
}
|
|
363
393
|
}
|
|
@@ -365,7 +395,7 @@ export class Agent402 {
|
|
|
365
395
|
const spentHost = s.log.filter((e) => e.host === host).reduce((a, e) => a + e.usd, 0);
|
|
366
396
|
if (spentHost + usd > s.perHost) {
|
|
367
397
|
throw new SpendingLimitError(
|
|
368
|
-
`refusing to pay $${usd} for "${slug}"
|
|
398
|
+
`refusing to pay $${usd} for "${slug}" - would bring 24h spend to ${host} to $${(spentHost + usd).toFixed(6)}, over maxPerHostUsd $${s.perHost}`,
|
|
369
399
|
{ limit: "maxPerHostUsd", slug, host, priceUsd: usd, spent: spentHost, cap: s.perHost });
|
|
370
400
|
}
|
|
371
401
|
}
|
|
@@ -397,7 +427,7 @@ export class Agent402 {
|
|
|
397
427
|
if (i >= 0) this._spend.log.splice(i, 1);
|
|
398
428
|
}
|
|
399
429
|
|
|
400
|
-
/** Rolling-24h spend summary (settled paid calls only)
|
|
430
|
+
/** Rolling-24h spend summary (settled paid calls only) - for observability. */
|
|
401
431
|
spendingSummary() {
|
|
402
432
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
403
433
|
const log = this._spend.log.filter((e) => e.ts >= cutoff && !e.pending);
|
|
@@ -436,7 +466,7 @@ export class SpendingLimitError extends Error {
|
|
|
436
466
|
}
|
|
437
467
|
}
|
|
438
468
|
|
|
439
|
-
// Parse the USD amount an x402 `402` challenge actually requires
|
|
469
|
+
// Parse the USD amount an x402 `402` challenge actually requires - the max across
|
|
440
470
|
// the offered rails. x402 is stablecoin-settled (USDC/USDG), so
|
|
441
471
|
// atomic / 10^decimals ≈ USD. Returns null if the body isn't a parseable 402
|
|
442
472
|
// challenge, so the caller fails open to the advertised catalog price.
|
|
@@ -464,7 +494,7 @@ function parseUsd(price) {
|
|
|
464
494
|
function hostOf(url) { try { return new URL(url).host; } catch { return String(url); } }
|
|
465
495
|
|
|
466
496
|
/**
|
|
467
|
-
* Restrict + order which chains an @x402 client will pay on (duck-typed
|
|
497
|
+
* Restrict + order which chains an @x402 client will pay on (duck-typed - any
|
|
468
498
|
* client version with createPaymentPayload works, zero new dependencies).
|
|
469
499
|
* Multi-chain sellers list Base first, so an unmodified client effectively
|
|
470
500
|
* always settles there; this makes rails like USDG on Robinhood Chain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|