x402-proxy 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
@@ -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,31 @@ 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
 
49
+ # Force a specific network
50
+ $ npx x402-proxy --network base https://api.example.com/data
51
+
56
52
  # Pipe-safe
57
- x402-proxy https://api.example.com/data | jq '.results'
53
+ $ npx x402-proxy https://api.example.com/data | jq '.results'
58
54
  ```
59
55
 
60
56
  ## Commands
61
57
 
62
58
  ```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)
59
+ $ npx x402-proxy <url> # paid HTTP request (default command)
60
+ $ npx x402-proxy mcp <url> # MCP stdio proxy for agents
61
+ $ npx x402-proxy setup # onboarding wizard
62
+ $ npx x402-proxy status # config + wallet + spend summary
63
+ $ npx x402-proxy wallet # show addresses and balances
64
+ $ npx x402-proxy wallet history # payment history
65
+ $ npx x402-proxy wallet export-key <target> # bare key/mnemonic to stdout (evm|solana|mnemonic)
71
66
  ```
72
67
 
73
68
  All commands support `--help` for details.
@@ -84,8 +79,8 @@ Config stored at `$XDG_CONFIG_HOME/x402-proxy/` (default `~/.config/x402-proxy/`
84
79
 
85
80
  ```bash
86
81
  # 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)
