x402-bazaar 1.2.1 → 2.0.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
@@ -4,6 +4,10 @@ import { Command } from 'commander';
4
4
  import { initCommand } from '../src/commands/init.js';
5
5
  import { configCommand } from '../src/commands/config.js';
6
6
  import { statusCommand } from '../src/commands/status.js';
7
+ import { listCommand } from '../src/commands/list.js';
8
+ import { searchCommand } from '../src/commands/search.js';
9
+ import { callCommand } from '../src/commands/call.js';
10
+ import { walletCommand } from '../src/commands/wallet.js';
7
11
  import chalk from 'chalk';
8
12
 
9
13
  // Global error handler
@@ -21,7 +25,7 @@ const program = new Command();
21
25
  program
22
26
  .name('x402-bazaar')
23
27
  .description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
24
- .version('1.2.1');
28
+ .version('2.0.0');
25
29
 
26
30
  program
27
31
  .command('init')
@@ -49,17 +53,53 @@ program
49
53
  .option('--server-url <url>', 'Server URL to check', 'https://x402-api.onrender.com')
50
54
  .action(statusCommand);
51
55
 
56
+ program
57
+ .command('list')
58
+ .description('List all services on x402 Bazaar')
59
+ .option('--chain <chain>', 'Filter by chain (base or skale)')
60
+ .option('--category <category>', 'Filter by category (ai, data, weather, etc.)')
61
+ .option('--free', 'Show only free services')
62
+ .option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
63
+ .action(listCommand);
64
+
65
+ program
66
+ .command('search <query>')
67
+ .description('Search for services by keyword')
68
+ .option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
69
+ .action(searchCommand);
70
+
71
+ program
72
+ .command('call <endpoint>')
73
+ .description('Call a marketplace endpoint (testing/debugging)')
74
+ .option('--param <key=value>', 'Add parameter (can be used multiple times)', (value, previous) => {
75
+ return previous ? [...previous, value] : [value];
76
+ }, [])
77
+ .option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
78
+ .action(callCommand);
79
+
80
+ program
81
+ .command('wallet')
82
+ .description('Check USDC wallet balance on Base')
83
+ .option('--address <address>', 'Ethereum address to check')
84
+ .action(walletCommand);
85
+
52
86
  // Default: show help if no command given
