agent402-openclaw 0.3.1 → 0.4.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 CHANGED
@@ -11,12 +11,21 @@ Guide: https://agent402.tools/guides/openclaw-model-provider
11
11
 
12
12
  ```bash
13
13
  openclaw plugins install agent402-openclaw
14
- AGENT402_CREDITS_KEY=a402_... npx agent402-openclaw setup --write # key by env (or `--credits-key -` on stdin); buy one by card at https://agent402.tools/credits
14
+ npx agent402-openclaw setup --write # no key? it mints a wallet and prints the address to fund
15
15
  openclaw gateway restart
16
16
  ```
17
17
 
18
+ With no credits key and no `AGENT402_WALLET_KEY`, `setup` generates an EVM
19
+ wallet into `~/.openclaw/agent402/wallet.key` (0600, the only copy, never
20
+ printed) and prints its address: send it USDC on Base and every call is paid
21
+ from it over x402. `agent402-openclaw wallet` shows the address and balance;
22
+ `--no-wallet` skips the generation. Prefer a card? Buy a pack at
23
+ https://agent402.tools/credits and run
24
+ `AGENT402_CREDITS_KEY=a402_... npx agent402-openclaw setup --write` (key by env,
25
+ or `--credits-key -` on stdin).
26
+
18
27
  (`openclaw plugins install` copies the plugin into `~/.openclaw/extensions` and
19
- does not link its CLI, hence `npx`.) `setup` stores the key under
28
+ does not link its CLI, hence `npx`.) `setup` stores a credits key under
20
29
  `~/.openclaw/agent402/credits.key` (0600) and prints the `openclaw.json` block;
21
30
  add `--write` to merge it in. The primary model it writes is the cheapest
22
31
  metered model that can hold OpenClaw's own prompt: OpenClaw sends roughly 70k
@@ -77,6 +86,7 @@ the key.
77
86
  - `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--write] [--port N] [--flat]`
78
87
  - `agent402-openclaw proxy [--port N] [--upstream URL]`
79
88
  - `agent402-openclaw doctor`
89
+ - `agent402-openclaw wallet [--rpc URL]` (address + USDC balance of the wallet the proxy pays from)
80
90
  - `agent402-openclaw permit2-approve [--rpc URL]` (one-time USDC approval so the wallet pays actual usage over upto)
81
91
 
82
92
  ## What else the same key buys
@@ -84,7 +94,7 @@ the key.
84
94
  The key or wallet that pays for chat also pays for the rest of the gateway and
85
95
  the catalog, with one 402 shape and one receipt shape: OpenAI Responses and
86
96
  Anthropic Messages wires, embeddings, rerank, images, video, speech,
87
- transcription, grounded answers with citations, 500+ deterministic tools over
97
+ transcription, grounded answers with citations, 500+ tools over
88
98
  MCP or HTTP, wallet-keyed memory, finished reports (dossiers, insider flow, 13F,
89
99
  domain audits, token risk, deep research) and monitors, plus routing that buys
90
100
  from proven external sellers on the agent's behalf.
package/cli.js CHANGED
@@ -7,10 +7,10 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, statSync
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
- import { STATE_DIR, CREDITS_KEY_FILE, resolveCreditsKey, resolvePayFetch, USDC_BASE, DEFAULT_BASE_RPC, uptoReady } from "./index.js";
10
+ import { STATE_DIR, CREDITS_KEY_FILE, WALLET_KEY_FILE, resolveCreditsKey, resolveWalletKey, resolvePayFetch, USDC_BASE, DEFAULT_BASE_RPC, uptoReady } from "./index.js";
11
11
  import { startProxy, loadRoutes, DEFAULT_UPSTREAM } from "./proxy.js";
12
12
  import { providerModelsConfig, stripTrailingSlashes, defaultPrimary, AUTO_ID, OPENCLAW_MIN_INPUT_CHARS } from "./models.js";
13
- import { DEFAULT_PORT, PROVIDER_ID } from "./provider.js";
13
+ import { DEFAULT_PORT, PLUGIN_ID, PROVIDER_ID } from "./provider.js";
14
14
 
15
15
  const args = process.argv.slice(2);
16
16
  const cmd = args[0] || "help";
@@ -38,14 +38,32 @@ function mergeInto(target, block, { port = DEFAULT_PORT } = {}) {
38
38
  // on 8412 and OpenClaw dialing a port nobody listened on (real-install test).
39
39
  if (port !== DEFAULT_PORT) {
40
40
  t.plugins = t.plugins || {}; t.plugins.entries = t.plugins.entries || {};
41
- t.plugins.entries[PROVIDER_ID] = { ...(t.plugins.entries[PROVIDER_ID] || {}), config: { ...(t.plugins.entries[PROVIDER_ID]?.config || {}), port } };
41
+ t.plugins.entries[PLUGIN_ID] = { ...(t.plugins.entries[PLUGIN_ID] || {}), config: { ...(t.plugins.entries[PLUGIN_ID]?.config || {}), port } };
42
42
  }
43
43
  return t;
44
44
  }
