x402-proxy 0.3.1 → 0.3.2

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
@@ -10,14 +10,7 @@ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
10
10
 
11
11
  That's it. The endpoint returns 402, x402-proxy pays and streams the response.
12
12
 
13
- First time? Set up a wallet:
14
-
15
- ```bash
16
- npx x402-proxy setup # generate wallet from BIP-39 mnemonic
17
- npx x402-proxy wallet fund # see where to send USDC
18
- ```
19
-
20
- One mnemonic derives both EVM (Base) and Solana keypairs. Fund either chain and go.
13
+ No wallet? It'll walk you through setup automatically. One mnemonic derives both EVM (Base) and Solana keypairs. Fund either chain and go.
21
14
 
22
15
  ## MCP Proxy
23
16
 
@@ -45,29 +38,28 @@ Works like curl. Response body streams to stdout, payment info goes to stderr.
45
38
 
46
39
  ```bash
47
40
  # GET request
48
- x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
41
+ $ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
49
42
 
50
43
  # POST with body and headers
51
- x402-proxy --method POST \
44
+ $ npx x402-proxy --method POST \
52
45
  --header "Content-Type: application/json" \
53
46
  --body '{"url":"https://x402.org"}' \
54
47
  https://web.surf.cascade.fyi/v1/crawl
55
48
 
56
49
  # Pipe-safe
57
- x402-proxy https://api.example.com/data | jq '.results'
50
+ $ npx x402-proxy https://api.example.com/data | jq '.results'
58
51
  ```
59
52
 
60
53
  ## Commands
61
54
 
62
55
  ```bash
63
- x402-proxy <url> # paid HTTP request (default command)
64
- x402-proxy mcp <url> # MCP stdio proxy for agents
65
- x402-proxy setup # onboarding wizard
66
- x402-proxy status # config + wallet + spend summary
67
- x402-proxy wallet # show addresses
68
- x402-proxy wallet history # payment history
69
- x402-proxy wallet fund # funding instructions
70
- x402-proxy wallet export-key <target> # bare key/mnemonic to stdout (evm|solana|mnemonic)
56
+ $ npx x402-proxy <url> # paid HTTP request (default command)
57
+ $ npx x402-proxy mcp <url> # MCP stdio proxy for agents
58
+ $ npx x402-proxy setup # onboarding wizard
59
+ $ npx x402-proxy status # config + wallet + spend summary
60
+ $ npx x402-proxy wallet # show addresses and balances
61
+ $ npx x402-proxy wallet history # payment history
62
+ $ npx x402-proxy wallet export-key <target> # bare key/mnemonic to stdout (evm|solana|mnemonic)
71
63
  ```
72
64
 
73
65
  All commands support `--help` for details.
