x402-bazaar 3.3.0 → 3.4.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  > Connect your AI agent to the x402 Bazaar marketplace in one command.
6
6
 
7
- x402 Bazaar is an autonomous marketplace where AI agents buy and sell API services using the HTTP 402 protocol with USDC payments on Base L2.
7
+ x402 Bazaar is an autonomous marketplace where AI agents buy and sell API services using the HTTP 402 protocol with USDC payments on Base L2, SKALE on Base, and Polygon.
8
8
 
9
9
  ## Quick Start
10
10
 
@@ -69,6 +69,7 @@ npx x402-bazaar list
69
69
  # Filter by blockchain network
70
70
  npx x402-bazaar list --chain base
71
71
  npx x402-bazaar list --chain skale
72
+ npx x402-bazaar list --chain polygon
72
73
 
73
74
  # Filter by category
74
75
  npx x402-bazaar list --category ai
@@ -126,10 +127,10 @@ npx x402-bazaar wallet --balance
126
127
 
127
128
  x402 Bazaar is a marketplace where AI agents autonomously trade API services:
128
129
 
129
- - **Agents pay with USDC** on Base L2 (Coinbase's Layer 2)
130
+ - **Agents pay with USDC** on Base L2, SKALE on Base, or Polygon
130
131
  - **HTTP 402 protocol** — the server responds with payment details, the agent pays, then retries
131
- - **Every payment is verifiable** on-chain via BaseScan
132
- - **70+ services** available (search, AI, crypto, weather, and more)
132
+ - **Every payment is verifiable** on-chain via BaseScan, SKALE Explorer, or PolygonScan
133
+ - **74+ services** available (search, AI, crypto, weather, and more)
133
134
 
134
135
  ### Pricing
135
136
 
@@ -149,7 +150,7 @@ x402 Bazaar is a marketplace where AI agents autonomously trade API services:
149
150
 
150
151
  | Repository | Description |
151
152
  |---|---|
152
- | **[x402-backend](https://github.com/Wintyx57/x402-backend)** | API server, 69 native endpoints, payment middleware, MCP server |
153
+ | **[x402-backend](https://github.com/Wintyx57/x402-backend)** | API server, 74 native endpoints, payment middleware, MCP server |
153
154
  | **[x402-frontend](https://github.com/Wintyx57/x402-frontend)** | React + TypeScript UI, wallet connect |
154
155
  | **[x402-bazaar-cli](https://github.com/Wintyx57/x402-bazaar-cli)** | `npx x402-bazaar` -- CLI with 7 commands (this repo) |
155
156
  | **[x402-sdk](https://github.com/Wintyx57/x402-sdk)** | TypeScript SDK for AI agents |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-bazaar",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
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",
@@ -104,10 +104,19 @@ export async function callCommand(endpoint, options) {
104
104
  const serverSplit = paymentInfo.payment_details?.split || null;
105
105
  const isSplitMode = !!(providerWallet);
106
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
+
107
112
  if (price) {
108
113
  log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
109
114
  }
110
- if (isSplitMode) {
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) {
111
120
  log.dim(` Mode: split native (95% provider / 5% platform)`);
112
121
  log.dim(` Provider wallet: ${providerWallet}`);
113
122
  log.dim(` Platform wallet: ${payTo}`);
@@ -118,7 +127,11 @@ export async function callCommand(endpoint, options) {
118
127
 
119
128
  // Auto-pay if key is available
120
129
  if (autoPay && price && payTo) {
121
- if (isSplitMode) {
130
+ if (isFacilitatorMode) {
131
+ await handleFacilitatorPayment(
132
+ privateKey, price, facilitatorUrl, paymentInfo.payment_details, finalUrl, fetchOptions
133
+ );
134
+ } else if (isSplitMode) {
122
135
  await handleSplitAutoPayment(
123
136
  privateKey, price, providerWallet, payTo, serverSplit, finalUrl, fetchOptions
124
137
  );
@@ -230,6 +243,85 @@ function normalizeKey(key) {
230
243
  return key;
231
244
  }
232
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
+
233
325
  /**
234
326
  * Handle split native payment (95% to provider, 5% to platform) and retry.
235
327
  *
@@ -146,7 +146,7 @@ export async function initCommand(options) {
146
146
  if (!existsSync(pkgJsonPath)) {
147
147
  writeFileSync(pkgJsonPath, JSON.stringify({
148
148
  name: 'x402-bazaar-mcp',
149
- version: '2.0.0',
149
+ version: '2.5.0',
150
150
  type: 'module',
151
151
  private: true,
152
152
  dependencies: {
@@ -1,6 +1,7 @@
1
1
  import { createWalletClient, createPublicClient, http, parseUnits, encodeFunctionData } from 'viem';
2
- import { base } from 'viem/chains';
2
+ import { base, polygon } from 'viem/chains';
3
3
  import { privateKeyToAccount } from 'viem/accounts';
4
+ import { randomBytes } from 'crypto';
4
5
 
5
6
  const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
6
7
  const USDC_ABI = [
@@ -25,6 +26,9 @@ const USDC_ABI = [
25
26
  /** Minimum amount in micro-USDC (6 decimals) to allow a split payment. */
26
27
  const MIN_SPLIT_AMOUNT_RAW = 100n; // 0.0001 USDC
27
28
 
29
+ /** USDC contract on Polygon mainnet (Circle native, 6 decimals). */
30
+ const POLYGON_USDC_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
31
+
28
32
  /**
29
33
  * Build viem wallet + public clients for Base mainnet.
30
34
  * @param {string} privateKey
@@ -38,6 +42,161 @@ function buildClients(privateKey) {
38
42
  return { walletClient, publicClient, account };
39
43
  }
40
44
 
45
+ /**
46
+ * Build viem wallet + public clients for Polygon mainnet.
47
+ * @param {string} privateKey
48
+ * @returns {{ walletClient, publicClient, account }}
49
+ */
50
+ function buildPolygonClients(privateKey) {
51
+ const account = privateKeyToAccount(privateKey);
52
+ const transport = http('https://polygon-bor-rpc.publicnode.com');
53
+ const walletClient = createWalletClient({ account, chain: polygon, transport });
54
+ const publicClient = createPublicClient({ chain: polygon, transport });
55
+ return { walletClient, publicClient, account };
56
+ }
57
+
58
+ /**
59
+ * Sign an EIP-3009 TransferWithAuthorization off-chain (zero gas).
60
+ * Used for Polygon facilitator payments.
61
+ *
62
+ * @param {object} walletClient - viem wallet client (Polygon chain)
63
+ * @param {object} account - viem account
64
+ * @param {string} amountRaw - amount as string (integer, 6 decimals)
65
+ * @param {string} to - recipient address
66
+ * @param {number} validAfter - unix timestamp (usually 0)
67
+ * @param {number} validBefore - unix timestamp (5 min from now)
68
+ * @returns {{ signature: string, authorization: object }}
69
+ */
70
+ async function signEIP3009Auth(walletClient, account, amountRaw, to, validAfter, validBefore) {
71
+ // Random bytes32 nonce (EIP-3009 uses random nonces, not sequential)
72
+ const nonce = '0x' + randomBytes(32).toString('hex');
73
+
74
+ const domain = {
75
+ name: 'USD Coin',
76
+ version: '2',
77
+ chainId: 137,
78
+ verifyingContract: POLYGON_USDC_CONTRACT,
79
+ };
80
+
81
+ const types = {
82
+ TransferWithAuthorization: [
83
+ { name: 'from', type: 'address' },
84
+ { name: 'to', type: 'address' },
85
+ { name: 'value', type: 'uint256' },
86
+ { name: 'validAfter', type: 'uint256' },
87
+ { name: 'validBefore', type: 'uint256' },
88
+ { name: 'nonce', type: 'bytes32' },
89
+ ],
90
+ };
91
+
92
+ const message = {
93
+ from: account.address,
94
+ to,
95
+ value: BigInt(amountRaw),
96
+ validAfter: BigInt(validAfter),
97
+ validBefore: BigInt(validBefore),
98
+ nonce,
99
+ };
100
+
101
+ const signature = await walletClient.signTypedData({
102
+ domain,
103
+ types,
104
+ primaryType: 'TransferWithAuthorization',
105
+ message,
106
+ });
107
+
108
+ return {
109
+ signature,
110
+ authorization: {
111
+ from: account.address,
112
+ to,
113
+ value: amountRaw.toString(),
114
+ validAfter: validAfter.toString(),
115
+ validBefore: validBefore.toString(),
116
+ nonce,
117
+ },
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Pay via Polygon facilitator (EIP-3009, gas-free for the user).
123
+ *
124
+ * Flow:
125
+ * 1. Sign EIP-3009 TransferWithAuthorization off-chain ($0 gas)
126
+ * 2. POST to facilitator /settle — facilitator executes on-chain
127
+ * 3. Return the txHash from the facilitator
128
+ *
129
+ * @param {string} privateKey - Hex private key (with 0x prefix)
130
+ * @param {string} facilitatorUrl - Base URL of the facilitator (e.g. https://x402.polygon.technology)
131
+ * @param {object} details - Payment details from the 402 response body
132
+ * @param {string} details.amount - Amount in USDC (e.g. "0.01")
133
+ * @param {string} details.recipient - Recipient address (FeeSplitter contract or platform wallet)
134
+ * @param {string} apiUrl - Original API URL (used as resource in paymentRequirements)
135
+ * @returns {string} txHash
136
+ * @throws {Error} if the facilitator rejects the settlement
137
+ */
138
+ export async function sendViaFacilitator(privateKey, facilitatorUrl, details, apiUrl) {
139
+ const { walletClient, account } = buildPolygonClients(privateKey);
140
+
141
+ const cost = parseFloat(details.amount);
142
+ const amountRaw = BigInt(Math.round(cost * 1e6));
143
+
144
+ const validAfter = 0;
145
+ const validBefore = Math.floor(Date.now() / 1000) + 300; // 5 minutes
146
+
147
+ const recipient = details.recipient;
148
+
149
+ // Step 1: Sign EIP-3009 TransferWithAuthorization off-chain (zero gas)
150
+ const { signature, authorization } = await signEIP3009Auth(
151
+ walletClient,
152
+ account,
153
+ amountRaw.toString(),
154
+ recipient,
155
+ validAfter,
156
+ validBefore,
157
+ );
158
+
159
+ // Step 2: Build x402 paymentPayload (Version 1, exact scheme, EVM)
160
+ const paymentPayload = {
161
+ x402Version: 1,
162
+ scheme: 'exact',
163
+ network: 'polygon',
164
+ payload: { signature, authorization },
165
+ };
166
+
167
+ const paymentRequirements = {
168
+ scheme: 'exact',
169
+ network: 'polygon',
170
+ maxAmountRequired: amountRaw.toString(),
171
+ resource: apiUrl,
172
+ description: 'x402 Bazaar API payment',
173
+ mimeType: 'application/json',
174
+ payTo: recipient,
175
+ asset: POLYGON_USDC_CONTRACT,
176
+ maxTimeoutSeconds: 60,
177
+ };
178
+
179
+ // Step 3: POST to facilitator /settle
180
+ const settleUrl = `${facilitatorUrl}/settle`;
181
+ const settleRes = await fetch(settleUrl, {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ x402Version: 1, paymentPayload, paymentRequirements }),
185
+ signal: AbortSignal.timeout(30000),
186
+ });
187
+
188
+ const settleData = await settleRes.json();
189
+
190
+ if (!settleData.success) {
191
+ throw new Error(
192
+ `Facilitator settlement failed: ${settleData.errorReason || 'unknown'} — ` +
193
+ `${settleData.errorMessage || JSON.stringify(settleData)}`
194
+ );
195
+ }
196
+
197
+ return settleData.transaction;
198
+ }
199
+
41
200
  /**
42
201
  * Send a single USDC transfer on Base mainnet and wait for confirmation.
43
202
  * Caller is responsible for balance checks.