agent402-openclaw 0.5.4 → 0.5.6

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.
Files changed (4) hide show
  1. package/README.md +5 -3
  2. package/cli.js +1 -1
  3. package/package.json +2 -2
  4. package/proxy.js +19 -9
package/README.md CHANGED
@@ -5,8 +5,10 @@ Part of [Agent402](https://agent402.tools), the open-source paid door for AI age
5
5
  Agent402 as an [OpenClaw](https://openclaw.ai) model provider: the auto-routed
6
6
  tier at a **flat per-call price** and explicit frontier models **priced per
7
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.
8
+ prepaid credits key (agent402.tools/credits says whether packs are on sale). The
9
+ same key and gateway reach Agent402's 500+ pay-per-call tools; credits pay
10
+ Agent402's own services, so the routes that buy from an outside seller
11
+ (the route-execute tiers, `seller-payability`, `decide-execute` and the flight routes) need a wallet.
10
12
 
11
13
  Guide: https://agent402.tools/guides/openclaw-model-provider
12
14
 
@@ -22,7 +24,7 @@ With no credits key and no `AGENT402_WALLET_KEY`, `setup` generates an EVM
22
24
  wallet into `~/.openclaw/agent402/wallet.key` (0600, the only copy, never
23
25
  printed) and prints its address: send it USDC on Base and every call is paid
24
26
  from it over x402. `agent402-openclaw wallet` shows the address and balance;
25
- `--no-wallet` skips the generation. Holding a credits key already issued? Run
27
+ `--no-wallet` skips the generation. Holding a credits key? Run
26
28
  `AGENT402_CREDITS_KEY=a402_... npx agent402-openclaw setup --write` (key by env,
27
29
  or `--credits-key -` on stdin).
28
30
 
package/cli.js CHANGED
@@ -100,7 +100,7 @@ async function main() {
100
100
  out(` key: ${WALLET_KEY_FILE()} (0600; back it up, it is the only copy)`);
101
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.`);
102
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.`);
103
- out(`holding a credits key already issued? rerun with --credits-key a402_... instead`);
103
+ out(`holding a credits key? rerun with --credits-key a402_... instead`);
104
104
  } else {
105
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`);
106
106
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent402-openclaw",
3
- "version": "0.5.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 already issued, through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
3
+ "version": "0.5.6",
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": {
package/proxy.js CHANGED
@@ -140,6 +140,12 @@ export function refusalAsAnthropicError(status, doc) {
140
140
  // body, any model id the gateway lists (dated Claude ids resolve to the live one).
141
141
  export const MESSAGES_UPSTREAM_PATH = "/v1/metered/messages";
142
142
 
143
+ // Each client wire's own error shapes: mapRefusal turns an upstream refusal
144
+ // that carries an offer into the wire's error; error(type, message) is an
145
+ // error the proxy makes itself (a refused redirect, an unreachable upstream).
146
+ const OPENAI_WIRE = { mapRefusal: refusalAsOpenAIError, error: (type, message) => ({ error: { message, type } }) };
147
+ const ANTHROPIC_WIRE = { mapRefusal: refusalAsAnthropicError, error: (type, message) => ({ type: "error", error: { type, message } }) };
148
+
143
149
  const json = (res, status, obj, headers = {}) => {
144
150
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
145
151
  res.end(JSON.stringify(obj));
@@ -168,8 +174,8 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
168
174
 
169
175
  // Pays and forwards one call. Streams pass through byte for byte; a refused
170
176
  // paid call's offer-carrying JSON body is answered in the client wire's own
171
- // error shape (mapRefusal); every other upstream body is relayed as sent.
172
- async function forward(req, res, endpoint, outbound, extraHeaders, mapRefusal) {
177
+ // error shape (wire.mapRefusal); every other upstream body is relayed as sent.
178
+ async function forward(req, res, endpoint, outbound, extraHeaders, wire) {
173
179
  const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
174
180
  const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}`, ...extraHeaders };
175
181
  if (key) headers.authorization = `Bearer ${key}`;
@@ -180,7 +186,7 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
180
186
  stats.errors++;
181
187
  const loc = up.headers.get("location");
182
188
  const to = loc ? new URL(loc, `${upstream}${endpoint}`).href : null;
183
- return json(res, 502, { error: { message: `refused to follow a redirect on a paid call: ${upstream}${endpoint} answered HTTP ${up.status || "redirect"}${to ? ` pointing to ${to}` : ""}. Paid requests never follow redirects${to ? `; if that is the address you meant, set the upstream to ${new URL(to).origin}` : ""}.`, type: "upstream_redirect_refused" } });
189
+ return json(res, 502, wire.error("upstream_redirect_refused", `refused to follow a redirect on a paid call: ${upstream}${endpoint} answered HTTP ${up.status || "redirect"}${to ? ` pointing to ${to}` : ""}. Paid requests never follow redirects${to ? `; if that is the address you meant, set the upstream to ${new URL(to).origin}` : ""}.`));
184
190
  }