@@ -84,8 +76,8 @@ Config stored at `$XDG_CONFIG_HOME/x402-proxy/` (default `~/.config/x402-proxy/`
84
76
 
85
77
  ```bash
86
78
  # Pipe-safe - outputs bare key/mnemonic to stdout
87
- MY_KEY=$(npx x402-proxy wallet export-key evm)
88
- MY_MNEMONIC=$(npx x402-proxy wallet export-key mnemonic)
79
+ $ MY_KEY=$(npx x402-proxy wallet export-key evm)
80
+ $ MY_MNEMONIC=$(npx x402-proxy wallet export-key mnemonic)
89
81
  ```
90
82
 
91
83
  ## Env Vars
package/dist/bin/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { C as appendHistory, E as readHistory, S as saveWalletFile, T as formatTxLine, _ as getWalletPath, a as resolveWallet, b as loadWalletFile, c as generateMnemonic, d as info, f as isTTY, g as getHistoryPath, h as getConfigDirShort, i as buildX402Client, l as dim, m as ensureConfigDir, n as statusCommand, o as deriveEvmKeypair, p as warn, r as walletInfoCommand, s as deriveSolanaKeypair, u as error, v as isConfigured, w as calcSpend, x as saveConfig, y as loadConfig } from "../status-BAzavcCj.js";
2
+ import { a as resolveWallet, c as info, d as appendHistory, f as calcSpend, i as buildX402Client, l as isTTY, m as readHistory, n as statusCommand, o as dim, p as formatTxLine, r as walletInfoCommand, s as error, u as warn } from "../status-Cp51UDnh.js";
3
+ import { c as isConfigured, i as ensureConfigDir, l as loadConfig, o as getHistoryPath, u as loadWalletFile } from "../derive-Rikypf5B.js";
4
+ import { n as setupCommand } from "../setup-CvB0go56.js";
3
5
  import { buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
4
6
  import pc from "picocolors";
5
7
  import { decodePaymentResponseHeader, wrapFetchWithPayment } from "@x402/fetch";
@@ -109,7 +111,7 @@ Examples:
109
111
  async func(flags, url) {
110
112
  if (!url) {
111
113
  if (isConfigured()) {
112
- const { displayStatus } = await import("../status-CeXiUR6L.js");
114
+ const { displayStatus } = await import("../status-4nQP3Fpu.js");
113
115
  await displayStatus();
114
116
  console.log();
115
117
  console.log(pc.dim(" Commands:"));
@@ -118,7 +120,6 @@ Examples:
118
120
  console.log(` ${pc.cyan("$ npx x402-proxy setup")} Reconfigure wallet`);
119
121
  console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
120
122
  console.log(` ${pc.cyan("$ npx x402-proxy wallet history")} Full payment history`);
121
- console.log(` ${pc.cyan("$ npx x402-proxy wallet fund")} Funding instructions`);
122
123
  console.log();
123
124
  console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi"));
124
125
  console.log();
@@ -135,7 +136,6 @@ Examples:
135
136
  console.log(` ${pc.cyan("$ npx x402-proxy mcp <url>")} MCP proxy for AI agents`);
136
137
  console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
137
138
  console.log(` ${pc.cyan("$ npx x402-proxy wallet history")} Payment history`);
138
- console.log(` ${pc.cyan("$ npx x402-proxy wallet fund")} Funding instructions`);
139
139
  console.log(` ${pc.cyan("$ npx x402-proxy --help")} All options`);
140
140
  console.log();
141
141
  console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy setup"));
@@ -152,14 +152,22 @@ Examples:
152
152
  error(`Invalid URL: ${url}`);
153
153
  process.exit(1);
154
154
  }
155
- const wallet = resolveWallet({
155
+ let wallet = resolveWallet({
156
156
  evmKey: flags.evmKey,
157
157
  solanaKey: flags.solanaKey
158
158
  });
159
159
  if (wallet.source === "none") {
160
- error("No wallet configured.");
161
- console.error(pc.dim(`Run ${pc.cyan("x402-proxy setup")} or set X402_PROXY_WALLET_MNEMONIC`));
162
- process.exit(1);
160
+ if (!isTTY()) {
161
+ error("No wallet configured.");
162
+ console.error(pc.dim(`Run:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set X402_PROXY_WALLET_MNEMONIC`));
163
+ process.exit(1);
164
+ }
165
+ dim(" No wallet found. Let's set one up first.\n");
166
+ const { runSetup } = await import("../setup-DtPaKQ37.js");
167
+ await runSetup();
168
+ console.log();
169
+ wallet = resolveWallet();
170
+ if (wallet.source === "none") return;
163
171
  }
164
172
  const config = loadConfig();
165
173
  const { x402Fetch, shiftPayment } = createX402ProxyHandler({ client: await buildX402Client(wallet, {
@@ -190,8 +198,39 @@ Examples:
190
198
  const elapsedMs = Date.now() - startMs;
191
199
  const payment = shiftPayment();
192
200
  const txSig = extractTxSignature(response);
201
+ if (response.status === 402 && isTTY()) {
202
+ const prHeader = response.headers.get("PAYMENT-REQUIRED") ?? response.headers.get("X-PAYMENT-REQUIRED");
203
+ let accepts = [];
204
+ if (prHeader) try {
205
+ accepts = JSON.parse(Buffer.from(prHeader, "base64").toString()).accepts ?? [];
206
+ } catch {}
207
+ if (accepts.length > 0) {
208
+ const cheapest = accepts.reduce((min, a) => Number(a.amount) < Number(min.amount) ? a : min);
209
+ error(`Payment required: ${(Number(cheapest.amount) / 1e6).toFixed(4)} USDC`);
210
+ } else error("Payment required");
211
+ const hasEvm = accepts.some((a) => a.network.startsWith("eip155:"));
212
+ const hasSolana = accepts.some((a) => a.network.startsWith("solana:"));
213
+ const hasOther = accepts.some((a) => !a.network.startsWith("eip155:") && !a.network.startsWith("solana:"));
214
+ if (hasEvm || hasSolana) {
215
+ console.error();
216
+ dim(" Fund your wallet with USDC:");
217
+ if (hasEvm && wallet.evmAddress) console.error(` Base: ${pc.cyan(wallet.evmAddress)}`);
218
+ if (hasSolana && wallet.solanaAddress) console.error(` Solana: ${pc.cyan(wallet.solanaAddress)}`);
219
+ if (hasEvm && !wallet.evmAddress) dim(" Base: endpoint accepts EVM but no EVM wallet configured");
220
+ if (hasSolana && !wallet.solanaAddress) dim(" Solana: endpoint accepts Solana but no Solana wallet configured");
221
+ } else if (hasOther) {
222
+ const networks = [...new Set(accepts.map((a) => a.network))].join(", ");
223
+ console.error();
224
+ error(`This endpoint only accepts payment on unsupported networks: ${networks}`);
225
+ }
226
+ console.error();
227
+ dim(" Then re-run:");
228
+ console.error(` ${pc.cyan(`$ npx x402-proxy ${url}`)}`);
229
+ console.error();
230
+ return;
231
+ }
193
232
  if (payment && isTTY()) {
194
- info(` Payment: ${payment.amount ?? "?"} (${payment.network ?? "unknown"})`);
233
+ info(` Payment: ${payment.amount ? (Number(payment.amount) / 1e6).toFixed(4) : "?"} USDC (${payment.network ?? "unknown"})`);
195
234
  if (txSig) dim(` Tx: ${txSig}`);
196
235
  }
197
236
  if (isTTY()) dim(` ${response.status} ${response.statusText} (${elapsedMs}ms)`);
@@ -269,7 +308,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
269
308
  solanaKey: flags.solanaKey
270
309
  });
271
310
  if (wallet.source === "none") {
272
- error("No wallet configured. Set X402_PROXY_WALLET_MNEMONIC or run x402-proxy setup.");
311
+ error("No wallet configured.\nRun:\n $ npx x402-proxy setup\n\nOr set X402_PROXY_WALLET_MNEMONIC");
273
312
  process.exit(1);
274
313
  }
275
314
  warn("Note: MCP proxy is alpha - please report issues.");
@@ -290,7 +329,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
290
329
  const { x402MCPClient } = await import("@x402/mcp");
291
330
  const x402Mcp = new x402MCPClient(new Client({
292
331
  name: "x402-proxy",
293
- version: "0.3.0"
332
+ version: "0.3.2"
294
333
  }), x402PaymentClient, {
295
334
  autoPayment: true,
296
335
  onPaymentRequested: (ctx) => {
@@ -335,7 +374,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
335
374
  dim(` ${tools.length} tools available`);
336
375
  const localServer = new McpServer({
337
376
  name: "x402-proxy",
338
- version: "0.3.0"
377
+ version: "0.3.2"
339
378
  });
340
379
  for (const tool of tools) localServer.tool(tool.name, tool.description ?? "", tool.inputSchema?.properties ? Object.fromEntries(Object.entries(tool.inputSchema.properties).map(([k, v]) => [k, v])) : {}, async (args) => {
341
380
  const result = await x402Mcp.callTool(tool.name, args);
@@ -366,84 +405,6 @@ Add to your MCP client config (Claude, Cursor, etc.):
366
405
  }
367
406
  });
368
407
 
369
- //#endregion
370
- //#region src/commands/setup.ts
371
- const setupCommand = buildCommand({
372
- docs: { brief: "Set up x402-proxy with a new wallet" },
373
- parameters: {
374
- flags: { force: {
375
- kind: "boolean",
376
- brief: "Overwrite existing configuration",
377
- default: false
378
- } },
379
- positional: {
380
- kind: "tuple",
381
- parameters: []
382
- }
383
- },
384
- async func(flags) {
385
- if (isConfigured() && !flags.force) {
386
- prompts.log.warn(`Already configured. Wallet at ${pc.dim(getWalletPath())}\nUse ${pc.cyan("x402-proxy setup --force")} to reconfigure.`);
387
- return;
388
- }
389
- prompts.intro(pc.cyan("x402-proxy setup"));
390
- prompts.log.info("This will generate a single BIP-39 mnemonic that derives wallets for both Solana and EVM chains.");
391
- const action = await prompts.select({
392
- message: "How would you like to set up your wallet?",
393
- options: [{
394
- value: "generate",
395
- label: "Generate a new mnemonic"
396
- }, {
397
- value: "import",
398
- label: "Import an existing mnemonic"
399
- }]
400
- });
401
- if (prompts.isCancel(action)) {
402
- prompts.cancel("Setup cancelled.");
403
- process.exit(0);
404
- }
405
- let mnemonic;
406
- if (action === "generate") {
407
- mnemonic = generateMnemonic();
408
- prompts.log.warn("Write down your mnemonic and store it safely. It will NOT be shown again.");
409
- prompts.log.message(pc.bold(mnemonic));
410
- } else {
411
- const input = await prompts.text({
412
- message: "Enter your 24-word mnemonic:",
413
- validate: (v = "") => {
414
- const words = v.trim().split(/\s+/);
415
- if (words.length !== 12 && words.length !== 24) return "Mnemonic must be 12 or 24 words";
416
- }
417
- });
418
- if (prompts.isCancel(input)) {
419
- prompts.cancel("Setup cancelled.");
420
- process.exit(0);
421
- }
422
- mnemonic = input.trim();
423
- }
424
- const evm = deriveEvmKeypair(mnemonic);
425
- const sol = deriveSolanaKeypair(mnemonic);
426
- prompts.log.success(`Base address: ${pc.green(evm.address)}`);
427
- prompts.log.success(`Solana address: ${pc.green(sol.address)}`);
428
- saveWalletFile({
429
- version: 1,
430
- mnemonic,
431
- addresses: {
432
- evm: evm.address,
433
- solana: sol.address
434
- }
435
- });
436
- saveConfig({});
437
- prompts.log.info(`Config directory: ${pc.dim(getConfigDirShort())}`);
438
- prompts.log.step("Fund your wallets to start using x402 resources:");
439
- prompts.log.message(` Solana (USDC): Send USDC to ${pc.cyan(sol.address)}`);
440
- prompts.log.message(` Base (USDC): Send USDC to ${pc.cyan(evm.address)}`);
441
- prompts.log.step("Try your first request:");
442
- prompts.log.message(` ${pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi")}`);
443
- prompts.outro(pc.green("Setup complete!"));
444
- }
445
- });
446
-
447
408
  //#endregion
448
409
  //#region src/commands/wallet-export.ts
449
410
  const walletExportCommand = buildCommand({
@@ -498,44 +459,6 @@ const walletExportCommand = buildCommand({
498
459
  }
499
460
  });
500
461
 
501
- //#endregion
502
- //#region src/commands/wallet-fund.ts
503
- const walletFundCommand = buildCommand({
504
- docs: { brief: "Show wallet funding instructions" },
505
- parameters: {
506
- flags: {},
507
- positional: {
508
- kind: "tuple",
509
- parameters: []
510
- }
511
- },
512
- func() {
513
- const wallet = resolveWallet();
514
- if (wallet.source === "none") {
515
- console.log(pc.yellow("No wallet configured."));
516
- console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
517
- process.exit(1);
518
- }
519
- console.log();
520
- info("Funding Instructions");
521
- console.log();
522
- if (wallet.solanaAddress) {
523
- console.log(pc.bold(" Solana (USDC):"));
524
- console.log(` Send USDC to: ${pc.green(wallet.solanaAddress)}`);
525
- console.log(pc.dim(" Network: Solana Mainnet"));
526
- console.log();
527
- }
528
- if (wallet.evmAddress) {
529
- console.log(pc.bold(" Base (USDC):"));
530
- console.log(` Send USDC to: ${pc.green(wallet.evmAddress)}`);
531
- console.log(pc.dim(" Network: Base (Chain ID 8453)"));
532
- console.log();
533
- }
534
- console.log(pc.dim(" Tip: Most x402 services accept USDC on Base or Solana."));
535
- console.log();
536
- }
537
- });
538
-
539
462
  //#endregion
540
463
  //#region src/commands/wallet-history.ts
541
464
  const walletHistoryCommand = buildCommand({
@@ -595,7 +518,6 @@ const routes = buildRouteMap({
595
518
  routes: {
596
519
  info: walletInfoCommand,
597
520
  history: walletHistoryCommand,
598
- fund: walletFundCommand,
599
521
  "export-key": walletExportCommand
600
522
  },
601
523
  defaultCommand: "info",
@@ -609,7 +531,7 @@ const routes = buildRouteMap({
609
531
  });
610
532
  const app = buildApplication(routes, {
611
533
  name: "x402-proxy",
612
- versionInfo: { currentVersion: "0.3.1" },
534
+ versionInfo: { currentVersion: "0.3.2" },
613
535
  scanner: { caseStyle: "allow-kebab-for-camel" }
614
536
  });
615
537
 
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { parse, stringify } from "yaml";
6
+ import { base58 } from "@scure/base";
7
+ import { ed25519 } from "@noble/curves/ed25519.js";
8
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
9
+ import { hmac } from "@noble/hashes/hmac.js";
10
+ import { sha512 } from "@noble/hashes/sha2.js";
11
+ import { keccak_256 } from "@noble/hashes/sha3.js";
12
+ import { HDKey } from "@scure/bip32";
13
+ import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
14
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
15
+
16
+ //#region src/lib/config.ts
17
+ const APP_NAME = "x402-proxy";
18
+ function getConfigDir() {
19
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
20
+ return path.join(xdg, APP_NAME);
21
+ }
22
+ /** Config dir with $HOME replaced by ~ for display */
23
+ function getConfigDirShort() {
24
+ const dir = getConfigDir();
25
+ const home = os.homedir();
26
+ return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
27
+ }
28
+ function getWalletPath() {
29
+ return path.join(getConfigDir(), "wallet.json");
30
+ }
31
+ function getHistoryPath() {
32
+ return path.join(getConfigDir(), "history.jsonl");
33
+ }
34
+ function ensureConfigDir() {
35
+ fs.mkdirSync(getConfigDir(), { recursive: true });
36
+ }
37
+ function loadWalletFile() {
38
+ const p = getWalletPath();
39
+ if (!fs.existsSync(p)) return null;
40
+ try {
41
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
42
+ if (data.version === 1 && typeof data.mnemonic === "string") return data;
43
+ return null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function saveWalletFile(wallet) {
49
+ ensureConfigDir();
50
+ fs.writeFileSync(getWalletPath(), JSON.stringify(wallet, null, 2), {
51
+ mode: 384,
52
+ encoding: "utf-8"
53
+ });
54
+ }
55
+ function loadConfig() {
56
+ const dir = getConfigDir();
57
+ for (const name of [
58
+ "config.yaml",
59
+ "config.yml",
60
+ "config.jsonc",
61
+ "config.json"
62
+ ]) {
63
+ const p = path.join(dir, name);
64
+ if (!fs.existsSync(p)) continue;
65
+ try {
66
+ const raw = fs.readFileSync(p, "utf-8");
67
+ if (name.endsWith(".yaml") || name.endsWith(".yml")) return parse(raw);
68
+ if (name.endsWith(".jsonc")) {
69
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
70
+ return JSON.parse(stripped);
71
+ }
72
+ return JSON.parse(raw);
73
+ } catch {}
74
+ }
75
+ return null;
76
+ }
77
+ function saveConfig(config) {
78
+ ensureConfigDir();
79
+ const p = path.join(getConfigDir(), "config.yaml");
80
+ fs.writeFileSync(p, stringify(config), "utf-8");
81
+ }
82
+ function isConfigured() {
83
+ if (process.env.X402_PROXY_WALLET_MNEMONIC) return true;
84
+ if (process.env.X402_PROXY_WALLET_EVM_KEY) return true;
85
+ if (process.env.X402_PROXY_WALLET_SOLANA_KEY) return true;
86
+ return loadWalletFile() !== null;
87
+ }
88
+
89
+ //#endregion
90
+ //#region src/lib/derive.ts
91
+ /**
92
+ * Wallet derivation from BIP-39 mnemonic.
93
+ *
94
+ * A single 24-word mnemonic is the root secret. Both Solana and EVM keypairs
95
+ * are deterministically derived from it.
96
+ *
97
+ * Solana: SLIP-10 Ed25519 at m/44'/501'/0'/0'
98
+ * EVM: BIP-32 secp256k1 at m/44'/60'/0'/0/0
99
+ *
100
+ * Ported from agentbox/packages/openclaw-x402/src/wallet.ts
101
+ */
102
+ function generateMnemonic$1() {
103
+ return generateMnemonic(wordlist, 256);
104
+ }
105
+ /**
106
+ * SLIP-10 Ed25519 derivation at m/44'/501'/0'/0' (Phantom/Backpack compatible).
107
+ */
108
+ const enc = new TextEncoder();
109
+ function deriveSolanaKeypair(mnemonic) {
110
+ const seed = mnemonicToSeedSync(mnemonic);
111
+ let I = hmac(sha512, enc.encode("ed25519 seed"), seed);
112
+ let key = I.slice(0, 32);
113
+ let chainCode = I.slice(32);
114
+ for (const index of [
115
+ 2147483692,
116
+ 2147484149,
117
+ 2147483648,
118
+ 2147483648
119
+ ]) {
120
+ const data = new Uint8Array(37);
121
+ data[0] = 0;
122
+ data.set(key, 1);
123
+ data[33] = index >>> 24 & 255;
124
+ data[34] = index >>> 16 & 255;
125
+ data[35] = index >>> 8 & 255;
126
+ data[36] = index & 255;
127
+ I = hmac(sha512, chainCode, data);
128
+ key = I.slice(0, 32);
129
+ chainCode = I.slice(32);
130
+ }
131
+ const secretKey = new Uint8Array(key);
132
+ const publicKey = ed25519.getPublicKey(secretKey);
133
+ return {
134
+ secretKey,
135
+ publicKey,
136
+ address: base58.encode(publicKey)
137
+ };
138
+ }
139
+ /**
140
+ * BIP-32 secp256k1 derivation at m/44'/60'/0'/0/0.
141
+ */
142
+ function deriveEvmKeypair(mnemonic) {
143
+ const seed = mnemonicToSeedSync(mnemonic);
144
+ const derived = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
145
+ if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
146
+ const privateKey = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
147
+ const hash = keccak_256(secp256k1.getPublicKey(derived.privateKey, false).slice(1));
148
+ return {
149
+ privateKey,
150
+ address: checksumAddress(Buffer.from(hash.slice(-20)).toString("hex"))
151
+ };
152
+ }
153
+ function checksumAddress(addr) {
154
+ const hash = Buffer.from(keccak_256(enc.encode(addr))).toString("hex");
155
+ let out = "0x";
156
+ for (let i = 0; i < 40; i++) out += Number.parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
157
+ return out;
158
+ }
159
+
160
+ //#endregion
161
+ export { getConfigDirShort as a, isConfigured as c, saveConfig as d, saveWalletFile as f, ensureConfigDir as i, loadConfig as l, deriveSolanaKeypair as n, getHistoryPath as o, generateMnemonic$1 as r, getWalletPath as s, deriveEvmKeypair as t, loadWalletFile as u };
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { a as getConfigDirShort, c as isConfigured, d as saveConfig, f as saveWalletFile, n as deriveSolanaKeypair, r as generateMnemonic, s as getWalletPath, t as deriveEvmKeypair } from "./derive-Rikypf5B.js";
3
+ import { buildCommand } from "@stricli/core";
4
+ import pc from "picocolors";
5
+ import * as prompts from "@clack/prompts";
6
+
7
+ //#region src/commands/setup.ts
8
+ async function runSetup(opts) {
9
+ if (isConfigured() && !opts?.force) {
10
+ prompts.log.warn(`Already configured. Wallet at ${pc.dim(getWalletPath())}\nTo reconfigure, run:\n ${pc.cyan("$ npx x402-proxy setup --force")}`);
11
+ return;
12
+ }
13
+ prompts.intro(pc.cyan("x402-proxy setup"));
14
+ prompts.log.info("This will generate a single BIP-39 mnemonic that derives wallets for both Solana and EVM chains.");
15
+ const action = await prompts.select({
16
+ message: "How would you like to set up your wallet?",
17
+ options: [{
18
+ value: "generate",
19
+ label: "Generate a new mnemonic"
20
+ }, {
21
+ value: "import",
22
+ label: "Import an existing mnemonic"
23
+ }]
24
+ });
25
+ if (prompts.isCancel(action)) {
26
+ prompts.cancel("Setup cancelled.");
27
+ process.exit(0);
28
+ }
29
+ let mnemonic;
30
+ if (action === "generate") {
31
+ mnemonic = generateMnemonic();
32
+ prompts.log.warn("Write down your mnemonic and store it safely. It will NOT be shown again.");
33
+ prompts.log.message(pc.bold(mnemonic));
34
+ } else {
35
+ const input = await prompts.text({
36
+ message: "Enter your 24-word mnemonic:",
37
+ validate: (v = "") => {
38
+ const words = v.trim().split(/\s+/);
39
+ if (words.length !== 12 && words.length !== 24) return "Mnemonic must be 12 or 24 words";
40
+ }
41
+ });
42
+ if (prompts.isCancel(input)) {
43
+ prompts.cancel("Setup cancelled.");
44
+ process.exit(0);
45
+ }
46
+ mnemonic = input.trim();
47
+ }
48
+ const evm = deriveEvmKeypair(mnemonic);
49
+ const sol = deriveSolanaKeypair(mnemonic);
50
+ prompts.log.success(`Base address: ${pc.green(evm.address)}`);
51
+ prompts.log.success(`Solana address: ${pc.green(sol.address)}`);
52
+ saveWalletFile({
53
+ version: 1,
54
+ mnemonic,
55
+ addresses: {
56
+ evm: evm.address,
57
+ solana: sol.address
58
+ }
59
+ });
60
+ saveConfig({});
61
+ prompts.log.info(`Config directory: ${pc.dim(getConfigDirShort())}`);
62
+ prompts.log.step("Fund your wallets to start using x402 resources:");
63
+ prompts.log.message(` Solana (USDC): Send USDC to ${pc.cyan(sol.address)}`);
64
+ prompts.log.message(` Base (USDC): Send USDC to ${pc.cyan(evm.address)}`);
65
+ prompts.log.step("Try your first request:");
66
+ prompts.log.message(` ${pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi")}`);
67
+ prompts.outro(pc.green("Setup complete!"));
68
+ }
69
+ const setupCommand = buildCommand({
70
+ docs: { brief: "Set up x402-proxy with a new wallet" },
71
+ parameters: {
72
+ flags: { force: {
73
+ kind: "boolean",
74
+ brief: "Overwrite existing configuration",
75
+ default: false
76
+ } },
77
+ positional: {
78
+ kind: "tuple",
79
+ parameters: []
80
+ }
81
+ },
82
+ async func(flags) {
83
+ await runSetup({ force: flags.force });
84
+ }
85
+ });
86
+
87
+ //#endregion
88
+ export { setupCommand as n, runSetup as t };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { n as setupCommand, t as runSetup } from "./setup-CvB0go56.js";
3
+
4
+ export { runSetup };
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { n as statusCommand, t as displayStatus } from "./status-BAzavcCj.js";
2
+ import { n as statusCommand, t as displayStatus } from "./status-Cp51UDnh.js";
3
3
 
4
4
  export { displayStatus };
@@ -1,25 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { a as getConfigDirShort, l as loadConfig, n as deriveSolanaKeypair, o as getHistoryPath, t as deriveEvmKeypair, u as loadWalletFile } from "./derive-Rikypf5B.js";
2
3
  import { buildCommand } from "@stricli/core";
3
4
  import pc from "picocolors";
4
5
  import { x402Client } from "@x402/fetch";
5
- import fs, { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
- import os from "node:os";
7
- import path from "node:path";
8
- import { parse, stringify } from "yaml";
6
+ import { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
7
  import { base58 } from "@scure/base";
10
8
  import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
11
9
  import { ExactSvmScheme } from "@x402/svm/exact/client";
12
10
  import { createPublicClient, http } from "viem";
13
11
  import { privateKeyToAccount } from "viem/accounts";
14
12
  import { base } from "viem/chains";
15
- import { ed25519 } from "@noble/curves/ed25519.js";
16
- import { secp256k1 } from "@noble/curves/secp256k1.js";
17
- import { hmac } from "@noble/hashes/hmac.js";
18
- import { sha512 } from "@noble/hashes/sha2.js";
19
- import { keccak_256 } from "@noble/hashes/sha3.js";
20
- import { HDKey } from "@scure/bip32";
21
- import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
22
- import { wordlist } from "@scure/bip39/wordlists/english.js";
23
13
 
24
14
  //#region src/history.ts
25
15
  const HISTORY_MAX_LINES = 1e3;
@@ -132,80 +122,6 @@ function formatTxLine(r, opts) {
132
122
  return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
133
123
  }
134
124
 
135
- //#endregion
136
- //#region src/lib/config.ts
137
- const APP_NAME = "x402-proxy";
138
- function getConfigDir() {
139
- const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
140
- return path.join(xdg, APP_NAME);
141
- }
142
- /** Config dir with $HOME replaced by ~ for display */
143
- function getConfigDirShort() {
144
- const dir = getConfigDir();
145
- const home = os.homedir();
146
- return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
147
- }
148
- function getWalletPath() {
149
- return path.join(getConfigDir(), "wallet.json");
150
- }
151
- function getHistoryPath() {
152
- return path.join(getConfigDir(), "history.jsonl");
153
- }
154
- function ensureConfigDir() {
155
- fs.mkdirSync(getConfigDir(), { recursive: true });
156
- }
157
- function loadWalletFile() {
158
- const p = getWalletPath();
159
- if (!fs.existsSync(p)) return null;
160
- try {
161
- const data = JSON.parse(fs.readFileSync(p, "utf-8"));
162
- if (data.version === 1 && typeof data.mnemonic === "string") return data;
163
- return null;
164
- } catch {
165
- return null;
166
- }
167
- }
168
- function saveWalletFile(wallet) {
169
- ensureConfigDir();
170
- fs.writeFileSync(getWalletPath(), JSON.stringify(wallet, null, 2), {
171
- mode: 384,
172
- encoding: "utf-8"
173
- });
174
- }
175
- function loadConfig() {
176
- const dir = getConfigDir();
177
- for (const name of [
178
- "config.yaml",
179
- "config.yml",
180
- "config.jsonc",
181
- "config.json"
182
- ]) {
183
- const p = path.join(dir, name);
184
- if (!fs.existsSync(p)) continue;
185
- try {
186
- const raw = fs.readFileSync(p, "utf-8");
187
- if (name.endsWith(".yaml") || name.endsWith(".yml")) return parse(raw);
188
- if (name.endsWith(".jsonc")) {
189
- const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
190
- return JSON.parse(stripped);
191
- }
192
- return JSON.parse(raw);
193
- } catch {}
194
- }
195
- return null;
196
- }
197
- function saveConfig(config) {
198
- ensureConfigDir();
199
- const p = path.join(getConfigDir(), "config.yaml");
200
- fs.writeFileSync(p, stringify(config), "utf-8");
201
- }
202
- function isConfigured() {
203
- if (process.env.X402_PROXY_WALLET_MNEMONIC) return true;
204
- if (process.env.X402_PROXY_WALLET_EVM_KEY) return true;
205
- if (process.env.X402_PROXY_WALLET_SOLANA_KEY) return true;
206
- return loadWalletFile() !== null;
207
- }
208
-
209
125
  //#endregion
210
126
  //#region src/lib/output.ts
211
127
  function isTTY() {
@@ -224,77 +140,6 @@ function dim(msg) {
224
140
  process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
225
141
  }
226
142
 
227
- //#endregion
228
- //#region src/lib/derive.ts
229
- /**
230
- * Wallet derivation from BIP-39 mnemonic.
231
- *
232
- * A single 24-word mnemonic is the root secret. Both Solana and EVM keypairs
233
- * are deterministically derived from it.
234
- *
235
- * Solana: SLIP-10 Ed25519 at m/44'/501'/0'/0'
236
- * EVM: BIP-32 secp256k1 at m/44'/60'/0'/0/0
237
- *
238
- * Ported from agentbox/packages/openclaw-x402/src/wallet.ts
239
- */
240
- function generateMnemonic$1() {
241
- return generateMnemonic(wordlist, 256);
242
- }
243
- /**
244
- * SLIP-10 Ed25519 derivation at m/44'/501'/0'/0' (Phantom/Backpack compatible).
245
- */
246
- const enc = new TextEncoder();
247
- function deriveSolanaKeypair(mnemonic) {
248
- const seed = mnemonicToSeedSync(mnemonic);
249
- let I = hmac(sha512, enc.encode("ed25519 seed"), seed);
250
- let key = I.slice(0, 32);
251
- let chainCode = I.slice(32);
252
- for (const index of [
253
- 2147483692,
254
- 2147484149,
255
- 2147483648,
256
- 2147483648
257
- ]) {
258
- const data = new Uint8Array(37);
259
- data[0] = 0;
260
- data.set(key, 1);
261
- data[33] = index >>> 24 & 255;
262
- data[34] = index >>> 16 & 255;
263
- data[35] = index >>> 8 & 255;
264
- data[36] = index & 255;
265
- I = hmac(sha512, chainCode, data);
266
- key = I.slice(0, 32);
267
- chainCode = I.slice(32);
268
- }
269
- const secretKey = new Uint8Array(key);
270
- const publicKey = ed25519.getPublicKey(secretKey);
271
- return {
272
- secretKey,
273
- publicKey,
274
- address: base58.encode(publicKey)
275
- };
276
- }
277
- /**
278
- * BIP-32 secp256k1 derivation at m/44'/60'/0'/0/0.
279
- */
280
- function deriveEvmKeypair(mnemonic) {
281
- const seed = mnemonicToSeedSync(mnemonic);
282
- const derived = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
283
- if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
284
- const privateKey = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
285
- const hash = keccak_256(secp256k1.getPublicKey(derived.privateKey, false).slice(1));
286
- return {
287
- privateKey,
288
- address: checksumAddress(Buffer.from(hash.slice(-20)).toString("hex"))
289
- };
290
- }
291
- function checksumAddress(addr) {
292
- const hash = Buffer.from(keccak_256(enc.encode(addr))).toString("hex");
293
- let out = "0x";
294
- for (let i = 0; i < 40; i++) out += Number.parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
295
- return out;
296
- }
297
-
298
143
  //#endregion
299
144
  //#region src/lib/resolve-wallet.ts
300
145
  /**
@@ -468,8 +313,7 @@ const walletInfoCommand = buildCommand({
468
313
  const wallet = resolveWallet();
469
314
  if (wallet.source === "none") {
470
315
  console.log(pc.yellow("No wallet configured."));
471
- console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
472
- console.log(pc.dim(`Or set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
316
+ console.log(pc.dim(`\nRun:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
473
317
  process.exit(1);
474
318
  }
475
319
  console.log();
@@ -487,6 +331,12 @@ const walletInfoCommand = buildCommand({
487
331
  const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
488
332
  console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
489
333
  }
334
+ const evmEmpty = !evm || evm.usdc === "0.00";
335
+ const solEmpty = !sol || sol.usdc === "0.00";
336
+ if (evmEmpty && solEmpty) {
337
+ console.log();
338
+ dim(" Send USDC to either address above to start using x402 APIs.");
339
+ }
490
340
  console.log();
491
341
  const records = readHistory(getHistoryPath());
492
342
  if (records.length > 0) {
@@ -501,7 +351,7 @@ const walletInfoCommand = buildCommand({
501
351
  console.log(pc.dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`));
502
352
  } else dim(" No transactions yet.");
503
353
  console.log();
504
- console.log(pc.dim(" See also: wallet history, wallet fund, wallet export-key"));
354
+ console.log(pc.dim(" See also: wallet history, wallet export-key"));
505
355
  console.log();
506
356
  }
507
357
  });
@@ -572,4 +422,4 @@ const statusCommand = buildCommand({
572
422
  });
573
423
 
574
424
  //#endregion
575
- export { appendHistory as C, readHistory as E, saveWalletFile as S, formatTxLine as T, getWalletPath as _, resolveWallet as a, loadWalletFile as b, generateMnemonic$1 as c, info as d, isTTY as f, getHistoryPath as g, getConfigDirShort as h, buildX402Client as i, dim as l, ensureConfigDir as m, statusCommand as n, deriveEvmKeypair as o, warn as p, walletInfoCommand as r, deriveSolanaKeypair as s, displayStatus as t, error as u, isConfigured as v, calcSpend as w, saveConfig as x, loadConfig as y };
425
+ export { resolveWallet as a, info as c, appendHistory as d, calcSpend as f, buildX402Client as i, isTTY as l, readHistory as m, statusCommand as n, dim as o, formatTxLine as p, walletInfoCommand as r, error as s, displayStatus as t, warn as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-proxy",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "curl for x402 paid APIs. Auto-pays any endpoint on Base and Solana.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -41,7 +41,8 @@
41
41
  "@types/node": "^22.0.0",
42
42
  "publint": "^0.3.17",
43
43
  "tsdown": "^0.20.3",
44
- "typescript": "^5.9.0"
44
+ "typescript": "^5.9.0",
45
+ "vitest": "^4.0.18"
45
46
  },
46
47
  "files": [
47
48
  "dist/**",
@@ -78,6 +79,7 @@
78
79
  "build": "rm -rf dist && tsdown --publint",
79
80
  "dev": "tsdown --watch",
80
81
  "type-check": "tsc --noEmit",
82
+ "test": "vitest run",
81
83
  "check": "pnpm type-check && biome check --write"
82
84
  }
83
85
  }