x402-bazaar 1.0.0 → 1.2.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/bin/cli.js CHANGED
@@ -21,7 +21,7 @@ const program = new Command();
21
21
  program
22
22
  .name('x402-bazaar')
23
23
  .description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
24
- .version('1.0.0');
24
+ .version('1.2.0');
25
25
 
26
26
  program
27
27
  .command('init')
@@ -29,6 +29,8 @@ program
29
29
  .option('--env <environment>', 'Force environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
30
30
  .option('--no-wallet', 'Skip wallet configuration (read-only mode)')
31
31
  .option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
32
+ .option('--network <network>', 'Network: mainnet or testnet', 'mainnet')
33
+ .option('--budget <amount>', 'Max USDC budget per session', '1.00')
32
34
  .action(initCommand);
33
35
 
34
36
  program
@@ -36,6 +38,9 @@ program
36
38
  .description('Generate MCP configuration file for your environment')
37
39
  .option('--env <environment>', 'Target environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
38
40
  .option('--output <path>', 'Output file path')
41
+ .option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
42
+ .option('--network <network>', 'Network: mainnet or testnet', 'mainnet')
43
+ .option('--budget <amount>', 'Max USDC budget per session', '1.00')
39
44
  .action(configCommand);
40
45
 
41
46
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-bazaar",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI to set up x402 Bazaar MCP server for AI agents. One command to connect your agent to the marketplace.",
5
5
  "type": "module",
6
6
  "main": "bin/cli.js",
@@ -2,6 +2,7 @@ import inquirer from 'inquirer';
2
2
  import { writeFileSync, existsSync, readFileSync } from 'fs';
3
3
  import chalk from 'chalk';
4
4
  import { log } from '../utils/logger.js';
5
+ import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
5
6
  import { detectEnvironment, getDefaultInstallDir } from '../detectors/environment.js';
6
7
  import { generateMcpConfig } from '../generators/mcp-config.js';
7
8
 
@@ -17,6 +18,8 @@ export async function configCommand(options) {
17
18
  const environments = detectEnvironment();
18
19
  const detected = environments.filter(e => e.detected);
19
20
 
21
+ const defaultEnv = detected.length > 0 ? detected[0].name : 'claude-desktop';
22
+
20
23
  const choices = [
21
24
  ...environments.map(e => ({
22
25
  name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
@@ -25,20 +28,23 @@ export async function configCommand(options) {
25
28
  { name: 'Generic (print JSON to stdout)', value: 'generic' },
26
29
  ];
27
30
 
28
- const { env } = await inquirer.prompt([
29
- {
30
- type: 'list',
31
- name: 'env',
32
- message: 'Which environment do you want to configure?',
33
- choices,
34
- default: detected.length > 0 ? detected[0].name : 'claude-desktop',
35
- },
36
- ]);
31
+ const { env } = await promptOrDefault([{
32
+ type: 'list',
33
+ name: 'env',
34
+ message: 'Which environment do you want to configure?',
35
+ choices,
36
+ default: defaultEnv,
37
+ }]);
37
38
  targetEnv = env;
38
39
  }
39
40
 
40
- // Ask for config values
41
- const { serverUrl, maxBudget, network } = await inquirer.prompt([
41
+ // Config values use CLI flags as overrides
42
+ const configOverrides = {};
43
+ if (options.serverUrl) configOverrides.serverUrl = options.serverUrl;
44
+ if (options.budget) configOverrides.maxBudget = options.budget;
45
+ if (options.network) configOverrides.network = options.network;
46
+
47
+ const { serverUrl, maxBudget, network } = await promptOrDefault([
42
48
  {
43
49
  type: 'input',
44
50
  name: 'serverUrl',
@@ -61,42 +67,46 @@ export async function configCommand(options) {
61
67
  ],
62
68
  default: 'mainnet',
63
69
  },
64
- ]);
70
+ ], configOverrides);
65
71
 
66
- // Ask for wallet config
67
- const { walletMode } = await inquirer.prompt([
68
- {
69
- type: 'list',
70
- name: 'walletMode',
71
- message: 'Wallet configuration:',
72
- choices: [
73
- { name: 'I have Coinbase API keys', value: 'existing' },
74
- { name: 'Read-only mode (browse only, no payments)', value: 'readonly' },
75
- ],
76
- },
77
- ]);
72
+ // Wallet config
73
+ const { walletMode } = await promptOrDefault([{
74
+ type: 'list',
75
+ name: 'walletMode',
76
+ message: 'Wallet configuration:',
77
+ choices: [
78
+ { name: 'I have Coinbase API keys', value: 'existing' },
79
+ { name: 'Read-only mode (browse only, no payments)', value: 'readonly' },
80
+ ],
81
+ default: 'readonly',
82
+ }]);
78
83
 
79
84
  let coinbaseApiKey = '';
80
85
  let coinbaseApiSecret = '';
81
86
 
82
87
  if (walletMode === 'existing') {
83
- const walletAnswers = await inquirer.prompt([
84
- {
85
- type: 'input',
86
- name: 'coinbaseApiKey',
87
- message: 'Coinbase API Key:',
88
- validate: (v) => v.length > 0 || 'API key is required',
89
- },
90
- {
91
- type: 'password',
92
- name: 'coinbaseApiSecret',
93
- message: 'Coinbase API Secret:',
94
- mask: '*',
95
- validate: (v) => v.length > 0 || 'API secret is required',
96
- },
97
- ]);
98
- coinbaseApiKey = walletAnswers.coinbaseApiKey;
99
- coinbaseApiSecret = walletAnswers.coinbaseApiSecret;
88
+ if (!isInteractive()) {
89
+ log.warn('Cannot enter API credentials in non-interactive mode.');
90
+ log.info('Config will be generated in read-only mode.');
91
+ } else {
92
+ const walletAnswers = await promptOrDefault([
93
+ {
94
+ type: 'input',
95
+ name: 'coinbaseApiKey',
96
+ message: 'Coinbase API Key:',
97
+ validate: (v) => v.length > 0 || 'API key is required',
98
+ },
99
+ {
100
+ type: 'password',
101
+ name: 'coinbaseApiSecret',
102
+ message: 'Coinbase API Secret:',
103
+ mask: '*',
104
+ validate: (v) => v.length > 0 || 'API secret is required',
105
+ },
106
+ ]);
107
+ coinbaseApiKey = walletAnswers.coinbaseApiKey;
108
+ coinbaseApiSecret = walletAnswers.coinbaseApiSecret;
109
+ }
100
110
  }
101
111
 
102
112
  const installDir = getDefaultInstallDir();
@@ -110,7 +120,7 @@ export async function configCommand(options) {
110
120
  network,
111
121
  coinbaseApiKey,
112
122
  coinbaseApiSecret,
113
- readOnly: walletMode === 'readonly',
123
+ readOnly: walletMode === 'readonly' || (!isInteractive() && walletMode === 'existing'),
114
124
  });
115
125
 
116
126
  log.separator();
@@ -134,4 +144,6 @@ export async function configCommand(options) {
134
144
  }
135
145
 
136
146
  console.log('');
147
+
148
+ printNonInteractiveHint('config');
137
149
  }
@@ -4,8 +4,10 @@ import chalk from 'chalk';
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
5
5
  import { join, dirname, resolve } from 'path';
6
6
  import { execSync } from 'child_process';
7
+ import { randomBytes } from 'crypto';
7
8
  import { fileURLToPath } from 'url';
8
9
  import { log } from '../utils/logger.js';
10
+ import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
9
11
  import { detectEnvironment, getOsLabel, getDefaultInstallDir } from '../detectors/environment.js';
10
12
  import { generateMcpConfig } from '../generators/mcp-config.js';
11
13
  import { generateEnvContent } from '../generators/env-file.js';
@@ -39,6 +41,10 @@ export async function initCommand(options) {
39
41
 
40
42
  log.banner();
41
43
 
44
+ if (!isInteractive()) {
45
+ log.info(chalk.yellow('Non-interactive terminal detected. Using smart defaults.'));
46
+ }
47
+
42
48
  const osLabel = getOsLabel();
43
49
  log.info(`OS: ${chalk.bold(osLabel)} | Node: ${chalk.bold(process.versions.node)}`);
44
50
  log.separator();
@@ -74,21 +80,23 @@ export async function initCommand(options) {
74
80
  log.warn('No AI client detected automatically.');
75
81
  }
76
82
 
83
+ const defaultEnv = detected.length > 0 ? detected[0].name : 'claude-desktop';
84
+
77
85
  const choices = [
78
86
  ...environments.map(e => ({
79
87
  name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
80
88
  value: e.name,
81
89
  })),
82
- new inquirer.Separator(),
90
+ ...(isInteractive() ? [new inquirer.Separator()] : []),
83
91
  { name: 'Generic (I\'ll configure manually)', value: 'generic' },
84
92
  ];
85
93
 
86
- const { env } = await inquirer.prompt([{
94
+ const { env } = await promptOrDefault([{
87
95
  type: 'list',
88
96
  name: 'env',
89
97
  message: 'Which AI client are you using?',
90
98
  choices,
91
- default: detected.length > 0 ? detected[0].name : 'claude-desktop',
99
+ default: defaultEnv,
92
100
  }]);
93
101
 
94
102
  targetEnv = environments.find(e => e.name === env) || {
@@ -119,8 +127,18 @@ export async function initCommand(options) {
119
127
  copyFileSync(mcpSource, mcpServerDest);
120
128
  spinner.text = 'Copied mcp-server.mjs from local project...';
121
129
  } else {
122
- spinner.text = 'Generating MCP server file...';
123
- writeFileSync(mcpServerDest, generateMcpServerFile());
130
+ spinner.text = 'Downloading MCP server from GitHub...';
131
+ try {
132
+ const dlRes = await fetch('https://raw.githubusercontent.com/Wintyx57/x402-backend/main/mcp-server.mjs');
133
+ if (!dlRes.ok) throw new Error(`HTTP ${dlRes.status}`);
134
+ writeFileSync(mcpServerDest, await dlRes.text());
135
+ } catch (dlErr) {
136
+ spinner.fail(`Could not download MCP server: ${dlErr.message}`);
137
+ log.error('Download the file manually from:');
138
+ log.dim(' https://github.com/Wintyx57/x402-backend/blob/main/mcp-server.mjs');
139
+ log.dim(` Save it to: ${mcpServerDest}`);
140
+ process.exit(1);
141
+ }
124
142
  }
125
143
 
126
144
  // Create package.json for the MCP server runtime
@@ -128,14 +146,16 @@ export async function initCommand(options) {
128
146
  if (!existsSync(pkgJsonPath)) {
129
147
  writeFileSync(pkgJsonPath, JSON.stringify({
130
148
  name: 'x402-bazaar-mcp',
131
- version: '1.0.0',
149
+ version: '2.0.0',
132
150
  type: 'module',
133
151
  private: true,
134
152
  dependencies: {
135
- '@coinbase/coinbase-sdk': '^0.25.0',
153
+ 'viem': '^2.0.0',
136
154
  '@modelcontextprotocol/sdk': '^1.26.0',
137
155
  'dotenv': '^17.2.4',
138
156
  'zod': '^4.3.6',
157
+ 'ed2curve': '^0.3.0',
158
+ '@scure/bip32': '^1.6.0',
139
159
  },
140
160
  }, null, 2));
141
161
  }
@@ -176,9 +196,9 @@ export async function initCommand(options) {
176
196
  console.log('');
177
197
 
178
198
  let walletMode = 'readonly';
199
+ let agentPrivateKey = '';
179
200
  let coinbaseApiKey = '';
180
201
  let coinbaseApiSecret = '';
181
- let seedPath = '';
182
202
  let maxBudget = '1.00';
183
203
  let network = 'mainnet';
184
204
  let serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
@@ -187,83 +207,118 @@ export async function initCommand(options) {
187
207
  log.info('Skipping wallet setup (--no-wallet)');
188
208
  walletMode = 'readonly';
189
209
  } else {
190
- const { mode } = await inquirer.prompt([{
210
+ const { mode } = await promptOrDefault([{
191
211
  type: 'list',
192
212
  name: 'mode',
193
213
  message: 'How do you want to configure payments?',
194
214
  choices: [
195
215
  {
196
- name: `${chalk.bold('I have Coinbase API keys')} — Full access (search, register, pay)`,
197
- value: 'existing',
216
+ name: `${chalk.bold('Generate a new wallet')} — Creates a fresh Ethereum wallet automatically (Recommended)`,
217
+ value: 'generate',
218
+ },
219
+ {
220
+ name: `${chalk.bold('Import private key')} — Use an existing Ethereum private key`,
221
+ value: 'import',
198
222
  },
199
223
  {
200
- name: `${chalk.bold('Create a new wallet')} — Guide me through setup`,
201
- value: 'new',
224
+ name: `${chalk.bold('Coinbase API keys')} — Legacy: use Coinbase CDP seed file`,
225
+ value: 'coinbase',
202
226
  },
203
227
  {
204
228
  name: `${chalk.bold('Read-only mode')} — Browse marketplace for free (no payments)`,
205
229
  value: 'readonly',
206
230
  },
207
231
  ],
232
+ default: 'generate',
208
233
  }]);
209
234
 
210
235
  walletMode = mode;
211
236
 
212
- if (mode === 'existing') {
213
- const walletAnswers = await inquirer.prompt([
214
- {
215
- type: 'input',
216
- name: 'coinbaseApiKey',
217
- message: 'Coinbase API Key (from portal.cdp.coinbase.com):',
218
- validate: (v) => v.trim().length > 0 || 'API key is required',
219
- },
220
- {
237
+ if (mode === 'generate') {
238
+ agentPrivateKey = '0x' + randomBytes(32).toString('hex');
239
+ log.success('New wallet generated!');
240
+
241
+ // Derive address using viem (installed in step 2)
242
+ try {
243
+ const addr = execSync(
244
+ `node --input-type=module -e "import{privateKeyToAccount}from'viem/accounts';console.log(privateKeyToAccount('${agentPrivateKey}').address)"`,
245
+ { cwd: installDir, stdio: 'pipe', timeout: 15000 }
246
+ ).toString().trim();
247
+ log.info(`Wallet address: ${chalk.bold(addr)}`);
248
+ console.log('');
249
+ log.dim(' Fund this address with USDC on Base to start paying for APIs.');
250
+ log.dim(` View on BaseScan: https://basescan.org/address/${addr}`);
251
+ } catch {
252
+ log.info('Wallet address will be shown when you first use the MCP server.');
253
+ log.dim(' Use the get_wallet_balance tool to see your address.');
254
+ }
255
+ }
256
+
257
+ if (mode === 'import') {
258
+ if (!isInteractive()) {
259
+ log.warn('Cannot enter private key in a non-interactive terminal.');
260
+ log.info('Set AGENT_PRIVATE_KEY in your .env file manually after setup.');
261
+ walletMode = 'readonly';
262
+ } else {
263
+ const { key } = await promptOrDefault([{
221
264
  type: 'password',
222
- name: 'coinbaseApiSecret',
223
- message: 'Coinbase API Secret:',
265
+ name: 'key',
266
+ message: 'Ethereum private key (0x...):',
224
267
  mask: '*',
225
- validate: (v) => v.trim().length > 0 || 'API secret is required',
226
- },
227
- ]);
228
- coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
229
- coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
230
-
231
- // Check if agent-seed.json exists
232
- const existingSeed = join(installDir, 'agent-seed.json');
233
- if (!existsSync(existingSeed)) {
234
- log.warn('No agent-seed.json found. You will need to create a wallet.');
235
- log.dim(' Run: cd "' + installDir + '" && node -e "const{Coinbase,Wallet}=require(\'@coinbase/coinbase-sdk\');...');
236
- log.dim(' Or copy your existing agent-seed.json to: ' + installDir);
268
+ validate: (v) => {
269
+ const trimmed = v.trim();
270
+ if (trimmed.length === 0) return 'Private key is required';
271
+ const hex = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
272
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) return 'Invalid private key (expected 64 hex characters)';
273
+ return true;
274
+ },
275
+ }]);
276
+ agentPrivateKey = key.trim().startsWith('0x') ? key.trim() : `0x${key.trim()}`;
277
+ log.success('Private key imported.');
237
278
  }
238
279
  }
239
280
 
240
- if (mode === 'new') {
241
- console.log('');
242
- log.info('To get Coinbase API keys:');
243
- console.log('');
244
- log.dim(' 1. Go to https://portal.cdp.coinbase.com/');
245
- log.dim(' 2. Create a project (free)');
246
- log.dim(' 3. Go to "API Keys" and generate a key pair');
247
- log.dim(' 4. Save both the API Key Name and the Private Key');
248
- log.dim(' 5. Run npx x402-bazaar init again with your keys');
249
- console.log('');
250
-
251
- const { proceed } = await inquirer.prompt([{
252
- type: 'confirm',
253
- name: 'proceed',
254
- message: 'Continue in read-only mode for now?',
255
- default: true,
256
- }]);
257
-
258
- if (!proceed) {
259
- log.info('Run npx x402-bazaar init when you have your API keys.');
260
- process.exit(0);
281
+ if (mode === 'coinbase') {
282
+ if (!isInteractive()) {
283
+ log.warn('Cannot enter API credentials in a non-interactive terminal.');
284
+ log.info('Falling back to read-only mode.');
285
+ walletMode = 'readonly';
286
+ } else {
287
+ log.dim(' Legacy mode: requires Coinbase CDP API keys + agent-seed.json');
288
+ console.log('');
289
+ const walletAnswers = await promptOrDefault([
290
+ {
291
+ type: 'input',
292
+ name: 'coinbaseApiKey',
293
+ message: 'Coinbase API Key (from portal.cdp.coinbase.com):',
294
+ validate: (v) => v.trim().length > 0 || 'API key is required',
295
+ },
296
+ {
297
+ type: 'password',
298
+ name: 'coinbaseApiSecret',
299
+ message: 'Coinbase API Secret:',
300
+ mask: '*',
301
+ validate: (v) => v.trim().length > 0 || 'API secret is required',
302
+ },
303
+ ]);
304
+ coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
305
+ coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
306
+
307
+ const existingSeed = join(installDir, 'agent-seed.json');
308
+ if (!existsSync(existingSeed)) {
309
+ log.warn('No agent-seed.json found.');
310
+ log.dim(' Copy your existing agent-seed.json to: ' + installDir);
311
+ log.dim(' Or re-run init and choose "Generate a new wallet" instead.');
312
+ }
261
313
  }
262
- walletMode = 'readonly';
263
314
  }
264
315
 
265
- // Network & Budget
266
- const configAnswers = await inquirer.prompt([
316
+ // Network & Budget — use CLI flags as overrides
317
+ const configOverrides = {};
318
+ if (options.network) configOverrides.network = options.network;
319
+ if (options.budget) configOverrides.maxBudget = options.budget;
320
+
321
+ const configAnswers = await promptOrDefault([
267
322
  {
268
323
  type: 'list',
269
324
  name: 'network',
@@ -286,11 +341,10 @@ export async function initCommand(options) {
286
341
  return true;
287
342
  },
288
343
  },
289
- ]);
344
+ ], configOverrides);
290
345
 
291
346
  network = configAnswers.network;
292
347
  maxBudget = configAnswers.maxBudget;
293
- seedPath = join(installDir, 'agent-seed.json');
294
348
  }
295
349
 
296
350
  console.log('');
@@ -305,9 +359,9 @@ export async function initCommand(options) {
305
359
  serverUrl,
306
360
  maxBudget,
307
361
  network,
362
+ agentPrivateKey,
308
363
  coinbaseApiKey,
309
364
  coinbaseApiSecret,
310
- seedPath,
311
365
  readOnly: walletMode === 'readonly',
312
366
  });
313
367
 
@@ -317,9 +371,9 @@ export async function initCommand(options) {
317
371
  serverUrl,
318
372
  maxBudget,
319
373
  network,
374
+ agentPrivateKey,
320
375
  coinbaseApiKey,
321
376
  coinbaseApiSecret,
322
- seedPath,
323
377
  });
324
378
  const envPath = join(installDir, '.env');
325
379
  writeFileSync(envPath, envContent);
@@ -407,6 +461,8 @@ export async function initCommand(options) {
407
461
  log.dim(' Dashboard: https://x402-api.onrender.com/dashboard');
408
462
  log.dim(' Re-configure: npx x402-bazaar init');
409
463
  console.log('');
464
+
465
+ printNonInteractiveHint('init');
410
466
  }
411
467
 
412
468
  /**
@@ -460,319 +516,3 @@ function writeConfig(envInfo, newConfig) {
460
516
  }
461
517
  }
462
518
 
463
- /**
464
- * Generate a standalone MCP server file.
465
- * Used as fallback when the local x402-bazaar project is not found.
466
- */
467
- function generateMcpServerFile() {
468
- return `import 'dotenv/config';
469
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
470
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
471
- import { z } from 'zod';
472
- import { createRequire } from 'module';
473
-
474
- const require = createRequire(import.meta.url);
475
- const { Coinbase, Wallet } = require('@coinbase/coinbase-sdk');
476
-
477
- // ─── Config ──────────────────────────────────────────────────────────
478
- const SERVER_URL = process.env.X402_SERVER_URL || 'https://x402-api.onrender.com';
479
- const MAX_BUDGET = parseFloat(process.env.MAX_BUDGET_USDC || '1.00');
480
- const NETWORK = process.env.NETWORK || 'mainnet';
481
- const explorerBase = NETWORK === 'testnet'
482
- ? 'https://sepolia.basescan.org'
483
- : 'https://basescan.org';
484
- const networkLabel = NETWORK === 'testnet' ? 'Base Sepolia' : 'Base Mainnet';
485
-
486
- // ─── Budget Tracking ─────────────────────────────────────────────────
487
- let sessionSpending = 0;
488
- const sessionPayments = [];
489
-
490
- // ─── Wallet ──────────────────────────────────────────────────────────
491
- let wallet = null;
492
- let walletReady = false;
493
-
494
- async function initWallet() {
495
- if (walletReady) return;
496
-
497
- Coinbase.configure({
498
- apiKeyName: process.env.COINBASE_API_KEY,
499
- privateKey: process.env.COINBASE_API_SECRET,
500
- });
501
-
502
- const seedPath = process.env.AGENT_SEED_PATH || 'agent-seed.json';
503
- const fs = await import('fs');
504
- const seedData = JSON.parse(fs.readFileSync(seedPath, 'utf-8'));
505
- const seedWalletId = Object.keys(seedData)[0];
506
-
507
- wallet = await Wallet.fetch(seedWalletId);
508
- await wallet.loadSeed(seedPath);
509
- walletReady = true;
510
- }
511
-
512
- // ─── x402 Payment Flow ──────────────────────────────────────────────
513
- async function payAndRequest(url, options = {}) {
514
- const res = await fetch(url, options);
515
- const body = await res.json();
516
-
517
- if (res.status !== 402) {
518
- return body;
519
- }
520
-
521
- // HTTP 402 — Payment Required
522
- const details = body.payment_details;
523
- const cost = parseFloat(details.amount);
524
-
525
- // Budget check
526
- if (sessionSpending + cost > MAX_BUDGET) {
527
- throw new Error(
528
- \`Budget limit reached. Spent: \${sessionSpending.toFixed(2)} USDC / \${MAX_BUDGET.toFixed(2)} USDC limit. \` +
529
- \`This call costs \${cost} USDC. Increase MAX_BUDGET_USDC env var to allow more spending.\`
530
- );
531
- }
532
-
533
- await initWallet();
534
-
535
- const transfer = await wallet.createTransfer({
536
- amount: details.amount,
537
- assetId: Coinbase.assets.Usdc,
538
- destination: details.recipient,
539
- });
540
- const confirmed = await transfer.wait({ timeoutSeconds: 120 });
541
- const txHash = confirmed.getTransactionHash();
542
-
543
- // Track spending
544
- sessionSpending += cost;
545
- sessionPayments.push({
546
- amount: cost,
547
- txHash,
548
- timestamp: new Date().toISOString(),
549
- endpoint: url.replace(SERVER_URL, ''),
550
- });
551
-
552
- // Retry with payment proof
553
- const retryHeaders = { ...options.headers, 'X-Payment-TxHash': txHash };
554
- const retryRes = await fetch(url, { ...options, headers: retryHeaders });
555
- const result = await retryRes.json();
556
-
557
- // Enrich result with payment info
558
- result._payment = {
559
- amount: details.amount,
560
- currency: 'USDC',
561
- txHash,
562
- explorer: \`\${explorerBase}/tx/\${txHash}\`,
563
- session_spent: sessionSpending.toFixed(2),
564
- session_remaining: (MAX_BUDGET - sessionSpending).toFixed(2),
565
- };
566
-
567
- return result;
568
- }
569
-
570
- // ─── MCP Server ─────────────────────────────────────────────────────
571
- const server = new McpServer({
572
- name: 'x402-bazaar',
573
- version: '1.1.0',
574
- });
575
-
576
- // --- Tool: discover_marketplace (FREE) ---
577
- server.tool(
578
- 'discover_marketplace',
579
- 'Discover the x402 Bazaar marketplace. Returns available endpoints, total services, and protocol info. Free — no payment needed.',
580
- {},
581
- async () => {
582
- try {
583
- const res = await fetch(SERVER_URL);
584
- const data = await res.json();
585
- return {
586
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
587
- };
588
- } catch (err) {
589
- return {
590
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
591
- isError: true,
592
- };
593
- }
594
- }
595
- );
596
-
597
- // --- Tool: search_services (0.05 USDC) ---
598
- server.tool(
599
- 'search_services',
600
- \`Search for API services on x402 Bazaar by keyword. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session. Check get_budget_status before calling if unsure about remaining budget.\`,
601
- { query: z.string().describe('Search keyword (e.g. "weather", "crypto", "ai")') },
602
- async ({ query }) => {
603
- try {
604
- const result = await payAndRequest(
605
- \`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
606
- );
607
- return {
608
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
609
- };
610
- } catch (err) {
611
- return {
612
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
613
- isError: true,
614
- };
615
- }
616
- }
617
- );
618
-
619
- // --- Tool: list_services (0.05 USDC) ---
620
- server.tool(
621
- 'list_services',
622
- \`List all API services available on x402 Bazaar. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
623
- {},
624
- async () => {
625
- try {
626
- const result = await payAndRequest(\`\${SERVER_URL}/services\`);
627
- return {
628
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
629
- };
630
- } catch (err) {
631
- return {
632
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
633
- isError: true,
634
- };
635
- }
636
- }
637
- );
638
-
639
- // --- Tool: find_tool_for_task (0.05 USDC — smart service lookup) ---
640
- server.tool(
641
- 'find_tool_for_task',
642
- \`Describe what you need in plain English and get the best matching API service ready to call. Returns the single best match with name, URL, price, and usage instructions. Costs 0.05 USDC. Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
643
- { task: z.string().describe('What you need, in natural language (e.g. "get current weather for a city", "translate text to French")') },
644
- async ({ task }) => {
645
- try {
646
- const stopWords = new Set(['i', 'need', 'want', 'to', 'a', 'an', 'the', 'for', 'of', 'and', 'or', 'in', 'on', 'with', 'that', 'this', 'get', 'find', 'me', 'my', 'some', 'can', 'you', 'do', 'is', 'it', 'be', 'have', 'use', 'please', 'should', 'would', 'could']);
647
- const keywords = task.toLowerCase()
648
- .replace(/[^a-z0-9\\s]/g, '')
649
- .split(/\\s+/)
650
- .filter(w => w.length > 2 && !stopWords.has(w));
651
- const query = keywords.slice(0, 3).join(' ') || task.slice(0, 30);
652
-
653
- const result = await payAndRequest(
654
- \`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
655
- );
656
-
657
- const services = result.data || result.services || [];
658
- if (services.length === 0) {
659
- return {
660
- content: [{ type: 'text', text: JSON.stringify({
661
- found: false,
662
- query_used: query,
663
- message: \`No services found matching "\${task}". Try rephrasing or use search_services with different keywords.\`,
664
- _payment: result._payment,
665
- }, null, 2) }],
666
- };
667
- }
668
-
669
- const best = services[0];
670
- return {
671
- content: [{ type: 'text', text: JSON.stringify({
672
- found: true,
673
- query_used: query,
674
- service: {
675
- name: best.name,
676
- description: best.description,
677
- url: best.url,
678
- price_usdc: best.price_usdc,
679
- tags: best.tags,
680
- },
681
- action: \`Call this API using call_api("\${best.url}"). \${Number(best.price_usdc) === 0 ? 'This API is free.' : \`This API costs \${best.price_usdc} USDC per call.\`}\`,
682
- alternatives_count: services.length - 1,
683
- _payment: result._payment,
684
- }, null, 2) }],
685
- };
686
- } catch (err) {
687
- return {
688
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
689
- isError: true,
690
- };
691
- }
692
- }
693
- );
694
-
695
- // --- Tool: call_api (FREE — calls external APIs) ---
696
- server.tool(
697
- 'call_api',
698
- 'Call an external API URL and return the response. Use this to fetch real data from service URLs discovered on the marketplace. Free — no marketplace payment needed.',
699
- { url: z.string().url().describe('The full API URL to call') },
700
- async ({ url }) => {
701
- try {
702
- const res = await fetch(url);
703
- const text = await res.text();
704
- let parsed;
705
- try {
706
- parsed = JSON.parse(text);
707
- } catch {
708
- parsed = { response: text.slice(0, 5000) };
709
- }
710
- return {
711
- content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
712
- };
713
- } catch (err) {
714
- return {
715
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
716
- isError: true,
717
- };
718
- }
719
- }
720
- );
721
-
722
- // --- Tool: get_wallet_balance (FREE) ---
723
- server.tool(
724
- 'get_wallet_balance',
725
- 'Check the USDC balance of the agent wallet on-chain. Free.',
726
- {},
727
- async () => {
728
- try {
729
- await initWallet();
730
- const balance = await wallet.getBalance(Coinbase.assets.Usdc);
731
- const address = (await wallet.getDefaultAddress()).getId();
732
- return {
733
- content: [{
734
- type: 'text',
735
- text: JSON.stringify({
736
- address,
737
- balance_usdc: balance.toString(),
738
- network: networkLabel,
739
- explorer: \`\${explorerBase}/address/\${address}\`,
740
- }, null, 2),
741
- }],
742
- };
743
- } catch (err) {
744
- return {
745
- content: [{ type: 'text', text: \`Error: \${err.message}\` }],
746
- isError: true,
747
- };
748
- }
749
- }
750
- );
751
-
752
- // --- Tool: get_budget_status (FREE) ---
753
- server.tool(
754
- 'get_budget_status',
755
- 'Check the session spending budget. Shows how much USDC has been spent, remaining budget, and a list of all payments made this session. Free — call this before paid requests to verify budget.',
756
- {},
757
- async () => {
758
- return {
759
- content: [{
760
- type: 'text',
761
- text: JSON.stringify({
762
- budget_limit: MAX_BUDGET.toFixed(2) + ' USDC',
763
- spent: sessionSpending.toFixed(2) + ' USDC',
764
- remaining: (MAX_BUDGET - sessionSpending).toFixed(2) + ' USDC',
765
- payments_count: sessionPayments.length,
766
- payments: sessionPayments,
767
- network: networkLabel,
768
- }, null, 2),
769
- }],
770
- };
771
- }
772
- );
773
-
774
- // ─── Start ──────────────────────────────────────────────────────────
775
- const transport = new StdioServerTransport();
776
- await server.connect(transport);
777
- `;
778
- }
@@ -5,11 +5,11 @@ export function generateEnvContent({
5
5
  serverUrl = 'https://x402-api.onrender.com',
6
6
  maxBudget = '1.00',
7
7
  network = 'mainnet',
8
+ agentPrivateKey = '',
8
9
  coinbaseApiKey = '',
9
10
  coinbaseApiSecret = '',
10
- seedPath = 'agent-seed.json',
11
11
  }) {
12
- return `# x402 Bazaar MCP Server Configuration
12
+ let content = `# x402 Bazaar MCP Server Configuration
13
13
  # Generated by: npx x402-bazaar init
14
14
 
15
15
  # Marketplace server URL
@@ -20,13 +20,22 @@ MAX_BUDGET_USDC=${maxBudget}
20
20
 
21
21
  # Network: mainnet or testnet
22
22
  NETWORK=${network}
23
+ `;
23
24
 
24
- # Coinbase Developer Platform API credentials
25
+ if (agentPrivateKey) {
26
+ content += `
27
+ # Agent wallet private key (Ethereum)
28
+ # WARNING: Keep this secret! Never commit to git.
29
+ AGENT_PRIVATE_KEY=${agentPrivateKey}
30
+ `;
31
+ } else if (coinbaseApiKey) {
32
+ content += `
33
+ # Coinbase Developer Platform API credentials (legacy mode)
25
34
  # Get yours at: https://portal.cdp.coinbase.com/
26
35
  COINBASE_API_KEY=${coinbaseApiKey}
27
36
  COINBASE_API_SECRET=${coinbaseApiSecret}
28
-
29
- # Path to the agent wallet seed file
30
- AGENT_SEED_PATH=${seedPath}
31
37
  `;
38
+ }
39
+
40
+ return content;
32
41
  }
@@ -10,16 +10,12 @@ export function generateMcpConfig({
10
10
  serverUrl = 'https://x402-api.onrender.com',
11
11
  maxBudget = '1.00',
12
12
  network = 'mainnet',
13
+ agentPrivateKey = '',
13
14
  coinbaseApiKey = '',
14
15
  coinbaseApiSecret = '',
15
- seedPath = '',
16
16
  readOnly = false,
17
17
  }) {
18
- const os = platform();
19
- const sep = os === 'win32' ? '\\' : '/';
20
-
21
18
  const mcpServerPath = join(installDir, 'mcp-server.mjs');
22
- const defaultSeedPath = seedPath || join(installDir, 'agent-seed.json');
23
19
 
24
20
  const env = {
25
21
  X402_SERVER_URL: serverUrl,
@@ -28,9 +24,12 @@ export function generateMcpConfig({
28
24
  };
29
25
 
30
26
  if (!readOnly) {
31
- env.COINBASE_API_KEY = coinbaseApiKey || 'YOUR_COINBASE_API_KEY';
32
- env.COINBASE_API_SECRET = coinbaseApiSecret || 'YOUR_COINBASE_API_SECRET';
33
- env.AGENT_SEED_PATH = defaultSeedPath;
27
+ if (agentPrivateKey) {
28
+ env.AGENT_PRIVATE_KEY = agentPrivateKey;
29
+ } else if (coinbaseApiKey) {
30
+ env.COINBASE_API_KEY = coinbaseApiKey;
31
+ env.COINBASE_API_SECRET = coinbaseApiSecret;
32
+ }
34
33
  }
35
34
 
36
35
  const serverEntry = {
@@ -41,27 +40,6 @@ export function generateMcpConfig({
41
40
 
42
41
  // Format for the target environment
43
42
  switch (environment) {
44
- case 'claude-desktop':
45
- return {
46
- mcpServers: {
47
- 'x402-bazaar': serverEntry,
48
- },
49
- };
50
-
51
- case 'cursor':
52
- return {
53
- mcpServers: {
54
- 'x402-bazaar': serverEntry,
55
- },
56
- };
57
-
58
- case 'claude-code':
59
- return {
60
- mcpServers: {
61
- 'x402-bazaar': serverEntry,
62
- },
63
- };
64
-
65
43
  case 'vscode-continue':
66
44
  return {
67
45
  models: [],
@@ -0,0 +1,107 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { log } from './logger.js';
4
+
5
+ /**
6
+ * Returns true if the terminal supports interactive prompts.
7
+ */
8
+ export function isInteractive() {
9
+ if (process.env.CI === 'true' || process.env.CI === '1') return false;
10
+ return Boolean(process.stdin.isTTY);
11
+ }
12
+
13
+ /**
14
+ * Safe wrapper around inquirer.prompt().
15
+ *
16
+ * Interactive mode (TTY): behaves exactly like inquirer.prompt().
17
+ * Non-interactive mode: resolves each question with its default value
18
+ * and logs what was chosen.
19
+ *
20
+ * @param {Array} questions - inquirer question array
21
+ * @param {Object} overrides - optional { questionName: value } to force
22
+ * @returns {Promise<Object>} answers
23
+ */
24
+ export async function promptOrDefault(questions, overrides = {}) {
25
+ const questionList = Array.isArray(questions) ? questions : [questions];
26
+
27
+ if (isInteractive()) {
28
+ const remaining = questionList.filter(q => !(q.name in overrides));
29
+ const answers = { ...overrides };
30
+ if (remaining.length > 0) {
31
+ const prompted = await inquirer.prompt(remaining);
32
+ Object.assign(answers, prompted);
33
+ }
34
+ return answers;
35
+ }
36
+
37
+ // Non-interactive path
38
+ const answers = {};
39
+
40
+ for (const q of questionList) {
41
+ if (q.name in overrides) {
42
+ answers[q.name] = overrides[q.name];
43
+ log.info(`${q.message} ${chalk.bold(overrides[q.name])} ${chalk.dim('(flag)')}`);
44
+ continue;
45
+ }
46
+
47
+ let defaultVal = q.default;
48
+
49
+ // For list prompts without explicit default, use first choice
50
+ if (q.type === 'list' && q.choices && defaultVal === undefined) {
51
+ const firstChoice = q.choices.find(c => c.value !== undefined);
52
+ defaultVal = firstChoice ? firstChoice.value : undefined;
53
+ }
54
+
55
+ // For confirm prompts, default to true
56
+ if (q.type === 'confirm' && defaultVal === undefined) {
57
+ defaultVal = true;
58
+ }
59
+
60
+ if (defaultVal === undefined) {
61
+ answers[q.name] = '';
62
+ log.warn(`${q.message} ${chalk.dim('(skipped — use flags to provide)')}`);
63
+ continue;
64
+ }
65
+
66
+ answers[q.name] = defaultVal;
67
+
68
+ // Build human-readable label for list choices
69
+ let displayVal = String(defaultVal);
70
+ if (q.type === 'list' && q.choices) {
71
+ const match = q.choices.find(c => c.value === defaultVal);
72
+ if (match && match.name) {
73
+ displayVal = match.name.replace(/\x1b\[[0-9;]*m/g, '');
74
+ }
75
+ }
76
+
77
+ log.info(`${q.message} ${chalk.bold(displayVal)} ${chalk.dim('(auto)')}`);
78
+ }
79
+
80
+ return answers;
81
+ }
82
+
83
+ /**
84
+ * Print a hint showing available flags for customization.
85
+ * Only prints in non-interactive mode.
86
+ */
87
+ export function printNonInteractiveHint(command = 'init') {
88
+ if (isInteractive()) return;
89
+
90
+ console.log('');
91
+ log.info(chalk.yellow('Running in non-interactive mode (IDE terminal detected).'));
92
+ log.info('To customize, use flags:');
93
+ console.log('');
94
+
95
+ if (command === 'init') {
96
+ log.dim(' npx x402-bazaar init --env cursor');
97
+ log.dim(' npx x402-bazaar init --network testnet --budget 5.00');
98
+ log.dim(' npx x402-bazaar init --no-wallet');
99
+ } else if (command === 'config') {
100
+ log.dim(' npx x402-bazaar config --env cursor');
101
+ log.dim(' npx x402-bazaar config --network testnet --budget 5.00');
102
+ }
103
+
104
+ console.log('');
105
+ log.dim(' For full interactive setup, run in a standalone terminal.');
106
+ console.log('');
107
+ }