82
+ $ MY_KEY=$(npx x402-proxy wallet export-key evm)
83
+ $ MY_MNEMONIC=$(npx x402-proxy wallet export-key mnemonic)
89
84
  ```
90
85
 
91
86
  ## Env Vars
package/dist/bin/cli.js CHANGED
@@ -1,5 +1,8 @@
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 buildX402Client, c as error, d as warn, f as appendHistory, g as readHistory, h as formatTxLine, i as walletInfoCommand, l as info, m as displayNetwork, o as resolveWallet, p as calcSpend, s as dim, u as isTTY } from "../wallet-Vp9zRQzC.js";
3
+ import { c as isConfigured, i as ensureConfigDir, l as loadConfig, o as getHistoryPath, u as loadWalletFile } from "../derive-CISr_ond.js";
4
+ import { n as setupCommand } from "../setup-gla-Qyqi.js";
5
+ import { n as statusCommand } from "../status-B7iFmj8f.js";
3
6
  import { buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
4
7
  import pc from "picocolors";
5
8
  import { decodePaymentResponseHeader, wrapFetchWithPayment } from "@x402/fetch";
@@ -36,7 +39,7 @@ function createX402ProxyHandler(opts) {
36
39
  paymentQueue.push({
37
40
  network: hookCtx.selectedRequirements.network,
38
41
  payTo: hookCtx.selectedRequirements.payTo,
39
- amount: raw?.startsWith("debug.") ? raw.slice(6) : raw,
42
+ amount: raw,
40
43
  asset: hookCtx.selectedRequirements.asset
41
44
  });
42
45
  });
@@ -91,6 +94,12 @@ Examples:
91
94
  parse: String,
92
95
  optional: true
93
96
  },
97
+ network: {
98
+ kind: "parsed",
99
+ brief: "Require specific network (base, solana)",
100
+ parse: String,
101
+ optional: true
102
+ },
94
103
  json: {
95
104
  kind: "boolean",
96
105
  brief: "Force JSON output",
@@ -109,7 +118,7 @@ Examples:
109
118
  async func(flags, url) {
110
119
  if (!url) {
111
120
  if (isConfigured()) {
112
- const { displayStatus } = await import("../status-CeXiUR6L.js");
121
+ const { displayStatus } = await import("../status-0rAVbrke.js");
113
122
  await displayStatus();
114
123
  console.log();
115
124
  console.log(pc.dim(" Commands:"));
@@ -118,7 +127,6 @@ Examples:
118
127
  console.log(` ${pc.cyan("$ npx x402-proxy setup")} Reconfigure wallet`);
119
128
  console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
120
129
  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
130
  console.log();
123
131
  console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi"));
124
132
  console.log();
@@ -135,7 +143,6 @@ Examples:
135
143
  console.log(` ${pc.cyan("$ npx x402-proxy mcp <url>")} MCP proxy for AI agents`);
136
144
  console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
137
145
  console.log(` ${pc.cyan("$ npx x402-proxy wallet history")} Payment history`);
138
- console.log(` ${pc.cyan("$ npx x402-proxy wallet fund")} Funding instructions`);
139
146
  console.log(` ${pc.cyan("$ npx x402-proxy --help")} All options`);
140
147
  console.log();
141
148
  console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy setup"));
@@ -152,18 +159,36 @@ Examples:
152
159
  error(`Invalid URL: ${url}`);
153
160
  process.exit(1);
154
161
  }
155
- const wallet = resolveWallet({
162
+ let wallet = resolveWallet({
156
163
  evmKey: flags.evmKey,
157
164
  solanaKey: flags.solanaKey
158
165
  });
159
166
  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);
167
+ if (!isTTY()) {
168
+ error("No wallet configured.");
169
+ console.error(pc.dim(`Run:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set X402_PROXY_WALLET_MNEMONIC`));
170
+ process.exit(1);
171
+ }
172
+ dim(" No wallet found. Let's set one up first.\n");
173
+ const { runSetup } = await import("../setup-Crq9TylJ.js");
174
+ await runSetup();
175
+ console.log();
176
+ wallet = resolveWallet();
177
+ if (wallet.source === "none") return;
163
178
  }
164
179
  const config = loadConfig();
180
+ let preferredNetwork = config?.defaultNetwork;
181
+ if (!preferredNetwork && wallet.evmAddress && wallet.solanaAddress) {
182
+ const { fetchEvmBalances, fetchSolanaBalances } = await import("../wallet-BX5vQtUz.js");
183
+ const [evmBal, solBal] = await Promise.allSettled([fetchEvmBalances(wallet.evmAddress), fetchSolanaBalances(wallet.solanaAddress)]);
184
+ const evmUsdc = evmBal.status === "fulfilled" ? Number(evmBal.value?.usdc ?? 0) : 0;
185
+ const solUsdc = solBal.status === "fulfilled" ? Number(solBal.value?.usdc ?? 0) : 0;
186
+ if (evmUsdc > solUsdc) preferredNetwork = "base";
187
+ else if (solUsdc > evmUsdc) preferredNetwork = "solana";
188
+ }
165
189
  const { x402Fetch, shiftPayment } = createX402ProxyHandler({ client: await buildX402Client(wallet, {
166
- preferredNetwork: config?.defaultNetwork,
190
+ preferredNetwork,
191
+ network: flags.network,
167
192
  spendLimitDaily: config?.spendLimitDaily,
168
193
  spendLimitPerTx: config?.spendLimitPerTx
169
194
  }) });
@@ -190,8 +215,81 @@ Examples:
190
215
  const elapsedMs = Date.now() - startMs;
191
216
  const payment = shiftPayment();
192
217
  const txSig = extractTxSignature(response);
218
+ if (response.status === 402 && isTTY()) {
219
+ const prHeader = response.headers.get("PAYMENT-REQUIRED") ?? response.headers.get("X-PAYMENT-REQUIRED");
220
+ let accepts = [];
221
+ if (prHeader) try {
222
+ accepts = JSON.parse(Buffer.from(prHeader, "base64").toString()).accepts ?? [];
223
+ } catch {}
224
+ let costNum = 0;
225
+ let costStr = "?";
226
+ if (accepts.length > 0) {
227
+ const cheapest = accepts.reduce((min, a) => Number(a.amount) < Number(min.amount) ? a : min);
228
+ costNum = Number(cheapest.amount) / 1e6;
229
+ costStr = costNum.toFixed(4);
230
+ }
231
+ const hasEvm = accepts.some((a) => a.network.startsWith("eip155:"));
232
+ const hasSolana = accepts.some((a) => a.network.startsWith("solana:"));
233
+ const hasOther = accepts.some((a) => !a.network.startsWith("eip155:") && !a.network.startsWith("solana:"));
234
+ const { fetchEvmBalances, fetchSolanaBalances } = await import("../wallet-BX5vQtUz.js");
235
+ let evmUsdc = 0;
236
+ let solUsdc = 0;
237
+ if (hasEvm && wallet.evmAddress) try {
238
+ const bal = await fetchEvmBalances(wallet.evmAddress);
239
+ evmUsdc = Number(bal.usdc);
240
+ } catch {}
241
+ if (hasSolana && wallet.solanaAddress) try {
242
+ const bal = await fetchSolanaBalances(wallet.solanaAddress);
243
+ solUsdc = Number(bal.usdc);
244
+ } catch {}
245
+ if (hasEvm && evmUsdc >= costNum || hasSolana && solUsdc >= costNum) {
246
+ let serverReason;
247
+ try {
248
+ const body = await response.text();
249
+ if (body) {
250
+ const parsed = JSON.parse(body);
251
+ serverReason = parsed.error || parsed.message;
252
+ }
253
+ } catch {}
254
+ error(`Payment failed: ${costStr} USDC`);
255
+ console.error();
256
+ if (payment) dim(" Payment was signed and sent but rejected by the server.");
257
+ else dim(" Payment was not attempted despite sufficient balance.");
258
+ if (serverReason) dim(` Reason: ${serverReason}`);
259
+ if (hasEvm && wallet.evmAddress && evmUsdc > 0) console.error(` Base: ${pc.cyan(wallet.evmAddress)} ${pc.dim(`(${evmUsdc.toFixed(4)} USDC)`)}`);
260
+ if (hasSolana && wallet.solanaAddress && solUsdc > 0) console.error(` Solana: ${pc.cyan(wallet.solanaAddress)} ${pc.dim(`(${solUsdc.toFixed(4)} USDC)`)}`);
261
+ console.error();
262
+ dim(" This may be a temporary server-side issue. Try again in a moment.");
263
+ console.error();
264
+ } else {
265
+ error(`Payment required: ${costStr} USDC`);
266
+ if (hasEvm || hasSolana) {
267
+ console.error();
268
+ dim(" Fund your wallet with USDC:");
269
+ if (hasEvm && wallet.evmAddress) {
270
+ const balHint = evmUsdc > 0 ? pc.dim(` (${evmUsdc.toFixed(4)} USDC)`) : "";
271
+ console.error(` Base: ${pc.cyan(wallet.evmAddress)}${balHint}`);
272
+ }
273
+ if (hasSolana && wallet.solanaAddress) {
274
+ const balHint = solUsdc > 0 ? pc.dim(` (${solUsdc.toFixed(4)} USDC)`) : "";
275
+ console.error(` Solana: ${pc.cyan(wallet.solanaAddress)}${balHint}`);
276
+ }
277
+ if (hasEvm && !wallet.evmAddress) dim(" Base: endpoint accepts EVM but no EVM wallet configured");
278
+ if (hasSolana && !wallet.solanaAddress) dim(" Solana: endpoint accepts Solana but no Solana wallet configured");
279
+ } else if (hasOther) {
280
+ const networks = [...new Set(accepts.map((a) => a.network))].join(", ");
281
+ console.error();
282
+ error(`This endpoint only accepts payment on unsupported networks: ${networks}`);
283
+ }
284
+ console.error();
285
+ dim(" Then re-run:");
286
+ console.error(` ${pc.cyan(`$ npx x402-proxy ${url}`)}`);
287
+ console.error();
288
+ }
289
+ return;
290
+ }
193
291
  if (payment && isTTY()) {
194
- info(` Payment: ${payment.amount ?? "?"} (${payment.network ?? "unknown"})`);
292
+ info(` Payment: ${payment.amount ? (Number(payment.amount) / 1e6).toFixed(4) : "?"} USDC (${displayNetwork(payment.network ?? "unknown")})`);
195
293
  if (txSig) dim(` Tx: ${txSig}`);
196
294
  }
197
295
  if (isTTY()) dim(` ${response.status} ${response.statusText} (${elapsedMs}ms)`);
@@ -253,6 +351,12 @@ Add to your MCP client config (Claude, Cursor, etc.):
253
351
  brief: "Solana private key (base58)",
254
352
  parse: String,
255
353
  optional: true
354
+ },
355
+ network: {
356
+ kind: "parsed",
357
+ brief: "Require specific network (base, solana)",
358
+ parse: String,
359
+ optional: true
256
360
  }
257
361
  },
258
362
  positional: {
@@ -269,7 +373,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
269
373
  solanaKey: flags.solanaKey
270
374
  });
271
375
  if (wallet.source === "none") {
272
- error("No wallet configured. Set X402_PROXY_WALLET_MNEMONIC or run x402-proxy setup.");
376
+ error("No wallet configured.\nRun:\n $ npx x402-proxy setup\n\nOr set X402_PROXY_WALLET_MNEMONIC");
273
377
  process.exit(1);
274
378
  }
275
379
  warn("Note: MCP proxy is alpha - please report issues.");
@@ -277,8 +381,18 @@ Add to your MCP client config (Claude, Cursor, etc.):
277
381
  if (wallet.evmAddress) dim(` EVM: ${wallet.evmAddress}`);
278
382
  if (wallet.solanaAddress) dim(` Solana: ${wallet.solanaAddress}`);
279
383
  const config = loadConfig();
384
+ let preferredNetwork = config?.defaultNetwork;
385
+ if (!preferredNetwork && wallet.evmAddress && wallet.solanaAddress) {
386
+ const { fetchEvmBalances, fetchSolanaBalances } = await import("../wallet-BX5vQtUz.js");
387
+ const [evmBal, solBal] = await Promise.allSettled([fetchEvmBalances(wallet.evmAddress), fetchSolanaBalances(wallet.solanaAddress)]);
388
+ const evmUsdc = evmBal.status === "fulfilled" ? Number(evmBal.value?.usdc ?? 0) : 0;
389
+ const solUsdc = solBal.status === "fulfilled" ? Number(solBal.value?.usdc ?? 0) : 0;
390
+ if (evmUsdc > solUsdc) preferredNetwork = "base";
391
+ else if (solUsdc > evmUsdc) preferredNetwork = "solana";
392
+ }
280
393
  const x402PaymentClient = await buildX402Client(wallet, {
281
- preferredNetwork: config?.defaultNetwork,
394
+ preferredNetwork,
395
+ network: flags.network,
282
396
  spendLimitDaily: config?.spendLimitDaily,
283
397
  spendLimitPerTx: config?.spendLimitPerTx
284
398
  });
@@ -290,26 +404,28 @@ Add to your MCP client config (Claude, Cursor, etc.):
290
404
  const { x402MCPClient } = await import("@x402/mcp");
291
405
  const x402Mcp = new x402MCPClient(new Client({
292
406
  name: "x402-proxy",
293
- version: "0.3.0"
407
+ version: "0.4.0"
294
408
  }), x402PaymentClient, {
295
409
  autoPayment: true,
296
410
  onPaymentRequested: (ctx) => {
297
411
  const accept = ctx.paymentRequired.accepts?.[0];
298
- if (accept) warn(` Payment: ${accept.amount} on ${accept.network} for tool "${ctx.toolName}"`);
412
+ if (accept) warn(` Payment: ${accept.amount ? (Number(accept.amount) / 1e6).toFixed(4) : "?"} USDC on ${displayNetwork(accept.network)} for tool "${ctx.toolName}"`);
299
413
  return true;
300
414
  }
301
415
  });
302
416
  x402Mcp.onAfterPayment(async (ctx) => {
303
417
  ensureConfigDir();
418
+ const accepted = ctx.paymentPayload.accepted;
304
419
  const tx = ctx.settleResponse?.transaction;
305
- const accept = ctx.paymentPayload;
306
420
  const record = {
307
421
  t: Date.now(),
308
422
  ok: true,
309
423
  kind: "x402_payment",
310
- net: accept.network ?? "unknown",
424
+ net: accepted?.network ?? "unknown",
311
425
  from: wallet.evmAddress ?? wallet.solanaAddress ?? "unknown",
426
+ to: accepted?.payTo,
312
427
  tx: typeof tx === "string" ? tx : void 0,
428
+ amount: accepted?.amount ? Number(accepted.amount) / 1e6 : void 0,
313
429
  token: "USDC",
314
430
  label: `mcp:${ctx.toolName}`
315
431
  };
@@ -335,7 +451,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
335
451
  dim(` ${tools.length} tools available`);
336
452
  const localServer = new McpServer({
337
453
  name: "x402-proxy",
338
- version: "0.3.0"
454
+ version: "0.4.0"
339
455
  });
340
456
  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
457
  const result = await x402Mcp.callTool(tool.name, args);
@@ -366,84 +482,6 @@ Add to your MCP client config (Claude, Cursor, etc.):
366
482
  }
367
483
  });
368
484
 
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
485
  //#endregion
448
486
  //#region src/commands/wallet-export.ts
449
487
  const walletExportCommand = buildCommand({
@@ -498,44 +536,6 @@ const walletExportCommand = buildCommand({
498
536
  }
499
537
  });
500
538
 
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
539
  //#endregion
540
540
  //#region src/commands/wallet-history.ts
541
541
  const walletHistoryCommand = buildCommand({
@@ -595,7 +595,6 @@ const routes = buildRouteMap({
595
595
  routes: {
596
596
  info: walletInfoCommand,
597
597
  history: walletHistoryCommand,
598
- fund: walletFundCommand,
599
598
  "export-key": walletExportCommand
600
599
  },
601
600
  defaultCommand: "info",
@@ -609,7 +608,7 @@ const routes = buildRouteMap({
609
608
  });
610
609
  const app = buildApplication(routes, {
611
610
  name: "x402-proxy",
612
- versionInfo: { currentVersion: "0.3.1" },
611
+ versionInfo: { currentVersion: "0.4.0" },
613
612
  scanner: { caseStyle: "allow-kebab-for-camel" }
614
613
  });
615
614
 
@@ -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 { ed25519 } from "@noble/curves/ed25519.js";
7
+ import { base58 } from "@scure/base";
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 };
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function createX402ProxyHandler(opts) {
35
35
  paymentQueue.push({
36
36
  network: hookCtx.selectedRequirements.network,
37
37
  payTo: hookCtx.selectedRequirements.payTo,
38
- amount: raw?.startsWith("debug.") ? raw.slice(6) : raw,
38
+ amount: raw,
39
39
  asset: hookCtx.selectedRequirements.asset
40
40
  });
41
41
  });
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { n as setupCommand, t as runSetup } from "./setup-gla-Qyqi.js";
3
+
4
+ export { runSetup };
@@ -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-CISr_ond.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 };
@@ -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-B7iFmj8f.js";
3
3
 
4
4
  export { displayStatus };
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import { g as readHistory, h as formatTxLine, n as fetchEvmBalances, o as resolveWallet, p as calcSpend, r as fetchSolanaBalances, s as dim, t as balanceLine } from "./wallet-Vp9zRQzC.js";
3
+ import { a as getConfigDirShort, l as loadConfig, o as getHistoryPath } from "./derive-CISr_ond.js";
4
+ import { buildCommand } from "@stricli/core";
5
+ import pc from "picocolors";
6
+
7
+ //#region src/commands/status.ts
8
+ async function displayStatus() {
9
+ const wallet = resolveWallet();
10
+ const config = loadConfig();
11
+ const records = readHistory(getHistoryPath());
12
+ const spend = calcSpend(records);
13
+ console.log();
14
+ console.log(pc.cyan(pc.bold("x402-proxy")));
15
+ console.log(pc.dim("curl for x402 paid APIs"));
16
+ console.log();
17
+ if (wallet.source === "none") {
18
+ console.log(pc.yellow(" No wallet configured."));
19
+ console.log(pc.dim(` Run ${pc.cyan("$ npx x402-proxy setup")} to create one.`));
20
+ } else {
21
+ const [evmResult, solResult] = await Promise.allSettled([wallet.evmAddress ? fetchEvmBalances(wallet.evmAddress) : Promise.resolve(null), wallet.solanaAddress ? fetchSolanaBalances(wallet.solanaAddress) : Promise.resolve(null)]);
22
+ const evm = evmResult.status === "fulfilled" ? evmResult.value : null;
23
+ const sol = solResult.status === "fulfilled" ? solResult.value : null;
24
+ if (wallet.evmAddress) {
25
+ const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
26
+ console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
27
+ }
28
+ if (wallet.solanaAddress) {
29
+ const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
30
+ console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
31
+ }
32
+ if (config?.spendLimitDaily || config?.spendLimitPerTx) {
33
+ console.log();
34
+ if (config.spendLimitDaily) {
35
+ const pct = config.spendLimitDaily > 0 ? Math.round(spend.today / config.spendLimitDaily * 100) : 0;
36
+ dim(` Daily limit: ${spend.today.toFixed(4)} / ${config.spendLimitDaily} USDC (${pct}%)`);
37
+ }
38
+ if (config.spendLimitPerTx) dim(` Per-tx limit: ${config.spendLimitPerTx} USDC`);
39
+ }
40
+ }
41
+ console.log();
42
+ if (spend.count > 0) {
43
+ const recent = records.slice(-5);
44
+ dim(" Recent transactions:");
45
+ for (const r of recent) {
46
+ const line = formatTxLine(r).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
47
+ console.log(line);
48
+ }
49
+ console.log();
50
+ dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`);
51
+ } else dim(" No payment history yet.");
52
+ console.log();
53
+ if (config?.defaultNetwork) dim(` Network: ${config.defaultNetwork}`);
54
+ dim(` Config: ${getConfigDirShort()}`);
55
+ }
56
+ const statusCommand = buildCommand({
57
+ docs: { brief: "Show configuration and wallet status" },
58
+ parameters: {
59
+ flags: {},
60
+ positional: {
61
+ kind: "tuple",
62
+ parameters: []
63
+ }
64
+ },
65
+ async func() {
66
+ await displayStatus();
67
+ console.log();
68
+ }
69
+ });
70
+
71
+ //#endregion
72
+ export { statusCommand as n, displayStatus as t };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { i as walletInfoCommand, n as fetchEvmBalances, r as fetchSolanaBalances, t as balanceLine } from "./wallet-Vp9zRQzC.js";
3
+
4
+ export { fetchEvmBalances, fetchSolanaBalances };
@@ -1,25 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import { n as deriveSolanaKeypair, o as getHistoryPath, t as deriveEvmKeypair, u as loadWalletFile } from "./derive-CISr_ond.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";
7
+ import { ed25519 } from "@noble/curves/ed25519.js";
9
8
  import { base58 } from "@scure/base";
