x402-bazaar 1.2.2 → 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 +47 -7
- package/package.json +2 -2
- package/src/commands/call.js +197 -0
- package/src/commands/list.js +125 -0
- package/src/commands/search.js +120 -0
- package/src/commands/wallet.js +156 -0
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('
|
|
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('
|
|
58
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
59
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
60
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
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
|
|
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": "
|
|
4
|
-
"description": "CLI to set up x402 Bazaar MCP server for AI agents. One command to connect your agent
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|