agent402-openclaw 0.4.4 → 0.5.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 CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  Part of [Agent402](https://agent402.tools), the open-source paid door for AI agents.
4
4
 
5
- Agent402 as an [OpenClaw](https://openclaw.ai) model provider: auto-routed and
6
- explicit frontier models at a **flat per-call price**, paid **by card** (a prepaid
7
- credits key, no wallet) or in **USDC over x402** from a wallet. The same key and
8
- gateway reach Agent402's 500+ pay-per-call tools.
5
+ Agent402 as an [OpenClaw](https://openclaw.ai) model provider: the auto-routed
6
+ tier at a **flat per-call price** and explicit frontier models **priced per
7
+ request** from the body, paid in **USDC over x402** from a wallet or with a
8
+ prepaid credits key already issued (new credits are not on sale). The same key
9
+ and gateway reach Agent402's 500+ pay-per-call tools.
9
10
 
10
11
  Guide: https://agent402.tools/guides/openclaw-model-provider
11
12
 
@@ -21,8 +22,7 @@ With no credits key and no `AGENT402_WALLET_KEY`, `setup` generates an EVM
21
22
  wallet into `~/.openclaw/agent402/wallet.key` (0600, the only copy, never
22
23
  printed) and prints its address: send it USDC on Base and every call is paid
23
24
  from it over x402. `agent402-openclaw wallet` shows the address and balance;
24
- `--no-wallet` skips the generation. Prefer a card? Buy a pack at
25
- https://agent402.tools/credits and run
25
+ `--no-wallet` skips the generation. Holding a credits key already issued? Run
26
26
  `AGENT402_CREDITS_KEY=a402_... npx agent402-openclaw setup --write` (key by env,
27
27
  or `--credits-key -` on stdin).
28
28
 
@@ -43,6 +43,16 @@ model listing, gateway boot and one agent turn (`test-real-install.js`).
43
43
  No OpenClaw? `agent402-openclaw proxy` runs the proxy alone; point any OpenAI
44
44
  client at `http://127.0.0.1:8412/v1` with model `auto`.
45
45
 
46
+ **Claude Code and other Anthropic-wire clients** (0.5.0): the proxy also answers
47
+ `POST /v1/messages` and forwards it to the gateway's metered Messages route,
48
+ paid from the same wallet:
49
+
50
+ ```bash
51
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8412
52
+ export ANTHROPIC_AUTH_TOKEN=unused # the proxy pays; this value is ignored
53
+ claude --model claude-sonnet-5
54
+ ```
55
+
46
56
  ## Pay from a wallet instead
47
57
 
48
58
  ```bash
@@ -64,7 +74,7 @@ Two ways the wallet can pay a metered call:
64
74
  what the call actually used, so a short answer costs a fraction of the
65
75
  quote. `agent402-openclaw doctor` says which mode the wallet is in.
66
76
 
67
- Card / credits buyers already pay actual usage on metered calls.
77
+ Credits-key buyers already pay actual usage on metered calls.
68
78
 
69
79
  ## Models and pricing
70
80
 
@@ -79,8 +89,9 @@ Either way OpenClaw's per-token cost fields stay zero; the price is per call. A
79
89
  tier's route is priced at its home tier: the 402 quotes that tier's price, and the paid call is served under
80
90
  that tier's caps (the answer says so in `agent402_tier`). The router, grounded and stealth tiers still answer a
81
91
  400 naming the right route; nothing is charged. A client-supplied `Idempotency-Key`
82
- is passed through (an x402 retry with the same key replays the paid answer);
83
- without one, each call is its own payment.
92
+ is passed through: on a credits key, a retry with the same key replays the paid
93
+ answer; on a wallet, each forwarded call signs a fresh payment, which the server
94
+ treats as a new purchase. Without a key, each call is its own payment.
84
95
 
85
96
  The proxy answers native clients on loopback only: requests carrying a browser
86
97
  `Origin` header or a non-loopback `Host` are refused, so a web page cannot spend
@@ -88,7 +99,7 @@ the key.
88
99
 
89
100
  ## Commands
90
101
 
91
- - `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--write] [--port N] [--flat]`
102
+ - `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--no-wallet] [--write] [--port N] [--flat]`
92
103
  - `agent402-openclaw proxy [--port N] [--upstream URL]`
93
104
  - `agent402-openclaw doctor`
94
105
  - `agent402-openclaw wallet [--rpc URL]` (address + USDC balance of the wallet the proxy pays from)
@@ -102,11 +113,12 @@ Anthropic Messages wires, embeddings, rerank, images, video, speech,
102
113
  transcription, grounded answers with citations, 500+ tools over
103
114
  MCP or HTTP, wallet-keyed memory, finished reports (dossiers, insider flow, 13F,
104
115
  domain audits, token risk, deep research) and monitors, plus routing that buys
105
- from proven external sellers on the agent's behalf.
116
+ from external sellers on the agent's behalf.
106
117
 
107
118
  Why pay here, with every claim linked to its proof: https://agent402.tools/why.
108
119
  Usage is priced under a ceiling quoted before you pay, a failed call is not
109
- charged and the receipt proves it, a keyed retry never pays twice, and uptime and transactions are
110
- published from outside production.
120
+ charged and the receipt proves it, a keyed retry on a credits key is not
121
+ debited twice, and uptime and transactions are published from outside
122
+ production.
111
123
 
112
124
  Zero dependencies. MIT. Maintained by Havok Holdings LLC.
package/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // agent402-openclaw <command>
3
3
  // setup [--credits-key a402_...] [--write] store the key, print (or merge) the openclaw.json block
4
- // proxy [--port N] [--upstream URL] run the local proxy on its own (no OpenClaw needed)
4
+ // proxy [--port N] [--upstream URL] run the local proxy on its own (no OpenClaw needed;
5
+ // serves the OpenAI wire and Anthropic POST /v1/messages)
5
6
  // doctor show what is configured and whether the gateway answers
6
7
  import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, statSync, realpathSync, renameSync } from "node:fs";
7
8
  import { pathToFileURL } from "node:url";
@@ -90,7 +91,7 @@ async function main() {
90
91
  // per-token router that prints a wallet at install; the card path
91
92
  // (credits key) stays available. --no-wallet keeps the old behaviour.
92
93
  if (has("--no-wallet")) {
93
- out(`no payment method yet: buy a pack by card at ${upstream}/credits and rerun with --credits-key a402_..., or set AGENT402_WALLET_KEY`);
94
+ out(`no payment method yet: set AGENT402_WALLET_KEY, rerun without --no-wallet to generate a wallet, or rerun with --credits-key a402_... if you hold a credits key`);
94
95
  } else {
95
96
  const w = await generateWallet();
96
97
  if (w) {
@@ -99,9 +100,9 @@ async function main() {
99
100
  out(` key: ${WALLET_KEY_FILE()} (0600; back it up, it is the only copy)`);
100
101
  out(`fund it with USDC on Base (any amount; a call costs from $0.001), then every call is paid from it over x402.`);
101
102
  out(`optional: \`agent402-openclaw permit2-approve\` once (needs a little ETH on Base for gas) to pay actual usage instead of the per-request quote.`);
