x402-bazaar 3.5.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,511 +1,629 @@
1
- import ora from 'ora';
2
- import chalk from 'chalk';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { log } from '../utils/logger.js';
6
-
7
- export async function callCommand(endpoint, options) {
8
- if (!endpoint || endpoint.trim().length === 0) {
9
- log.error('Endpoint is required');
10
- log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
11
- log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
12
- log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
13
- console.log('');
14
- process.exit(1);
15
- }
16
-
17
- const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
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
- for (const p of paramArray) {
30
- const [key, ...valueParts] = p.split('=');
31
- if (!key || valueParts.length === 0) {
32
- log.warn(`Invalid param format: ${p} (expected key=value)`);
33
- continue;
34
- }
35
- let value = valueParts.join('=');
36
- if ((value.startsWith('"') && value.endsWith('"')) ||
37
- (value.startsWith("'") && value.endsWith("'"))) {
38
- value = value.slice(1, -1);
39
- }
40
- params[key.trim()] = value;
41
- }
42
- }
43
-
44
- if (Object.keys(params).length > 0) {
45
- log.info('Parameters:');
46
- for (const [k, v] of Object.entries(params)) {
47
- log.dim(` ${k}: ${v}`);
48
- }
49
- console.log('');
50
- }
51
-
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
67
- let finalUrl = fullUrl;
68
- if (Object.keys(params).length > 0) {
69
- const queryString = new URLSearchParams(params).toString();
70
- finalUrl = `${fullUrl}?${queryString}`;
71
- }
72
-
73
- const spinner = ora(`GET ${finalUrl}...`).start();
74
-
75
- try {
76
- const fetchOptions = {
77
- method: 'GET',
78
- headers: { 'Content-Type': 'application/json' },
79
- signal: AbortSignal.timeout(30000),
80
- };
81
-
82
- const res = await fetch(finalUrl, fetchOptions);
83
- spinner.stop();
84
-
85
- // Handle 402 Payment Required
86
- if (res.status === 402) {
87
- console.log('');
88
- log.warn(chalk.bold('Payment Required (HTTP 402)'));
89
- console.log('');
90
-
91
- let paymentInfo;
92
- try {
93
- paymentInfo = await res.json();
94
- } catch {
95
- log.error('Could not parse payment details');
96
- process.exit(1);
97
- }
98
-
99
- const price = paymentInfo.payment_details?.amount || paymentInfo.price;
100
- const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
101
-
102
- // Split-native mode: provider_wallet is present in payment_details
103
- const providerWallet = paymentInfo.payment_details?.provider_wallet || null;
104
- const serverSplit = paymentInfo.payment_details?.split || null;
105
- const isSplitMode = !!(providerWallet);
106
-
107
- // Facilitator mode (Polygon Phase 2): payment_mode === 'fee_splitter'
108
- const paymentMode = paymentInfo.payment_details?.payment_mode || null;
109
- const facilitatorUrl = paymentInfo.payment_details?.facilitator || paymentInfo.facilitator || null;
110
- const isFacilitatorMode = paymentMode === 'fee_splitter' && !!facilitatorUrl;
111
-
112
- if (price) {
113
- log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
114
- }
115
- if (isFacilitatorMode) {
116
- log.dim(` Mode: fee_splitter via Polygon facilitator (gas-free)`);
117
- log.dim(` Facilitator: ${facilitatorUrl}`);
118
- log.dim(` Recipient: ${payTo}`);
119
- } else if (isSplitMode) {
120
- log.dim(` Mode: split native (95% provider / 5% platform)`);
121
- log.dim(` Provider wallet: ${providerWallet}`);
122
- log.dim(` Platform wallet: ${payTo}`);
123
- } else if (payTo) {
124
- log.dim(` Pay to: ${payTo}`);
125
- }
126
- console.log('');
127
-
128
- // Auto-pay if key is available
129
- if (autoPay && price && payTo) {
130
- if (isFacilitatorMode) {
131
- await handleFacilitatorPayment(
132
- privateKey, price, facilitatorUrl, paymentInfo.payment_details, finalUrl, fetchOptions
133
- );
134
- } else if (isSplitMode) {
135
- await handleSplitAutoPayment(
136
- privateKey, price, providerWallet, payTo, serverSplit, finalUrl, fetchOptions
137
- );
138
- } else {
139
- await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
140
- }
141
- return;
142
- }
143
-
144
- // No auto-pay show instructions
145
- log.separator();
146
- console.log('');
147
- log.info('To pay automatically, provide your private key:');
148
- console.log('');
149
- log.dim(' Option 1: Environment variable (recommended)');
150
- log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
151
- log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
152
- console.log('');
153
- log.dim(' Option 2: Generate a wallet file');
154
- log.dim(' npx x402-bazaar wallet --setup');
155
- console.log('');
156
- log.dim(' Option 3: --key flag (reads from wallet.json file)');
157
- log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key ~/.x402-bazaar/wallet.json');
158
- console.log('');
159
- log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
160
- log.dim(' npx x402-bazaar init');
161
- console.log('');
162
- return;
163
- }
164
-
165
- // Handle other errors
166
- if (!res.ok) {
167
- console.log('');
168
- log.error(`HTTP ${res.status}: ${res.statusText}`);
169
- try {
170
- const errorBody = await res.text();
171
- if (errorBody) {
172
- console.log('');
173
- log.dim('Response:');
174
- console.log(chalk.red(errorBody));
175
- }
176
- } catch { /* ignore */ }
177
- console.log('');
178
- process.exit(1);
179
- }
180
-
181
- // Success
182
- await displayResponse(res);
183
-
184
- } catch (err) {
185
- spinner.fail('Request failed');
186
- console.log('');
187
-
188
- if (err.name === 'AbortError') {
189
- log.error('Request timeout (30s)');
190
- log.dim(' Try again or check status: npx x402-bazaar status');
191
- } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
192
- log.error('Cannot connect to server');
193
- log.dim(` Server URL: ${serverUrl}`);
194
- } else {
195
- log.error(err.message);
196
- }
197
-
198
- console.log('');
199
- process.exit(1);
200
- }
201
- }
202
-
203
- /**
204
- * Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
205
- */
206
- function resolvePrivateKey(options) {
207
- if (options.key) {
208
- const raw = options.key.trim();
209
- // Warn if a raw private key is passed directly as CLI argument (visible in ps aux, shell history)
210
- const hex = raw.startsWith('0x') ? raw.slice(2) : raw;
211
- if (/^[0-9a-fA-F]{64}$/.test(hex)) {
212
- console.log('');
213
- console.warn(chalk.yellow('⚠️ Warning: Using --key on the command line exposes your private key in shell history.'));
214
- console.warn(chalk.yellow('⚠️ Prefer setting the X402_PRIVATE_KEY environment variable instead.'));
215
- console.log('');
216
- }
217
- return normalizeKey(options.key);
218
- }
219
-
220
- if (process.env.X402_PRIVATE_KEY) {
221
- return normalizeKey(process.env.X402_PRIVATE_KEY);
222
- }
223
-
224
- // Try local wallet file
225
- try {
226
- const home = process.env.HOME || process.env.USERPROFILE;
227
- const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
228
- if (fs.existsSync(walletPath)) {
229
- const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
230
- if (data.privateKey) {
231
- return normalizeKey(data.privateKey);
232
- }
233
- }
234
- } catch { /* ignore */ }
235
-
236
- return null;
237
- }
238
-
239
- function normalizeKey(key) {
240
- key = key.trim();
241
- if (!key.startsWith('0x')) key = '0x' + key;
242
- if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
243
- return key;
244
- }
245
-
246
- /**
247
- * Handle Polygon facilitator payment (EIP-3009 gas-free, fee_splitter mode) and retry.
248
- *
249
- * Flow:
250
- * 1. Sign EIP-3009 TransferWithAuthorization off-chain ($0 gas for user)
251
- * 2. POST to facilitator /settle — facilitator executes on-chain
252
- * 3. Retry the API call with the txHash as proof (X-Payment-TxHash header)
253
- *
254
- * Falls back to standard sendUsdcPayment on the same recipient if the facilitator fails.
255
- *
256
- * @param {string} privateKey - Agent private key (hex, with 0x)
257
- * @param {number} price - Full price in USDC
258
- * @param {string} facilitatorUrl - Facilitator base URL
259
- * @param {object} details - payment_details from the 402 response
260
- * @param {string} url - API endpoint URL
261
- * @param {object} fetchOptions - Fetch options for the retry request
262
- */
263
- async function handleFacilitatorPayment(
264
- privateKey, price, facilitatorUrl, details, url, fetchOptions
265
- ) {
266
- const spinner = ora(
267
- `Signing EIP-3009 permit and settling via Polygon facilitator (gas-free)...`
268
- ).start();
269
-
270
- let txHash;
271
-
272
- try {
273
- const { sendViaFacilitator } = await import('../lib/payment.js');
274
- txHash = await sendViaFacilitator(privateKey, facilitatorUrl, details, url);
275
-
276
- spinner.succeed(
277
- `Facilitator settlement confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)} ` +
278
- `(gas-free via Polygon facilitator)`
279
- );
280
- log.dim(` Tx: https://polygonscan.com/tx/${txHash}`);
281
- console.log('');
282
- } catch (facilitatorErr) {
283
- spinner.warn(`Facilitator payment failed falling back to direct transfer`);
284
- log.dim(` Reason: ${facilitatorErr.message}`);
285
- console.log('');
286
-
287
- // Fallback: direct USDC transfer on Polygon (standard sendUsdcPayment on Base
288
- // is intentionally NOT used here; we log a clear message and exit so the user
289
- // knows they need MATIC gas or can retry manually)
290
- log.error('Fallback direct transfer not available for Polygon in facilitator mode.');
291
- log.dim(' Ensure POLYGON_FACILITATOR_URL is reachable or retry later.');
292
- log.dim(' You can also send USDC manually and provide --key with sufficient MATIC for gas.');
293
- console.log('');
294
- process.exit(1);
295
- }
296
-
297
- // Retry with facilitator payment proof
298
- const retrySpinner = ora('Retrying with facilitator payment proof...').start();
299
-
300
- const retryRes = await fetch(url, {
301
- ...fetchOptions,
302
- headers: {
303
- ...fetchOptions.headers,
304
- 'X-Payment-TxHash': txHash,
305
- 'X-Payment-Chain': 'polygon',
306
- },
307
- });
308
-
309
- retrySpinner.stop();
310
-
311
- if (!retryRes.ok) {
312
- console.log('');
313
- log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
314
- try {
315
- const body = await retryRes.text();
316
- if (body) console.log(chalk.red(body));
317
- } catch { /* ignore */ }
318
- console.log('');
319
- process.exit(1);
320
- }
321
-
322
- await displayResponse(retryRes);
323
- }
324
-
325
- /**
326
- * Handle split native payment (95% to provider, 5% to platform) and retry.
327
- *
328
- * On success the retry request carries two separate tx-hash headers:
329
- * X-Payment-TxHash-Provider — hash of the 95% transfer to the provider
330
- * X-Payment-TxHash-Platform hash of the 5% transfer to the platform
331
- *
332
- * @param {string} privateKey - Agent private key (hex, with 0x)
333
- * @param {number} totalPrice - Full price in USDC
334
- * @param {string} providerWallet - Provider wallet address (95% recipient)
335
- * @param {string} platformWallet - Platform wallet address (5% recipient)
336
- * @param {object|null} serverSplit - Optional split amounts from 402 payment_details.split
337
- * @param {string} url - API endpoint URL
338
- * @param {object} fetchOptions - Fetch options passed to the retry request
339
- */
340
- async function handleSplitAutoPayment(
341
- privateKey, totalPrice, providerWallet, platformWallet, serverSplit, url, fetchOptions
342
- ) {
343
- const spinner = ora(
344
- `Sending ${totalPrice} USDC (split: 95% provider / 5% platform)...`
345
- ).start();
346
-
347
- try {
348
- const { sendSplitUsdcPayment } = await import('../lib/payment.js');
349
-
350
- const result = await sendSplitUsdcPayment(privateKey, {
351
- totalAmountUsdc: totalPrice,
352
- providerWallet,
353
- platformWallet,
354
- serverSplit,
355
- });
356
-
357
- spinner.succeed(
358
- `Split payment confirmed: ` +
359
- chalk.hex('#34D399').bold(`${result.providerAmountUsdc.toFixed(6)} USDC`) +
360
- ` to provider + ` +
361
- chalk.hex('#34D399').bold(`${result.platformAmountUsdc.toFixed(6)} USDC`) +
362
- ` to platform`
363
- );
364
- log.dim(` Provider tx: ${result.explorerProvider}`);
365
- log.dim(` Platform tx: ${result.explorerPlatform}`);
366
- console.log('');
367
-
368
- // Retry with both payment proofs
369
- const retrySpinner = ora('Retrying with split payment proof...').start();
370
-
371
- const retryRes = await fetch(url, {
372
- ...fetchOptions,
373
- headers: {
374
- ...fetchOptions.headers,
375
- 'X-Payment-TxHash-Provider': result.txHashProvider,
376
- 'X-Payment-TxHash-Platform': result.txHashPlatform,
377
- },
378
- });
379
-
380
- retrySpinner.stop();
381
-
382
- if (!retryRes.ok) {
383
- console.log('');
384
- log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
385
- try {
386
- const body = await retryRes.text();
387
- if (body) console.log(chalk.red(body));
388
- } catch { /* ignore */ }
389
- console.log('');
390
- process.exit(1);
391
- }
392
-
393
- await displayResponse(retryRes);
394
-
395
- } catch (err) {
396
- spinner.fail('Split payment failed');
397
- console.log('');
398
-
399
- if (err.message.includes('Insufficient USDC')) {
400
- log.error(err.message);
401
- log.dim(' Fund your wallet with USDC on Base.');
402
- log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
403
- } else if (err.message.includes('Amount too small')) {
404
- log.error(err.message);
405
- log.dim(' The service price is too low for a split payment (minimum 0.0001 USDC).');
406
- } else {
407
- log.error(err.message);
408
- }
409
-
410
- console.log('');
411
- process.exit(1);
412
- }
413
- }
414
-
415
- /**
416
- * Handle automatic x402 payment and retry
417
- */
418
- async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
419
- const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
420
-
421
- try {
422
- const { sendUsdcPayment } = await import('../lib/payment.js');
423
- const payment = await sendUsdcPayment(privateKey, payTo, price);
424
-
425
- spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
426
- log.dim(` Tx: ${payment.explorer}`);
427
- console.log('');
428
-
429
- // Retry with payment proof
430
- const retrySpinner = ora('Retrying with payment proof...').start();
431
-
432
- const retryRes = await fetch(url, {
433
- ...fetchOptions,
434
- headers: {
435
- ...fetchOptions.headers,
436
- 'X-Payment-TxHash': payment.txHash,
437
- },
438
- });
439
-
440
- retrySpinner.stop();
441
-
442
- if (!retryRes.ok) {
443
- console.log('');
444
- log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
445
- try {
446
- const body = await retryRes.text();
447
- if (body) console.log(chalk.red(body));
448
- } catch { /* ignore */ }
449
- console.log('');
450
- process.exit(1);
451
- }
452
-
453
- await displayResponse(retryRes);
454
-
455
- } catch (err) {
456
- spinner.fail('Payment failed');
457
- console.log('');
458
-
459
- if (err.message.includes('Insufficient USDC')) {
460
- log.error(err.message);
461
- log.dim(' Fund your wallet with USDC on Base.');
462
- log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
463
- } else {
464
- log.error(err.message);
465
- }
466
-
467
- console.log('');
468
- process.exit(1);
469
- }
470
- }
471
-
472
- /**
473
- * Display API response with JSON highlighting
474
- */
475
- async function displayResponse(res) {
476
- console.log('');
477
- log.success(`${chalk.bold(res.status)} ${res.statusText}`);
478
- console.log('');
479
-
480
- const contentType = res.headers.get('content-type') || '';
481
-
482
- if (contentType.includes('application/json')) {
483
- const responseData = await res.json();
484
- log.separator();
485
- console.log('');
486
- log.info('Response (JSON):');
487
- console.log('');
488
- console.log(highlightJson(JSON.stringify(responseData, null, 2)));
489
- console.log('');
490
- log.separator();
491
- } else {
492
- const responseText = await res.text();
493
- log.separator();
494
- console.log('');
495
- log.info('Response:');
496
- console.log('');
497
- console.log(chalk.white(responseText));
498
- console.log('');
499
- log.separator();
500
- }
501
- console.log('');
502
- }
503
-
504
- function highlightJson(jsonString) {
505
- return jsonString
506
- .replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
507
- .replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
508
- .replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
509
- .replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
510
- .replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
511
- }
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { log } from "../utils/logger.js";
6
+
7
+ /**
8
+ * Parse an array of "key=value" strings into a params object.
9
+ * Splits on the first `=` only; strips surrounding single or double quotes from values.
10
+ *
11
+ * @param {string[]} paramArray
12
+ * @returns {Record<string, string>}
13
+ */
14
+ export function parseParams(paramArray) {
15
+ const params = {};
16
+ if (!paramArray || paramArray.length === 0) return params;
17
+
18
+ for (const p of paramArray) {
19
+ const [key, ...valueParts] = p.split("=");
20
+ if (!key || valueParts.length === 0) {
21
+ continue; // Skip invalid params
22
+ }
23
+ let value = valueParts.join("=");
24
+ if (
25
+ (value.startsWith('"') && value.endsWith('"')) ||
26
+ (value.startsWith("'") && value.endsWith("'"))
27
+ ) {
28
+ value = value.slice(1, -1);
29
+ }
30
+ params[key.trim()] = value;
31
+ }
32
+
33
+ return params;
34
+ }
35
+
36
+ /**
37
+ * Build a full URL from a base URL, an endpoint path, and optional query params.
38
+ * Ensures the endpoint always starts with `/`.
39
+ *
40
+ * @param {string} baseUrl
41
+ * @param {string} endpoint
42
+ * @param {Record<string, string>} params
43
+ * @returns {string}
44
+ */
45
+ export function constructUrl(baseUrl, endpoint, params) {
46
+ const normalizedEndpoint = endpoint.startsWith("/")
47
+ ? endpoint
48
+ : `/${endpoint}`;
49
+ let url = `${baseUrl}${normalizedEndpoint}`;
50
+
51
+ if (Object.keys(params).length > 0) {
52
+ const queryString = new URLSearchParams(params).toString();
53
+ url = `${url}?${queryString}`;
54
+ }
55
+
56
+ return url;
57
+ }
58
+
59
+ export async function callCommand(endpoint, options) {
60
+ if (!endpoint || endpoint.trim().length === 0) {
61
+ log.error("Endpoint is required");
62
+ log.dim(" Usage: x402-bazaar call <endpoint> [--param key=value...]");
63
+ log.dim(" Example: x402-bazaar call /api/weather --param city=Paris");
64
+ log.dim(
65
+ " Example: x402-bazaar call /api/hash --param text=hello --key 0x...",
66
+ );
67
+ console.log("");
68
+ process.exit(1);
69
+ }
70
+
71
+ const serverUrl = options.serverUrl || "https://x402-api.onrender.com";
72
+ const normalizedEndpoint = endpoint.startsWith("/")
73
+ ? endpoint
74
+ : `/${endpoint}`;
75
+
76
+ log.banner();
77
+ log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
78
+ console.log("");
79
+
80
+ // Parse params — warn on invalid format before delegating to parseParams()
81
+ const rawParamArray = options.param
82
+ ? Array.isArray(options.param)
83
+ ? options.param
84
+ : [options.param]
85
+ : [];
86
+ for (const p of rawParamArray) {
87
+ if (!p.includes("="))
88
+ log.warn(`Invalid param format: ${p} (expected key=value)`);
89
+ }
90
+ const params = parseParams(rawParamArray);
91
+
92
+ if (Object.keys(params).length > 0) {
93
+ log.info("Parameters:");
94
+ for (const [k, v] of Object.entries(params)) {
95
+ log.dim(` ${k}: ${v}`);
96
+ }
97
+ console.log("");
98
+ }
99
+
100
+ // Resolve private key for auto-payment
101
+ const privateKey = resolvePrivateKey(options);
102
+ const autoPay = !!privateKey;
103
+
104
+ if (autoPay) {
105
+ log.info(`Auto-payment: ${chalk.hex("#34D399").bold("enabled")}`);
106
+ try {
107
+ const { getAddressFromKey } = await import("../lib/payment.js");
108
+ const address = getAddressFromKey(privateKey);
109
+ log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
110
+ } catch {
111
+ /* ignore display errors */
112
+ }
113
+ console.log("");
114
+ }
115
+
116
+ const finalUrl = constructUrl(serverUrl, normalizedEndpoint, params);
117
+
118
+ const spinner = ora(`GET ${finalUrl}...`).start();
119
+
120
+ try {
121
+ const fetchOptions = {
122
+ method: "GET",
123
+ headers: { "Content-Type": "application/json" },
124
+ signal: AbortSignal.timeout(30000),
125
+ };
126
+
127
+ const res = await fetch(finalUrl, fetchOptions);
128
+ spinner.stop();
129
+
130
+ // Handle 402 Payment Required
131
+ if (res.status === 402) {
132
+ console.log("");
133
+ log.warn(chalk.bold("Payment Required (HTTP 402)"));
134
+ console.log("");
135
+
136
+ let paymentInfo;
137
+ try {
138
+ paymentInfo = await res.json();
139
+ } catch {
140
+ log.error("Could not parse payment details");
141
+ process.exit(1);
142
+ }
143
+
144
+ const price = paymentInfo.payment_details?.amount || paymentInfo.price;
145
+ const payTo =
146
+ paymentInfo.payment_details?.recipient ||
147
+ paymentInfo.payment_details?.walletAddress ||
148
+ paymentInfo.paymentAddress;
149
+
150
+ // Split-native mode: provider_wallet is present in payment_details
151
+ const providerWallet =
152
+ paymentInfo.payment_details?.provider_wallet || null;
153
+ const serverSplit = paymentInfo.payment_details?.split || null;
154
+ const isSplitMode = !!providerWallet;
155
+
156
+ // Facilitator mode (Polygon Phase 2): payment_mode === 'fee_splitter'
157
+ const paymentMode = paymentInfo.payment_details?.payment_mode || null;
158
+ const facilitatorUrl =
159
+ paymentInfo.payment_details?.facilitator ||
160
+ paymentInfo.facilitator ||
161
+ null;
162
+ const isFacilitatorMode =
163
+ paymentMode === "fee_splitter" && !!facilitatorUrl;
164
+
165
+ if (price) {
166
+ log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
167
+ }
168
+ if (isFacilitatorMode) {
169
+ log.dim(` Mode: fee_splitter via Polygon facilitator (gas-free)`);
170
+ log.dim(` Facilitator: ${facilitatorUrl}`);
171
+ log.dim(` Recipient: ${payTo}`);
172
+ } else if (isSplitMode) {
173
+ log.dim(` Mode: split native (95% provider / 5% platform)`);
174
+ log.dim(` Provider wallet: ${providerWallet}`);
175
+ log.dim(` Platform wallet: ${payTo}`);
176
+ } else if (payTo) {
177
+ log.dim(` Pay to: ${payTo}`);
178
+ }
179
+ console.log("");
180
+
181
+ // Auto-pay if key is available
182
+ if (autoPay && price && payTo) {
183
+ if (isFacilitatorMode) {
184
+ await handleFacilitatorPayment(
185
+ privateKey,
186
+ price,
187
+ facilitatorUrl,
188
+ paymentInfo.payment_details,
189
+ finalUrl,
190
+ fetchOptions,
191
+ );
192
+ } else if (isSplitMode) {
193
+ await handleSplitAutoPayment(
194
+ privateKey,
195
+ price,
196
+ providerWallet,
197
+ payTo,
198
+ serverSplit,
199
+ finalUrl,
200
+ fetchOptions,
201
+ );
202
+ } else {
203
+ await handleAutoPayment(
204
+ privateKey,
205
+ payTo,
206
+ price,
207
+ finalUrl,
208
+ fetchOptions,
209
+ );
210
+ }
211
+ return;
212
+ }
213
+
214
+ // No auto-pay show instructions
215
+ log.separator();
216
+ console.log("");
217
+ log.info("To pay automatically, provide your private key:");
218
+ console.log("");
219
+ log.dim(" Option 1: Environment variable (recommended)");
220
+ log.dim(" export X402_PRIVATE_KEY=0xYourPrivateKey");
221
+ log.dim(" npx x402-bazaar call /api/weather --param city=Paris");
222
+ console.log("");
223
+ log.dim(" Option 2: Generate a wallet file");
224
+ log.dim(" npx x402-bazaar wallet --setup");
225
+ console.log("");
226
+ log.dim(" Option 3: --key flag (reads from wallet.json file)");
227
+ log.dim(
228
+ " npx x402-bazaar call /api/weather --param city=Paris --key ~/.x402-bazaar/wallet.json",
229
+ );
230
+ console.log("");
231
+ log.dim(" Option 4: Use the MCP server (via Claude/Cursor)");
232
+ log.dim(" npx x402-bazaar init");
233
+ console.log("");
234
+ return;
235
+ }
236
+
237
+ // Handle other errors
238
+ if (!res.ok) {
239
+ console.log("");
240
+ log.error(`HTTP ${res.status}: ${res.statusText}`);
241
+ try {
242
+ const errorBody = await res.text();
243
+ if (errorBody) {
244
+ console.log("");
245
+ log.dim("Response:");
246
+ console.log(chalk.red(errorBody));
247
+ }
248
+ } catch {
249
+ /* ignore */
250
+ }
251
+ console.log("");
252
+ process.exit(1);
253
+ }
254
+
255
+ // Success
256
+ await displayResponse(res);
257
+ } catch (err) {
258
+ spinner.fail("Request failed");
259
+ console.log("");
260
+
261
+ if (err.name === "AbortError") {
262
+ log.error("Request timeout (30s)");
263
+ log.dim(" Try again or check status: npx x402-bazaar status");
264
+ } else if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
265
+ log.error("Cannot connect to server");
266
+ log.dim(` Server URL: ${serverUrl}`);
267
+ } else {
268
+ log.error(err.message);
269
+ }
270
+
271
+ console.log("");
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
278
+ */
279
+ function resolvePrivateKey(options) {
280
+ if (options.key) {
281
+ const raw = options.key.trim();
282
+ // Warn if a raw private key is passed directly as CLI argument (visible in ps aux, shell history)
283
+ const hex = raw.startsWith("0x") ? raw.slice(2) : raw;
284
+ if (/^[0-9a-fA-F]{64}$/.test(hex)) {
285
+ console.log("");
286
+ console.warn(
287
+ chalk.yellow(
288
+ "⚠️ Warning: Using --key on the command line exposes your private key in shell history.",
289
+ ),
290
+ );
291
+ console.warn(
292
+ chalk.yellow(
293
+ "⚠️ Prefer setting the X402_PRIVATE_KEY environment variable instead.",
294
+ ),
295
+ );
296
+ console.log("");
297
+ }
298
+ return normalizeKey(options.key);
299
+ }
300
+
301
+ if (process.env.X402_PRIVATE_KEY) {
302
+ return normalizeKey(process.env.X402_PRIVATE_KEY);
303
+ }
304
+
305
+ // Try local wallet file
306
+ try {
307
+ const home = process.env.HOME || process.env.USERPROFILE;
308
+ const walletPath = path.join(home, ".x402-bazaar", "wallet.json");
309
+ if (fs.existsSync(walletPath)) {
310
+ const data = JSON.parse(fs.readFileSync(walletPath, "utf-8"));
311
+ if (data.privateKey) {
312
+ return normalizeKey(data.privateKey);
313
+ }
314
+ }
315
+ } catch {
316
+ /* ignore */
317
+ }
318
+
319
+ return null;
320
+ }
321
+
322
+ function normalizeKey(key) {
323
+ key = key.trim();
324
+ if (!key.startsWith("0x")) key = "0x" + key;
325
+ if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
326
+ return key;
327
+ }
328
+
329
+ /**
330
+ * Handle Polygon facilitator payment (EIP-3009 gas-free, fee_splitter mode) and retry.
331
+ *
332
+ * Flow:
333
+ * 1. Sign EIP-3009 TransferWithAuthorization off-chain ($0 gas for user)
334
+ * 2. POST to facilitator /settle facilitator executes on-chain
335
+ * 3. Retry the API call with the txHash as proof (X-Payment-TxHash header)
336
+ *
337
+ * Falls back to standard sendUsdcPayment on the same recipient if the facilitator fails.
338
+ *
339
+ * @param {string} privateKey - Agent private key (hex, with 0x)
340
+ * @param {number} price - Full price in USDC
341
+ * @param {string} facilitatorUrl - Facilitator base URL
342
+ * @param {object} details - payment_details from the 402 response
343
+ * @param {string} url - API endpoint URL
344
+ * @param {object} fetchOptions - Fetch options for the retry request
345
+ */
346
+ async function handleFacilitatorPayment(
347
+ privateKey,
348
+ price,
349
+ facilitatorUrl,
350
+ details,
351
+ url,
352
+ fetchOptions,
353
+ ) {
354
+ const spinner = ora(
355
+ `Signing EIP-3009 permit and settling via Polygon facilitator (gas-free)...`,
356
+ ).start();
357
+
358
+ let txHash;
359
+
360
+ try {
361
+ const { sendViaFacilitator } = await import("../lib/payment.js");
362
+ txHash = await sendViaFacilitator(privateKey, facilitatorUrl, details, url);
363
+
364
+ spinner.succeed(
365
+ `Facilitator settlement confirmed: ${chalk.hex("#34D399").bold(`${price} USDC`)} ` +
366
+ `(gas-free via Polygon facilitator)`,
367
+ );
368
+ log.dim(` Tx: https://polygonscan.com/tx/${txHash}`);
369
+ console.log("");
370
+ } catch (facilitatorErr) {
371
+ spinner.warn(
372
+ `Facilitator payment failed — falling back to direct transfer`,
373
+ );
374
+ log.dim(` Reason: ${facilitatorErr.message}`);
375
+ console.log("");
376
+
377
+ // Fallback: direct USDC transfer on Polygon (standard sendUsdcPayment on Base
378
+ // is intentionally NOT used here; we log a clear message and exit so the user
379
+ // knows they need MATIC gas or can retry manually)
380
+ log.error(
381
+ "Fallback direct transfer not available for Polygon in facilitator mode.",
382
+ );
383
+ log.dim(" Ensure POLYGON_FACILITATOR_URL is reachable or retry later.");
384
+ log.dim(
385
+ " You can also send USDC manually and provide --key with sufficient MATIC for gas.",
386
+ );
387
+ console.log("");
388
+ process.exit(1);
389
+ }
390
+
391
+ // Retry with facilitator payment proof
392
+ const retrySpinner = ora(
393
+ "Retrying with facilitator payment proof...",
394
+ ).start();
395
+
396
+ const retryRes = await fetch(url, {
397
+ ...fetchOptions,
398
+ headers: {
399
+ ...fetchOptions.headers,
400
+ "X-Payment-TxHash": txHash,
401
+ "X-Payment-Chain": "polygon",
402
+ },
403
+ });
404
+
405
+ retrySpinner.stop();
406
+
407
+ if (!retryRes.ok) {
408
+ console.log("");
409
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
410
+ try {
411
+ const body = await retryRes.text();
412
+ if (body) console.log(chalk.red(body));
413
+ } catch {
414
+ /* ignore */
415
+ }
416
+ console.log("");
417
+ process.exit(1);
418
+ }
419
+
420
+ await displayResponse(retryRes);
421
+ }
422
+
423
+ /**
424
+ * Handle split native payment (95% to provider, 5% to platform) and retry.
425
+ *
426
+ * On success the retry request carries two separate tx-hash headers:
427
+ * X-Payment-TxHash-Provider — hash of the 95% transfer to the provider
428
+ * X-Payment-TxHash-Platform — hash of the 5% transfer to the platform
429
+ *
430
+ * @param {string} privateKey - Agent private key (hex, with 0x)
431
+ * @param {number} totalPrice - Full price in USDC
432
+ * @param {string} providerWallet - Provider wallet address (95% recipient)
433
+ * @param {string} platformWallet - Platform wallet address (5% recipient)
434
+ * @param {object|null} serverSplit - Optional split amounts from 402 payment_details.split
435
+ * @param {string} url - API endpoint URL
436
+ * @param {object} fetchOptions - Fetch options passed to the retry request
437
+ */
438
+ async function handleSplitAutoPayment(
439
+ privateKey,
440
+ totalPrice,
441
+ providerWallet,
442
+ platformWallet,
443
+ serverSplit,
444
+ url,
445
+ fetchOptions,
446
+ ) {
447
+ const spinner = ora(
448
+ `Sending ${totalPrice} USDC (split: 95% provider / 5% platform)...`,
449
+ ).start();
450
+
451
+ try {
452
+ const { sendSplitUsdcPayment } = await import("../lib/payment.js");
453
+
454
+ const result = await sendSplitUsdcPayment(privateKey, {
455
+ totalAmountUsdc: totalPrice,
456
+ providerWallet,
457
+ platformWallet,
458
+ serverSplit,
459
+ });
460
+
461
+ spinner.succeed(
462
+ `Split payment confirmed: ` +
463
+ chalk
464
+ .hex("#34D399")
465
+ .bold(`${result.providerAmountUsdc.toFixed(6)} USDC`) +
466
+ ` to provider + ` +
467
+ chalk
468
+ .hex("#34D399")
469
+ .bold(`${result.platformAmountUsdc.toFixed(6)} USDC`) +
470
+ ` to platform`,
471
+ );
472
+ log.dim(` Provider tx: ${result.explorerProvider}`);
473
+ log.dim(` Platform tx: ${result.explorerPlatform}`);
474
+ console.log("");
475
+
476
+ // Retry with both payment proofs
477
+ const retrySpinner = ora("Retrying with split payment proof...").start();
478
+
479
+ const retryRes = await fetch(url, {
480
+ ...fetchOptions,
481
+ headers: {
482
+ ...fetchOptions.headers,
483
+ "X-Payment-TxHash-Provider": result.txHashProvider,
484
+ "X-Payment-TxHash-Platform": result.txHashPlatform,
485
+ },
486
+ });
487
+
488
+ retrySpinner.stop();
489
+
490
+ if (!retryRes.ok) {
491
+ console.log("");
492
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
493
+ try {
494
+ const body = await retryRes.text();
495
+ if (body) console.log(chalk.red(body));
496
+ } catch {
497
+ /* ignore */
498
+ }
499
+ console.log("");
500
+ process.exit(1);
501
+ }
502
+
503
+ await displayResponse(retryRes);
504
+ } catch (err) {
505
+ spinner.fail("Split payment failed");
506
+ console.log("");
507
+
508
+ if (err.message.includes("Insufficient USDC")) {
509
+ log.error(err.message);
510
+ log.dim(" Fund your wallet with USDC on Base.");
511
+ log.dim(
512
+ " Check balance: npx x402-bazaar wallet --address <your-address>",
513
+ );
514
+ } else if (err.message.includes("Amount too small")) {
515
+ log.error(err.message);
516
+ log.dim(
517
+ " The service price is too low for a split payment (minimum 0.0001 USDC).",
518
+ );
519
+ } else {
520
+ log.error(err.message);
521
+ }
522
+
523
+ console.log("");
524
+ process.exit(1);
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Handle automatic x402 payment and retry
530
+ */
531
+ async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
532
+ const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
533
+
534
+ try {
535
+ const { sendUsdcPayment } = await import("../lib/payment.js");
536
+ const payment = await sendUsdcPayment(privateKey, payTo, price);
537
+
538
+ spinner.succeed(
539
+ `Payment confirmed: ${chalk.hex("#34D399").bold(`${price} USDC`)}`,
540
+ );
541
+ log.dim(` Tx: ${payment.explorer}`);
542
+ console.log("");
543
+
544
+ // Retry with payment proof
545
+ const retrySpinner = ora("Retrying with payment proof...").start();
546
+
547
+ const retryRes = await fetch(url, {
548
+ ...fetchOptions,
549
+ headers: {
550
+ ...fetchOptions.headers,
551
+ "X-Payment-TxHash": payment.txHash,
552
+ },
553
+ });
554
+
555
+ retrySpinner.stop();
556
+
557
+ if (!retryRes.ok) {
558
+ console.log("");
559
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
560
+ try {
561
+ const body = await retryRes.text();
562
+ if (body) console.log(chalk.red(body));
563
+ } catch {
564
+ /* ignore */
565
+ }
566
+ console.log("");
567
+ process.exit(1);
568
+ }
569
+
570
+ await displayResponse(retryRes);
571
+ } catch (err) {
572
+ spinner.fail("Payment failed");
573
+ console.log("");
574
+
575
+ if (err.message.includes("Insufficient USDC")) {
576
+ log.error(err.message);
577
+ log.dim(" Fund your wallet with USDC on Base.");
578
+ log.dim(
579
+ " Check balance: npx x402-bazaar wallet --address <your-address>",
580
+ );
581
+ } else {
582
+ log.error(err.message);
583
+ }
584
+
585
+ console.log("");
586
+ process.exit(1);
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Display API response with JSON highlighting
592
+ */
593
+ async function displayResponse(res) {
594
+ console.log("");
595
+ log.success(`${chalk.bold(res.status)} ${res.statusText}`);
596
+ console.log("");
597
+
598
+ const contentType = res.headers.get("content-type") || "";
599
+
600
+ if (contentType.includes("application/json")) {
601
+ const responseData = await res.json();
602
+ log.separator();
603
+ console.log("");
604
+ log.info("Response (JSON):");
605
+ console.log("");
606
+ console.log(highlightJson(JSON.stringify(responseData, null, 2)));
607
+ console.log("");
608
+ log.separator();
609
+ } else {
610
+ const responseText = await res.text();
611
+ log.separator();
612
+ console.log("");
613
+ log.info("Response:");
614
+ console.log("");
615
+ console.log(chalk.white(responseText));
616
+ console.log("");
617
+ log.separator();
618
+ }
619
+ console.log("");
620
+ }
621
+
622
+ function highlightJson(jsonString) {
623
+ return jsonString
624
+ .replace(/"([^"]+)":/g, chalk.hex("#60A5FA")('"$1"') + ":")
625
+ .replace(/: "([^"]+)"/g, ": " + chalk.hex("#34D399")('"$1"'))
626
+ .replace(/: (\d+\.?\d*)/g, ": " + chalk.hex("#FBBF24")("$1"))
627
+ .replace(/: (true|false)/g, ": " + chalk.hex("#9333EA")("$1"))
628
+ .replace(/: null/g, ": " + chalk.hex("#6B7280")("null"));
629
+ }