x402-bazaar 3.0.0 → 3.2.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,216 +1,217 @@
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
- const BASE_RPC_URL = 'https://mainnet.base.org';
8
- const USDC_CONTRACT_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
9
- const BALANCE_OF_SELECTOR = '0x70a08231';
10
-
11
- export async function walletCommand(options) {
12
- log.banner();
13
-
14
- // Handle --setup: generate a new wallet
15
- if (options.setup) {
16
- await setupWallet();
17
- return;
18
- }
19
-
20
- if (!options.address) {
21
- log.info('Check USDC balance or generate a new wallet.');
22
- console.log('');
23
- log.dim(' Usage:');
24
- log.dim(' x402-bazaar wallet --address 0xYourAddress');
25
- log.dim(' x402-bazaar wallet --setup');
26
- console.log('');
27
- log.dim(' Examples:');
28
- log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
29
- log.dim(' x402-bazaar wallet --setup (generate a new wallet for auto-payment)');
30
- console.log('');
31
- return;
32
- }
33
-
34
- const address = options.address.trim();
35
-
36
- if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
37
- log.error('Invalid Ethereum address format');
38
- log.dim(' Expected: 0x followed by 40 hexadecimal characters');
39
- log.dim(` Got: ${address}`);
40
- console.log('');
41
- process.exit(1);
42
- }
43
-
44
- log.info(`Checking wallet: ${chalk.bold(maskAddress(address))}`);
45
- console.log('');
46
-
47
- const spinner = ora('Fetching USDC balance from Base...').start();
48
-
49
- try {
50
- const paddedAddress = address.slice(2).padStart(64, '0');
51
- const data = BALANCE_OF_SELECTOR + paddedAddress;
52
-
53
- const rpcPayload = {
54
- jsonrpc: '2.0',
55
- id: 1,
56
- method: 'eth_call',
57
- params: [{ to: USDC_CONTRACT_BASE, data }, 'latest'],
58
- };
59
-
60
- const res = await fetch(BASE_RPC_URL, {
61
- method: 'POST',
62
- headers: { 'Content-Type': 'application/json' },
63
- body: JSON.stringify(rpcPayload),
64
- signal: AbortSignal.timeout(15000),
65
- });
66
-
67
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
68
-
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');
72
-
73
- const balanceRaw = BigInt(rpcResponse.result);
74
- const balanceUsdc = Number(balanceRaw) / 1_000_000;
75
-
76
- spinner.succeed('Balance fetched');
77
- console.log('');
78
- log.separator();
79
- console.log('');
80
-
81
- log.info(`Address: ${chalk.hex('#34D399')(maskAddress(address))}`);
82
- log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')} (Chain ID: 8453)`);
83
- log.info(`Balance: ${chalk.cyan.bold(balanceUsdc.toFixed(6))} ${chalk.dim('USDC')}`);
84
-
85
- console.log('');
86
- log.separator();
87
- console.log('');
88
- log.dim(` Explorer: https://basescan.org/address/${address}`);
89
-
90
- if (balanceUsdc === 0) {
91
- console.log('');
92
- log.warn('This wallet has no USDC.');
93
- log.dim(' Send USDC on Base to this address to start using paid APIs.');
94
- } else if (balanceUsdc < 0.1) {
95
- console.log('');
96
- log.warn('Low balance — consider adding more USDC.');
97
- log.dim(' Most x402 Bazaar APIs cost $0.001-$0.05 per call.');
98
- } else {
99
- console.log('');
100
- log.success('Wallet is funded and ready!');
101
- const estimatedCalls = Math.floor(balanceUsdc / 0.005);
102
- log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.005/call)`);
103
- }
104
- console.log('');
105
-
106
- } catch (err) {
107
- spinner.fail('Failed to fetch balance');
108
- console.log('');
109
-
110
- if (err.name === 'AbortError') {
111
- log.error('Request timeout — Base RPC may be slow');
112
- } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
113
- log.error('Cannot connect to Base RPC');
114
- } else {
115
- log.error(err.message);
116
- }
117
- console.log('');
118
- process.exit(1);
119
- }
120
- }
121
-
122
- /**
123
- * Generate a new wallet and save to ~/.x402-bazaar/wallet.json
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
-
213
- function maskAddress(address) {
214
- if (address.length < 12) return address;
215
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
216
- }
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
+ const BASE_RPC_URL = 'https://mainnet.base.org';
8
+ const USDC_CONTRACT_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
9
+ const BALANCE_OF_SELECTOR = '0x70a08231';
10
+
11
+ export async function walletCommand(options) {
12
+ log.banner();
13
+
14
+ // Handle --setup: generate a new wallet
15
+ if (options.setup) {
16
+ await setupWallet();
17
+ return;
18
+ }
19
+
20
+ if (!options.address) {
21
+ log.info('Check USDC balance or generate a new wallet.');
22
+ console.log('');
23
+ log.dim(' Usage:');
24
+ log.dim(' x402-bazaar wallet --address 0xYourAddress');
25
+ log.dim(' x402-bazaar wallet --setup');
26
+ console.log('');
27
+ log.dim(' Examples:');
28
+ log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
29
+ log.dim(' x402-bazaar wallet --setup (generate a new wallet for auto-payment)');
30
+ console.log('');
31
+ return;
32
+ }
33
+
34
+ const address = options.address.trim();
35
+
36
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
37
+ log.error('Invalid Ethereum address format');
38
+ log.dim(' Expected: 0x followed by 40 hexadecimal characters');
39
+ log.dim(` Got: ${address}`);
40
+ console.log('');
41
+ process.exit(1);
42
+ }
43
+
44
+ log.info(`Checking wallet: ${chalk.bold(maskAddress(address))}`);
45
+ console.log('');
46
+
47
+ const spinner = ora('Fetching USDC balance from Base...').start();
48
+
49
+ try {
50
+ const paddedAddress = address.slice(2).padStart(64, '0');
51
+ const data = BALANCE_OF_SELECTOR + paddedAddress;
52
+
53
+ const rpcPayload = {
54
+ jsonrpc: '2.0',
55
+ id: 1,
56
+ method: 'eth_call',
57
+ params: [{ to: USDC_CONTRACT_BASE, data }, 'latest'],
58
+ };
59
+
60
+ const res = await fetch(BASE_RPC_URL, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(rpcPayload),
64
+ signal: AbortSignal.timeout(15000),
65
+ });
66
+
67
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
68
+
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');
72
+
73
+ const balanceRaw = BigInt(rpcResponse.result);
74
+ const balanceUsdc = Number(balanceRaw) / 1_000_000;
75
+
76
+ spinner.succeed('Balance fetched');
77
+ console.log('');
78
+ log.separator();
79
+ console.log('');
80
+
81
+ log.info(`Address: ${chalk.hex('#34D399')(maskAddress(address))}`);
82
+ log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')} (Chain ID: 8453)`);
83
+ log.info(`Balance: ${chalk.cyan.bold(balanceUsdc.toFixed(6))} ${chalk.dim('USDC')}`);
84
+
85
+ console.log('');
86
+ log.separator();
87
+ console.log('');
88
+ log.dim(` Explorer: https://basescan.org/address/${address}`);
89
+
90
+ if (balanceUsdc === 0) {
91
+ console.log('');
92
+ log.warn('This wallet has no USDC.');
93
+ log.dim(' Send USDC on Base to this address to start using paid APIs.');
94
+ } else if (balanceUsdc < 0.1) {
95
+ console.log('');
96
+ log.warn('Low balance — consider adding more USDC.');
97
+ log.dim(' Most x402 Bazaar APIs cost $0.001-$0.05 per call.');
98
+ } else {
99
+ console.log('');
100
+ log.success('Wallet is funded and ready!');
101
+ const estimatedCalls = Math.floor(balanceUsdc / 0.005);
102
+ log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.005/call)`);
103
+ }
104
+ console.log('');
105
+
106
+ } catch (err) {
107
+ spinner.fail('Failed to fetch balance');
108
+ console.log('');
109
+
110
+ if (err.name === 'AbortError') {
111
+ log.error('Request timeout — Base RPC may be slow');
112
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
113
+ log.error('Cannot connect to Base RPC');
114
+ } else {
115
+ log.error(err.message);
116
+ }
117
+ console.log('');
118
+ process.exit(1);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Generate a new wallet and save to ~/.x402-bazaar/wallet.json
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
+ try { fs.chmodSync(walletPath, 0o600); } catch {}
173
+
174
+ spinner.succeed('Wallet generated!');
175
+ console.log('');
176
+ log.separator();
177
+ console.log('');
178
+
179
+ log.info(`Address: ${chalk.hex('#34D399').bold(account.address)}`);
180
+ log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')}`);
181
+ log.info(`Saved to: ${chalk.dim(walletPath)}`);
182
+
183
+ console.log('');
184
+ log.separator();
185
+ console.log('');
186
+
187
+ log.info('Next steps:');
188
+ console.log('');
189
+ log.dim(' 1. Fund this wallet with USDC on Base:');
190
+ log.dim(` Send USDC to ${chalk.hex('#34D399')(account.address)}`);
191
+ log.dim(' + a tiny amount of ETH for gas (~$0.01)');
192
+ console.log('');
193
+ log.dim(' 2. Call paid APIs automatically:');
194
+ log.dim(` ${chalk.cyan('npx x402-bazaar call /api/weather --param city=Paris')}`);
195
+ log.dim(' (auto-payment will use your saved wallet)');
196
+ console.log('');
197
+ log.dim(' 3. The CLI reads your wallet file automatically — no need to export the key.');
198
+ log.dim(` The private key is stored securely in: ${walletPath}`);
199
+ console.log('');
200
+
201
+ log.warn('Keep your private key safe! Anyone with it can spend your funds.');
202
+ log.dim(` Backup: ${walletPath}`);
203
+ console.log('');
204
+
205
+ } catch (err) {
206
+ spinner.fail('Wallet generation failed');
207
+ console.log('');
208
+ log.error(err.message);
209
+ console.log('');
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ function maskAddress(address) {
215
+ if (address.length < 12) return address;
216
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
217
+ }
@@ -22,27 +22,59 @@ const USDC_ABI = [
22
22
  },
23
23
  ];
