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 +25 -13
- package/cli.js +5 -4
- package/index.js +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +7 -3
- package/proxy.js +69 -27
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
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
|
|
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
|
|
110
|
-
published from outside
|
|
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:
|
|
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(`
|
|
103
|
+
out(`holding a credits key already issued? rerun with --credits-key a402_... instead`);
|
|
103
104
|
} else {
|
|
104
|
-
out(`no payment method yet:
|
|
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
|
|
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/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "agent402",
|
|
3
3
|
"name": "Agent402",
|
|
4
|
-
"description": "Agent402 model provider: auto-routed
|
|
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": "
|
|
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
|
-
"description": "OpenClaw model provider for Agent402: auto-routed
|
|
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
|
|
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");
|
|
@@ -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.
|
|
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
|
-
|
|
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) {
|