agent402-openclaw 0.4.5 → 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 +11 -1
- package/cli.js +2 -1
- package/index.js +1 -1
- package/package.json +6 -2
- package/proxy.js +68 -26
package/README.md
CHANGED
|
@@ -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
|
|
@@ -103,7 +113,7 @@ Anthropic Messages wires, embeddings, rerank, images, video, speech,
|
|
|
103
113
|
transcription, grounded answers with citations, 500+ tools over
|
|
104
114
|
MCP or HTTP, wallet-keyed memory, finished reports (dossiers, insider flow, 13F,
|
|
105
115
|
domain audits, token risk, deep research) and monitors, plus routing that buys
|
|
106
|
-
from external sellers
|
|
116
|
+
from external sellers and resells the result.
|
|
107
117
|
|
|
108
118
|
Why pay here, with every claim linked to its proof: https://agent402.tools/why.
|
|
109
119
|
Usage is priced under a ceiling quoted before you pay, a failed call is not
|
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";
|
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
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
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",
|
|
@@ -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
|
|
2
|
-
//
|
|
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");
|
|
@@ -136,31 +187,22 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
|
|
|
136
187
|
}
|
|
137
188
|
const outbound = { ...body };
|
|
138
189
|
if (requested === AUTO_ID) delete outbound.model; else outbound.model = route.id;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
req
|
|
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) {
|