agent402-openclaw 0.3.1 → 0.4.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
@@ -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
package/cli.js CHANGED
@@ -7,7 +7,7 @@ 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
13
  import { DEFAULT_PORT, PROVIDER_ID } from "./provider.js";
@@ -43,9 +43,27 @@ function mergeInto(target, block, { port = DEFAULT_PORT } = {}) {
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
@@ -11,6 +11,19 @@ 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"),
@@ -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.0",
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.7.1"
30
+ },
31
+ "build": {
32
+ "openclawVersion": "2026.7.1"
33
+ },
34
+ "install": {
35
+ "minHostVersion": ">=2026.7.1",
36
+ "npmSpec": "agent402-openclaw"
37
+ }
28
38
  },
29
39
  "engines": {
30
40
  "node": ">=18"