x402-bazaar 3.6.0 → 3.6.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.
@@ -1,644 +1,805 @@
1
- import inquirer from 'inquirer';
2
- import ora from 'ora';
3
- import chalk from 'chalk';
4
- import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, chmodSync, cpSync, readdirSync } from 'fs';
5
- import { join, dirname, resolve } from 'path';
6
- import { execSync } from 'child_process';
7
- import { randomBytes } from 'crypto';
8
- import { fileURLToPath } from 'url';
9
- import { log } from '../utils/logger.js';
10
- import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
11
- import { detectEnvironment, getOsLabel, getDefaultInstallDir } from '../detectors/environment.js';
12
- import { generateMcpConfig } from '../generators/mcp-config.js';
13
- import { generateEnvContent } from '../generators/env-file.js';
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
-
18
- // Possible locations for the MCP server source
19
- const MCP_SERVER_CANDIDATES = [
20
- join(__dirname, '..', '..', '..', 'x402-bazaar', 'mcp-server.mjs'), // Monorepo layout
21
- join(__dirname, '..', '..', 'mcp-server.mjs'), // Bundled with CLI
22
- join(process.cwd(), 'mcp-server.mjs'), // Current directory
23
- join(process.cwd(), 'x402-bazaar', 'mcp-server.mjs'), // Subdirectory
24
- ];
25
-
26
- function findMcpServerSource() {
27
- for (const candidate of MCP_SERVER_CANDIDATES) {
28
- if (existsSync(candidate)) return candidate;
29
- }
30
- return null;
31
- }
32
-
33
- export async function initCommand(options) {
34
- // Node version check
35
- const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
36
- if (nodeVersion < 18) {
37
- log.error(`Node.js >= 18 is required (you have ${process.versions.node}).`);
38
- log.dim(' Download the latest version at https://nodejs.org');
39
- process.exit(1);
40
- }
41
-
42
- log.banner();
43
-
44
- if (!isInteractive()) {
45
- log.info(chalk.yellow('Non-interactive terminal detected. Using smart defaults.'));
46
- }
47
-
48
- const osLabel = getOsLabel();
49
- log.info(`OS: ${chalk.bold(osLabel)} | Node: ${chalk.bold(process.versions.node)}`);
50
- log.separator();
51
- console.log('');
52
-
53
- // ─── Step 1: Detect Environment ─────────────────────────────────────
54
- log.step(1, 'Detecting AI client environment...');
55
- console.log('');
56
-
57
- const environments = detectEnvironment();
58
- const detected = environments.filter(e => e.detected);
59
-
60
- let targetEnv;
61
-
62
- if (options.env) {
63
- targetEnv = environments.find(e => e.name === options.env) || {
64
- name: options.env,
65
- label: options.env,
66
- configPath: null,
67
- detected: false,
68
- };
69
- log.info(`Using: ${chalk.bold(targetEnv.label)}`);
70
- } else if (detected.length === 1) {
71
- targetEnv = detected[0];
72
- log.success(`Auto-detected: ${chalk.bold(targetEnv.label)}`);
73
- if (targetEnv.configPath) {
74
- log.dim(` Config: ${targetEnv.configPath}`);
75
- }
76
- } else {
77
- if (detected.length > 1) {
78
- log.info(`Found ${detected.length} AI clients.`);
79
- } else {
80
- log.warn('No AI client detected automatically.');
81
- }
82
-
83
- const defaultEnv = detected.length > 0 ? detected[0].name : 'claude-desktop';
84
-
85
- const choices = [
86
- ...environments.map(e => ({
87
- name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
88
- value: e.name,
89
- })),
90
- ...(isInteractive() ? [new inquirer.Separator()] : []),
91
- { name: 'Generic (I\'ll configure manually)', value: 'generic' },
92
- ];
93
-
94
- const { env } = await promptOrDefault([{
95
- type: 'list',
96
- name: 'env',
97
- message: 'Which AI client are you using?',
98
- choices,
99
- default: defaultEnv,
100
- }]);
101
-
102
- targetEnv = environments.find(e => e.name === env) || {
103
- name: env, label: env, configPath: null, detected: false,
104
- };
105
- }
106
-
107
- console.log('');
108
-
109
- // ─── Step 2: Install MCP Server ─────────────────────────────────────
110
- log.step(2, 'Setting up MCP server files...');
111
- console.log('');
112
-
113
- const installDir = getDefaultInstallDir();
114
- log.info(`Install directory: ${chalk.dim(installDir)}`);
115
-
116
- const spinner = ora('Creating directory and copying files...').start();
117
-
118
- try {
119
- if (!existsSync(installDir)) {
120
- mkdirSync(installDir, { recursive: true });
121
- }
122
-
123
- const mcpServerDest = join(installDir, 'mcp-server.mjs');
124
- const mcpSource = findMcpServerSource();
125
-
126
- if (mcpSource) {
127
- copyFileSync(mcpSource, mcpServerDest);
128
- spinner.text = 'Copied mcp-server.mjs from local project...';
129
-
130
- // Copy lib/ and schemas/ directories (required by mcp-server.mjs)
131
- const mcpSourceDir = dirname(mcpSource);
132
- for (const subdir of ['lib', 'schemas']) {
133
- const srcDir = join(mcpSourceDir, subdir);
134
- const destDir = join(installDir, subdir);
135
- if (existsSync(srcDir)) {
136
- cpSync(srcDir, destDir, { recursive: true });
137
- spinner.text = `Copied ${subdir}/ directory...`;
138
- }
139
- }
140
- } else {
141
- spinner.text = 'Downloading MCP server from GitHub...';
142
- try {
143
- const dlRes = await fetch('https://raw.githubusercontent.com/Wintyx57/x402-backend/main/mcp-server.mjs');
144
- if (!dlRes.ok) throw new Error(`HTTP ${dlRes.status}`);
145
- writeFileSync(mcpServerDest, await dlRes.text());
146
-
147
- // Download lib/protocolAdapter.js (required dependency)
148
- const libDir = join(installDir, 'lib');
149
- if (!existsSync(libDir)) mkdirSync(libDir, { recursive: true });
150
- for (const libFile of ['protocolAdapter.js']) {
151
- const libRes = await fetch(`https://raw.githubusercontent.com/Wintyx57/x402-backend/main/lib/${libFile}`);
152
- if (libRes.ok) writeFileSync(join(libDir, libFile), await libRes.text());
153
- }
154
- } catch (dlErr) {
155
- spinner.fail(`Could not download MCP server: ${dlErr.message}`);
156
- log.error('Download the file manually from:');
157
- log.dim(' https://github.com/Wintyx57/x402-backend/blob/main/mcp-server.mjs');
158
- log.dim(` Save it to: ${mcpServerDest}`);
159
- process.exit(1);
160
- }
161
- }
162
-
163
- // Create package.json for the MCP server runtime
164
- const pkgJsonPath = join(installDir, 'package.json');
165
- if (!existsSync(pkgJsonPath)) {
166
- writeFileSync(pkgJsonPath, JSON.stringify({
167
- name: 'x402-bazaar-mcp',
168
- version: '2.6.0',
169
- type: 'commonjs',
170
- private: true,
171
- dependencies: {
172
- 'viem': '^2.45.0',
173
- '@modelcontextprotocol/sdk': '^1.27.0',
174
- 'dotenv': '^17.3.0',
175
- 'zod': '^4.3.6',
176
- 'ed2curve': '^0.3.0',
177
- '@scure/bip32': '^1.6.0',
178
- },
179
- }, null, 2));
180
- }
181
-
182
- // Create .gitignore in install dir
183
- const gitignorePath = join(installDir, '.gitignore');
184
- if (!existsSync(gitignorePath)) {
185
- writeFileSync(gitignorePath, 'node_modules/\n.env\n*.seed.json\n');
186
- }
187
-
188
- spinner.succeed('MCP server files ready');
189
- } catch (err) {
190
- spinner.fail(`Failed to set up files: ${err.message}`);
191
- log.error('You may need to create the directory manually:');
192
- log.dim(` mkdir "${installDir}"`);
193
- log.dim('Then re-run: npx x402-bazaar init');
194
- process.exit(1);
195
- }
196
-
197
- // npm install in the install directory
198
- const spinnerNpm = ora('Installing dependencies (this may take a minute)...').start();
199
- try {
200
- execSync('npm install --no-fund --no-audit', {
201
- cwd: installDir,
202
- stdio: 'pipe',
203
- timeout: 180000,
204
- });
205
- spinnerNpm.succeed('Dependencies installed');
206
- } catch (err) {
207
- spinnerNpm.warn('npm install had issues');
208
- log.dim(` You can install manually: cd "${installDir}" && npm install`);
209
- }
210
-
211
- console.log('');
212
-
213
- // ─── Step 3: Network & Wallet Configuration ────────────────────────
214
- log.step(3, 'Configuring network and wallet...');
215
- console.log('');
216
-
217
- let walletMode = 'readonly';
218
- let agentPrivateKey = '';
219
- let coinbaseApiKey = '';
220
- let coinbaseApiSecret = '';
221
- let maxBudget = '1.00';
222
- let network = 'skale';
223
- let serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
224
-
225
- if (options.wallet === false) {
226
- log.info('Skipping wallet setup (--no-wallet)');
227
- walletMode = 'readonly';
228
- } else {
229
- // Network & Budget first — needed for wallet funding instructions
230
- const configOverrides = {};
231
- if (options.network) configOverrides.network = options.network;
232
- if (options.budget) configOverrides.maxBudget = options.budget;
233
-
234
- const configAnswers = await promptOrDefault([
235
- {
236
- type: 'list',
237
- name: 'network',
238
- message: 'Which network?',
239
- choices: [
240
- { name: 'SKALE on Base (ultra-low gas ~$0.0007/tx recommended for AI agents)', value: 'skale' },
241
- { name: 'Polygon (gas-free via x402 facilitator no POL needed)', value: 'polygon' },
242
- { name: 'Base Mainnet (real USDC, requires ETH for gas)', value: 'mainnet' },
243
- { name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
244
- ],
245
- default: 'skale',
246
- },
247
- {
248
- type: 'input',
249
- name: 'maxBudget',
250
- message: 'Max USDC budget per session (safety limit):',
251
- default: '1.00',
252
- validate: (v) => {
253
- const n = parseFloat(v);
254
- if (isNaN(n) || n <= 0) return 'Must be a positive number';
255
- if (n > 100) return 'Maximum is 100 USDC per session';
256
- return true;
257
- },
258
- },
259
- ], configOverrides);
260
-
261
- network = configAnswers.network;
262
- maxBudget = configAnswers.maxBudget;
263
-
264
- console.log('');
265
-
266
- const { mode } = await promptOrDefault([{
267
- type: 'list',
268
- name: 'mode',
269
- message: 'How do you want to configure payments?',
270
- choices: [
271
- {
272
- name: `${chalk.bold('Generate a new wallet')} — Creates a fresh Ethereum wallet automatically (Recommended)`,
273
- value: 'generate',
274
- },
275
- {
276
- name: `${chalk.bold('Import private key')} — Use an existing Ethereum private key`,
277
- value: 'import',
278
- },
279
- {
280
- name: `${chalk.bold('Coinbase API keys')} Legacy: use Coinbase CDP seed file`,
281
- value: 'coinbase',
282
- },
283
- {
284
- name: `${chalk.bold('Read-only mode')} Browse marketplace for free (no payments)`,
285
- value: 'readonly',
286
- },
287
- ],
288
- default: 'generate',
289
- }]);
290
-
291
- walletMode = mode;
292
-
293
- if (mode === 'generate') {
294
- agentPrivateKey = '0x' + randomBytes(32).toString('hex');
295
- log.success('New wallet generated!');
296
-
297
- // Derive address using viem (installed in step 2)
298
- // Pass private key via env var to avoid shell exposure (ps aux, bash history)
299
- let walletAddress = '';
300
- try {
301
- walletAddress = execSync(
302
- `node --input-type=module -e "import{privateKeyToAccount}from'viem/accounts';console.log(privateKeyToAccount(process.env._X402_PK).address)"`,
303
- { cwd: installDir, stdio: 'pipe', timeout: 15000, env: { ...process.env, _X402_PK: agentPrivateKey } }
304
- ).toString().trim();
305
- console.log('');
306
- log.info(`Wallet address: ${chalk.bold(walletAddress)}`);
307
- if (network === 'skale') {
308
- log.dim(` Explorer: https://skale-base-explorer.skalenodes.com/address/${walletAddress}`);
309
- } else if (network === 'polygon') {
310
- log.dim(` PolygonScan: https://polygonscan.com/address/${walletAddress}`);
311
- } else {
312
- log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
313
- }
314
- console.log('');
315
- log.separator();
316
- if (network === 'skale') {
317
- log.info(chalk.bold('To activate payments, fund this wallet:'));
318
- console.log('');
319
- log.dim(` ${chalk.white('1.')} Bridge USDC from any chain SKALE in 1 click:`);
320
- log.dim(` ${chalk.cyan('https://x402bazaar.org/fund')} (Trails SDK ETH, Polygon, Base, Arbitrum, Optimism)`);
321
- log.dim(` ${chalk.white('2.')} Or send ${chalk.bold('USDC')} directly to: ${chalk.hex('#34D399')(walletAddress)}`);
322
- log.dim(` (Even $1 USDC is enough — each API call costs $0.005-$0.05)`);
323
- log.dim(` ${chalk.white('3.')} Gas (CREDITS) is auto-funded — no ETH needed on SKALE!`);
324
- console.log('');
325
- log.warn(`IMPORTANT: Send USDC on ${chalk.bold('SKALE on Base')} (chain ID 1187947933) — not Base or Ethereum!`);
326
- } else if (network === 'polygon') {
327
- log.info(chalk.bold('To activate payments, fund this wallet:'));
328
- console.log('');
329
- log.dim(` ${chalk.white('1.')} Bridge USDC from any chain → Polygon via:`);
330
- log.dim(` ${chalk.cyan('https://jumper.exchange')} or ${chalk.cyan('https://x402bazaar.org/fund')}`);
331
- log.dim(` ${chalk.white('2.')} Or send ${chalk.bold('USDC')} directly to: ${chalk.hex('#34D399')(walletAddress)}`);
332
- log.dim(` (Even $1 USDC is enough — each API call costs $0.005-$0.05)`);
333
- log.dim(` ${chalk.white('3.')} ${chalk.bold('No gas needed!')} The x402 facilitator sponsors gas via PIP-82`);
334
- console.log('');
335
- log.warn(`IMPORTANT: Send ${chalk.bold('native USDC')} on ${chalk.bold('Polygon')} (chain ID 137) — not USDC.e!`);
336
- } else {
337
- log.info(chalk.bold('To activate payments, fund this wallet:'));
338
- console.log('');
339
- log.dim(` ${chalk.white('1.')} Bridge USDC from any chain → Base in 1 click:`);
340
- log.dim(` ${chalk.cyan('https://x402bazaar.org/fund')} (Trails SDK ETH, Polygon, Arbitrum, Optimism)`);
341
- log.dim(` ${chalk.white('2.')} Or send ${chalk.bold('USDC')} directly to: ${chalk.hex('#34D399')(walletAddress)}`);
342
- log.dim(` (Even $1 USDC is enough to start — each API call costs $0.005-$0.05)`);
343
- log.dim(` ${chalk.white('3.')} Send a tiny bit of ${chalk.bold('ETH')} to the same address for gas`);
344
- log.dim(` (${chalk.white('~$0.01 of ETH on Base')} is enough for hundreds of transactions)`);
345
- console.log('');
346
- log.warn(`IMPORTANT: Send on the ${chalk.bold('Base')} network only — not Ethereum mainnet!`);
347
- }
348
- log.separator();
349
- } catch {
350
- log.info('Wallet address will be shown when you first use the MCP server.');
351
- log.dim(' Use the get_wallet_balance tool to see your address.');
352
- }
353
- }
354
-
355
- if (mode === 'import') {
356
- if (!isInteractive()) {
357
- log.warn('Cannot enter private key in a non-interactive terminal.');
358
- log.info('Set AGENT_PRIVATE_KEY in your .env file manually after setup.');
359
- walletMode = 'readonly';
360
- } else {
361
- const { key } = await promptOrDefault([{
362
- type: 'password',
363
- name: 'key',
364
- message: 'Ethereum private key (0x...):',
365
- mask: '*',
366
- validate: (v) => {
367
- const trimmed = v.trim();
368
- if (trimmed.length === 0) return 'Private key is required';
369
- const hex = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
370
- if (!/^[0-9a-fA-F]{64}$/.test(hex)) return 'Invalid private key (expected 64 hex characters)';
371
- return true;
372
- },
373
- }]);
374
- agentPrivateKey = key.trim().startsWith('0x') ? key.trim() : `0x${key.trim()}`;
375
- log.success('Private key imported.');
376
- }
377
- }
378
-
379
- if (mode === 'coinbase') {
380
- if (!isInteractive()) {
381
- log.warn('Cannot enter API credentials in a non-interactive terminal.');
382
- log.info('Falling back to read-only mode.');
383
- walletMode = 'readonly';
384
- } else {
385
- log.dim(' Legacy mode: requires Coinbase CDP API keys + agent-seed.json');
386
- console.log('');
387
- const walletAnswers = await promptOrDefault([
388
- {
389
- type: 'input',
390
- name: 'coinbaseApiKey',
391
- message: 'Coinbase API Key (from portal.cdp.coinbase.com):',
392
- validate: (v) => v.trim().length > 0 || 'API key is required',
393
- },
394
- {
395
- type: 'password',
396
- name: 'coinbaseApiSecret',
397
- message: 'Coinbase API Secret:',
398
- mask: '*',
399
- validate: (v) => v.trim().length > 0 || 'API secret is required',
400
- },
401
- ]);
402
- coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
403
- coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
404
-
405
- const existingSeed = join(installDir, 'agent-seed.json');
406
- if (!existsSync(existingSeed)) {
407
- log.warn('No agent-seed.json found.');
408
- log.dim(' Copy your existing agent-seed.json to: ' + installDir);
409
- log.dim(' Or re-run init and choose "Generate a new wallet" instead.');
410
- }
411
- }
412
- }
413
- }
414
-
415
- console.log('');
416
-
417
- // ─── Step 4: Generate Config ────────────────────────────────────────
418
- log.step(4, 'Generating configuration...');
419
- console.log('');
420
-
421
- const config = generateMcpConfig({
422
- environment: targetEnv.name,
423
- installDir,
424
- serverUrl,
425
- maxBudget,
426
- network,
427
- agentPrivateKey,
428
- coinbaseApiKey,
429
- coinbaseApiSecret,
430
- readOnly: walletMode === 'readonly',
431
- });
432
-
433
- // Write .env file in install dir
434
- if (walletMode !== 'readonly') {
435
- const envContent = generateEnvContent({
436
- serverUrl,
437
- maxBudget,
438
- network,
439
- agentPrivateKey,
440
- coinbaseApiKey,
441
- coinbaseApiSecret,
442
- });
443
- const envPath = join(installDir, '.env');
444
- writeFileSync(envPath, envContent);
445
- try { chmodSync(envPath, 0o600); } catch {}
446
- log.success(`.env created at ${chalk.dim(envPath)}`);
447
- }
448
-
449
- // Write or merge config into the AI client config file
450
- if (targetEnv.configPath) {
451
- const configWritten = writeConfig(targetEnv, config);
452
- if (configWritten) {
453
- log.success(`Config written to ${chalk.dim(targetEnv.configPath)}`);
454
- }
455
- } else {
456
- log.info('Generated MCP config copy this into your client config:');
457
- console.log('');
458
- console.log(chalk.dim(JSON.stringify(config, null, 2)));
459
- }
460
-
461
- console.log('');
462
-
463
- // ─── Step 5: Verify Connection ──────────────────────────────────────
464
- log.step(5, 'Verifying connection to x402 Bazaar...');
465
- console.log('');
466
-
467
- const spinnerCheck = ora('Connecting to marketplace...').start();
468
- let serverOnline = false;
469
- let serviceCount = 0;
470
-
471
- try {
472
- const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(10000) });
473
- const data = await res.json();
474
- spinnerCheck.succeed(`Connected! Server is online (${data.network})`);
475
- serverOnline = true;
476
- } catch (err) {
477
- spinnerCheck.warn('Could not reach server');
478
- log.dim(' The server may be sleeping (Render free tier wakes up in ~30s).');
479
- log.dim(' Run: npx x402-bazaar status');
480
- }
481
-
482
- if (serverOnline) {
483
- try {
484
- const res = await fetch(serverUrl, { signal: AbortSignal.timeout(10000) });
485
- const data = await res.json();
486
- serviceCount = data.total_services || 0;
487
- log.dim(` Marketplace: ${data.name} ${serviceCount} services available`);
488
- } catch {
489
- // silent
490
- }
491
- }
492
-
493
- console.log('');
494
- log.separator();
495
-
496
- // ─── Summary ────────────────────────────────────────────────────────
497
- console.log('');
498
- log.success(chalk.bold('Setup complete!'));
499
- console.log('');
500
-
501
- const restartMsg = {
502
- 'claude-desktop': 'Restart Claude Desktop to activate the MCP server.',
503
- 'cursor': 'Restart Cursor to activate the MCP server.',
504
- 'claude-code': 'Restart Claude Code to activate the MCP server.',
505
- };
506
-
507
- const walletLabel = walletMode === 'readonly'
508
- ? 'Read-only (no payments)'
509
- : walletMode === 'generate'
510
- ? 'New wallet (needs funding)'
511
- : 'Configured';
512
-
513
- const summaryLines = [
514
- `Environment: ${targetEnv.label}`,
515
- `Install dir: ${installDir}`,
516
- `Server: ${serverUrl}`,
517
- `Network: ${network === 'mainnet' ? 'Base Mainnet' : network === 'skale' ? 'SKALE on Base' : network === 'polygon' ? 'Polygon' : 'Base Sepolia'}`,
518
- `Budget limit: ${maxBudget} USDC / session`,
519
- `Wallet: ${walletLabel}`,
520
- `Services: ${serviceCount > 0 ? serviceCount + ' available' : 'check with npx x402-bazaar status'}`,
521
- '',
522
- restartMsg[targetEnv.name] || 'Configure your AI client with the generated JSON above.',
523
- '',
524
- ...(walletMode === 'generate' ? [
525
- 'Before your agent can pay for APIs:',
526
- network === 'skale'
527
- ? ' 1. Bridge USDC → SKALE on Base: https://x402bazaar.org/fund (CREDITS auto-funded!)'
528
- : network === 'polygon'
529
- ? ' 1. Send USDC to your wallet on Polygon (gas-free via x402 facilitator!)'
530
- : ' 1. Send USDC + a little ETH to your wallet on Base',
531
- ' 2. Restart your IDE',
532
- '',
533
- ] : []),
534
- 'Then try asking your agent:',
535
- ' "Search for weather APIs on x402 Bazaar"',
536
- ' "List all available services on the marketplace"',
537
- ];
538
-
539
- log.box('What\'s next?', summaryLines.join('\n'));
540
-
541
- log.dim(' Need help? https://x402bazaar.org');
542
- log.dim(' Re-configure: npx x402-bazaar init');
543
- console.log('');
544
-
545
- printNonInteractiveHint('init');
546
-
547
- // === CRITICAL: Print AFTER everything else so it's the LAST thing in output ===
548
- // Claude Code AI summarizes tool output — only tail lines are reliably shown
549
- if (walletMode === 'generate' && network === 'skale') {
550
- console.log('');
551
- console.log('='.repeat(70));
552
- console.log('');
553
- console.log(' IMPORTANT — After restarting, ask your agent to run: setup_wallet');
554
- console.log('');
555
- console.log(' This will:');
556
- console.log(' - Auto-fund 0.1 CREDITS for gas on SKALE (free, no ETH needed)');
557
- console.log(' - Show your wallet balance on both Base and SKALE');
558
- console.log(' - Provide bridge links to fund your wallet with USDC');
559
- console.log('');
560
- console.log(' Fund USDC: https://x402bazaar.org/fund (bridge from any chain)');
561
- console.log(' Alternative bridge: https://bridge.skale.space');
562
- console.log('');
563
- console.log('='.repeat(70));
564
- console.log('');
565
- } else if (walletMode === 'generate' && network === 'polygon') {
566
- console.log('');
567
- console.log('='.repeat(70));
568
- console.log('');
569
- console.log(' IMPORTANT — After restarting, ask your agent to run: setup_wallet');
570
- console.log('');
571
- console.log(' This will:');
572
- console.log(' - Show your wallet balance on Base, SKALE, and Polygon');
573
- console.log(' - Provide bridge links to fund your wallet with USDC');
574
- console.log('');
575
- console.log(' Polygon uses the x402 facilitator — NO gas needed (PIP-82)!');
576
- console.log(' Just send USDC to your wallet on Polygon.');
577
- console.log(' Bridge: https://jumper.exchange or https://x402bazaar.org/fund');
578
- console.log('');
579
- console.log('='.repeat(70));
580
- console.log('');
581
- } else if (walletMode === 'generate') {
582
- console.log('');
583
- console.log('='.repeat(70));
584
- console.log('');
585
- console.log(' IMPORTANT — After restarting, ask your agent to run: setup_wallet');
586
- console.log(' Then fund your wallet with USDC + ETH on Base.');
587
- console.log(' Bridge: https://x402bazaar.org/fund');
588
- console.log('');
589
- console.log('='.repeat(70));
590
- console.log('');
591
- }
592
- }
593
-
594
- /**
595
- * Write config to the target AI client config file.
596
- * Merges with existing config if file already exists.
597
- */
598
- function writeConfig(envInfo, newConfig) {
599
- try {
600
- const configPath = envInfo.configPath;
601
- const configDir = dirname(configPath);
602
-
603
- if (!existsSync(configDir)) {
604
- mkdirSync(configDir, { recursive: true });
605
- }
606
-
607
- let finalConfig = newConfig;
608
-
609
- if (existsSync(configPath)) {
610
- try {
611
- const existing = JSON.parse(readFileSync(configPath, 'utf-8'));
612
-
613
- if (envInfo.name === 'vscode-continue') {
614
- if (!existing.mcpServers) existing.mcpServers = [];
615
- const idx = existing.mcpServers.findIndex(s => s.name === 'x402-bazaar');
616
- if (idx >= 0) {
617
- existing.mcpServers[idx] = newConfig.mcpServers[0];
618
- } else {
619
- existing.mcpServers.push(newConfig.mcpServers[0]);
620
- }
621
- finalConfig = existing;
622
- } else {
623
- if (!existing.mcpServers) existing.mcpServers = {};
624
- existing.mcpServers['x402-bazaar'] = newConfig.mcpServers['x402-bazaar'];
625
- finalConfig = existing;
626
- }
627
-
628
- log.info('Merged with existing config file');
629
- } catch {
630
- log.warn('Could not parse existing config — creating new file');
631
- }
632
- }
633
-
634
- writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
635
- return true;
636
- } catch (err) {
637
- log.error(`Could not write config: ${err.message}`);
638
- log.info('Copy this JSON manually into your config file:');
639
- console.log('');
640
- console.log(JSON.stringify(newConfig, null, 2));
641
- return false;
642
- }
643
- }
644
-
1
+ import inquirer from "inquirer";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ readFileSync,
9
+ copyFileSync,
10
+ chmodSync,
11
+ cpSync,
12
+ readdirSync,
13
+ } from "fs";
14
+ import { join, dirname, resolve } from "path";
15
+ import { execSync } from "child_process";
16
+ import { randomBytes } from "crypto";
17
+ import { fileURLToPath } from "url";
18
+ import { log } from "../utils/logger.js";
19
+ import {
20
+ isInteractive,
21
+ promptOrDefault,
22
+ printNonInteractiveHint,
23
+ } from "../utils/prompt.js";
24
+ import {
25
+ detectEnvironment,
26
+ getOsLabel,
27
+ getDefaultInstallDir,
28
+ } from "../detectors/environment.js";
29
+ import { generateMcpConfig } from "../generators/mcp-config.js";
30
+ import { generateEnvContent } from "../generators/env-file.js";
31
+
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+
35
+ // Possible locations for the MCP server source
36
+ const MCP_SERVER_CANDIDATES = [
37
+ join(__dirname, "..", "..", "..", "x402-bazaar", "mcp-server.mjs"), // Monorepo layout
38
+ join(__dirname, "..", "..", "mcp-server.mjs"), // Bundled with CLI
39
+ join(process.cwd(), "mcp-server.mjs"), // Current directory
40
+ join(process.cwd(), "x402-bazaar", "mcp-server.mjs"), // Subdirectory
41
+ ];
42
+
43
+ function findMcpServerSource() {
44
+ for (const candidate of MCP_SERVER_CANDIDATES) {
45
+ if (existsSync(candidate)) return candidate;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export async function initCommand(options) {
51
+ // Node version check
52
+ const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
53
+ if (nodeVersion < 18) {
54
+ log.error(`Node.js >= 18 is required (you have ${process.versions.node}).`);
55
+ log.dim(" Download the latest version at https://nodejs.org");
56
+ process.exit(1);
57
+ }
58
+
59
+ log.banner();
60
+
61
+ if (!isInteractive()) {
62
+ log.info(
63
+ chalk.yellow("Non-interactive terminal detected. Using smart defaults."),
64
+ );
65
+ }
66
+
67
+ const osLabel = getOsLabel();
68
+ log.info(
69
+ `OS: ${chalk.bold(osLabel)} | Node: ${chalk.bold(process.versions.node)}`,
70
+ );
71
+ log.separator();
72
+ console.log("");
73
+
74
+ // ─── Step 1: Detect Environment ─────────────────────────────────────
75
+ log.step(1, "Detecting AI client environment...");
76
+ console.log("");
77
+
78
+ const environments = detectEnvironment();
79
+ const detected = environments.filter((e) => e.detected);
80
+
81
+ let targetEnv;
82
+
83
+ if (options.env) {
84
+ targetEnv = environments.find((e) => e.name === options.env) || {
85
+ name: options.env,
86
+ label: options.env,
87
+ configPath: null,
88
+ detected: false,
89
+ };
90
+ log.info(`Using: ${chalk.bold(targetEnv.label)}`);
91
+ } else if (detected.length === 1) {
92
+ targetEnv = detected[0];
93
+ log.success(`Auto-detected: ${chalk.bold(targetEnv.label)}`);
94
+ if (targetEnv.configPath) {
95
+ log.dim(` Config: ${targetEnv.configPath}`);
96
+ }
97
+ } else {
98
+ if (detected.length > 1) {
99
+ log.info(`Found ${detected.length} AI clients.`);
100
+ } else {
101
+ log.warn("No AI client detected automatically.");
102
+ }
103
+
104
+ const defaultEnv =
105
+ detected.length > 0 ? detected[0].name : "claude-desktop";
106
+
107
+ const choices = [
108
+ ...environments.map((e) => ({
109
+ name: `${e.label}${e.detected ? chalk.hex("#34D399")(" (detected)") : ""}`,
110
+ value: e.name,
111
+ })),
112
+ ...(isInteractive() ? [new inquirer.Separator()] : []),
113
+ { name: "Generic (I'll configure manually)", value: "generic" },
114
+ ];
115
+
116
+ const { env } = await promptOrDefault([
117
+ {
118
+ type: "list",
119
+ name: "env",
120
+ message: "Which AI client are you using?",
121
+ choices,
122
+ default: defaultEnv,
123
+ },
124
+ ]);
125
+
126
+ targetEnv = environments.find((e) => e.name === env) || {
127
+ name: env,
128
+ label: env,
129
+ configPath: null,
130
+ detected: false,
131
+ };
132
+ }
133
+
134
+ console.log("");
135
+
136
+ // ─── Step 2: Install MCP Server ─────────────────────────────────────
137
+ log.step(2, "Setting up MCP server files...");
138
+ console.log("");
139
+
140
+ const installDir = getDefaultInstallDir();
141
+ log.info(`Install directory: ${chalk.dim(installDir)}`);
142
+
143
+ const spinner = ora("Creating directory and copying files...").start();
144
+
145
+ try {
146
+ if (!existsSync(installDir)) {
147
+ mkdirSync(installDir, { recursive: true });
148
+ }
149
+
150
+ const mcpServerDest = join(installDir, "mcp-server.mjs");
151
+ const mcpSource = findMcpServerSource();
152
+
153
+ if (mcpSource) {
154
+ copyFileSync(mcpSource, mcpServerDest);
155
+ spinner.text = "Copied mcp-server.mjs from local project...";
156
+
157
+ // Copy lib/ and schemas/ directories (required by mcp-server.mjs)
158
+ const mcpSourceDir = dirname(mcpSource);
159
+ for (const subdir of ["lib", "schemas"]) {
160
+ const srcDir = join(mcpSourceDir, subdir);
161
+ const destDir = join(installDir, subdir);
162
+ if (existsSync(srcDir)) {
163
+ cpSync(srcDir, destDir, { recursive: true });
164
+ spinner.text = `Copied ${subdir}/ directory...`;
165
+ }
166
+ }
167
+ } else {
168
+ spinner.text = "Downloading MCP server from GitHub...";
169
+ try {
170
+ const dlRes = await fetch(
171
+ "https://raw.githubusercontent.com/Wintyx57/x402-backend/main/mcp-server.mjs",
172
+ );
173
+ if (!dlRes.ok) throw new Error(`HTTP ${dlRes.status}`);
174
+ writeFileSync(mcpServerDest, await dlRes.text());
175
+
176
+ // Download lib/ files required by mcp-server.mjs
177
+ const libDir = join(installDir, "lib");
178
+ if (!existsSync(libDir)) mkdirSync(libDir, { recursive: true });
179
+ const requiredLibFiles = [
180
+ "protocolAdapter.js",
181
+ "logger.js",
182
+ "chains.js",
183
+ ];
184
+ for (const libFile of requiredLibFiles) {
185
+ try {
186
+ const libRes = await fetch(
187
+ `https://raw.githubusercontent.com/Wintyx57/x402-backend/main/lib/${libFile}`,
188
+ );
189
+ if (libRes.ok)
190
+ writeFileSync(join(libDir, libFile), await libRes.text());
191
+ } catch {
192
+ /* non-critical — only protocolAdapter.js is required */
193
+ }
194
+ }
195
+ } catch (dlErr) {
196
+ spinner.fail(`Could not download MCP server: ${dlErr.message}`);
197
+ log.error("Download the file manually from:");
198
+ log.dim(
199
+ " https://github.com/Wintyx57/x402-backend/blob/main/mcp-server.mjs",
200
+ );
201
+ log.dim(` Save it to: ${mcpServerDest}`);
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ // Create package.json for the MCP server runtime
207
+ const pkgJsonPath = join(installDir, "package.json");
208
+ if (!existsSync(pkgJsonPath)) {
209
+ writeFileSync(
210
+ pkgJsonPath,
211
+ JSON.stringify(
212
+ {
213
+ name: "x402-bazaar-mcp",
214
+ version: "2.7.0",
215
+ type: "commonjs",
216
+ private: true,
217
+ dependencies: {
218
+ viem: "^2.45.0",
219
+ "@modelcontextprotocol/sdk": "^1.27.0",
220
+ dotenv: "^17.3.0",
221
+ zod: "^4.3.6",
222
+ ed2curve: "^0.3.0",
223
+ "@scure/bip32": "^1.6.0",
224
+ },
225
+ },
226
+ null,
227
+ 2,
228
+ ),
229
+ );
230
+ }
231
+
232
+ // Create .gitignore in install dir
233
+ const gitignorePath = join(installDir, ".gitignore");
234
+ if (!existsSync(gitignorePath)) {
235
+ writeFileSync(gitignorePath, "node_modules/\n.env\n*.seed.json\n");
236
+ }
237
+
238
+ spinner.succeed("MCP server files ready");
239
+ } catch (err) {
240
+ spinner.fail(`Failed to set up files: ${err.message}`);
241
+ log.error("You may need to create the directory manually:");
242
+ log.dim(` mkdir "${installDir}"`);
243
+ log.dim("Then re-run: npx x402-bazaar init");
244
+ process.exit(1);
245
+ }
246
+
247
+ // npm install in the install directory
248
+ const spinnerNpm = ora(
249
+ "Installing dependencies (this may take a minute)...",
250
+ ).start();
251
+ try {
252
+ execSync("npm install --no-fund --no-audit", {
253
+ cwd: installDir,
254
+ stdio: "pipe",
255
+ timeout: 180000,
256
+ });
257
+ spinnerNpm.succeed("Dependencies installed");
258
+ } catch (err) {
259
+ spinnerNpm.warn("npm install had issues");
260
+ log.dim(` You can install manually: cd "${installDir}" && npm install`);
261
+ }
262
+
263
+ console.log("");
264
+
265
+ // ─── Step 3: Network & Wallet Configuration ────────────────────────
266
+ log.step(3, "Configuring network and wallet...");
267
+ console.log("");
268
+
269
+ let walletMode = "readonly";
270
+ let agentPrivateKey = "";
271
+ let coinbaseApiKey = "";
272
+ let coinbaseApiSecret = "";
273
+ let maxBudget = "1.00";
274
+ let network = "skale";
275
+ let serverUrl = options.serverUrl || "https://x402-api.onrender.com";
276
+
277
+ if (options.wallet === false) {
278
+ log.info("Skipping wallet setup (--no-wallet)");
279
+ walletMode = "readonly";
280
+ } else {
281
+ // Network & Budget first — needed for wallet funding instructions
282
+ const configOverrides = {};
283
+ if (options.network) configOverrides.network = options.network;
284
+ if (options.budget) configOverrides.maxBudget = options.budget;
285
+
286
+ const configAnswers = await promptOrDefault(
287
+ [
288
+ {
289
+ type: "list",
290
+ name: "network",
291
+ message: "Which network?",
292
+ choices: [
293
+ {
294
+ name: "SKALE on Base (ultra-low gas ~$0.0007/tx — recommended for AI agents)",
295
+ value: "skale",
296
+ },
297
+ {
298
+ name: "Polygon (gas-free via x402 facilitator no POL needed)",
299
+ value: "polygon",
300
+ },
301
+ {
302
+ name: "Base Mainnet (real USDC, requires ETH for gas)",
303
+ value: "mainnet",
304
+ },
305
+ {
306
+ name: "Base Sepolia (testnet, free tokens for testing)",
307
+ value: "testnet",
308
+ },
309
+ ],
310
+ default: "skale",
311
+ },
312
+ {
313
+ type: "input",
314
+ name: "maxBudget",
315
+ message: "Max USDC budget per session (safety limit):",
316
+ default: "1.00",
317
+ validate: (v) => {
318
+ const n = parseFloat(v);
319
+ if (isNaN(n) || n <= 0) return "Must be a positive number";
320
+ if (n > 100) return "Maximum is 100 USDC per session";
321
+ return true;
322
+ },
323
+ },
324
+ ],
325
+ configOverrides,
326
+ );
327
+
328
+ network = configAnswers.network;
329
+ maxBudget = configAnswers.maxBudget;
330
+
331
+ console.log("");
332
+
333
+ const { mode } = await promptOrDefault([
334
+ {
335
+ type: "list",
336
+ name: "mode",
337
+ message: "How do you want to configure payments?",
338
+ choices: [
339
+ {
340
+ name: `${chalk.bold("Generate a new wallet")} Creates a fresh Ethereum wallet automatically (Recommended)`,
341
+ value: "generate",
342
+ },
343
+ {
344
+ name: `${chalk.bold("Import private key")} Use an existing Ethereum private key`,
345
+ value: "import",
346
+ },
347
+ {
348
+ name: `${chalk.bold("Coinbase API keys")} — Legacy: use Coinbase CDP seed file`,
349
+ value: "coinbase",
350
+ },
351
+ {
352
+ name: `${chalk.bold("Read-only mode")} — Browse marketplace for free (no payments)`,
353
+ value: "readonly",
354
+ },
355
+ ],
356
+ default: "generate",
357
+ },
358
+ ]);
359
+
360
+ walletMode = mode;
361
+
362
+ if (mode === "generate") {
363
+ agentPrivateKey = "0x" + randomBytes(32).toString("hex");
364
+ log.success("New wallet generated!");
365
+
366
+ // Derive address using viem (installed in step 2)
367
+ // Pass private key via env var to avoid shell exposure (ps aux, bash history)
368
+ let walletAddress = "";
369
+ try {
370
+ walletAddress = execSync(
371
+ `node --input-type=module -e "import{privateKeyToAccount}from'viem/accounts';console.log(privateKeyToAccount(process.env._X402_PK).address)"`,
372
+ {
373
+ cwd: installDir,
374
+ stdio: "pipe",
375
+ timeout: 15000,
376
+ env: { ...process.env, _X402_PK: agentPrivateKey },
377
+ },
378
+ )
379
+ .toString()
380
+ .trim();
381
+ console.log("");
382
+ log.info(`Wallet address: ${chalk.bold(walletAddress)}`);
383
+ if (network === "skale") {
384
+ log.dim(
385
+ ` Explorer: https://skale-base-explorer.skalenodes.com/address/${walletAddress}`,
386
+ );
387
+ } else if (network === "polygon") {
388
+ log.dim(
389
+ ` PolygonScan: https://polygonscan.com/address/${walletAddress}`,
390
+ );
391
+ } else {
392
+ log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
393
+ }
394
+ console.log("");
395
+ log.separator();
396
+ if (network === "skale") {
397
+ log.info(chalk.bold("To activate payments, fund this wallet:"));
398
+ console.log("");
399
+ log.dim(
400
+ ` ${chalk.white("1.")} Bridge USDC from any chain → SKALE in 1 click:`,
401
+ );
402
+ log.dim(
403
+ ` ${chalk.cyan("https://x402bazaar.org/fund")} (Trails SDK — ETH, Polygon, Base, Arbitrum, Optimism)`,
404
+ );
405
+ log.dim(
406
+ ` ${chalk.white("2.")} Or send ${chalk.bold("USDC")} directly to: ${chalk.hex("#34D399")(walletAddress)}`,
407
+ );
408
+ log.dim(
409
+ ` (Even $1 USDC is enough each API call costs $0.005-$0.05)`,
410
+ );
411
+ log.dim(
412
+ ` ${chalk.white("3.")} Gas (CREDITS) is auto-funded — no ETH needed on SKALE!`,
413
+ );
414
+ console.log("");
415
+ log.warn(
416
+ `IMPORTANT: Send USDC on ${chalk.bold("SKALE on Base")} (chain ID 1187947933) — not Base or Ethereum!`,
417
+ );
418
+ } else if (network === "polygon") {
419
+ log.info(chalk.bold("To activate payments, fund this wallet:"));
420
+ console.log("");
421
+ log.dim(
422
+ ` ${chalk.white("1.")} Bridge USDC from any chain → Polygon via:`,
423
+ );
424
+ log.dim(
425
+ ` ${chalk.cyan("https://jumper.exchange")} or ${chalk.cyan("https://x402bazaar.org/fund")}`,
426
+ );
427
+ log.dim(
428
+ ` ${chalk.white("2.")} Or send ${chalk.bold("USDC")} directly to: ${chalk.hex("#34D399")(walletAddress)}`,
429
+ );
430
+ log.dim(
431
+ ` (Even $1 USDC is enough — each API call costs $0.005-$0.05)`,
432
+ );
433
+ log.dim(
434
+ ` ${chalk.white("3.")} ${chalk.bold("No gas needed!")} The x402 facilitator sponsors gas via PIP-82`,
435
+ );
436
+ console.log("");
437
+ log.warn(
438
+ `IMPORTANT: Send ${chalk.bold("native USDC")} on ${chalk.bold("Polygon")} (chain ID 137) — not USDC.e!`,
439
+ );
440
+ } else {
441
+ log.info(chalk.bold("To activate payments, fund this wallet:"));
442
+ console.log("");
443
+ log.dim(
444
+ ` ${chalk.white("1.")} Bridge USDC from any chain → Base in 1 click:`,
445
+ );
446
+ log.dim(
447
+ ` ${chalk.cyan("https://x402bazaar.org/fund")} (Trails SDK — ETH, Polygon, Arbitrum, Optimism)`,
448
+ );
449
+ log.dim(
450
+ ` ${chalk.white("2.")} Or send ${chalk.bold("USDC")} directly to: ${chalk.hex("#34D399")(walletAddress)}`,
451
+ );
452
+ log.dim(
453
+ ` (Even $1 USDC is enough to start — each API call costs $0.005-$0.05)`,
454
+ );
455
+ log.dim(
456
+ ` ${chalk.white("3.")} Send a tiny bit of ${chalk.bold("ETH")} to the same address for gas`,
457
+ );
458
+ log.dim(
459
+ ` (${chalk.white("~$0.01 of ETH on Base")} is enough for hundreds of transactions)`,
460
+ );
461
+ console.log("");
462
+ log.warn(
463
+ `IMPORTANT: Send on the ${chalk.bold("Base")} network only — not Ethereum mainnet!`,
464
+ );
465
+ }
466
+ log.separator();
467
+ } catch {
468
+ log.info(
469
+ "Wallet address will be shown when you first use the MCP server.",
470
+ );
471
+ log.dim(" Use the get_wallet_balance tool to see your address.");
472
+ }
473
+ }
474
+
475
+ if (mode === "import") {
476
+ if (!isInteractive()) {
477
+ log.warn("Cannot enter private key in a non-interactive terminal.");
478
+ log.info(
479
+ "Set AGENT_PRIVATE_KEY in your .env file manually after setup.",
480
+ );
481
+ walletMode = "readonly";
482
+ } else {
483
+ const { key } = await promptOrDefault([
484
+ {
485
+ type: "password",
486
+ name: "key",
487
+ message: "Ethereum private key (0x...):",
488
+ mask: "*",
489
+ validate: (v) => {
490
+ const trimmed = v.trim();
491
+ if (trimmed.length === 0) return "Private key is required";
492
+ const hex = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
493
+ if (!/^[0-9a-fA-F]{64}$/.test(hex))
494
+ return "Invalid private key (expected 64 hex characters)";
495
+ return true;
496
+ },
497
+ },
498
+ ]);
499
+ agentPrivateKey = key.trim().startsWith("0x")
500
+ ? key.trim()
501
+ : `0x${key.trim()}`;
502
+ log.success("Private key imported.");
503
+ }
504
+ }
505
+
506
+ if (mode === "coinbase") {
507
+ if (!isInteractive()) {
508
+ log.warn("Cannot enter API credentials in a non-interactive terminal.");
509
+ log.info("Falling back to read-only mode.");
510
+ walletMode = "readonly";
511
+ } else {
512
+ log.dim(
513
+ " Legacy mode: requires Coinbase CDP API keys + agent-seed.json",
514
+ );
515
+ console.log("");
516
+ const walletAnswers = await promptOrDefault([
517
+ {
518
+ type: "input",
519
+ name: "coinbaseApiKey",
520
+ message: "Coinbase API Key (from portal.cdp.coinbase.com):",
521
+ validate: (v) => v.trim().length > 0 || "API key is required",
522
+ },
523
+ {
524
+ type: "password",
525
+ name: "coinbaseApiSecret",
526
+ message: "Coinbase API Secret:",
527
+ mask: "*",
528
+ validate: (v) => v.trim().length > 0 || "API secret is required",
529
+ },
530
+ ]);
531
+ coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
532
+ coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
533
+
534
+ const existingSeed = join(installDir, "agent-seed.json");
535
+ if (!existsSync(existingSeed)) {
536
+ log.warn("No agent-seed.json found.");
537
+ log.dim(" Copy your existing agent-seed.json to: " + installDir);
538
+ log.dim(
539
+ ' Or re-run init and choose "Generate a new wallet" instead.',
540
+ );
541
+ }
542
+ }
543
+ }
544
+ }
545
+
546
+ console.log("");
547
+
548
+ // ─── Step 4: Generate Config ────────────────────────────────────────
549
+ log.step(4, "Generating configuration...");
550
+ console.log("");
551
+
552
+ const config = generateMcpConfig({
553
+ environment: targetEnv.name,
554
+ installDir,
555
+ serverUrl,
556
+ maxBudget,
557
+ network,
558
+ agentPrivateKey,
559
+ coinbaseApiKey,
560
+ coinbaseApiSecret,
561
+ readOnly: walletMode === "readonly",
562
+ });
563
+
564
+ // Write .env file in install dir
565
+ if (walletMode !== "readonly") {
566
+ const envContent = generateEnvContent({
567
+ serverUrl,
568
+ maxBudget,
569
+ network,
570
+ agentPrivateKey,
571
+ coinbaseApiKey,
572
+ coinbaseApiSecret,
573
+ });
574
+ const envPath = join(installDir, ".env");
575
+ writeFileSync(envPath, envContent);
576
+ try {
577
+ chmodSync(envPath, 0o600);
578
+ } catch {}
579
+ log.success(`.env created at ${chalk.dim(envPath)}`);
580
+ }
581
+
582
+ // Write or merge config into the AI client config file
583
+ if (targetEnv.configPath) {
584
+ const configWritten = writeConfig(targetEnv, config);
585
+ if (configWritten) {
586
+ log.success(`Config written to ${chalk.dim(targetEnv.configPath)}`);
587
+ }
588
+ } else {
589
+ log.info("Generated MCP config — copy this into your client config:");
590
+ console.log("");
591
+ console.log(chalk.dim(JSON.stringify(config, null, 2)));
592
+ }
593
+
594
+ console.log("");
595
+
596
+ // ─── Step 5: Verify Connection ──────────────────────────────────────
597
+ log.step(5, "Verifying connection to x402 Bazaar...");
598
+ console.log("");
599
+
600
+ const spinnerCheck = ora("Connecting to marketplace...").start();
601
+ let serverOnline = false;
602
+ let serviceCount = 0;
603
+
604
+ try {
605
+ const res = await fetch(`${serverUrl}/health`, {
606
+ signal: AbortSignal.timeout(10000),
607
+ });
608
+ const data = await res.json();
609
+ spinnerCheck.succeed(`Connected! Server is online (${data.network})`);
610
+ serverOnline = true;
611
+ } catch (err) {
612
+ spinnerCheck.warn("Could not reach server");
613
+ log.dim(
614
+ " The server may be sleeping (Render free tier wakes up in ~30s).",
615
+ );
616
+ log.dim(" Run: npx x402-bazaar status");
617
+ }
618
+
619
+ if (serverOnline) {
620
+ try {
621
+ const res = await fetch(serverUrl, {
622
+ signal: AbortSignal.timeout(10000),
623
+ });
624
+ const data = await res.json();
625
+ serviceCount = data.total_services || 0;
626
+ log.dim(
627
+ ` Marketplace: ${data.name} — ${serviceCount} services available`,
628
+ );
629
+ } catch {
630
+ // silent
631
+ }
632
+ }
633
+
634
+ console.log("");
635
+ log.separator();
636
+
637
+ // ─── Summary ────────────────────────────────────────────────────────
638
+ console.log("");
639
+ log.success(chalk.bold("Setup complete!"));
640
+ console.log("");
641
+
642
+ const restartMsg = {
643
+ "claude-desktop": "Restart Claude Desktop to activate the MCP server.",
644
+ cursor: "Restart Cursor to activate the MCP server.",
645
+ "claude-code": "Restart Claude Code to activate the MCP server.",
646
+ };
647
+
648
+ const walletLabel =
649
+ walletMode === "readonly"
650
+ ? "Read-only (no payments)"
651
+ : walletMode === "generate"
652
+ ? "New wallet (needs funding)"
653
+ : "Configured";
654
+
655
+ const summaryLines = [
656
+ `Environment: ${targetEnv.label}`,
657
+ `Install dir: ${installDir}`,
658
+ `Server: ${serverUrl}`,
659
+ `Network: ${network === "mainnet" ? "Base Mainnet" : network === "skale" ? "SKALE on Base" : network === "polygon" ? "Polygon" : "Base Sepolia"}`,
660
+ `Budget limit: ${maxBudget} USDC / session`,
661
+ `Wallet: ${walletLabel}`,
662
+ `Services: ${serviceCount > 0 ? serviceCount + " available" : "check with npx x402-bazaar status"}`,
663
+ "",
664
+ restartMsg[targetEnv.name] ||
665
+ "Configure your AI client with the generated JSON above.",
666
+ "",
667
+ ...(walletMode === "generate"
668
+ ? [
669
+ "Before your agent can pay for APIs:",
670
+ network === "skale"
671
+ ? " 1. Bridge USDC → SKALE on Base: https://x402bazaar.org/fund (CREDITS auto-funded!)"
672
+ : network === "polygon"
673
+ ? " 1. Send USDC to your wallet on Polygon (gas-free via x402 facilitator!)"
674
+ : " 1. Send USDC + a little ETH to your wallet on Base",
675
+ " 2. Restart your IDE",
676
+ "",
677
+ ]
678
+ : []),
679
+ "Then try asking your agent:",
680
+ ' "Search for weather APIs on x402 Bazaar"',
681
+ ' "List all available services on the marketplace"',
682
+ ];
683
+
684
+ log.box("What's next?", summaryLines.join("\n"));
685
+
686
+ log.dim(" Need help? https://x402bazaar.org");
687
+ log.dim(" Re-configure: npx x402-bazaar init");
688
+ console.log("");
689
+
690
+ printNonInteractiveHint("init");
691
+
692
+ // === CRITICAL: Print AFTER everything else so it's the LAST thing in output ===
693
+ // Claude Code AI summarizes tool output — only tail lines are reliably shown
694
+ if (walletMode === "generate" && network === "skale") {
695
+ console.log("");
696
+ console.log("=".repeat(70));
697
+ console.log("");
698
+ console.log(
699
+ " IMPORTANT — After restarting, ask your agent to run: setup_wallet",
700
+ );
701
+ console.log("");
702
+ console.log(" This will:");
703
+ console.log(
704
+ " - Auto-fund 0.1 CREDITS for gas on SKALE (free, no ETH needed)",
705
+ );
706
+ console.log(" - Show your wallet balance on both Base and SKALE");
707
+ console.log(" - Provide bridge links to fund your wallet with USDC");
708
+ console.log("");
709
+ console.log(
710
+ " Fund USDC: https://x402bazaar.org/fund (bridge from any chain)",
711
+ );
712
+ console.log(" Alternative bridge: https://bridge.skale.space");
713
+ console.log("");
714
+ console.log("=".repeat(70));
715
+ console.log("");
716
+ } else if (walletMode === "generate" && network === "polygon") {
717
+ console.log("");
718
+ console.log("=".repeat(70));
719
+ console.log("");
720
+ console.log(
721
+ " IMPORTANT — After restarting, ask your agent to run: setup_wallet",
722
+ );
723
+ console.log("");
724
+ console.log(" This will:");
725
+ console.log(" - Show your wallet balance on Base, SKALE, and Polygon");
726
+ console.log(" - Provide bridge links to fund your wallet with USDC");
727
+ console.log("");
728
+ console.log(
729
+ " Polygon uses the x402 facilitator — NO gas needed (PIP-82)!",
730
+ );
731
+ console.log(" Just send USDC to your wallet on Polygon.");
732
+ console.log(
733
+ " Bridge: https://jumper.exchange or https://x402bazaar.org/fund",
734
+ );
735
+ console.log("");
736
+ console.log("=".repeat(70));
737
+ console.log("");
738
+ } else if (walletMode === "generate") {
739
+ console.log("");
740
+ console.log("=".repeat(70));
741
+ console.log("");
742
+ console.log(
743
+ " IMPORTANT — After restarting, ask your agent to run: setup_wallet",
744
+ );
745
+ console.log(" Then fund your wallet with USDC + ETH on Base.");
746
+ console.log(" Bridge: https://x402bazaar.org/fund");
747
+ console.log("");
748
+ console.log("=".repeat(70));
749
+ console.log("");
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Write config to the target AI client config file.
755
+ * Merges with existing config if file already exists.
756
+ */
757
+ function writeConfig(envInfo, newConfig) {
758
+ try {
759
+ const configPath = envInfo.configPath;
760
+ const configDir = dirname(configPath);
761
+
762
+ if (!existsSync(configDir)) {
763
+ mkdirSync(configDir, { recursive: true });
764
+ }
765
+
766
+ let finalConfig = newConfig;
767
+
768
+ if (existsSync(configPath)) {
769
+ try {
770
+ const existing = JSON.parse(readFileSync(configPath, "utf-8"));
771
+
772
+ if (envInfo.name === "vscode-continue") {
773
+ if (!existing.mcpServers) existing.mcpServers = [];
774
+ const idx = existing.mcpServers.findIndex(
775
+ (s) => s.name === "x402-bazaar",
776
+ );
777
+ if (idx >= 0) {
778
+ existing.mcpServers[idx] = newConfig.mcpServers[0];
779
+ } else {
780
+ existing.mcpServers.push(newConfig.mcpServers[0]);
781
+ }
782
+ finalConfig = existing;
783
+ } else {
784
+ if (!existing.mcpServers) existing.mcpServers = {};
785
+ existing.mcpServers["x402-bazaar"] =
786
+ newConfig.mcpServers["x402-bazaar"];
787
+ finalConfig = existing;
788
+ }
789
+
790
+ log.info("Merged with existing config file");
791
+ } catch {
792
+ log.warn("Could not parse existing config — creating new file");
793
+ }
794
+ }
795
+
796
+ writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
797
+ return true;
798
+ } catch (err) {
799
+ log.error(`Could not write config: ${err.message}`);
800
+ log.info("Copy this JSON manually into your config file:");
801
+ console.log("");
802
+ console.log(JSON.stringify(newConfig, null, 2));
803
+ return false;
804
+ }
805
+ }