185
191
  stats.forwarded++;
186
192
  const passthrough = {};
@@ -191,7 +197,7 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
191
197
  const raw = await up.text();
192
198
  let doc = null;
193
199
  try { doc = JSON.parse(raw); } catch { /* not JSON after all: relayed as sent */ }
194
- const mapped = mapRefusal(up.status, doc);
200
+ const mapped = wire.mapRefusal(up.status, doc);
195
201
  if (mapped) {
196
202
  const { "content-type": _ct, ...rest } = passthrough;
197
203
  return json(res, up.status, mapped, rest);
@@ -207,6 +213,9 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
207
213
  return res.end();
208
214
  }
209
215
 
216
+ // forward() is awaited inside the try below, so a payment fetch that throws
217
+ // (an unreachable upstream, a refused signature) answers a 502 in the wire's
218
+ // own shape instead of escaping as an unhandled rejection.
210
219
  const server = createServer(async (req, res) => {
211
220
  stats.requests++;
212
221
  const url = new URL(req.url, "http://localhost");
@@ -234,11 +243,11 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
234
243
  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" } });
235
244
  }
236
245
  if (!paid) {
237
- 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 });
246
+ 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 prepaid credits key.", type: "payment_required", code: "agent402_unconfigured" }, topup: `${upstream}/credits`, priceUsd: route.priceUsd });
238
247
  }
239
248
  const outbound = { ...body };
240
249
  if (requested === AUTO_ID) delete outbound.model; else outbound.model = route.id;
241
- return forward(req, res, route.endpoint, outbound, {}, refusalAsOpenAIError);
250
+ return await forward(req, res, route.endpoint, outbound, {}, OPENAI_WIRE);
242
251
  }
243
252
  if (req.method === "POST" && url.pathname === "/v1/messages") {
244
253
  const raw = await readBody(req);
@@ -248,18 +257,19 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
248
257
  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)" } });
249
258
  }
250
259
  if (!paid) {
251
- 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." } });
260
+ 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 prepaid credits key." } });
252
261
  }
253
262
  const outbound = { ...body, model: body.model.trim().replace(/^agent402\//, "") };
254
263
  const extra = {};
255
264
  for (const h of ["anthropic-version", "anthropic-beta"]) { const v = req.headers[h]; if (typeof v === "string" && v.length <= 512) extra[h] = v; }
256
- return forward(req, res, MESSAGES_UPSTREAM_PATH, outbound, extra, refusalAsAnthropicError);
265
+ return await forward(req, res, MESSAGES_UPSTREAM_PATH, outbound, extra, ANTHROPIC_WIRE);
257
266
  }
258
267
  return json(res, 404, { error: { message: `No route for ${req.method} ${url.pathname}`, type: "invalid_request_error" } });
259
268
  } catch (e) {
260
269
  stats.errors++;
261
270
  log(`[agent402-openclaw] ${req.method} ${url.pathname}: ${e?.message || e}`);
262
- if (!res.headersSent) return json(res, 502, { error: { message: `Upstream error: ${String(e?.message || e).slice(0, 200)}`, type: "upstream_error" } });
271
+ const wire = url.pathname === "/v1/messages" ? ANTHROPIC_WIRE : OPENAI_WIRE;
272
+ if (!res.headersSent) return json(res, 502, wire.error("upstream_error", `Upstream error: ${String(e?.message || e).slice(0, 200)}`));
263
273
  try { res.end(); } catch { /* ignore */ }
264
274
  }
265
275
  });