45
45
 
46
+ // Mint a fresh EVM wallet into WALLET_KEY_FILE (0600). Needs viem (an optional
47
+ // peer, same as x402 payment itself); returns null when it is not installed so
48
+ // setup can fall back to the card path instead of failing.
49
+ async function generateWallet() {
50
+ try {
51
+ const [{ generatePrivateKey, privateKeyToAccount }] = await Promise.all([import("viem/accounts")]);
52
+ const pk = generatePrivateKey();
53
+ const account = privateKeyToAccount(pk);
54
+ mkdirSync(STATE_DIR(), { recursive: true });
55
+ writeFileSync(WALLET_KEY_FILE(), pk + "\n", { mode: 0o600, flag: "wx" }); // never overwrite an existing key
56
+ try { chmodSync(WALLET_KEY_FILE(), 0o600); } catch { /* best effort */ }
57
+ return { address: account.address };
58
+ } catch (e) {
59
+ if (e?.code === "EEXIST") { try { return { address: (await import("viem/accounts")).privateKeyToAccount(readFileSync(WALLET_KEY_FILE(), "utf8").trim()).address }; } catch { return null; } }
60
+ return null;
61
+ }
62
+ }
63
+
46
64
  async function main() {
47
65
  if (cmd === "help" || has("--help")) {
48
- out("agent402-openclaw setup [--credits-key a402_...|-] [--write] [--flat] | proxy [--port N] [--upstream URL] [--flat] | doctor | permit2-approve [--rpc URL]");
66
+ out("agent402-openclaw setup [--credits-key a402_...|-] [--no-wallet] [--write] [--flat] | proxy [--port N] [--upstream URL] [--flat] | wallet [--rpc URL] | doctor | permit2-approve [--rpc URL]");
49
67
  return 0;
50
68
  }
51
69
  const upstream = stripTrailingSlashes(opt("--upstream") || process.env.AGENT402_UPSTREAM || DEFAULT_UPSTREAM);
@@ -66,8 +84,26 @@ async function main() {
66
84
  writeFileSync(CREDITS_KEY_FILE(), key + "\n", { mode: 0o600 });
67
85
  try { chmodSync(CREDITS_KEY_FILE(), 0o600); } catch { /* best effort */ }
68
86
  out(`stored credits key at ${CREDITS_KEY_FILE()} (0600)`);
69
- } else if (!resolveCreditsKey()) {
70
- out(`no credits key yet: buy a pack by card at ${upstream}/credits, then rerun with --credits-key a402_...`);
87
+ } else if (!resolveCreditsKey() && !resolveWalletKey()) {
88
+ // No payment method at all: mint a wallet right here, print the address,
89
+ // and the user funds it with USDC on Base. Same first-run shape as a
90
+ // per-token router that prints a wallet at install; the card path
91
+ // (credits key) stays available. --no-wallet keeps the old behaviour.
92
+ 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
+ } else {
95
+ const w = await generateWallet();
96
+ if (w) {
97
+ out(`no credits key found, so a wallet was generated for you:`);
98
+ out(` address: ${w.address}`);
99
+ out(` key: ${WALLET_KEY_FILE()} (0600; back it up, it is the only copy)`);
100
+ 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
+ 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
+ } 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
+ }
106
+ }
71
107
  }
72
108
  let routes;
73
109
  try { routes = await loadRoutes(upstream, fetch, { pricing }); } catch (e) { console.error(`could not read ${upstream}/v1/models: ${e?.message || e}`); return 2; }
