agent402-client 0.1.0
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 +62 -0
- package/index.js +122 -0
- package/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# agent402-client
|
|
2
|
+
|
|
3
|
+
A tiny buyer-side client for [Agent402](https://agent402.tools) (and any Agent402
|
|
4
|
+
instance). **Resolve a task to a tool, then call it — with payment handled for
|
|
5
|
+
you.** Free pure-CPU tools settle with a built-in proof-of-work (no wallet, zero
|
|
6
|
+
dependencies); wallet-only tools settle via an x402-wrapped fetch you provide.
|
|
7
|
+
Results are cached, and retries reuse an `Idempotency-Key` so a lost response
|
|
8
|
+
never double-charges.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install agent402-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Free tier (proof-of-work, no wallet)
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { Agent402 } from "agent402-client";
|
|
18
|
+
|
|
19
|
+
const a = new Agent402(); // → https://agent402.tools
|
|
20
|
+
|
|
21
|
+
// Don't know the slug? Resolve a task in one call.
|
|
22
|
+
const matches = await a.find("extract the article from a url");
|
|
23
|
+
// → [{ slug: "extract", route, price, inputSchema, example, … }]
|
|
24
|
+
|
|
25
|
+
// Call it — proof-of-work is solved automatically for free tools.
|
|
26
|
+
const out = await a.call("hash", { text: "hello world", algo: "sha256" });
|
|
27
|
+
console.log(out.hex);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Paid tools (USDC via x402)
|
|
31
|
+
|
|
32
|
+
Wallet-only tools (live search, headless browser, PDFs, durable memory) settle
|
|
33
|
+
in USDC. Pass an [`@x402/fetch`](https://www.npmjs.com/package/@x402/fetch)-wrapped
|
|
34
|
+
fetch — your wallet signs, the client never touches your key:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
38
|
+
import { x402Client } from "@x402/core/client";
|
|
39
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
40
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
41
|
+
|
|
42
|
+
const client = new x402Client();
|
|
43
|
+
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.AGENT_KEY) });
|
|
44
|
+
const payFetch = wrapFetchWithPayment(fetch, client);
|
|
45
|
+
|
|
46
|
+
const a = new Agent402({ fetch: payFetch });
|
|
47
|
+
const article = await a.call("extract", { url: "https://example.com/article" });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
| Method | What |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `new Agent402({ baseUrl?, fetch?, cache?, fetchImpl? })` | `fetch` is your x402-wrapped fetch for paid tools (optional); `cache` (default `true`) memoizes deterministic results |
|
|
55
|
+
| `await a.find(task, { k = 5 })` | Resolve a plain-language task to the best-matching tools (route, price, schema, example) |
|
|
56
|
+
| `await a.call(slug, params, { idempotencyKey?, cache? })` | Call a tool; auto-pays (PoW for free tools, x402 for wallet-only); returns the JSON result |
|
|
57
|
+
| `Agent402.solvePow(pow)` | Solve a proof-of-work challenge object → an `X-Pow-Solution` value |
|
|
58
|
+
| `a.clearCache()` | Drop the in-memory result cache |
|
|
59
|
+
|
|
60
|
+
- **Zero dependencies** for the free/proof-of-work path (uses `node:crypto`).
|
|
61
|
+
- **Non-custodial:** paid settlement is your `@x402/fetch` + wallet; this client never sees your key.
|
|
62
|
+
- MIT licensed. Part of [Agent402](https://github.com/MikeyPetrillo/Agent402).
|
package/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// agent402-client — a tiny buyer-side client for agent402.tools (or any Agent402
|
|
2
|
+
// instance). Resolve a task to a tool, then call it with payment handled for you:
|
|
3
|
+
// - free pure-CPU tools settle with a built-in proof-of-work (no wallet, zero deps),
|
|
4
|
+
// - wallet-only tools settle via an x402-wrapped fetch you provide (@x402/fetch),
|
|
5
|
+
// results are cached (tools are deterministic), and retries reuse an
|
|
6
|
+
// Idempotency-Key so a lost response never double-charges.
|
|
7
|
+
//
|
|
8
|
+
// import { Agent402 } from "agent402-client";
|
|
9
|
+
// const a = new Agent402(); // free tier, proof-of-work
|
|
10
|
+
// const [best] = await a.find("extract the article from a url");
|
|
11
|
+
// const out = await a.call("extract", { url: "https://example.com/article" });
|
|
12
|
+
//
|
|
13
|
+
// // paid tools: pass an x402-wrapped fetch (your wallet signs)
|
|
14
|
+
// const a = new Agent402({ fetch: payFetch });
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
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
|
+
|
|
19
|
+
export class Agent402 {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} [opts]
|
|
22
|
+
* @param {string} [opts.baseUrl="https://agent402.tools"]
|
|
23
|
+
* @param {typeof fetch} [opts.fetch] an x402-wrapped fetch for wallet-only tools (optional)
|
|
24
|
+
* @param {boolean} [opts.cache=true] cache results in memory (deterministic tools)
|
|
25
|
+
* @param {typeof fetch} [opts.fetchImpl] plain fetch (defaults to global fetch)
|
|
26
|
+
*/
|
|
27
|
+
constructor({ baseUrl = "https://agent402.tools", fetch: payFetch, cache = true, fetchImpl = globalThis.fetch } = {}) {
|
|
28
|
+
if (typeof fetchImpl !== "function") throw new Error("No fetch available — pass { fetchImpl } on Node < 18");
|
|
29
|
+
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
30
|
+
this.payFetch = payFetch || null;
|
|
31
|
+
this.f = fetchImpl;
|
|
32
|
+
this._catalog = null;
|
|
33
|
+
this._cache = cache ? new Map() : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _loadCatalog() {
|
|
37
|
+
if (this._catalog) return this._catalog;
|
|
38
|
+
const r = await this.f(`${this.baseUrl}/api/pricing`);
|
|
39
|
+
if (!r.ok) throw new Error(`could not load catalog: HTTP ${r.status}`);
|
|
40
|
+
const j = await r.json();
|
|
41
|
+
const m = new Map();
|
|
42
|
+
for (const e of j.endpoints || []) m.set(e.slug, { method: e.method, path: e.path, computePayable: e.computePayable, price: e.price });
|
|
43
|
+
this._catalog = m;
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve a plain-language task to the best-matching tools (route, price, schema, example). */
|
|
48
|
+
async find(task, { k = 5 } = {}) {
|
|
49
|
+
const r = await this.f(`${this.baseUrl}/api/find?q=${encodeURIComponent(task)}&k=${k}`);
|
|
50
|
+
if (!r.ok) throw new Error(`find failed: HTTP ${r.status}`);
|
|
51
|
+
return (await r.json()).results || [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Solve a proof-of-work challenge object (from a 402 body) into an X-Pow-Solution value. */
|
|
55
|
+
static solvePow(pow) {
|
|
56
|
+
let n = 0;
|
|
57
|
+
while (leadingZeroBits(createHash("sha256").update(`${pow.challenge}:${n}`).digest()) < pow.difficulty) n++;
|
|
58
|
+
return `${pow.token}:${n}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Call a tool by slug; pays automatically (PoW for free tools, x402 for
|
|
63
|
+
* wallet-only) and returns the parsed JSON result.
|
|
64
|
+
*/
|
|
65
|
+
async call(slug, params = {}, { idempotencyKey, cache = true } = {}) {
|
|
66
|
+
const cat = await this._loadCatalog();
|
|
67
|
+
const tool = cat.get(slug);
|
|
68
|
+
if (!tool) throw new Error(`unknown tool "${slug}" — use client.find(task) to discover one`);
|
|
69
|
+
|
|
70
|
+
const cacheKey = `${slug}:${JSON.stringify(params)}`;
|
|
71
|
+
if (this._cache && cache && this._cache.has(cacheKey)) return this._cache.get(cacheKey);
|
|
72
|
+
|
|
73
|
+
const idem = idempotencyKey || `a402-${createHash("sha256").update(`${cacheKey}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 24)}`;
|
|
74
|
+
const send = (extraHeaders = {}, useFetch = this.f) => {
|
|
75
|
+
const headers = { "Idempotency-Key": idem, ...extraHeaders };
|
|
76
|
+
let url = `${this.baseUrl}${tool.path}`;
|
|
77
|
+
const init = { method: tool.method, headers };
|
|
78
|
+
if (tool.method === "GET") {
|
|
79
|
+
const qs = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])).toString();
|
|
80
|
+
if (qs) url += `?${qs}`;
|
|
81
|
+
} else {
|
|
82
|
+
headers["Content-Type"] = "application/json";
|
|
83
|
+
init.body = JSON.stringify(params);
|
|
84
|
+
}
|
|
85
|
+
return useFetch(url, init);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Wallet-only tool → settle in USDC via the provided x402 fetch.
|
|
89
|
+
if (!tool.computePayable) {
|
|
90
|
+
if (this.payFetch) {
|
|
91
|
+
const r = await send({}, this.payFetch);
|
|
92
|
+
if (!r.ok) throw new Error(`call "${slug}" failed: HTTP ${r.status}`);
|
|
93
|
+
return this._store(cacheKey, await r.json(), cache);
|
|
94
|
+
}
|
|
95
|
+
const r = await send(); // no wallet — succeeds only on a FREE_MODE instance
|
|
96
|
+
if (r.ok) return this._store(cacheKey, await r.json(), cache);
|
|
97
|
+
throw new Error(`call "${slug}" failed: HTTP ${r.status} — wallet-only tool; construct with { fetch: payFetch } (an @x402/fetch-wrapped fetch)`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Free (compute-payable) tool: succeeds plainly on a FREE_MODE instance,
|
|
101
|
+
// otherwise pay with a proof-of-work (fetched from /api/pow/challenge — the
|
|
102
|
+
// Agent402 server signals it via the X-Pow-Challenge header, not the 402 body).
|
|
103
|
+
let r = await send();
|
|
104
|
+
if (!r.ok) {
|
|
105
|
+
const chal = await this._powChallenge(slug);
|
|
106
|
+
r = await send({ "X-Pow-Solution": Agent402.solvePow(chal) });
|
|
107
|
+
}
|
|
108
|
+
if (!r.ok) throw new Error(`call "${slug}" failed after proof-of-work: HTTP ${r.status}`);
|
|
109
|
+
return this._store(cacheKey, await r.json(), cache);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async _powChallenge(slug) {
|
|
113
|
+
const r = await this.f(`${this.baseUrl}/api/pow/challenge?slug=${encodeURIComponent(slug)}`);
|
|
114
|
+
if (!r.ok) throw new Error(`proof-of-work challenge for "${slug}" failed: HTTP ${r.status}`);
|
|
115
|
+
return r.json();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_store(key, val, cache) { if (this._cache && cache) this._cache.set(key, val); return val; }
|
|
119
|
+
clearCache() { this._cache?.clear(); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default Agent402;
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent402-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny buyer-side client for Agent402 (x402): resolve a task to a tool 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. Caching + idempotent retries.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": { ".": "./index.js" },
|
|
8
|
+
"files": ["index.js", "README.md"],
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"scripts": { "test": "node test.js" },
|
|
11
|
+
"keywords": [
|
|
12
|
+
"x402", "ai-agents", "agent-tools", "llm-tools", "usdc", "base",
|
|
13
|
+
"proof-of-work", "http-402", "micropayments", "mcp", "client", "sdk"
|
|
14
|
+
],
|
|
15
|
+
"author": "Mikey Petrillo (https://github.com/MikeyPetrillo)",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": { "type": "git", "url": "https://github.com/MikeyPetrillo/Agent402.git", "directory": "client" },
|
|
18
|
+
"homepage": "https://github.com/MikeyPetrillo/Agent402/tree/main/client"
|
|
19
|
+
}
|