x402-bazaar 2.0.0 → 3.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 +8 -6
- package/package.json +4 -3
- package/src/commands/call.js +176 -68
- package/src/commands/wallet.js +110 -50
- package/src/lib/payment.js +106 -0
package/bin/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const program = new Command();
|
|
|
25
25
|
program
|
|
26
26
|
.name('x402-bazaar')
|
|
27
27
|
.description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
|
|
28
|
-
.version('
|
|
28
|
+
.version('3.0.0');
|
|
29
29
|
|
|
30
30
|
program
|
|
31
31
|
.command('init')
|
|
@@ -70,23 +70,25 @@ program
|
|
|
70
70
|
|
|
71
71
|
program
|
|
72
72
|
.command('call <endpoint>')
|
|
73
|
-
.description('Call a marketplace endpoint
|
|
73
|
+
.description('Call a marketplace endpoint with automatic x402 payment')
|
|
74
74
|
.option('--param <key=value>', 'Add parameter (can be used multiple times)', (value, previous) => {
|
|
75
75
|
return previous ? [...previous, value] : [value];
|
|
76
76
|
}, [])
|
|
77
|
+
.option('--key <privateKey>', 'Private key for auto-payment (or set X402_PRIVATE_KEY env)')
|
|
77
78
|
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
78
79
|
.action(callCommand);
|
|
79
80
|
|
|
80
81
|
program
|
|
81
82
|
.command('wallet')
|
|
82
|
-
.description('Check USDC wallet balance
|
|
83
|
+
.description('Check USDC wallet balance or generate a new wallet')
|
|
83
84
|
.option('--address <address>', 'Ethereum address to check')
|
|
85
|
+
.option('--setup', 'Generate a new wallet for auto-payment')
|
|
84
86
|
.action(walletCommand);
|
|
85
87
|
|
|
86
88
|
// Default: show help if no command given
|
|
87
89
|
if (process.argv.length <= 2) {
|
|
88
90
|
console.log('');
|
|
89
|
-
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI
|
|
91
|
+
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v3'));
|
|
90
92
|
console.log('');
|
|
91
93
|
console.log(' Setup commands:');
|
|
92
94
|
console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
|
|
@@ -96,8 +98,8 @@ if (process.argv.length <= 2) {
|
|
|
96
98
|
console.log(' Marketplace commands:');
|
|
97
99
|
console.log(chalk.cyan(' npx x402-bazaar list') + chalk.dim(' Browse all services'));
|
|
98
100
|
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('
|
|
100
|
-
console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check wallet
|
|
101
|
+
console.log(chalk.cyan(' npx x402-bazaar call <endpoint>') + chalk.dim(' Call API with auto-payment'));
|
|
102
|
+
console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check balance / setup wallet'));
|
|
101
103
|
console.log('');
|
|
102
104
|
console.log(chalk.dim(' Run any command with --help for detailed options'));
|
|
103
105
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bazaar",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CLI for x402 Bazaar — AI Agent Marketplace. Browse, call, and auto-pay APIs with USDC on Base. One command to connect your agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"commander": "^12.1.0",
|
|
46
46
|
"inquirer": "^9.3.0",
|
|
47
|
-
"ora": "^8.0.1"
|
|
47
|
+
"ora": "^8.0.1",
|
|
48
|
+
"viem": "^2.45.3"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/src/commands/call.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
3
5
|
import { log } from '../utils/logger.js';
|
|
4
6
|
|
|
5
7
|
export async function callCommand(endpoint, options) {
|
|
@@ -7,14 +9,12 @@ export async function callCommand(endpoint, options) {
|
|
|
7
9
|
log.error('Endpoint is required');
|
|
8
10
|
log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
|
|
9
11
|
log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
|
|
10
|
-
log.dim(' Example: x402-bazaar call /api/
|
|
12
|
+
log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
|
|
11
13
|
console.log('');
|
|
12
14
|
process.exit(1);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
16
|
-
|
|
17
|
-
// Normalize endpoint (add leading slash if missing)
|
|
18
18
|
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
19
19
|
const fullUrl = `${serverUrl}${normalizedEndpoint}`;
|
|
20
20
|
|
|
@@ -26,15 +26,13 @@ export async function callCommand(endpoint, options) {
|
|
|
26
26
|
const params = {};
|
|
27
27
|
if (options.param) {
|
|
28
28
|
const paramArray = Array.isArray(options.param) ? options.param : [options.param];
|
|
29
|
-
|
|
30
29
|
for (const p of paramArray) {
|
|
31
30
|
const [key, ...valueParts] = p.split('=');
|
|
32
31
|
if (!key || valueParts.length === 0) {
|
|
33
32
|
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
34
33
|
continue;
|
|
35
34
|
}
|
|
36
|
-
let value = valueParts.join('=');
|
|
37
|
-
// Strip quotes if present
|
|
35
|
+
let value = valueParts.join('=');
|
|
38
36
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
39
37
|
(value.startsWith("'") && value.endsWith("'"))) {
|
|
40
38
|
value = value.slice(1, -1);
|
|
@@ -51,7 +49,21 @@ export async function callCommand(endpoint, options) {
|
|
|
51
49
|
console.log('');
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
//
|
|
52
|
+
// Resolve private key for auto-payment
|
|
53
|
+
const privateKey = resolvePrivateKey(options);
|
|
54
|
+
const autoPay = !!privateKey;
|
|
55
|
+
|
|
56
|
+
if (autoPay) {
|
|
57
|
+
log.info(`Auto-payment: ${chalk.hex('#34D399').bold('enabled')}`);
|
|
58
|
+
try {
|
|
59
|
+
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
60
|
+
const address = getAddressFromKey(privateKey);
|
|
61
|
+
log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
62
|
+
} catch { /* ignore display errors */ }
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build URL with query params
|
|
55
67
|
let finalUrl = fullUrl;
|
|
56
68
|
if (Object.keys(params).length > 0) {
|
|
57
69
|
const queryString = new URLSearchParams(params).toString();
|
|
@@ -63,14 +75,11 @@ export async function callCommand(endpoint, options) {
|
|
|
63
75
|
try {
|
|
64
76
|
const fetchOptions = {
|
|
65
77
|
method: 'GET',
|
|
66
|
-
headers: {
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
},
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
79
|
signal: AbortSignal.timeout(30000),
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
const res = await fetch(finalUrl, fetchOptions);
|
|
73
|
-
|
|
74
83
|
spinner.stop();
|
|
75
84
|
|
|
76
85
|
// Handle 402 Payment Required
|
|
@@ -79,32 +88,49 @@ export async function callCommand(endpoint, options) {
|
|
|
79
88
|
log.warn(chalk.bold('Payment Required (HTTP 402)'));
|
|
80
89
|
console.log('');
|
|
81
90
|
|
|
91
|
+
let paymentInfo;
|
|
82
92
|
try {
|
|
83
|
-
|
|
93
|
+
paymentInfo = await res.json();
|
|
94
|
+
} catch {
|
|
95
|
+
log.error('Could not parse payment details');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
}
|
|
99
|
+
const price = paymentInfo.payment_details?.amount || paymentInfo.price;
|
|
100
|
+
const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
log.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
log.dim(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (price) {
|
|
103
|
+
log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
|
|
104
|
+
}
|
|
105
|
+
if (payTo) {
|
|
106
|
+
log.dim(` Pay to: ${payTo}`);
|
|
107
|
+
}
|
|
108
|
+
console.log('');
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
// Auto-pay if key is available
|
|
111
|
+
if (autoPay && price && payTo) {
|
|
112
|
+
await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
|
|
113
|
+
return;
|
|
107
114
|
}
|
|
115
|
+
|
|
116
|
+
// No auto-pay — show instructions
|
|
117
|
+
log.separator();
|
|
118
|
+
console.log('');
|
|
119
|
+
log.info('To pay automatically, provide your private key:');
|
|
120
|
+
console.log('');
|
|
121
|
+
log.dim(' Option 1: Environment variable');
|
|
122
|
+
log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
|
|
123
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
|
|
124
|
+
console.log('');
|
|
125
|
+
log.dim(' Option 2: --key flag');
|
|
126
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key 0xYourKey');
|
|
127
|
+
console.log('');
|
|
128
|
+
log.dim(' Option 3: Generate a new wallet');
|
|
129
|
+
log.dim(' npx x402-bazaar wallet --setup');
|
|
130
|
+
console.log('');
|
|
131
|
+
log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
|
|
132
|
+
log.dim(' npx x402-bazaar init');
|
|
133
|
+
console.log('');
|
|
108
134
|
return;
|
|
109
135
|
}
|
|
110
136
|
|
|
@@ -112,7 +138,6 @@ export async function callCommand(endpoint, options) {
|
|
|
112
138
|
if (!res.ok) {
|
|
113
139
|
console.log('');
|
|
114
140
|
log.error(`HTTP ${res.status}: ${res.statusText}`);
|
|
115
|
-
|
|
116
141
|
try {
|
|
117
142
|
const errorBody = await res.text();
|
|
118
143
|
if (errorBody) {
|
|
@@ -120,61 +145,115 @@ export async function callCommand(endpoint, options) {
|
|
|
120
145
|
log.dim('Response:');
|
|
121
146
|
console.log(chalk.red(errorBody));
|
|
122
147
|
}
|
|
123
|
-
} catch {
|
|
124
|
-
// Ignore
|
|
125
|
-
}
|
|
126
|
-
|
|
148
|
+
} catch { /* ignore */ }
|
|
127
149
|
console.log('');
|
|
128
150
|
process.exit(1);
|
|
129
151
|
}
|
|
130
152
|
|
|
131
153
|
// Success
|
|
154
|
+
await displayResponse(res);
|
|
155
|
+
|
|
156
|
+
} catch (err) {
|
|
157
|
+
spinner.fail('Request failed');
|
|
132
158
|
console.log('');
|
|
133
|
-
|
|
159
|
+
|
|
160
|
+
if (err.name === 'AbortError') {
|
|
161
|
+
log.error('Request timeout (30s)');
|
|
162
|
+
log.dim(' Try again or check status: npx x402-bazaar status');
|
|
163
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
164
|
+
log.error('Cannot connect to server');
|
|
165
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
166
|
+
} else {
|
|
167
|
+
log.error(err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
console.log('');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
135
174
|
|
|
136
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
|
|
177
|
+
*/
|
|
178
|
+
function resolvePrivateKey(options) {
|
|
179
|
+
if (options.key) {
|
|
180
|
+
return normalizeKey(options.key);
|
|
181
|
+
}
|
|
137
182
|
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
if (process.env.X402_PRIVATE_KEY) {
|
|
184
|
+
return normalizeKey(process.env.X402_PRIVATE_KEY);
|
|
185
|
+
}
|
|
140
186
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
// Try local wallet file
|
|
188
|
+
try {
|
|
189
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
190
|
+
const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
|
|
191
|
+
if (fs.existsSync(walletPath)) {
|
|
192
|
+
const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
193
|
+
if (data.privateKey) {
|
|
194
|
+
return normalizeKey(data.privateKey);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch { /* ignore */ }
|
|
145
198
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const highlighted = highlightJson(jsonString);
|
|
149
|
-
console.log(highlighted);
|
|
150
|
-
console.log('');
|
|
151
|
-
log.separator();
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
152
201
|
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
function normalizeKey(key) {
|
|
203
|
+
key = key.trim();
|
|
204
|
+
if (!key.startsWith('0x')) key = '0x' + key;
|
|
205
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
|
|
206
|
+
return key;
|
|
207
|
+
}
|
|
155
208
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Handle automatic x402 payment and retry
|
|
211
|
+
*/
|
|
212
|
+
async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
|
|
213
|
+
const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const { sendUsdcPayment } = await import('../lib/payment.js');
|
|
217
|
+
const payment = await sendUsdcPayment(privateKey, payTo, price);
|
|
218
|
+
|
|
219
|
+
spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
|
|
220
|
+
log.dim(` Tx: ${payment.explorer}`);
|
|
221
|
+
console.log('');
|
|
222
|
+
|
|
223
|
+
// Retry with payment proof
|
|
224
|
+
const retrySpinner = ora('Retrying with payment proof...').start();
|
|
225
|
+
|
|
226
|
+
const retryRes = await fetch(url, {
|
|
227
|
+
...fetchOptions,
|
|
228
|
+
headers: {
|
|
229
|
+
...fetchOptions.headers,
|
|
230
|
+
'X-Payment-TxHash': payment.txHash,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
retrySpinner.stop();
|
|
235
|
+
|
|
236
|
+
if (!retryRes.ok) {
|
|
159
237
|
console.log('');
|
|
160
|
-
|
|
238
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
239
|
+
try {
|
|
240
|
+
const body = await retryRes.text();
|
|
241
|
+
if (body) console.log(chalk.red(body));
|
|
242
|
+
} catch { /* ignore */ }
|
|
161
243
|
console.log('');
|
|
162
|
-
|
|
244
|
+
process.exit(1);
|
|
163
245
|
}
|
|
164
246
|
|
|
165
|
-
|
|
247
|
+
await displayResponse(retryRes);
|
|
166
248
|
|
|
167
249
|
} catch (err) {
|
|
168
|
-
spinner.fail('
|
|
250
|
+
spinner.fail('Payment failed');
|
|
169
251
|
console.log('');
|
|
170
252
|
|
|
171
|
-
if (err.
|
|
172
|
-
log.error(
|
|
173
|
-
log.dim('
|
|
174
|
-
|
|
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');
|
|
253
|
+
if (err.message.includes('Insufficient USDC')) {
|
|
254
|
+
log.error(err.message);
|
|
255
|
+
log.dim(' Fund your wallet with USDC on Base.');
|
|
256
|
+
log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
|
|
178
257
|
} else {
|
|
179
258
|
log.error(err.message);
|
|
180
259
|
}
|
|
@@ -185,8 +264,37 @@ export async function callCommand(endpoint, options) {
|
|
|
185
264
|
}
|
|
186
265
|
|
|
187
266
|
/**
|
|
188
|
-
*
|
|
267
|
+
* Display API response with JSON highlighting
|
|
189
268
|
*/
|
|
269
|
+
async function displayResponse(res) {
|
|
270
|
+
console.log('');
|
|
271
|
+
log.success(`${chalk.bold(res.status)} ${res.statusText}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
|
|
274
|
+
const contentType = res.headers.get('content-type') || '';
|
|
275
|
+
|
|
276
|
+
if (contentType.includes('application/json')) {
|
|
277
|
+
const responseData = await res.json();
|
|
278
|
+
log.separator();
|
|
279
|
+
console.log('');
|
|
280
|
+
log.info('Response (JSON):');
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(highlightJson(JSON.stringify(responseData, null, 2)));
|
|
283
|
+
console.log('');
|
|
284
|
+
log.separator();
|
|
285
|
+
} else {
|
|
286
|
+
const responseText = await res.text();
|
|
287
|
+
log.separator();
|
|
288
|
+
console.log('');
|
|
289
|
+
log.info('Response:');
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(chalk.white(responseText));
|
|
292
|
+
console.log('');
|
|
293
|
+
log.separator();
|
|
294
|
+
}
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
|
|
190
298
|
function highlightJson(jsonString) {
|
|
191
299
|
return jsonString
|
|
192
300
|
.replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
|
package/src/commands/wallet.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
3
5
|
import { log } from '../utils/logger.js';
|
|
4
6
|
|
|
5
7
|
const BASE_RPC_URL = 'https://mainnet.base.org';
|
|
@@ -9,26 +11,28 @@ const BALANCE_OF_SELECTOR = '0x70a08231';
|
|
|
9
11
|
export async function walletCommand(options) {
|
|
10
12
|
log.banner();
|
|
11
13
|
|
|
14
|
+
// Handle --setup: generate a new wallet
|
|
15
|
+
if (options.setup) {
|
|
16
|
+
await setupWallet();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
if (!options.address) {
|
|
13
|
-
log.info('Check USDC balance
|
|
21
|
+
log.info('Check USDC balance or generate a new wallet.');
|
|
14
22
|
console.log('');
|
|
15
23
|
log.dim(' Usage:');
|
|
16
24
|
log.dim(' x402-bazaar wallet --address 0xYourAddress');
|
|
25
|
+
log.dim(' x402-bazaar wallet --setup');
|
|
17
26
|
console.log('');
|
|
18
|
-
log.dim('
|
|
27
|
+
log.dim(' Examples:');
|
|
19
28
|
log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
|
|
20
|
-
|
|
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?"');
|
|
29
|
+
log.dim(' x402-bazaar wallet --setup (generate a new wallet for auto-payment)');
|
|
25
30
|
console.log('');
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const address = options.address.trim();
|
|
30
35
|
|
|
31
|
-
// Validate address format
|
|
32
36
|
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
|
|
33
37
|
log.error('Invalid Ethereum address format');
|
|
34
38
|
log.dim(' Expected: 0x followed by 40 hexadecimal characters');
|
|
@@ -43,24 +47,14 @@ export async function walletCommand(options) {
|
|
|
43
47
|
const spinner = ora('Fetching USDC balance from Base...').start();
|
|
44
48
|
|
|
45
49
|
try {
|
|
46
|
-
// Encode balanceOf(address) call
|
|
47
|
-
// balanceOf selector: 0x70a08231
|
|
48
|
-
// Param: address (32 bytes, left-padded)
|
|
49
50
|
const paddedAddress = address.slice(2).padStart(64, '0');
|
|
50
51
|
const data = BALANCE_OF_SELECTOR + paddedAddress;
|
|
51
52
|
|
|
52
|
-
// Make RPC call
|
|
53
53
|
const rpcPayload = {
|
|
54
54
|
jsonrpc: '2.0',
|
|
55
55
|
id: 1,
|
|
56
56
|
method: 'eth_call',
|
|
57
|
-
params: [
|
|
58
|
-
{
|
|
59
|
-
to: USDC_CONTRACT_BASE,
|
|
60
|
-
data: data,
|
|
61
|
-
},
|
|
62
|
-
'latest',
|
|
63
|
-
],
|
|
57
|
+
params: [{ to: USDC_CONTRACT_BASE, data }, 'latest'],
|
|
64
58
|
};
|
|
65
59
|
|
|
66
60
|
const res = await fetch(BASE_RPC_URL, {
|
|
@@ -70,28 +64,17 @@ export async function walletCommand(options) {
|
|
|
70
64
|
signal: AbortSignal.timeout(15000),
|
|
71
65
|
});
|
|
72
66
|
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
75
|
-
}
|
|
67
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
76
68
|
|
|
77
69
|
const rpcResponse = await res.json();
|
|
70
|
+
if (rpcResponse.error) throw new Error(rpcResponse.error.message || 'RPC error');
|
|
71
|
+
if (!rpcResponse.result) throw new Error('No result from RPC');
|
|
78
72
|
|
|
79
|
-
|
|
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);
|
|
73
|
+
const balanceRaw = BigInt(rpcResponse.result);
|
|
90
74
|
const balanceUsdc = Number(balanceRaw) / 1_000_000;
|
|
91
75
|
|
|
92
76
|
spinner.succeed('Balance fetched');
|
|
93
77
|
console.log('');
|
|
94
|
-
|
|
95
78
|
log.separator();
|
|
96
79
|
console.log('');
|
|
97
80
|
|
|
@@ -102,30 +85,22 @@ export async function walletCommand(options) {
|
|
|
102
85
|
console.log('');
|
|
103
86
|
log.separator();
|
|
104
87
|
console.log('');
|
|
105
|
-
|
|
106
|
-
// Show explorer link
|
|
107
88
|
log.dim(` Explorer: https://basescan.org/address/${address}`);
|
|
108
89
|
|
|
109
|
-
// Helpful tips based on balance
|
|
110
90
|
if (balanceUsdc === 0) {
|
|
111
91
|
console.log('');
|
|
112
92
|
log.warn('This wallet has no USDC.');
|
|
113
|
-
log.dim('
|
|
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');
|
|
93
|
+
log.dim(' Send USDC on Base to this address to start using paid APIs.');
|
|
118
94
|
} else if (balanceUsdc < 0.1) {
|
|
119
95
|
console.log('');
|
|
120
96
|
log.warn('Low balance — consider adding more USDC.');
|
|
121
|
-
log.dim(' Most x402 Bazaar APIs cost $0.
|
|
97
|
+
log.dim(' Most x402 Bazaar APIs cost $0.001-$0.05 per call.');
|
|
122
98
|
} else {
|
|
123
99
|
console.log('');
|
|
124
100
|
log.success('Wallet is funded and ready!');
|
|
125
|
-
const estimatedCalls = Math.floor(balanceUsdc / 0.
|
|
126
|
-
log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.
|
|
101
|
+
const estimatedCalls = Math.floor(balanceUsdc / 0.005);
|
|
102
|
+
log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.005/call)`);
|
|
127
103
|
}
|
|
128
|
-
|
|
129
104
|
console.log('');
|
|
130
105
|
|
|
131
106
|
} catch (err) {
|
|
@@ -134,22 +109,107 @@ export async function walletCommand(options) {
|
|
|
134
109
|
|
|
135
110
|
if (err.name === 'AbortError') {
|
|
136
111
|
log.error('Request timeout — Base RPC may be slow');
|
|
137
|
-
log.dim(' Try again in a few seconds');
|
|
138
112
|
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
139
113
|
log.error('Cannot connect to Base RPC');
|
|
140
|
-
log.dim(' Check your internet connection');
|
|
141
114
|
} else {
|
|
142
115
|
log.error(err.message);
|
|
143
116
|
}
|
|
144
|
-
|
|
145
117
|
console.log('');
|
|
146
118
|
process.exit(1);
|
|
147
119
|
}
|
|
148
120
|
}
|
|
149
121
|
|
|
150
122
|
/**
|
|
151
|
-
*
|
|
123
|
+
* Generate a new wallet and save to ~/.x402-bazaar/wallet.json
|
|
152
124
|
*/
|
|
125
|
+
async function setupWallet() {
|
|
126
|
+
log.info('Generating a new wallet for x402 Bazaar auto-payment...');
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
130
|
+
const dir = path.join(home, '.x402-bazaar');
|
|
131
|
+
const walletPath = path.join(dir, 'wallet.json');
|
|
132
|
+
|
|
133
|
+
// Check if wallet already exists
|
|
134
|
+
if (fs.existsSync(walletPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const existing = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
137
|
+
if (existing.privateKey) {
|
|
138
|
+
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
139
|
+
const address = getAddressFromKey(existing.privateKey.startsWith('0x') ? existing.privateKey : '0x' + existing.privateKey);
|
|
140
|
+
console.log('');
|
|
141
|
+
log.warn('A wallet already exists!');
|
|
142
|
+
log.info(`Address: ${chalk.hex('#34D399')(address)}`);
|
|
143
|
+
log.dim(` File: ${walletPath}`);
|
|
144
|
+
console.log('');
|
|
145
|
+
log.dim(' To use a different wallet, delete the file and run --setup again.');
|
|
146
|
+
log.dim(' To check balance: npx x402-bazaar wallet --address ' + address);
|
|
147
|
+
console.log('');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
} catch { /* ignore parse errors, will overwrite */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const spinner = ora('Generating cryptographic key pair...').start();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { generatePrivateKey, privateKeyToAccount } = await import('viem/accounts');
|
|
157
|
+
const privateKey = generatePrivateKey();
|
|
158
|
+
const account = privateKeyToAccount(privateKey);
|
|
159
|
+
|
|
160
|
+
// Create directory
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Save wallet (private key stored locally)
|
|
166
|
+
fs.writeFileSync(walletPath, JSON.stringify({
|
|
167
|
+
address: account.address,
|
|
168
|
+
privateKey: privateKey,
|
|
169
|
+
network: 'base',
|
|
170
|
+
created: new Date().toISOString(),
|
|
171
|
+
}, null, 2), 'utf-8');
|
|
172
|
+
|
|
173
|
+
spinner.succeed('Wallet generated!');
|
|
174
|
+
console.log('');
|
|
175
|
+
log.separator();
|
|
176
|
+
console.log('');
|
|
177
|
+
|
|
178
|
+
log.info(`Address: ${chalk.hex('#34D399').bold(account.address)}`);
|
|
179
|
+
log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')}`);
|
|
180
|
+
log.info(`Saved to: ${chalk.dim(walletPath)}`);
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
log.separator();
|
|
184
|
+
console.log('');
|
|
185
|
+
|
|
186
|
+
log.info('Next steps:');
|
|
187
|
+
console.log('');
|
|
188
|
+
log.dim(' 1. Fund this wallet with USDC on Base:');
|
|
189
|
+
log.dim(` Send USDC to ${chalk.hex('#34D399')(account.address)}`);
|
|
190
|
+
log.dim(' + a tiny amount of ETH for gas (~$0.01)');
|
|
191
|
+
console.log('');
|
|
192
|
+
log.dim(' 2. Call paid APIs automatically:');
|
|
193
|
+
log.dim(` ${chalk.cyan('npx x402-bazaar call /api/weather --param city=Paris')}`);
|
|
194
|
+
log.dim(' (auto-payment will use your saved wallet)');
|
|
195
|
+
console.log('');
|
|
196
|
+
log.dim(' 3. Or set the env variable for any tool:');
|
|
197
|
+
log.dim(` export X402_PRIVATE_KEY=${privateKey}`);
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
log.warn('Keep your private key safe! Anyone with it can spend your funds.');
|
|
201
|
+
log.dim(` Backup: ${walletPath}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
|
|
204
|
+
} catch (err) {
|
|
205
|
+
spinner.fail('Wallet generation failed');
|
|
206
|
+
console.log('');
|
|
207
|
+
log.error(err.message);
|
|
208
|
+
console.log('');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
153
213
|
function maskAddress(address) {
|
|
154
214
|
if (address.length < 12) return address;
|
|
155
215
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createWalletClient, createPublicClient, http, parseUnits, encodeFunctionData } from 'viem';
|
|
2
|
+
import { base } from 'viem/chains';
|
|
3
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
|
|
5
|
+
const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
6
|
+
const USDC_ABI = [
|
|
7
|
+
{
|
|
8
|
+
name: 'transfer',
|
|
9
|
+
type: 'function',
|
|
10
|
+
inputs: [
|
|
11
|
+
{ name: 'to', type: 'address' },
|
|
12
|
+
{ name: 'amount', type: 'uint256' },
|
|
13
|
+
],
|
|
14
|
+
outputs: [{ type: 'bool' }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'balanceOf',
|
|
18
|
+
type: 'function',
|
|
19
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
20
|
+
outputs: [{ type: 'uint256' }],
|
|
21
|
+
stateMutability: 'view',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Send USDC payment on Base mainnet
|
|
27
|
+
* @param {string} privateKey - Hex private key (with 0x prefix)
|
|
28
|
+
* @param {string} toAddress - Recipient wallet address
|
|
29
|
+
* @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
|
|
30
|
+
* @returns {{ txHash: string, explorer: string }}
|
|
31
|
+
*/
|
|
32
|
+
export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
|
|
33
|
+
const account = privateKeyToAccount(privateKey);
|
|
34
|
+
|
|
35
|
+
const walletClient = createWalletClient({
|
|
36
|
+
account,
|
|
37
|
+
chain: base,
|
|
38
|
+
transport: http('https://mainnet.base.org'),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const publicClient = createPublicClient({
|
|
42
|
+
chain: base,
|
|
43
|
+
transport: http('https://mainnet.base.org'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Convert USDC amount to 6-decimal units
|
|
47
|
+
const amount = parseUnits(amountUsdc.toString(), 6);
|
|
48
|
+
|
|
49
|
+
// Check balance first
|
|
50
|
+
const balance = await publicClient.readContract({
|
|
51
|
+
address: USDC_CONTRACT,
|
|
52
|
+
abi: USDC_ABI,
|
|
53
|
+
functionName: 'balanceOf',
|
|
54
|
+
args: [account.address],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (balance < amount) {
|
|
58
|
+
const balanceUsdc = Number(balance) / 1_000_000;
|
|
59
|
+
throw new Error(`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${amountUsdc} USDC)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Send the USDC transfer
|
|
63
|
+
const txHash = await walletClient.writeContract({
|
|
64
|
+
address: USDC_CONTRACT,
|
|
65
|
+
abi: USDC_ABI,
|
|
66
|
+
functionName: 'transfer',
|
|
67
|
+
args: [toAddress, amount],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Wait for confirmation
|
|
71
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
txHash,
|
|
75
|
+
explorer: `https://basescan.org/tx/${txHash}`,
|
|
76
|
+
from: account.address,
|
|
77
|
+
amount: amountUsdc,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the wallet address from a private key
|
|
83
|
+
*/
|
|
84
|
+
export function getAddressFromKey(privateKey) {
|
|
85
|
+
const account = privateKeyToAccount(privateKey);
|
|
86
|
+
return account.address;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get USDC balance for an address
|
|
91
|
+
*/
|
|
92
|
+
export async function getUsdcBalance(address) {
|
|
93
|
+
const publicClient = createPublicClient({
|
|
94
|
+
chain: base,
|
|
95
|
+
transport: http('https://mainnet.base.org'),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const balance = await publicClient.readContract({
|
|
99
|
+
address: USDC_CONTRACT,
|
|
100
|
+
abi: USDC_ABI,
|
|
101
|
+
functionName: 'balanceOf',
|
|
102
|
+
args: [address],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return Number(balance) / 1_000_000;
|
|
106
|
+
}
|