agent402-client 0.8.0 → 0.8.2

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 (3) hide show
  1. package/README.md +37 -4
  2. package/index.js +56 -26
  3. 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–7 catalog tools
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 — a tiny buyer-side client for agent402.tools (or any Agent402
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,10 +15,10 @@
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>` — a standard header, no extra
19
- // network calls — so a seller can attribute traffic (and settled payments)
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
- const VERSION = "0.7.0";
21
+ const VERSION = "0.8.2";
22
22
  const USER_AGENT = `agent402-client/${VERSION}`;
23
23
  // 32MB: about a hundred times any realistic response from this catalog (the
24
24
  // largest is a base64 image at a few MB), so it cannot break a legitimate
@@ -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 — pass { fetchImpl } on Node < 18");
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 — behavior unchanged).
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 }] — settled paid calls in the last 24h
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–7 catalog tools into a Claude-ready task template
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` — usable directly with
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 — the sellers earning the most USDC (or serving the
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 — safe to call freely.
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}" — use client.find(task) to discover one`);
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 — the one that settles real payments — always carries it.
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 — a hostile server could under-
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 — never block a legitimate payment on a parse miss.
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 — nothing settled
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 — succeeds only on a FREE_MODE instance
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} — wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch) or { creditsKey } (prepaid card credits from ${this.baseUrl}/credits)`);
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 — the
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}" — exceeds maxPerCallUsd $${s.maxPerCall}`,
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}" — would bring 24h spend to $${(spent + usd).toFixed(6)}, over dailyLimitUsd $${s.daily}`,
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}" — would bring 24h spend to ${host} to $${(spentHost + usd).toFixed(6)}, over maxPerHostUsd $${s.perHost}`,
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) — for observability. */
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 — the max across
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 — any
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.0",
3
+ "version": "0.8.2",
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",