24
24
 
25
+ /** Minimum amount in micro-USDC (6 decimals) to allow a split payment. */
26
+ const MIN_SPLIT_AMOUNT_RAW = 100n; // 0.0001 USDC
27
+
25
28
  /**
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 }}
29
+ * Build viem wallet + public clients for Base mainnet.
30
+ * @param {string} privateKey
31
+ * @returns {{ walletClient, publicClient, account }}
31
32
  */
32
- export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
33
+ function buildClients(privateKey) {
33
34
  const account = privateKeyToAccount(privateKey);
35
+ const transport = http('https://mainnet.base.org');
36
+ const walletClient = createWalletClient({ account, chain: base, transport });
37
+ const publicClient = createPublicClient({ chain: base, transport });
38
+ return { walletClient, publicClient, account };
39
+ }
34
40
 
35
- const walletClient = createWalletClient({
36
- account,
37
- chain: base,
38
- transport: http('https://mainnet.base.org'),
39
- });
41
+ /**
42
+ * Send a single USDC transfer on Base mainnet and wait for confirmation.
43
+ * Caller is responsible for balance checks.
44
+ *
45
+ * @param {{ walletClient, publicClient }} clients
46
+ * @param {string} toAddress
47
+ * @param {bigint} amountRaw - amount in micro-USDC (6 decimals)
48
+ * @returns {{ txHash: string, explorer: string }}
49
+ */
50
+ async function sendUsdcRaw(clients, toAddress, amountRaw) {
51
+ const { walletClient, publicClient } = clients;
40
52
 
41
- const publicClient = createPublicClient({
42
- chain: base,
43
- transport: http('https://mainnet.base.org'),
53
+ const txHash = await walletClient.writeContract({
54
+ address: USDC_CONTRACT,
55
+ abi: USDC_ABI,
56
+ functionName: 'transfer',
57
+ args: [toAddress, amountRaw],
44
58
  });
45
59
 
60
+ await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
61
+
62
+ return {
63
+ txHash,
64
+ explorer: `https://basescan.org/tx/${txHash}`,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Send USDC payment on Base mainnet (legacy mode — 100% to one recipient).
70
+ * @param {string} privateKey - Hex private key (with 0x prefix)
71
+ * @param {string} toAddress - Recipient wallet address
72
+ * @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
73
+ * @returns {{ txHash: string, explorer: string, from: string, amount: number }}
74
+ */
75
+ export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
76
+ const { walletClient, publicClient, account } = buildClients(privateKey);
77
+
46
78
  // Convert USDC amount to 6-decimal units
47
79
  const amount = parseUnits(amountUsdc.toString(), 6);
48
80
 
@@ -59,22 +91,116 @@ export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
59
91
  throw new Error(`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${amountUsdc} USDC)`);
60
92
  }
61
93
 
62
- // Send the USDC transfer
63
- const txHash = await walletClient.writeContract({
94
+ const { txHash, explorer } = await sendUsdcRaw({ walletClient, publicClient }, toAddress, amount);
95
+
96
+ return {
97
+ txHash,
98
+ explorer,
99
+ from: account.address,
100
+ amount: amountUsdc,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Send a split USDC payment on Base mainnet (native split mode — 95% to provider, 5% to platform).
106
+ *
107
+ * The split amounts are derived from the server-provided `split` object when available,
108
+ * or computed with floor arithmetic to guarantee provider + platform = total exactly.
109
+ *
110
+ * @param {string} privateKey - Hex private key (with 0x prefix)
111
+ * @param {object} splitDetails
112
+ * @param {number} splitDetails.totalAmountUsdc - Total price in USDC (e.g., 0.01)
113
+ * @param {string} splitDetails.providerWallet - Provider wallet address (95%)
114
+ * @param {string} splitDetails.platformWallet - Platform wallet address (5%)
115
+ * @param {object|null} [splitDetails.serverSplit] - Optional split object from server 402 response
116
+ * @param {number} [splitDetails.serverSplit.provider_amount] - Provider amount in USDC from server
117
+ * @param {number} [splitDetails.serverSplit.platform_amount] - Platform amount in USDC from server
118
+ *
119
+ * @returns {{
120
+ * txHashProvider: string,
121
+ * txHashPlatform: string,
122
+ * explorerProvider: string,
123
+ * explorerPlatform: string,
124
+ * from: string,
125
+ * providerAmountUsdc: number,
126
+ * platformAmountUsdc: number,
127
+ * }}
128
+ *
129
+ * @throws {Error} If total amount is too small for a meaningful split (< 0.0001 USDC)
130
+ * @throws {Error} If USDC balance is insufficient for the total amount
131
+ */
132
+ export async function sendSplitUsdcPayment(privateKey, splitDetails) {
133
+ const {
134
+ totalAmountUsdc,
135
+ providerWallet,
136
+ platformWallet,
137
+ serverSplit = null,
138
+ } = splitDetails;
139
+
140
+ const { walletClient, publicClient, account } = buildClients(privateKey);
141
+
142
+ // Compute raw amounts (6 decimals).
143
+ // Use server-provided amounts when present to avoid client/server rounding divergence.
144
+ let providerAmountRaw;
145
+ let platformAmountRaw;
146
+
147
+ if (serverSplit && serverSplit.provider_amount != null && serverSplit.platform_amount != null) {
148
+ providerAmountRaw = parseUnits(serverSplit.provider_amount.toString(), 6);
149
+ platformAmountRaw = parseUnits(serverSplit.platform_amount.toString(), 6);
150
+ } else {
151
+ const totalRaw = parseUnits(totalAmountUsdc.toString(), 6);
152
+ providerAmountRaw = (totalRaw * 95n) / 100n; // floor division via BigInt
153
+ platformAmountRaw = totalRaw - providerAmountRaw; // guarantees sum = total
154
+ }
155
+
156
+ const totalRawForCheck = providerAmountRaw + platformAmountRaw;
157
+
158
+ // Guard: minimum split amount
159
+ if (providerAmountRaw < MIN_SPLIT_AMOUNT_RAW || platformAmountRaw === 0n) {
160
+ throw new Error(
161
+ `Amount too small for split payment (minimum 0.0001 USDC). ` +
162
+ `Provider share would be ${Number(providerAmountRaw)} micro-USDC.`
163
+ );
164
+ }
165
+
166
+ // Check balance for the full total
167
+ const balance = await publicClient.readContract({
64
168
  address: USDC_CONTRACT,
65
169
  abi: USDC_ABI,
66
- functionName: 'transfer',
67
- args: [toAddress, amount],
170
+ functionName: 'balanceOf',
171
+ args: [account.address],
68
172
  });
69
173
 
70
- // Wait for confirmation
71
- await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
174
+ if (balance < totalRawForCheck) {
175
+ const balanceUsdc = Number(balance) / 1_000_000;
176
+ const needUsdc = Number(totalRawForCheck) / 1_000_000;
177
+ throw new Error(
178
+ `Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${needUsdc.toFixed(6)} USDC for split payment)`
179
+ );
180
+ }
181
+
182
+ // Transaction 1 — provider (95%)
183
+ const providerResult = await sendUsdcRaw(
184
+ { walletClient, publicClient },
185
+ providerWallet,
186
+ providerAmountRaw
187
+ );
188
+
189
+ // Transaction 2 — platform (5%)
190
+ const platformResult = await sendUsdcRaw(
191
+ { walletClient, publicClient },
192
+ platformWallet,
193
+ platformAmountRaw
194
+ );
72
195
 
73
196
  return {
74
- txHash,
75
- explorer: `https://basescan.org/tx/${txHash}`,
197
+ txHashProvider: providerResult.txHash,
198
+ txHashPlatform: platformResult.txHash,
199
+ explorerProvider: providerResult.explorer,
200
+ explorerPlatform: platformResult.explorer,
76
201
  from: account.address,
77
- amount: amountUsdc,
202
+ providerAmountUsdc: Number(providerAmountRaw) / 1_000_000,
203
+ platformAmountUsdc: Number(platformAmountRaw) / 1_000_000,
78
204
  };
79
205
  }
80
206