agent402-client 0.4.2 → 0.5.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 +31 -1
- package/index.js +88 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -82,15 +82,45 @@ await a.topSellers({ sort: "calls", include: "all" });
|
|
|
82
82
|
|
|
83
83
|
| Method | What |
|
|
84
84
|
|---|---|
|
|
85
|
-
| `new Agent402({ baseUrl?, fetch?, cache?, fetchImpl? })` | `fetch` is your x402-wrapped fetch for paid tools (optional); `cache` (default `true`) memoizes deterministic results |
|
|
85
|
+
| `new Agent402({ baseUrl?, fetch?, cache?, fetchImpl?, maxPerCallUsd?, dailyLimitUsd?, maxPerHostUsd? })` | `fetch` is your x402-wrapped fetch for paid tools (optional); `cache` (default `true`) memoizes deterministic results; the three USD caps set optional spending limits (see below) |
|
|
86
86
|
| `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools (route, price, schema, example) |
|
|
87
87
|
| `await a.findWorkflows(task, { k = 2 })` | Resolve a task to matching multi-tool workflow templates (skill packs) |
|
|
88
88
|
| `await a.getWorkflowPrompt(slug, args)` | Fetch the rendered prompt messages for a skill pack with arguments substituted in |
|
|
89
89
|
| `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) |
|
|
90
90
|
| `await a.call(slug, params, { idempotencyKey?, cache? })` | Call a tool; auto-pays (PoW for free tools, x402 for wallet-only); returns the JSON result |
|
|
91
91
|
| `Agent402.solvePow(pow)` | Solve a proof-of-work challenge object → an `X-Pow-Solution` value |
|
|
92
|
+
| `a.spendingSummary()` | Rolling-24h paid spend so far: `{ dailyUsd, calls, byHost, limits }` |
|
|
92
93
|
| `a.clearCache()` | Drop the in-memory result cache |
|
|
93
94
|
|
|
95
|
+
## Spending caps (never overpay)
|
|
96
|
+
|
|
97
|
+
By default the client pays whatever a tool costs. Set optional hard ceilings and a
|
|
98
|
+
call that would exceed one is **refused before any payment is signed** (it throws
|
|
99
|
+
`SpendingLimitError` — no funds move):
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { Agent402, SpendingLimitError } from "agent402-client";
|
|
103
|
+
|
|
104
|
+
const a = new Agent402({
|
|
105
|
+
fetch: payFetch,
|
|
106
|
+
maxPerCallUsd: 0.05, // reject any single call priced above $0.05
|
|
107
|
+
dailyLimitUsd: 5, // rolling-24h ceiling across all sellers
|
|
108
|
+
maxPerHostUsd: 1, // rolling-24h ceiling per seller host
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await a.call("some-expensive-tool", { … });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e instanceof SpendingLimitError) console.log(e.limit, e.priceUsd, e.cap);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Only **settled** paid calls count against the rolling window — a blocked or failed
|
|
119
|
+
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.
|
|
123
|
+
|
|
94
124
|
- **Zero dependencies** for the free/proof-of-work path (uses `node:crypto`).
|
|
95
125
|
- **Non-custodial:** paid settlement is your `@x402/fetch` + wallet; this client never sees your key.
|
|
96
126
|
- MIT licensed. Part of [Agent402](https://github.com/MikeyPetrillo/Agent402).
|
package/index.js
CHANGED
|
@@ -23,14 +23,30 @@ export class Agent402 {
|
|
|
23
23
|
* @param {typeof fetch} [opts.fetch] an x402-wrapped fetch for wallet-only tools (optional)
|
|
24
24
|
* @param {boolean} [opts.cache=true] cache results in memory (deterministic tools)
|
|
25
25
|
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch)
|
|
26
|
+
* @param {number} [opts.maxPerCallUsd] hard ceiling on a single paid call (USD); over → SpendingLimitError before paying
|
|
27
|
+
* @param {number} [opts.dailyLimitUsd] hard ceiling on rolling-24h paid spend (USD)
|
|
28
|
+
* @param {number} [opts.maxPerHostUsd] hard ceiling on rolling-24h paid spend to one seller host (USD)
|
|
26
29
|
*/
|
|
27
|
-
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = globalThis.fetch
|
|
30
|
+
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = globalThis.fetch,
|
|
31
|
+
maxPerCallUsd = null, dailyLimitUsd = null, maxPerHostUsd = null } = {}) {
|
|
28
32
|
if (typeof fetchImpl !== "function") throw new Error("No fetch available — pass { fetchImpl } on Node < 18");
|
|
29
33
|
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
30
34
|
this.payFetch = payFetch || null;
|
|
31
35
|
this.f = fetchImpl;
|
|
32
36
|
this._catalog = null;
|
|
33
37
|
this._cache = cache ? new Map() : null;
|
|
38
|
+
// Spending policy (defends the x402 "wallet drain via uncapped spending"
|
|
39
|
+
// failure mode): optional hard ceilings enforced BEFORE any payment is
|
|
40
|
+
// signed. A malicious or misconfigured 402 that quotes an inflated price is
|
|
41
|
+
// refused instead of paid. null = no limit (default — behavior unchanged).
|
|
42
|
+
// Amounts commit to the rolling window only on a settled paid call, so a
|
|
43
|
+
// failed/blocked call never counts against the budget.
|
|
44
|
+
this._spend = {
|
|
45
|
+
maxPerCall: numOrNull(maxPerCallUsd),
|
|
46
|
+
daily: numOrNull(dailyLimitUsd),
|
|
47
|
+
perHost: numOrNull(maxPerHostUsd),
|
|
48
|
+
log: [], // [{ ts, host, usd }] — settled paid calls in the last 24h
|
|
49
|
+
};
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
async _loadCatalog() {
|
|
@@ -167,8 +183,14 @@ export class Agent402 {
|
|
|
167
183
|
// Wallet-only tool → settle in USDC via the provided x402 fetch.
|
|
168
184
|
if (!tool.computePayable) {
|
|
169
185
|
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);
|
|
189
|
+
const host = hostOf(this.baseUrl);
|
|
190
|
+
this._spendCheck(host, usd, slug);
|
|
170
191
|
const r = await send({}, this.payFetch);
|
|
171
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
|
|
172
194
|
return this._store(cacheKey, await r.json(), cache);
|
|
173
195
|
}
|
|
174
196
|
const r = await send(); // no wallet — succeeds only on a FREE_MODE instance
|
|
@@ -196,7 +218,72 @@ export class Agent402 {
|
|
|
196
218
|
|
|
197
219
|
_store(key, val, cache) { if (this._cache && cache) this._cache.set(key, val); return val; }
|
|
198
220
|
clearCache() { this._cache?.clear(); }
|
|
221
|
+
|
|
222
|
+
/** Throw SpendingLimitError if paying `usd` to `host` now would break a cap.
|
|
223
|
+
* Prunes the rolling 24h window first; a null cap is unlimited. */
|
|
224
|
+
_spendCheck(host, usd, slug) {
|
|
225
|
+
const s = this._spend;
|
|
226
|
+
if (s.maxPerCall == null && s.daily == null && s.perHost == null) return;
|
|
227
|
+
if (s.maxPerCall != null && usd > s.maxPerCall) {
|
|
228
|
+
throw new SpendingLimitError(
|
|
229
|
+
`refusing to pay $${usd} for "${slug}" — exceeds maxPerCallUsd $${s.maxPerCall}`,
|
|
230
|
+
{ limit: "maxPerCallUsd", slug, priceUsd: usd, cap: s.maxPerCall });
|
|
231
|
+
}
|
|
232
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
233
|
+
s.log = s.log.filter((e) => e.ts >= cutoff);
|
|
234
|
+
if (s.daily != null) {
|
|
235
|
+
const spent = s.log.reduce((a, e) => a + e.usd, 0);
|
|
236
|
+
if (spent + usd > s.daily) {
|
|
237
|
+
throw new SpendingLimitError(
|
|
238
|
+
`refusing to pay $${usd} for "${slug}" — would bring 24h spend to $${(spent + usd).toFixed(6)}, over dailyLimitUsd $${s.daily}`,
|
|
239
|
+
{ limit: "dailyLimitUsd", slug, priceUsd: usd, spent, cap: s.daily });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (s.perHost != null) {
|
|
243
|
+
const spentHost = s.log.filter((e) => e.host === host).reduce((a, e) => a + e.usd, 0);
|
|
244
|
+
if (spentHost + usd > s.perHost) {
|
|
245
|
+
throw new SpendingLimitError(
|
|
246
|
+
`refusing to pay $${usd} for "${slug}" — would bring 24h spend to ${host} to $${(spentHost + usd).toFixed(6)}, over maxPerHostUsd $${s.perHost}`,
|
|
247
|
+
{ limit: "maxPerHostUsd", slug, host, priceUsd: usd, spent: spentHost, cap: s.perHost });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
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 }); }
|
|
254
|
+
|
|
255
|
+
/** Rolling-24h spend summary (settled paid calls only) — for observability. */
|
|
256
|
+
spendingSummary() {
|
|
257
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
258
|
+
const log = this._spend.log.filter((e) => e.ts >= cutoff);
|
|
259
|
+
const byHost = {};
|
|
260
|
+
for (const e of log) byHost[e.host] = Number(((byHost[e.host] || 0) + e.usd).toFixed(6));
|
|
261
|
+
return {
|
|
262
|
+
dailyUsd: Number(log.reduce((a, e) => a + e.usd, 0).toFixed(6)),
|
|
263
|
+
calls: log.length,
|
|
264
|
+
byHost,
|
|
265
|
+
limits: { maxPerCallUsd: this._spend.maxPerCall, dailyLimitUsd: this._spend.daily, maxPerHostUsd: this._spend.perHost },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Thrown when a paid call would exceed a configured spending ceiling. The call
|
|
271
|
+
* is refused BEFORE any payment is signed, so no funds move. */
|
|
272
|
+
export class SpendingLimitError extends Error {
|
|
273
|
+
constructor(message, details = {}) {
|
|
274
|
+
super(message);
|
|
275
|
+
this.name = "SpendingLimitError";
|
|
276
|
+
Object.assign(this, details);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function numOrNull(v) { if (v == null) return null; const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : null; }
|
|
281
|
+
function parseUsd(price) {
|
|
282
|
+
if (typeof price === "number") return Number.isFinite(price) ? price : 0;
|
|
283
|
+
const n = Number(String(price ?? "").replace(/[^0-9.]/g, ""));
|
|
284
|
+
return Number.isFinite(n) ? n : 0;
|
|
199
285
|
}
|
|
286
|
+
function hostOf(url) { try { return new URL(url).host; } catch { return String(url); } }
|
|
200
287
|
|
|
201
288
|
/**
|
|
202
289
|
* Restrict + order which chains an @x402 client will pay on (duck-typed — any
|
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 1,
|
|
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"llm-tools",
|
|
25
25
|
"usdc",
|
|
26
26
|
"base",
|
|
27
|
+
"solana",
|
|
28
|
+
"polygon",
|
|
29
|
+
"arbitrum",
|
|
30
|
+
"stellar",
|
|
27
31
|
"proof-of-work",
|
|
28
32
|
"http-402",
|
|
29
33
|
"micropayments",
|