10
- import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
11
- import { ExactSvmScheme } from "@x402/svm/exact/client";
9
+ import { toClientEvmSigner } from "@x402/evm";
10
+ import { registerExactEvmScheme } from "@x402/evm/exact/client";
11
+ import { registerExactSvmScheme } from "@x402/svm/exact/client";
12
12
  import { createPublicClient, http } from "viem";
13
13
  import { privateKeyToAccount } from "viem/accounts";
14
14
  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
15
 
24
16
  //#region src/history.ts
25
17
  const HISTORY_MAX_LINES = 1e3;
@@ -106,6 +98,12 @@ function shortModel(model) {
106
98
  const parts = model.split("/");
107
99
  return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
108
100
  }
101
+ function displayNetwork(net) {
102
+ if (net === "eip155:8453") return "Base";
103
+ if (net.startsWith("eip155:")) return `EVM (${net.split(":")[1]})`;
104
+ if (net.startsWith("solana:")) return "Solana";
105
+ return net;
106
+ }
109
107
  function shortNetwork(net) {
110
108
  if (net === "eip155:8453") return "base";
111
109
  if (net.startsWith("eip155:")) return `evm:${net.split(":")[1]}`;
@@ -132,80 +130,6 @@ function formatTxLine(r, opts) {
132
130
  return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
133
131
  }
134
132
 
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
133
  //#endregion
210
134
  //#region src/lib/output.ts
211
135
  function isTTY() {
@@ -224,77 +148,6 @@ function dim(msg) {
224
148
  process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
225
149
  }
226
150
 
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
151
  //#endregion
299
152
  //#region src/lib/resolve-wallet.ts
300
153
  /**
@@ -312,7 +165,10 @@ function resolveWallet(opts) {
312
165
  result.evmKey = hex;
313
166
  result.evmAddress = privateKeyToAccount(hex).address;
314
167
  }
315
- if (opts.solanaKey) result.solanaKey = parsesolanaKey(opts.solanaKey);
168
+ if (opts.solanaKey) {
169
+ result.solanaKey = parsesolanaKey(opts.solanaKey);
170
+ result.solanaAddress = solanaAddressFromKey(result.solanaKey);
171
+ }
316
172
  return result;
317
173
  }
318
174
  const envEvm = process.env.X402_PROXY_WALLET_EVM_KEY;
@@ -324,7 +180,10 @@ function resolveWallet(opts) {
324
180
  result.evmKey = hex;
325
181
  result.evmAddress = privateKeyToAccount(hex).address;
326
182
  }
327
- if (envSol) result.solanaKey = parsesolanaKey(envSol);
183
+ if (envSol) {
184
+ result.solanaKey = parsesolanaKey(envSol);
185
+ result.solanaAddress = solanaAddressFromKey(result.solanaKey);
186
+ }
328
187
  return result;
329
188
  }
330
189
  const envMnemonic = process.env.X402_PROXY_WALLET_MNEMONIC;
@@ -355,6 +214,10 @@ function parsesolanaKey(input) {
355
214
  }
356
215
  return base58.decode(trimmed);
357
216
  }
217
+ function solanaAddressFromKey(keyBytes) {
218
+ if (keyBytes.length >= 64) return base58.encode(keyBytes.slice(32));
219
+ return base58.encode(ed25519.getPublicKey(keyBytes));
220
+ }
358
221
  function networkToCaipPrefix(name) {
359
222
  switch (name.toLowerCase()) {
360
223
  case "base": return "eip155:8453";
@@ -374,17 +237,25 @@ async function buildX402Client(wallet, opts) {
374
237
  })() : void 0);
375
238
  if (wallet.evmKey) {
376
239
  const hex = wallet.evmKey;
377
- const signer = toClientEvmSigner(privateKeyToAccount(hex), createPublicClient({
240
+ registerExactEvmScheme(client, { signer: toClientEvmSigner(privateKeyToAccount(hex), createPublicClient({
378
241
  chain: base,
379
242
  transport: http()
380
- }));
381
- client.register("eip155:8453", new ExactEvmScheme(signer));
243
+ })) });
382
244
  }
383
245
  if (wallet.solanaKey) {
384
246
  const { createKeyPairSignerFromBytes } = await import("@solana/kit");
385
- const signer = await createKeyPairSignerFromBytes(wallet.solanaKey);
386
- client.register("solana:mainnet", new ExactSvmScheme(signer));
387
- client.register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme(signer));
247
+ registerExactSvmScheme(client, { signer: await createKeyPairSignerFromBytes(wallet.solanaKey) });
248
+ }
249
+ if (opts?.network) {
250
+ const prefix = networkToCaipPrefix(opts.network);
251
+ client.registerPolicy((_version, reqs) => {
252
+ const filtered = reqs.filter((r) => r.network.startsWith(prefix));
253
+ if (filtered.length === 0) {
254
+ const available = [...new Set(reqs.map((r) => displayNetwork(r.network)))].join(", ");
255
+ throw new Error(`Network '${opts.network}' not accepted. Available: ${available}`);
256
+ }
257
+ return filtered;
258
+ });
388
259
  }
389
260
  const daily = opts?.spendLimitDaily;
390
261
  const perTx = opts?.spendLimitPerTx;
@@ -432,7 +303,7 @@ async function fetchEvmBalances(address) {
432
303
  }, "latest"])]);
433
304
  return {
434
305
  eth: ethRes.result ? (Number(BigInt(ethRes.result)) / 0xde0b6b3a7640000).toFixed(6) : "?",
435
- usdc: usdcRes.result ? (Number(BigInt(usdcRes.result)) / 1e6).toFixed(2) : "?"
306
+ usdc: usdcRes.result ? (Number(BigInt(usdcRes.result)) / 1e6).toFixed(4) : "?"
436
307
  };
437
308
  }
438
309
  async function fetchSolanaBalances(address) {
@@ -445,7 +316,7 @@ async function fetchSolanaBalances(address) {
445
316
  const accounts = usdcRes.result?.value;
446
317
  return {
447
318
  sol,
448
- usdc: accounts?.length ? Number(accounts[0].account.data.parsed.info.tokenAmount.uiAmountString).toFixed(2) : "0.00"
319
+ usdc: accounts?.length ? Number(accounts[0].account.data.parsed.info.tokenAmount.uiAmountString).toFixed(4) : "0.0000"
449
320
  };
450
321
  }
451
322
  function balanceLine(usdc, native, nativeSymbol) {
@@ -468,8 +339,7 @@ const walletInfoCommand = buildCommand({
468
339
  const wallet = resolveWallet();
469
340
  if (wallet.source === "none") {
470
341
  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.`));
342
+ console.log(pc.dim(`\nRun:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
473
343
  process.exit(1);
474
344
  }
475
345
  console.log();
@@ -487,6 +357,12 @@ const walletInfoCommand = buildCommand({
487
357
  const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
488
358
  console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
489
359
  }
360
+ const evmEmpty = !evm || evm.usdc === "0.0000";
361
+ const solEmpty = !sol || sol.usdc === "0.0000";
362
+ if (evmEmpty && solEmpty) {
363
+ console.log();
364
+ dim(" Send USDC to either address above to start using x402 APIs.");
365
+ }
490
366
  console.log();
491
367
  const records = readHistory(getHistoryPath());
492
368
  if (records.length > 0) {
@@ -501,75 +377,10 @@ const walletInfoCommand = buildCommand({
501
377
  console.log(pc.dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`));
502
378
  } else dim(" No transactions yet.");
503
379
  console.log();
504
- console.log(pc.dim(" See also: wallet history, wallet fund, wallet export-key"));
505
- console.log();
506
- }
507
- });
508
-
509
- //#endregion
510
- //#region src/commands/status.ts
511
- async function displayStatus() {
512
- const wallet = resolveWallet();
513
- const config = loadConfig();
514
- const records = readHistory(getHistoryPath());
515
- const spend = calcSpend(records);
516
- console.log();
517
- console.log(pc.cyan(pc.bold("x402-proxy")));
518
- console.log(pc.dim("curl for x402 paid APIs"));
519
- console.log();
520
- if (wallet.source === "none") {
521
- console.log(pc.yellow(" No wallet configured."));
522
- console.log(pc.dim(` Run ${pc.cyan("$ npx x402-proxy setup")} to create one.`));
523
- } else {
524
- const [evmResult, solResult] = await Promise.allSettled([wallet.evmAddress ? fetchEvmBalances(wallet.evmAddress) : Promise.resolve(null), wallet.solanaAddress ? fetchSolanaBalances(wallet.solanaAddress) : Promise.resolve(null)]);
525
- const evm = evmResult.status === "fulfilled" ? evmResult.value : null;
526
- const sol = solResult.status === "fulfilled" ? solResult.value : null;
527
- if (wallet.evmAddress) {
528
- const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
529
- console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
530
- }
531
- if (wallet.solanaAddress) {
532
- const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
533
- console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
534
- }
535
- if (config?.spendLimitDaily || config?.spendLimitPerTx) {
536
- console.log();
537
- if (config.spendLimitDaily) {
538
- const pct = config.spendLimitDaily > 0 ? Math.round(spend.today / config.spendLimitDaily * 100) : 0;
539
- dim(` Daily limit: ${spend.today.toFixed(4)} / ${config.spendLimitDaily} USDC (${pct}%)`);
540
- }
541
- if (config.spendLimitPerTx) dim(` Per-tx limit: ${config.spendLimitPerTx} USDC`);
542
- }
543
- }
544
- console.log();
545
- if (spend.count > 0) {
546
- const recent = records.slice(-5);
547
- dim(" Recent transactions:");
548
- for (const r of recent) {
549
- const line = formatTxLine(r).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
550
- console.log(line);
551
- }
552
- console.log();
553
- dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`);
554
- } else dim(" No payment history yet.");
555
- console.log();
556
- if (config?.defaultNetwork) dim(` Network: ${config.defaultNetwork}`);
557
- dim(` Config: ${getConfigDirShort()}`);
558
- }
559
- const statusCommand = buildCommand({
560
- docs: { brief: "Show configuration and wallet status" },
561
- parameters: {
562
- flags: {},
563
- positional: {
564
- kind: "tuple",
565
- parameters: []
566
- }
567
- },
568
- async func() {
569
- await displayStatus();
380
+ console.log(pc.dim(" See also: wallet history, wallet export-key"));
570
381
  console.log();
571
382
  }
572
383
  });
573
384
 
574
385
  //#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 };
386
+ export { buildX402Client as a, error as c, warn as d, appendHistory as f, readHistory as g, formatTxLine as h, walletInfoCommand as i, info as l, displayNetwork as m, fetchEvmBalances as n, resolveWallet as o, calcSpend as p, fetchSolanaBalances as r, dim as s, balanceLine as t, isTTY 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.4.0",
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
  }