@@ -111,8 +147,8 @@ async function main() {
111
147
  // One-time USDC -> Permit2 approval on Base so the wallet can pay ACTUAL
112
148
  // usage over `upto` instead of the per-request quote over `exact`.
113
149
  if (cmd === "permit2-approve") {
114
- const pk = (process.env.AGENT402_WALLET_KEY || "").trim();
115
- if (!/^0x[0-9a-fA-F]{64}$/.test(pk)) { console.error("AGENT402_WALLET_KEY (0x + 64 hex) is required"); return 2; }
150
+ const pk = resolveWalletKey();
151
+ if (!pk) { console.error("no wallet: set AGENT402_WALLET_KEY (0x + 64 hex) or run `agent402-openclaw setup` to generate one"); return 2; }
116
152
  try {
117
153
  const [{ createPermit2ApprovalTx, getPermit2AllowanceReadParams }, { createWalletClient, createPublicClient, http }, { privateKeyToAccount }, { base }] = await Promise.all([import("@x402/evm/upto/client"), import("viem"), import("viem/accounts"), import("viem/chains")]);
118
154
  const account = privateKeyToAccount(pk);
@@ -130,15 +166,32 @@ async function main() {
130
166
  } catch (e) { console.error(`permit2-approve failed: ${String(e?.message || e).slice(0, 300)}`); return 1; }
131
167
  }
132
168
 
169
+ // Address + USDC balance of the wallet the proxy pays from. Read-only.
170
+ if (cmd === "wallet") {
171
+ const pk = resolveWalletKey();
172
+ if (!pk) { console.error("no wallet: run `agent402-openclaw setup` to generate one, or set AGENT402_WALLET_KEY"); return 2; }
173
+ try {
174
+ const [{ createPublicClient, http, erc20Abi, formatUnits }, { privateKeyToAccount }, { base }] = await Promise.all([import("viem"), import("viem/accounts"), import("viem/chains")]);
175
+ const account = privateKeyToAccount(pk);
176
+ const pub = createPublicClient({ chain: base, transport: http((opt("--rpc") || process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim()) });
177
+ const bal = await pub.readContract({ address: USDC_BASE, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
178
+ out(`address: ${account.address}`);
179
+ out(`USDC on Base: $${formatUnits(bal, 6)}`);
180
+ if (bal === 0n) out(`fund it: send USDC on Base (eip155:8453) to ${account.address}`);
181
+ return 0;
182
+ } catch (e) { console.error(`wallet: ${String(e?.message || e).slice(0, 200)}`); return 1; }
183
+ }
184
+
133
185
  if (cmd === "doctor") {
134
186
  const key = resolveCreditsKey();
187
+ const walletPk = resolveWalletKey();
135
188
  out(`upstream: ${upstream}`);
136
189
  out(`credits key: ${key ? `present (${key.slice(0, 8)}…)` : "none"}`);
137
- out(`wallet key: ${/^0x[0-9a-fA-F]{64}$/.test(process.env.AGENT402_WALLET_KEY || "") ? "present" : "none"}`);
138
- if (/^0x[0-9a-fA-F]{64}$/.test(process.env.AGENT402_WALLET_KEY || "")) {
190
+ out(`wallet key: ${walletPk ? "present" : "none"}`);
191
+ if (walletPk) {
139
192
  try {
140
193
  const [{ getPermit2AllowanceReadParams }, { createPublicClient, http }, { privateKeyToAccount }, { base }] = await Promise.all([import("@x402/evm/upto/client"), import("viem"), import("viem/accounts"), import("viem/chains")]);
141
- const account = privateKeyToAccount(process.env.AGENT402_WALLET_KEY.trim());
194
+ const account = privateKeyToAccount(walletPk);
142
195
  const pub = createPublicClient({ chain: base, transport: http((process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim()) });
143
196
  const allowance = await pub.readContract(getPermit2AllowanceReadParams({ tokenAddress: USDC_BASE, ownerAddress: account.address }));
144
197
  out(`wallet ${account.address}: ${uptoReady(allowance) ? "pays actual usage (upto; Permit2 approved)" : "pays the per-request quote (exact) - run permit2-approve once to pay actual usage"}`);
package/index.js CHANGED
@@ -5,12 +5,25 @@
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
- import { buildProvider, setActiveProxy, setCachedRoutes, DEFAULT_PORT, PROVIDER_ID } from "./provider.js";
8
+ import { buildProvider, setActiveProxy, setCachedRoutes, DEFAULT_PORT, PLUGIN_ID } from "./provider.js";
9
9
  import { startProxy, loadRoutes, DEFAULT_UPSTREAM } from "./proxy.js";
10
10
  import { stripTrailingSlashes } from "./models.js";
11
11
 
12
12
  export const STATE_DIR = () => join(process.env.AGENT402_OPENCLAW_HOME || join(homedir(), ".openclaw"), "agent402");
13
13
  export const CREDITS_KEY_FILE = () => join(STATE_DIR(), "credits.key");
14
+ export const WALLET_KEY_FILE = () => join(STATE_DIR(), "wallet.key");
15
+
16
+ /** Wallet key precedence: plugin config > env > ~/.openclaw/agent402/wallet.key
17
+ * (the wallet `setup` generates when no payment method exists yet). Only a
18
+ * well-formed 0x + 64 hex key counts; anything else is "no wallet". */
19
+ export function resolveWalletKey(pluginConfig = {}) {
20
+ const cands = [
21
+ typeof pluginConfig.walletKey === "string" ? pluginConfig.walletKey.trim() : "",
22
+ (process.env.AGENT402_WALLET_KEY || "").trim(),
23
+ ];
24
+ try { if (existsSync(WALLET_KEY_FILE())) cands.push(readFileSync(WALLET_KEY_FILE(), "utf8").trim()); } catch { /* unreadable */ }
25
+ return cands.find((k) => /^0x[0-9a-fA-F]{64}$/.test(k)) || null;
26
+ }
14
27
 
15
28
  /** Credits key precedence: plugin config > env > ~/.openclaw/agent402/credits.key. */
16
29
  export function resolveCreditsKey(pluginConfig = {}) {
@@ -42,8 +55,8 @@ export function selectAccept(accepts, { preferUpto = true } = {}) {
42
55
  export const uptoReady = (allowance) => { try { return BigInt(allowance ?? 0) >= PERMIT2_READY_MIN; } catch { return false; } };
43
56
 
44
57
  export async function resolvePayFetch(pluginConfig = {}, log = () => {}) {
45
- const pk = (pluginConfig.walletKey || process.env.AGENT402_WALLET_KEY || "").trim();
46
- if (!/^0x[0-9a-fA-F]{64}$/.test(pk)) return null;
58
+ const pk = resolveWalletKey(pluginConfig);
59
+ if (!pk) return null;
47
60
  try {
48
61
  const [{ wrapFetchWithPayment, x402Client }, { privateKeyToAccount }, { toClientEvmSigner }, { registerExactEvmScheme }] = await Promise.all([
49
62
  import("@x402/fetch"), import("viem/accounts"), import("@x402/evm"), import("@x402/evm/exact/client"),
@@ -81,7 +94,7 @@ let started = null;
81
94
  let registeredOnce = false;
82
95
 
83
96
  const plugin = {
84
- id: PROVIDER_ID,
97
+ id: PLUGIN_ID,
85
98
  name: "Agent402",
86
99
  description: "Agent402 model provider: routed + explicit models at a flat per-call price, paid by card or USDC.",
87
100
  register(api) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "agent402",
2
+ "id": "agent402-openclaw",
3
3
  "name": "Agent402",
4
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.",
5
5
  "activation": {
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "walletKey": {
21
21
  "type": "string",
22
- "description": "Optional EVM private key for x402 USDC payment (needs @x402/fetch + @x402/evm + viem installed)."
22
+ "description": "Optional EVM private key for x402 USDC payment (needs @x402/fetch + @x402/evm + viem installed). Precedence: this, then AGENT402_WALLET_KEY, then the wallet `setup` generates at ~/.openclaw/agent402/wallet.key."
23
23
  },
24
24
  "port": {
25
25
  "type": "number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-openclaw",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -24,7 +24,17 @@
24
24
  "openclaw": {
25
25
  "extensions": [
26
26
  "./index.js"
27
- ]
27
+ ],
28
+ "compat": {
29
+ "pluginApi": ">=2026.8.1"
30
+ },
31
+ "build": {
32
+ "openclawVersion": "2026.8.1"
33
+ },
34
+ "install": {
35
+ "minHostVersion": ">=2026.8.1",
36
+ "npmSpec": "agent402-openclaw"
37
+ }
28
38
  },
29
39
  "engines": {
30
40
  "node": ">=18"
package/provider.js CHANGED
@@ -4,7 +4,17 @@
4
4
  // would 402 without the proxy's payment step in between).
5
5
  import { providerModelsConfig, routesFromCatalog } from "./models.js";
6
6
 
7
+ // The MODEL PROVIDER id: what a user writes in openclaw.json ("agent402/auto").
8
+ // User-facing and stable - renaming it would break every existing config.
7
9
  export const PROVIDER_ID = "agent402";
10
+ // The PLUGIN id, which is a different thing: OpenClaw keys plugins.entries by
11
+ // the MANIFEST id, and since 2026.8.1 it requires that id to equal the npm
12
+ // package name ("Plugin manifest id ... differs from npm package name ...").
13
+ // They were both "agent402", so nothing distinguished them until the host
14
+ // started checking. Writing config under the provider id while the host reads
15
+ // it under the manifest id is the same defect as the 2026-08-26 --port bug,
16
+ // where setup moved the baseUrl and the service kept reading the old key.
17
+ export const PLUGIN_ID = "agent402-openclaw";
8
18
  export const DEFAULT_PORT = 8412;
9
19
 
10
20
  let activeProxy = null;