x402-bazaar 3.2.5 → 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 +6 -5
- package/bin/cli.js +4 -4
- package/package.json +1 -1
- package/src/commands/call.js +94 -2
- package/src/commands/init.js +34 -3
- package/src/generators/env-file.js +12 -1
- package/src/generators/mcp-config.js +6 -0
- package/src/lib/payment.js +160 -1
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
|
|
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
|
-
- **
|
|
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,
|
|
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/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('3.
|
|
28
|
+
.version('3.3.0');
|
|
29
29
|
|
|
30
30
|
program
|
|
31
31
|
.command('init')
|
|
@@ -33,7 +33,7 @@ program
|
|
|
33
33
|
.option('--env <environment>', 'Force environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
|
|
34
34
|
.option('--no-wallet', 'Skip wallet configuration (read-only mode)')
|
|
35
35
|
.option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
|
|
36
|
-
.option('--network <network>', 'Network: skale, mainnet, or testnet')
|
|
36
|
+
.option('--network <network>', 'Network: skale, polygon, mainnet, or testnet')
|
|
37
37
|
.option('--budget <amount>', 'Max USDC budget per session', '1.00')
|
|
38
38
|
.action(initCommand);
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ program
|
|
|
43
43
|
.option('--env <environment>', 'Target environment (claude-desktop, cursor, claude-code, vscode-continue, generic)')
|
|
44
44
|
.option('--output <path>', 'Output file path')
|
|
45
45
|
.option('--server-url <url>', 'Custom server URL', 'https://x402-api.onrender.com')
|
|
46
|
-
.option('--network <network>', 'Network: skale, mainnet, or testnet')
|
|
46
|
+
.option('--network <network>', 'Network: skale, polygon, mainnet, or testnet')
|
|
47
47
|
.option('--budget <amount>', 'Max USDC budget per session', '1.00')
|
|
48
48
|
.action(configCommand);
|
|
49
49
|
|
|
@@ -56,7 +56,7 @@ program
|
|
|
56
56
|
program
|
|
57
57
|
.command('list')
|
|
58
58
|
.description('List all services on x402 Bazaar')
|
|
59
|
-
.option('--chain <chain>', 'Filter by chain (base or
|
|
59
|
+
.option('--chain <chain>', 'Filter by chain (base, skale, or polygon)')
|
|
60
60
|
.option('--category <category>', 'Filter by category (ai, data, weather, etc.)')
|
|
61
61
|
.option('--free', 'Show only free services')
|
|
62
62
|
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
package/package.json
CHANGED
package/src/commands/call.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
*
|
package/src/commands/init.js
CHANGED
|
@@ -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.
|
|
149
|
+
version: '2.5.0',
|
|
150
150
|
type: 'module',
|
|
151
151
|
private: true,
|
|
152
152
|
dependencies: {
|
|
@@ -219,6 +219,7 @@ export async function initCommand(options) {
|
|
|
219
219
|
message: 'Which network?',
|
|
220
220
|
choices: [
|
|
221
221
|
{ name: 'SKALE on Base (ultra-low gas ~$0.0007/tx — recommended for AI agents)', value: 'skale' },
|
|
222
|
+
{ name: 'Polygon (gas-free via x402 facilitator — no POL needed)', value: 'polygon' },
|
|
222
223
|
{ name: 'Base Mainnet (real USDC, requires ETH for gas)', value: 'mainnet' },
|
|
223
224
|
{ name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
|
|
224
225
|
],
|
|
@@ -286,6 +287,8 @@ export async function initCommand(options) {
|
|
|
286
287
|
log.info(`Wallet address: ${chalk.bold(walletAddress)}`);
|
|
287
288
|
if (network === 'skale') {
|
|
288
289
|
log.dim(` Explorer: https://skale-base-explorer.skalenodes.com/address/${walletAddress}`);
|
|
290
|
+
} else if (network === 'polygon') {
|
|
291
|
+
log.dim(` PolygonScan: https://polygonscan.com/address/${walletAddress}`);
|
|
289
292
|
} else {
|
|
290
293
|
log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
|
|
291
294
|
}
|
|
@@ -301,6 +304,16 @@ export async function initCommand(options) {
|
|
|
301
304
|
log.dim(` ${chalk.white('3.')} Gas (CREDITS) is auto-funded — no ETH needed on SKALE!`);
|
|
302
305
|
console.log('');
|
|
303
306
|
log.warn(`IMPORTANT: Send USDC on ${chalk.bold('SKALE on Base')} (chain ID 1187947933) — not Base or Ethereum!`);
|
|
307
|
+
} else if (network === 'polygon') {
|
|
308
|
+
log.info(chalk.bold('To activate payments, fund this wallet:'));
|
|
309
|
+
console.log('');
|
|
310
|
+
log.dim(` ${chalk.white('1.')} Bridge USDC from any chain → Polygon via:`);
|
|
311
|
+
log.dim(` ${chalk.cyan('https://jumper.exchange')} or ${chalk.cyan('https://x402bazaar.org/fund')}`);
|
|
312
|
+
log.dim(` ${chalk.white('2.')} Or send ${chalk.bold('USDC')} directly to: ${chalk.hex('#34D399')(walletAddress)}`);
|
|
313
|
+
log.dim(` (Even $1 USDC is enough — each API call costs $0.005-$0.05)`);
|
|
314
|
+
log.dim(` ${chalk.white('3.')} ${chalk.bold('No gas needed!')} The x402 facilitator sponsors gas via PIP-82`);
|
|
315
|
+
console.log('');
|
|
316
|
+
log.warn(`IMPORTANT: Send ${chalk.bold('native USDC')} on ${chalk.bold('Polygon')} (chain ID 137) — not USDC.e!`);
|
|
304
317
|
} else {
|
|
305
318
|
log.info(chalk.bold('To activate payments, fund this wallet:'));
|
|
306
319
|
console.log('');
|
|
@@ -482,7 +495,7 @@ export async function initCommand(options) {
|
|
|
482
495
|
`Environment: ${targetEnv.label}`,
|
|
483
496
|
`Install dir: ${installDir}`,
|
|
484
497
|
`Server: ${serverUrl}`,
|
|
485
|
-
`Network: ${network === 'mainnet' ? 'Base Mainnet' : network === 'skale' ? 'SKALE on Base' : 'Base Sepolia'}`,
|
|
498
|
+
`Network: ${network === 'mainnet' ? 'Base Mainnet' : network === 'skale' ? 'SKALE on Base' : network === 'polygon' ? 'Polygon' : 'Base Sepolia'}`,
|
|
486
499
|
`Budget limit: ${maxBudget} USDC / session`,
|
|
487
500
|
`Wallet: ${walletLabel}`,
|
|
488
501
|
`Services: ${serviceCount > 0 ? serviceCount + ' available' : 'check with npx x402-bazaar status'}`,
|
|
@@ -493,7 +506,9 @@ export async function initCommand(options) {
|
|
|
493
506
|
'Before your agent can pay for APIs:',
|
|
494
507
|
network === 'skale'
|
|
495
508
|
? ' 1. Bridge USDC → SKALE on Base: https://x402bazaar.org/fund (CREDITS auto-funded!)'
|
|
496
|
-
:
|
|
509
|
+
: network === 'polygon'
|
|
510
|
+
? ' 1. Send USDC to your wallet on Polygon (gas-free via x402 facilitator!)'
|
|
511
|
+
: ' 1. Send USDC + a little ETH to your wallet on Base',
|
|
497
512
|
' 2. Restart your IDE',
|
|
498
513
|
'',
|
|
499
514
|
] : []),
|
|
@@ -528,6 +543,22 @@ export async function initCommand(options) {
|
|
|
528
543
|
console.log('');
|
|
529
544
|
console.log('='.repeat(70));
|
|
530
545
|
console.log('');
|
|
546
|
+
} else if (walletMode === 'generate' && network === 'polygon') {
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log('='.repeat(70));
|
|
549
|
+
console.log('');
|
|
550
|
+
console.log(' IMPORTANT — After restarting, ask your agent to run: setup_wallet');
|
|
551
|
+
console.log('');
|
|
552
|
+
console.log(' This will:');
|
|
553
|
+
console.log(' - Show your wallet balance on Base, SKALE, and Polygon');
|
|
554
|
+
console.log(' - Provide bridge links to fund your wallet with USDC');
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(' Polygon uses the x402 facilitator — NO gas needed (PIP-82)!');
|
|
557
|
+
console.log(' Just send USDC to your wallet on Polygon.');
|
|
558
|
+
console.log(' Bridge: https://jumper.exchange or https://x402bazaar.org/fund');
|
|
559
|
+
console.log('');
|
|
560
|
+
console.log('='.repeat(70));
|
|
561
|
+
console.log('');
|
|
531
562
|
} else if (walletMode === 'generate') {
|
|
532
563
|
console.log('');
|
|
533
564
|
console.log('='.repeat(70));
|
|
@@ -28,7 +28,18 @@ NETWORK=${network}
|
|
|
28
28
|
# WARNING: Keep this secret! Never commit to git.
|
|
29
29
|
AGENT_PRIVATE_KEY=${agentPrivateKey}
|
|
30
30
|
`;
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Polygon facilitator env vars (gas-free payments via PIP-82)
|
|
34
|
+
if (network === 'polygon') {
|
|
35
|
+
content += `
|
|
36
|
+
# Polygon x402 Facilitator (gas-free payments via PIP-82)
|
|
37
|
+
POLYGON_FACILITATOR_URL=https://x402.polygon.technology
|
|
38
|
+
POLYGON_FEE_SPLITTER_CONTRACT=0x820d4b07D09e5E07598464E6E36cB12561e0Ba56
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!agentPrivateKey && coinbaseApiKey) {
|
|
32
43
|
content += `
|
|
33
44
|
# Coinbase Developer Platform API credentials (legacy mode)
|
|
34
45
|
# Get yours at: https://portal.cdp.coinbase.com/
|
|
@@ -32,6 +32,12 @@ export function generateMcpConfig({
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Polygon facilitator — gas-free payments via PIP-82
|
|
36
|
+
if (network === 'polygon') {
|
|
37
|
+
env.POLYGON_FACILITATOR_URL = 'https://x402.polygon.technology';
|
|
38
|
+
env.POLYGON_FEE_SPLITTER_CONTRACT = '0x820d4b07D09e5E07598464E6E36cB12561e0Ba56';
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
const serverEntry = {
|
|
36
42
|
command: 'node',
|
|
37
43
|
args: [mcpServerPath],
|
package/src/lib/payment.js
CHANGED
|
@@ -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.
|