102
- out(`prefer a card? buy a pack at ${upstream}/credits and rerun with --credits-key a402_...`);
103
+ out(`holding a credits key already issued? rerun with --credits-key a402_... instead`);
103
104
  } else {
104
- out(`no payment method yet: buy a pack by card at ${upstream}/credits and rerun with --credits-key a402_..., or install viem + @x402/fetch + @x402/evm and rerun to generate a wallet`);
105
+ out(`no payment method yet: install viem + @x402/fetch + @x402/evm and rerun to generate a wallet, set AGENT402_WALLET_KEY, or rerun with --credits-key a402_... if you hold a credits key`);
105
106
  }
106
107
  }
107
108
  }
package/index.js CHANGED
@@ -37,7 +37,7 @@ export function resolveCreditsKey(pluginConfig = {}) {
37
37
 
38
38
  // x402 wallet payment - optional (peer deps) and, when the wallet has a
39
39
  // Permit2 allowance, METERED: the `upto` scheme authorizes a CEILING (the
40
- // per-request quote) and the gateway settles actual usage x 1.15, the way a
40
+ // per-request quote) and the gateway settles actual usage plus a markup, the way a
41
41
  // per-token router bills. Without the allowance it pays `exact` (the quote).
42
42
  export const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
43
43
  export const BASE_CAIP2 = "eip155:8453";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "agent402",
3
3
  "name": "Agent402",
4
- "description": "Agent402 model provider: auto-routed and explicit frontier models at a flat per-call price, paid by card (prepaid credits key) or in USDC over x402. Same key reaches 500+ pay-per-call tools.",
4
+ "description": "Agent402 model provider: an auto-routed tier at a flat per-call price and explicit frontier models priced per request, paid in USDC over x402 or with a prepaid credits key. Same key reaches 500+ pay-per-call tools.",
5
5
  "activation": {
6
6
  "onStartup": true
7
7
  },
@@ -49,7 +49,7 @@
49
49
  "uiHints": {
50
50
  "creditsKey": {
51
51
  "label": "Credits key",
52
- "help": "Buy by card at https://agent402.tools/credits",
52
+ "help": "Issued at https://agent402.tools/credits; that page says whether new keys are on sale",
53
53
  "sensitive": true
54
54
  },
55
55
  "walletKey": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent402-openclaw",
3
- "version": "0.4.4",
4
- "description": "OpenClaw model provider for Agent402: auto-routed and explicit LLM models at a flat per-call price, paid by card (prepaid credits key) or in USDC over x402 through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
3
+ "version": "0.5.0",
4
+ "description": "OpenClaw model provider for Agent402: an auto-routed tier at a flat per-call price and explicit LLM models priced per request, paid in USDC over x402 or with a prepaid credits key through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@x402/evm": ">=2.20.0",
47
- "@x402/fetch": ">=2.20.0"
47
+ "@x402/fetch": ">=2.20.0",
48
+ "viem": ">=2.48.11"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "@x402/evm": {
@@ -52,6 +53,9 @@
52
53
  },
53
54
  "@x402/fetch": {
54
55
  "optional": true
56
+ },
57
+ "viem": {
58
+ "optional": true
55
59
  }
56
60
  },
57
61
  "keywords": [
package/proxy.js CHANGED
@@ -1,5 +1,9 @@
1
- // Local OpenAI-compatible proxy: OpenClaw (or any client) talks to
2
- // http://127.0.0.1:<port>/v1, the proxy pays Agent402 and forwards.
1
+ // Local proxy: OpenClaw (or any client) talks to http://127.0.0.1:<port>, the
2
+ // proxy pays Agent402 and forwards. Two wires:
3
+ // OpenAI POST /v1/chat/completions -> the tier the model id names
4
+ // Anthropic POST /v1/messages -> the gateway's metered Messages
5
+ // route (/v1/metered/messages), so Claude Code and the Anthropic
6
+ // SDK pay from a wallet too (ANTHROPIC_BASE_URL=http://127.0.0.1:<port>).
3
7
  //
4
8
  // Two ways to pay, chosen at start:
5
9
  // creditsKey - a prepaid card-credits key (a402_...) sent as a Bearer; the
@@ -79,6 +83,20 @@ export function refusalAsOpenAIError(status, doc) {
79
83
  };
80
84
  }
81
85
 
86
+ /** The same refusal in the Anthropic error shape ({type:"error", error:{type,
87
+ * message}}), which Anthropic clients read their error text from. Null for any
88
+ * other body, which the caller relays unchanged. */
89
+ export function refusalAsAnthropicError(status, doc) {
90
+ const o = refusalAsOpenAIError(status, doc);
91
+ if (!o) return null;
92
+ const { error, ...fields } = o;
93
+ return { type: "error", error: { type: status === 402 ? "payment_required" : "api_error", message: error.message }, ...fields };
94
+ }
95
+
96
+ // The gateway's metered Anthropic Messages route: quoted per request from the
97
+ // body, any model id the gateway lists (dated Claude ids resolve to the live one).
98
+ export const MESSAGES_UPSTREAM_PATH = "/v1/metered/messages";
99
+
82
100
  const json = (res, status, obj, headers = {}) => {
83
101
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
84
102
  res.end(JSON.stringify(obj));
@@ -105,6 +123,39 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
105
123
  let table = routes || await loadRoutes(upstream, fetchImpl, { pricing });
106
124
  const stats = { requests: 0, forwarded: 0, errors: 0, startedAt: new Date().toISOString() };
107
125
 
126
+ // Pays and forwards one call. Streams pass through byte for byte; a refused
127
+ // paid call's offer-carrying JSON body is answered in the client wire's own
128
+ // error shape (mapRefusal); every other upstream body is relayed as sent.
129
+ async function forward(req, res, endpoint, outbound, extraHeaders, mapRefusal) {
130
+ const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
131
+ const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}`, ...extraHeaders };
132
+ if (key) headers.authorization = `Bearer ${key}`;
133
+ const up = await paid(`${upstream}${endpoint}`, { method: "POST", headers, body: JSON.stringify(outbound), signal: AbortSignal.timeout(300_000) });
134
+ stats.forwarded++;
135
+ const passthrough = {};
136
+ // payment-response / x-payment-response carry the x402 settlement receipt
137
+ // (v2 / v1); payment-receipt is MPP's. The client sees what it paid.
138
+ for (const h of ["content-type", "x-credits-balance", "payment-response", "x-payment-response", "payment-receipt", "x-cache", "cache-control"]) { const v = up.headers.get(h); if (v) passthrough[h] = v; }
139
+ if (!up.ok && /json/i.test(passthrough["content-type"] || "")) {
140
+ const raw = await up.text();
141
+ let doc = null;
142
+ try { doc = JSON.parse(raw); } catch { /* not JSON after all: relayed as sent */ }
143
+ const mapped = mapRefusal(up.status, doc);
144
+ if (mapped) {
145
+ const { "content-type": _ct, ...rest } = passthrough;
146
+ return json(res, up.status, mapped, rest);
147
+ }
148
+ res.writeHead(up.status, passthrough);
149
+ return res.end(raw);
150
+ }
151
+ res.writeHead(up.status, passthrough);
152
+ if (!up.body) return res.end();
153
+ const reader = up.body.getReader();
154
+ req.on("close", () => { reader.cancel().catch(() => {}); });
155
+ for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }
156
+ return res.end();
157
+ }
158
+
108
159
  const server = createServer(async (req, res) => {
109
160
  stats.requests++;
110
161
  const url = new URL(req.url, "http://localhost");
@@ -132,35 +183,26 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
132
183
  return json(res, 400, { error: { message: `Unknown model "${requested}". Use "auto" or an id from GET /v1/models (${table.size} available).`, type: "invalid_request_error", code: "model_not_found" } });
133
184
  }
134
185
  if (!paid) {
135
- return json(res, 402, { error: { message: "No payment method configured. Set AGENT402_CREDITS_KEY (buy a pack by card at agent402.tools/credits) or configure an x402 wallet (agent402-openclaw setup --wallet).", type: "payment_required", code: "agent402_unconfigured" }, topup: `${upstream}/credits`, priceUsd: route.priceUsd });
186
+ return json(res, 402, { error: { message: "No payment method configured. Run `agent402-openclaw setup` to generate an x402 wallet, set AGENT402_WALLET_KEY, or set AGENT402_CREDITS_KEY to a credits key already issued.", type: "payment_required", code: "agent402_unconfigured" }, topup: `${upstream}/credits`, priceUsd: route.priceUsd });
136
187
  }
137
188
  const outbound = { ...body };
138
189
  if (requested === AUTO_ID) delete outbound.model; else outbound.model = route.id;
139
- const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
140
- const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}` };
141
- if (key) headers.authorization = `Bearer ${key}`;
142
- const up = await paid(`${upstream}${route.endpoint}`, { method: "POST", headers, body: JSON.stringify(outbound), signal: AbortSignal.timeout(300_000) });
143
- stats.forwarded++;
144
- const passthrough = {};
145
- for (const h of ["content-type", "x-credits-balance", "payment-receipt", "x-cache", "cache-control"]) { const v = up.headers.get(h); if (v) passthrough[h] = v; }
146
- if (!up.ok && /json/i.test(passthrough["content-type"] || "")) {
147
- const raw = await up.text();
148
- let doc = null;
149
- try { doc = JSON.parse(raw); } catch { /* not JSON after all: relayed as sent */ }
150
- const mapped = refusalAsOpenAIError(up.status, doc);
151
- if (mapped) {
152
- const { "content-type": _ct, ...rest } = passthrough;
153
- return json(res, up.status, mapped, rest);
154
- }
155
- res.writeHead(up.status, passthrough);
156
- return res.end(raw);
190
+ return forward(req, res, route.endpoint, outbound, {}, refusalAsOpenAIError);
191
+ }
192
+ if (req.method === "POST" && url.pathname === "/v1/messages") {
193
+ const raw = await readBody(req);
194
+ let body;
195
+ try { body = JSON.parse(raw.toString("utf8") || "{}"); } catch { return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "Request body must be JSON" } }); }
196
+ if (typeof body.model !== "string" || !body.model.trim()) {
197
+ return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "\"model\" is required (any id from GET /v1/models, or a Claude id such as claude-sonnet-5)" } });
198
+ }
199
+ if (!paid) {
200
+ return json(res, 402, { type: "error", error: { type: "payment_required", message: "No payment method configured. Set AGENT402_WALLET_KEY to an EVM key holding USDC on Base (or run `agent402-openclaw setup`), or set AGENT402_CREDITS_KEY to a credits key already issued." } });
157
201
  }
158
- res.writeHead(up.status, passthrough);
159
- if (!up.body) return res.end();
160
- const reader = up.body.getReader();
161
- req.on("close", () => { reader.cancel().catch(() => {}); });
162
- for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }
163
- return res.end();
202
+ const outbound = { ...body, model: body.model.trim().replace(/^agent402\//, "") };
203
+ const extra = {};
204
+ for (const h of ["anthropic-version", "anthropic-beta"]) { const v = req.headers[h]; if (typeof v === "string" && v.length <= 512) extra[h] = v; }
205
+ return forward(req, res, MESSAGES_UPSTREAM_PATH, outbound, extra, refusalAsAnthropicError);
164
206
  }
165
207
  return json(res, 404, { error: { message: `No route for ${req.method} ${url.pathname}`, type: "invalid_request_error" } });
166
208
  } catch (e) {