53
87
  if (process.argv.length <= 2) {
54
88
  console.log('');
55
- console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI'));
89
+ console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v2'));
90
+ console.log('');
91
+ console.log(' Setup commands:');
92
+ console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
93
+ console.log(chalk.cyan(' npx x402-bazaar config') + chalk.dim(' Generate MCP config'));
94
+ console.log(chalk.cyan(' npx x402-bazaar status') + chalk.dim(' Check server connection'));
56
95
  console.log('');
57
- console.log(' Quick start:');
58
- console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
59
- console.log(chalk.cyan(' npx x402-bazaar status') + chalk.dim(' Check server connection'));
60
- console.log(chalk.cyan(' npx x402-bazaar config') + chalk.dim(' Generate MCP config'));
96
+ console.log(' Marketplace commands:');
97
+ console.log(chalk.cyan(' npx x402-bazaar list') + chalk.dim(' Browse all services'));
98
+ console.log(chalk.cyan(' npx x402-bazaar search <query>') + chalk.dim(' Find services by keyword'));
99
+ console.log(chalk.cyan(' npx x402-bazaar call <endpoint>') + chalk.dim(' Test an API endpoint'));
100
+ console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check wallet balance'));
61
101
  console.log('');
62
- console.log(chalk.dim(' Run with --help for all options'));
102
+ console.log(chalk.dim(' Run any command with --help for detailed options'));
63
103
  console.log('');
64
104
  process.exit(0);
65
105
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "x402-bazaar",
3
- "version": "1.2.1",
4
- "description": "CLI to set up x402 Bazaar MCP server for AI agents. One command to connect your agent to the marketplace.",
3
+ "version": "2.0.0",
4
+ "description": "CLI to set up x402 Bazaar MCP server for AI agents. Browse, search, and call marketplace APIs. One command to connect your agent.",
5
5
  "type": "module",
6
6
  "main": "bin/cli.js",
7
7
  "bin": {
@@ -0,0 +1,197 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { log } from '../utils/logger.js';
4
+
5
+ export async function callCommand(endpoint, options) {
6
+ if (!endpoint || endpoint.trim().length === 0) {
7
+ log.error('Endpoint is required');
8
+ log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
9
+ log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
10
+ log.dim(' Example: x402-bazaar call /api/search --param q="AI agents"');
11
+ console.log('');
12
+ process.exit(1);
13
+ }
14
+
15
+ const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
16
+
17
+ // Normalize endpoint (add leading slash if missing)
18
+ const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
19
+ const fullUrl = `${serverUrl}${normalizedEndpoint}`;
20
+
21
+ log.banner();
22
+ log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
23
+ console.log('');
24
+
25
+ // Parse params
26
+ const params = {};
27
+ if (options.param) {
28
+ const paramArray = Array.isArray(options.param) ? options.param : [options.param];
29
+
30
+ for (const p of paramArray) {
31
+ const [key, ...valueParts] = p.split('=');
32
+ if (!key || valueParts.length === 0) {
33
+ log.warn(`Invalid param format: ${p} (expected key=value)`);
34
+ continue;
35
+ }
36
+ let value = valueParts.join('='); // rejoin in case value contains '='
37
+ // Strip quotes if present
38
+ if ((value.startsWith('"') && value.endsWith('"')) ||
39
+ (value.startsWith("'") && value.endsWith("'"))) {
40
+ value = value.slice(1, -1);
41
+ }
42
+ params[key.trim()] = value;
43
+ }
44
+ }
45
+
46
+ if (Object.keys(params).length > 0) {
47
+ log.info('Parameters:');
48
+ for (const [k, v] of Object.entries(params)) {
49
+ log.dim(` ${k}: ${v}`);
50
+ }
51
+ console.log('');
52
+ }
53
+
54
+ // Build URL with query params (always use GET for marketplace APIs)
55
+ let finalUrl = fullUrl;
56
+ if (Object.keys(params).length > 0) {
57
+ const queryString = new URLSearchParams(params).toString();
58
+ finalUrl = `${fullUrl}?${queryString}`;
59
+ }
60
+
61
+ const spinner = ora(`GET ${finalUrl}...`).start();
62
+
63
+ try {
64
+ const fetchOptions = {
65
+ method: 'GET',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ signal: AbortSignal.timeout(30000),
70
+ };
71
+
72
+ const res = await fetch(finalUrl, fetchOptions);
73
+
74
+ spinner.stop();
75
+
76
+ // Handle 402 Payment Required
77
+ if (res.status === 402) {
78
+ console.log('');
79
+ log.warn(chalk.bold('Payment Required (HTTP 402)'));
80
+ console.log('');
81
+
82
+ try {
83
+ const paymentInfo = await res.json();
84
+
85
+ if (paymentInfo.price) {
86
+ log.info(`Price: ${chalk.cyan(`${paymentInfo.price} USDC`)}`);
87
+ }
88
+ if (paymentInfo.paymentAddress) {
89
+ log.info(`Payment address: ${chalk.hex('#34D399')(paymentInfo.paymentAddress)}`);
90
+ }
91
+ if (paymentInfo.message) {
92
+ log.dim(` ${paymentInfo.message}`);
93
+ }
94
+
95
+ console.log('');
96
+ log.separator();
97
+ console.log('');
98
+ log.info('To pay automatically, use the MCP server via Claude/Cursor.');
99
+ log.dim(' The MCP server handles x402 payments transparently.');
100
+ log.dim(' Install: npx x402-bazaar init');
101
+ console.log('');
102
+
103
+ } catch {
104
+ log.dim(' This endpoint requires payment.');
105
+ log.dim(' Use the MCP server for automatic payment handling.');
106
+ console.log('');
107
+ }
108
+ return;
109
+ }
110
+
111
+ // Handle other errors
112
+ if (!res.ok) {
113
+ console.log('');
114
+ log.error(`HTTP ${res.status}: ${res.statusText}`);
115
+
116
+ try {
117
+ const errorBody = await res.text();
118
+ if (errorBody) {
119
+ console.log('');
120
+ log.dim('Response:');
121
+ console.log(chalk.red(errorBody));
122
+ }
123
+ } catch {
124
+ // Ignore
125
+ }
126
+
127
+ console.log('');
128
+ process.exit(1);
129
+ }
130
+
131
+ // Success
132
+ console.log('');
133
+ log.success(`${chalk.bold(res.status)} ${res.statusText}`);
134
+ console.log('');
135
+
136
+ const contentType = res.headers.get('content-type') || '';
137
+
138
+ if (contentType.includes('application/json')) {
139
+ const responseData = await res.json();
140
+
141
+ log.separator();
142
+ console.log('');
143
+ log.info('Response (JSON):');
144
+ console.log('');
145
+
146
+ // Pretty-print JSON with syntax highlighting
147
+ const jsonString = JSON.stringify(responseData, null, 2);
148
+ const highlighted = highlightJson(jsonString);
149
+ console.log(highlighted);
150
+ console.log('');
151
+ log.separator();
152
+
153
+ } else {
154
+ const responseText = await res.text();
155
+
156
+ log.separator();
157
+ console.log('');
158
+ log.info('Response:');
159
+ console.log('');
160
+ console.log(chalk.white(responseText));
161
+ console.log('');
162
+ log.separator();
163
+ }
164
+
165
+ console.log('');
166
+
167
+ } catch (err) {
168
+ spinner.fail('Request failed');
169
+ console.log('');
170
+
171
+ if (err.name === 'AbortError') {
172
+ log.error('Request timeout (30s) — server may be slow or endpoint not responding');
173
+ log.dim(' Try again or check status: npx x402-bazaar status');
174
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
175
+ log.error('Cannot connect to server');
176
+ log.dim(` Server URL: ${serverUrl}`);
177
+ log.dim(' Check your internet connection or try: npx x402-bazaar status');
178
+ } else {
179
+ log.error(err.message);
180
+ }
181
+
182
+ console.log('');
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Simple JSON syntax highlighting for terminal
189
+ */
190
+ function highlightJson(jsonString) {
191
+ return jsonString
192
+ .replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
193
+ .replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
194
+ .replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
195
+ .replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
196
+ .replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
197
+ }
@@ -191,8 +191,8 @@ export async function initCommand(options) {
191
191
 
192
192
  console.log('');
193
193
 
194
- // ─── Step 3: Wallet Configuration ───────────────────────────────────
195
- log.step(3, 'Configuring wallet...');
194
+ // ─── Step 3: Network & Wallet Configuration ────────────────────────
195
+ log.step(3, 'Configuring network and wallet...');
196
196
  console.log('');
197
197
 
198
198
  let walletMode = 'readonly';
@@ -207,6 +207,42 @@ export async function initCommand(options) {
207
207
  log.info('Skipping wallet setup (--no-wallet)');
208
208
  walletMode = 'readonly';
209
209
  } else {
210
+ // Network & Budget first — needed for wallet funding instructions
211
+ const configOverrides = {};
212
+ if (options.network) configOverrides.network = options.network;
213
+ if (options.budget) configOverrides.maxBudget = options.budget;
214
+
215
+ const configAnswers = await promptOrDefault([
216
+ {
217
+ type: 'list',
218
+ name: 'network',
219
+ message: 'Which network?',
220
+ choices: [
221
+ { name: 'Base Mainnet (real USDC)', value: 'mainnet' },
222
+ { name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
223
+ { name: 'SKALE Europa (zero gas fees — advanced users)', value: 'skale' },
224
+ ],
225
+ default: 'mainnet',
226
+ },
227
+ {
228
+ type: 'input',
229
+ name: 'maxBudget',
230
+ message: 'Max USDC budget per session (safety limit):',
231
+ default: '1.00',
232
+ validate: (v) => {
233
+ const n = parseFloat(v);
234
+ if (isNaN(n) || n <= 0) return 'Must be a positive number';
235
+ if (n > 100) return 'Maximum is 100 USDC per session';
236
+ return true;
237
+ },
238
+ },
239
+ ], configOverrides);
240
+
241
+ network = configAnswers.network;
242
+ maxBudget = configAnswers.maxBudget;
243
+
244
+ console.log('');
245
+
210
246
  const { mode } = await promptOrDefault([{
211
247
  type: 'list',
212
248
  name: 'mode',
@@ -247,19 +283,36 @@ export async function initCommand(options) {
247
283
  ).toString().trim();
248
284
  console.log('');
249
285
  log.info(`Wallet address: ${chalk.bold(walletAddress)}`);
250
- log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
286
+ if (network === 'skale') {
287
+ log.dim(` Explorer: https://elated-tan-skat.explorer.mainnet.skalenodes.com/address/${walletAddress}`);
288
+ } else {
289
+ log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
290
+ }
251
291
  console.log('');
252
292
  log.separator();
253
- log.info(chalk.bold('To activate payments, fund this wallet from MetaMask:'));
254
- console.log('');
255
- log.dim(` ${chalk.white('1.')} Open MetaMask and switch to the ${chalk.bold('Base')} network`);
256
- log.dim(` (Chain ID: 8453 add it via https://chainlist.org/chain/8453)`);
257
- log.dim(` ${chalk.white('2.')} Send ${chalk.bold('USDC')} to: ${chalk.hex('#34D399')(walletAddress)}`);
258
- log.dim(` (Even $1 USDC is enough to start — each API call costs $0.005-$0.05)`);
259
- log.dim(` ${chalk.white('3.')} Send a tiny bit of ${chalk.bold('ETH')} to the same address for gas`);
260
- log.dim(` (${chalk.white('~$0.01 of ETH on Base')} is enough for hundreds of transactions)`);
261
- console.log('');
262
- log.warn(`IMPORTANT: Send on the ${chalk.bold('Base')} network only — not Ethereum mainnet!`);
293
+ if (network === 'skale') {
294
+ log.info(chalk.bold('To activate payments, fund this wallet:'));
295
+ console.log('');
296
+ log.dim(` ${chalk.white('1.')} Bridge USDC to SKALE Europa`);
297
+ log.dim(` (Use https://portal.skale.space/bridge or any SKALE bridge)`);
298
+ log.dim(` ${chalk.white('2.')} Send ${chalk.bold('USDC')} to: ${chalk.hex('#34D399')(walletAddress)}`);
299
+ log.dim(` (Even $1 USDC is enough each API call costs $0.005-$0.05)`);
300
+ log.dim(` ${chalk.white('3.')} Get free sFUEL for gas at https://www.sfuelstation.com/`);
301
+ log.dim(` (sFUEL is free — no ETH needed on SKALE!)`);
302
+ console.log('');
303
+ log.warn(`IMPORTANT: Send USDC on ${chalk.bold('SKALE Europa')} only — not Base or Ethereum!`);
304
+ } else {
305
+ log.info(chalk.bold('To activate payments, fund this wallet from MetaMask:'));
306
+ console.log('');
307
+ log.dim(` ${chalk.white('1.')} Open MetaMask and switch to the ${chalk.bold('Base')} network`);
308
+ log.dim(` (Chain ID: 8453 — add it via https://chainlist.org/chain/8453)`);
309
+ log.dim(` ${chalk.white('2.')} Send ${chalk.bold('USDC')} to: ${chalk.hex('#34D399')(walletAddress)}`);
310
+ log.dim(` (Even $1 USDC is enough to start — each API call costs $0.005-$0.05)`);
311
+ log.dim(` ${chalk.white('3.')} Send a tiny bit of ${chalk.bold('ETH')} to the same address for gas`);
312
+ log.dim(` (${chalk.white('~$0.01 of ETH on Base')} is enough for hundreds of transactions)`);
313
+ console.log('');
314
+ log.warn(`IMPORTANT: Send on the ${chalk.bold('Base')} network only — not Ethereum mainnet!`);
315
+ }
263
316
  log.separator();
264
317
  } catch {
265
318
  log.info('Wallet address will be shown when you first use the MCP server.');
@@ -325,39 +378,6 @@ export async function initCommand(options) {
325
378
  }
326
379
  }
327
380
  }
328
-
329
- // Network & Budget — use CLI flags as overrides
330
- const configOverrides = {};
331
- if (options.network) configOverrides.network = options.network;
332
- if (options.budget) configOverrides.maxBudget = options.budget;
333
-
334
- const configAnswers = await promptOrDefault([
335
- {
336
- type: 'list',
337
- name: 'network',
338
- message: 'Which network?',
339
- choices: [
340
- { name: 'Base Mainnet (real USDC)', value: 'mainnet' },
341
- { name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
342
- ],
343
- default: 'mainnet',
344
- },
345
- {
346
- type: 'input',
347
- name: 'maxBudget',
348
- message: 'Max USDC budget per session (safety limit):',
349
- default: '1.00',
350
- validate: (v) => {
351
- const n = parseFloat(v);
352
- if (isNaN(n) || n <= 0) return 'Must be a positive number';
353
- if (n > 100) return 'Maximum is 100 USDC per session';
354
- return true;
355
- },
356
- },
357
- ], configOverrides);
358
-
359
- network = configAnswers.network;
360
- maxBudget = configAnswers.maxBudget;
361
381
  }
362
382
 
363
383
  console.log('');
@@ -461,7 +481,7 @@ export async function initCommand(options) {
461
481
  `Environment: ${targetEnv.label}`,
462
482
  `Install dir: ${installDir}`,
463
483
  `Server: ${serverUrl}`,
464
- `Network: ${network === 'mainnet' ? 'Base Mainnet' : 'Base Sepolia'}`,
484
+ `Network: ${network === 'mainnet' ? 'Base Mainnet' : network === 'skale' ? 'SKALE Europa' : 'Base Sepolia'}`,
465
485
  `Budget limit: ${maxBudget} USDC / session`,
466
486
  `Wallet: ${walletLabel}`,
467
487
  `Services: ${serviceCount > 0 ? serviceCount + ' available' : 'check with npx x402-bazaar status'}`,
@@ -470,7 +490,9 @@ export async function initCommand(options) {
470
490
  '',
471
491
  ...(walletMode === 'generate' ? [
472
492
  'Before your agent can pay for APIs:',
473
- ' 1. Send USDC + a little ETH to your wallet on Base',
493
+ network === 'skale'
494
+ ? ' 1. Bridge USDC to your wallet on SKALE Europa + get free sFUEL'
495
+ : ' 1. Send USDC + a little ETH to your wallet on Base',
474
496
  ' 2. Restart your IDE',
475
497
  '',
476
498
  ] : []),
@@ -0,0 +1,125 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { log } from '../utils/logger.js';
4
+
5
+ const CATEGORIES = [
6
+ 'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
7
+ 'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
8
+ ];
9
+
10
+ export async function listCommand(options) {
11
+ const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
12
+
13
+ log.banner();
14
+ log.info(`Fetching services from ${chalk.bold('x402 Bazaar')}`);
15
+ console.log('');
16
+
17
+ // Build query params
18
+ const params = new URLSearchParams();
19
+ if (options.chain) params.append('chain', options.chain);
20
+ if (options.category) params.append('category', options.category);
21
+ if (options.free) params.append('free', 'true');
22
+
23
+ const url = `${serverUrl}/api/services${params.toString() ? '?' + params.toString() : ''}`;
24
+
25
+ const spinner = ora('Loading services...').start();
26
+
27
+ try {
28
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
29
+
30
+ if (!res.ok) {
31
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
32
+ }
33
+
34
+ const data = await res.json();
35
+ const services = data.services || data || [];
36
+
37
+ spinner.succeed(`Found ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
38
+ console.log('');
39
+
40
+ if (services.length === 0) {
41
+ log.warn('No services found with these filters.');
42
+ log.dim(' Try: x402-bazaar list --category ai');
43
+ console.log('');
44
+ return;
45
+ }
46
+
47
+ // Display services table
48
+ log.separator();
49
+ console.log('');
50
+
51
+ services.forEach((service, idx) => {
52
+ const name = service.name || service.title || 'Unnamed Service';
53
+ const price = parseFloat(service.price || 0);
54
+ const priceLabel = price === 0
55
+ ? chalk.hex('#34D399').bold('FREE')
56
+ : chalk.cyan(`$${price.toFixed(3)} USDC`);
57
+
58
+ // Extract category from tags
59
+ let category = 'other';
60
+ if (service.tags && Array.isArray(service.tags)) {
61
+ const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
62
+ if (realCategories.length > 0) {
63
+ category = realCategories[0];
64
+ }
65
+ }
66
+
67
+ const chain = service.chain || 'base';
68
+ const chainLabel = chain === 'skale'
69
+ ? chalk.hex('#9333EA')('SKALE')
70
+ : chalk.hex('#0052FF')('Base');
71
+
72
+ console.log(
73
+ chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
74
+ chalk.white.bold(name)
75
+ );
76
+ console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
77
+
78
+ if (service.description) {
79
+ const desc = service.description.length > 80
80
+ ? service.description.substring(0, 77) + '...'
81
+ : service.description;
82
+ console.log(` ${chalk.hex('#6B7280')(desc)}`);
83
+ }
84
+
85
+ if (service.endpoint) {
86
+ console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
87
+ }
88
+
89
+ console.log('');
90
+ });
91
+
92
+ log.separator();
93
+ console.log('');
94
+ log.info(`Total: ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
95
+
96
+ // Show filter info
97
+ if (options.chain || options.category || options.free) {
98
+ console.log('');
99
+ log.dim(' Active filters:');
100
+ if (options.chain) log.dim(` Chain: ${options.chain}`);
101
+ if (options.category) log.dim(` Category: ${options.category}`);
102
+ if (options.free) log.dim(' Price: free only');
103
+ log.dim(' Run without options to see all services');
104
+ }
105
+
106
+ console.log('');
107
+
108
+ } catch (err) {
109
+ spinner.fail('Failed to fetch services');
110
+
111
+ if (err.name === 'AbortError') {
112
+ log.error('Request timeout — server may be sleeping (Render free tier)');
113
+ log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
114
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
115
+ log.error('Cannot connect to server');
116
+ log.dim(` Server URL: ${serverUrl}`);
117
+ log.dim(' Check your internet connection or try: npx x402-bazaar status');
118
+ } else {
119
+ log.error(err.message);
120
+ }
121
+
122
+ console.log('');
123
+ process.exit(1);
124
+ }
125
+ }
@@ -0,0 +1,120 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { log } from '../utils/logger.js';
4
+
5
+ const CATEGORIES = [
6
+ 'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
7
+ 'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
8
+ ];
9
+
10
+ export async function searchCommand(query, options) {
11
+ if (!query || query.trim().length === 0) {
12
+ log.error('Search query is required');
13
+ log.dim(' Usage: x402-bazaar search <query>');
14
+ log.dim(' Example: x402-bazaar search "weather API"');
15
+ console.log('');
16
+ process.exit(1);
17
+ }
18
+
19
+ const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
20
+
21
+ log.banner();
22
+ log.info(`Searching for: ${chalk.bold(query)}`);
23
+ console.log('');
24
+
25
+ const url = `${serverUrl}/api/services?search=${encodeURIComponent(query)}`;
26
+ const spinner = ora('Searching...').start();
27
+
28
+ try {
29
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
30
+
31
+ if (!res.ok) {
32
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
33
+ }
34
+
35
+ const data = await res.json();
36
+ const services = data.services || data || [];
37
+
38
+ if (services.length === 0) {
39
+ spinner.fail('No results found');
40
+ console.log('');
41
+ log.warn(`No services match "${query}"`);
42
+ log.dim(' Tips:');
43
+ log.dim(' • Check your spelling');
44
+ log.dim(' • Try broader keywords (e.g., "AI" instead of "GPT-4")');
45
+ log.dim(' • Browse all services: x402-bazaar list');
46
+ console.log('');
47
+ return;
48
+ }
49
+
50
+ spinner.succeed(`Found ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
51
+ console.log('');
52
+
53
+ // Display results
54
+ log.separator();
55
+ console.log('');
56
+
57
+ services.forEach((service, idx) => {
58
+ const name = service.name || service.title || 'Unnamed Service';
59
+ const price = parseFloat(service.price || 0);
60
+ const priceLabel = price === 0
61
+ ? chalk.hex('#34D399').bold('FREE')
62
+ : chalk.cyan(`$${price.toFixed(3)} USDC`);
63
+
64
+ // Extract category from tags
65
+ let category = 'other';
66
+ if (service.tags && Array.isArray(service.tags)) {
67
+ const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
68
+ if (realCategories.length > 0) {
69
+ category = realCategories[0];
70
+ }
71
+ }
72
+
73
+ const chain = service.chain || 'base';
74
+ const chainLabel = chain === 'skale'
75
+ ? chalk.hex('#9333EA')('SKALE')
76
+ : chalk.hex('#0052FF')('Base');
77
+
78
+ console.log(
79
+ chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
80
+ chalk.white.bold(name)
81
+ );
82
+ console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
83
+
84
+ if (service.description) {
85
+ const desc = service.description.length > 80
86
+ ? service.description.substring(0, 77) + '...'
87
+ : service.description;
88
+ console.log(` ${chalk.hex('#6B7280')(desc)}`);
89
+ }
90
+
91
+ if (service.endpoint) {
92
+ console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
93
+ }
94
+
95
+ console.log('');
96
+ });
97
+
98
+ log.separator();
99
+ console.log('');
100
+ log.info(`Total: ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
101
+ console.log('');
102
+
103
+ } catch (err) {
104
+ spinner.fail('Search failed');
105
+
106
+ if (err.name === 'AbortError') {
107
+ log.error('Request timeout — server may be sleeping (Render free tier)');
108
+ log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
109
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
110
+ log.error('Cannot connect to server');
111
+ log.dim(` Server URL: ${serverUrl}`);
112
+ log.dim(' Check your internet connection or try: npx x402-bazaar status');
113
+ } else {
114
+ log.error(err.message);
115
+ }
116
+
117
+ console.log('');
118
+ process.exit(1);
119
+ }
120
+ }
@@ -0,0 +1,156 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { log } from '../utils/logger.js';
4
+
5
+ const BASE_RPC_URL = 'https://mainnet.base.org';
6
+ const USDC_CONTRACT_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
7
+ const BALANCE_OF_SELECTOR = '0x70a08231';
8
+
9
+ export async function walletCommand(options) {
10
+ log.banner();
11
+
12
+ if (!options.address) {
13
+ log.info('Check USDC balance for any wallet address on Base.');
14
+ console.log('');
15
+ log.dim(' Usage:');
16
+ log.dim(' x402-bazaar wallet --address 0xYourAddress');
17
+ console.log('');
18
+ log.dim(' Example:');
19
+ log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
20
+ console.log('');
21
+ log.dim(' To find your agent wallet address:');
22
+ log.dim(' 1. Run: npx x402-bazaar init');
23
+ log.dim(' 2. Or check your .env file: AGENT_PRIVATE_KEY');
24
+ log.dim(' 3. Or ask your AI agent: "What is my wallet address?"');
25
+ console.log('');
26
+ return;
27
+ }
28
+
29
+ const address = options.address.trim();
30
+
31
+ // Validate address format
32
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
33
+ log.error('Invalid Ethereum address format');
34
+ log.dim(' Expected: 0x followed by 40 hexadecimal characters');
35
+ log.dim(` Got: ${address}`);
36
+ console.log('');
37
+ process.exit(1);
38
+ }
39
+
40
+ log.info(`Checking wallet: ${chalk.bold(maskAddress(address))}`);
41
+ console.log('');
42
+
43
+ const spinner = ora('Fetching USDC balance from Base...').start();
44
+
45
+ try {
46
+ // Encode balanceOf(address) call
47
+ // balanceOf selector: 0x70a08231
48
+ // Param: address (32 bytes, left-padded)
49
+ const paddedAddress = address.slice(2).padStart(64, '0');
50
+ const data = BALANCE_OF_SELECTOR + paddedAddress;
51
+
52
+ // Make RPC call
53
+ const rpcPayload = {
54
+ jsonrpc: '2.0',
55
+ id: 1,
56
+ method: 'eth_call',
57
+ params: [
58
+ {
59
+ to: USDC_CONTRACT_BASE,
60
+ data: data,
61
+ },
62
+ 'latest',
63
+ ],
64
+ };
65
+
66
+ const res = await fetch(BASE_RPC_URL, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify(rpcPayload),
70
+ signal: AbortSignal.timeout(15000),
71
+ });
72
+
73
+ if (!res.ok) {
74
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
75
+ }
76
+
77
+ const rpcResponse = await res.json();
78
+
79
+ if (rpcResponse.error) {
80
+ throw new Error(rpcResponse.error.message || 'RPC error');
81
+ }
82
+
83
+ if (!rpcResponse.result) {
84
+ throw new Error('No result from RPC');
85
+ }
86
+
87
+ // Parse balance (USDC has 6 decimals)
88
+ const balanceHex = rpcResponse.result;
89
+ const balanceRaw = BigInt(balanceHex);
90
+ const balanceUsdc = Number(balanceRaw) / 1_000_000;
91
+
92
+ spinner.succeed('Balance fetched');
93
+ console.log('');
94
+
95
+ log.separator();
96
+ console.log('');
97
+
98
+ log.info(`Address: ${chalk.hex('#34D399')(maskAddress(address))}`);
99
+ log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')} (Chain ID: 8453)`);
100
+ log.info(`Balance: ${chalk.cyan.bold(balanceUsdc.toFixed(6))} ${chalk.dim('USDC')}`);
101
+
102
+ console.log('');
103
+ log.separator();
104
+ console.log('');
105
+
106
+ // Show explorer link
107
+ log.dim(` Explorer: https://basescan.org/address/${address}`);
108
+
109
+ // Helpful tips based on balance
110
+ if (balanceUsdc === 0) {
111
+ console.log('');
112
+ log.warn('This wallet has no USDC.');
113
+ log.dim(' To fund it:');
114
+ log.dim(' 1. Open MetaMask and switch to Base network');
115
+ log.dim(' 2. Send USDC to this address');
116
+ log.dim(' 3. Send a tiny amount of ETH for gas (~$0.01)');
117
+ log.dim(' Get USDC: bridge from Ethereum or buy on Base DEX');
118
+ } else if (balanceUsdc < 0.1) {
119
+ console.log('');
120
+ log.warn('Low balance — consider adding more USDC.');
121
+ log.dim(' Most x402 Bazaar APIs cost $0.005-$0.05 per call.');
122
+ } else {
123
+ console.log('');
124
+ log.success('Wallet is funded and ready!');
125
+ const estimatedCalls = Math.floor(balanceUsdc / 0.02);
126
+ log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.02/call)`);
127
+ }
128
+
129
+ console.log('');
130
+
131
+ } catch (err) {
132
+ spinner.fail('Failed to fetch balance');
133
+ console.log('');
134
+
135
+ if (err.name === 'AbortError') {
136
+ log.error('Request timeout — Base RPC may be slow');
137
+ log.dim(' Try again in a few seconds');
138
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
139
+ log.error('Cannot connect to Base RPC');
140
+ log.dim(' Check your internet connection');
141
+ } else {
142
+ log.error(err.message);
143
+ }
144
+
145
+ console.log('');
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Mask address for display: 0xabcd...1234
152
+ */
153
+ function maskAddress(address) {
154
+ if (address.length < 12) return address;
155
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
156
+ }