moltspay 1.3.0 → 1.4.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.
Files changed (58) hide show
  1. package/.env.example +14 -0
  2. package/README.md +319 -89
  3. package/dist/cdp/index.d.mts +4 -4
  4. package/dist/cdp/index.d.ts +4 -4
  5. package/dist/cdp/index.js +57 -0
  6. package/dist/cdp/index.js.map +1 -1
  7. package/dist/cdp/index.mjs +57 -0
  8. package/dist/cdp/index.mjs.map +1 -1
  9. package/dist/chains/index.d.mts +9 -8
  10. package/dist/chains/index.d.ts +9 -8
  11. package/dist/chains/index.js +57 -0
  12. package/dist/chains/index.js.map +1 -1
  13. package/dist/chains/index.mjs +57 -0
  14. package/dist/chains/index.mjs.map +1 -1
  15. package/dist/cli/index.js +2021 -285
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/index.mjs +2023 -277
  18. package/dist/cli/index.mjs.map +1 -1
  19. package/dist/client/index.d.mts +39 -3
  20. package/dist/client/index.d.ts +39 -3
  21. package/dist/client/index.js +563 -37
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/index.mjs +571 -35
  24. package/dist/client/index.mjs.map +1 -1
  25. package/dist/facilitators/index.d.mts +220 -1
  26. package/dist/facilitators/index.d.ts +220 -1
  27. package/dist/facilitators/index.js +664 -1
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +670 -1
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
  32. package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
  33. package/dist/index.d.mts +2 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +1440 -153
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1448 -151
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/server/index.d.mts +13 -3
  40. package/dist/server/index.d.ts +13 -3
  41. package/dist/server/index.js +909 -54
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +919 -54
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/verify/index.d.mts +1 -1
  46. package/dist/verify/index.d.ts +1 -1
  47. package/dist/verify/index.js +57 -0
  48. package/dist/verify/index.js.map +1 -1
  49. package/dist/verify/index.mjs +57 -0
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.d.mts +3 -3
  52. package/dist/wallet/index.d.ts +3 -3
  53. package/dist/wallet/index.js +57 -0
  54. package/dist/wallet/index.js.map +1 -1
  55. package/dist/wallet/index.mjs +57 -0
  56. package/dist/wallet/index.mjs.map +1 -1
  57. package/package.json +5 -2
  58. package/schemas/moltspay.services.schema.json +27 -132
@@ -21,16 +21,17 @@ var init_esm_shims = __esm({
21
21
  init_esm_shims();
22
22
  import { webcrypto } from "crypto";
23
23
  import { Command } from "commander";
24
- import { homedir as homedir2 } from "os";
25
- import { join as join4, dirname, resolve } from "path";
26
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync4, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
24
+ import { homedir as homedir3 } from "os";
25
+ import { join as join5, dirname, resolve } from "path";
26
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync5, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
27
27
  import { spawn } from "child_process";
28
+ import { ethers as ethers2 } from "ethers";
28
29
 
29
30
  // src/client/index.ts
30
31
  init_esm_shims();
31
- import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, chmodSync } from "fs";
32
- import { homedir } from "os";
33
- import { join } from "path";
32
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
33
+ import { homedir as homedir2 } from "os";
34
+ import { join as join2 } from "path";
34
35
  import { Wallet, ethers } from "ethers";
35
36
 
36
37
  // src/chains/index.ts
@@ -141,6 +142,63 @@ var CHAINS = {
141
142
  explorerTx: "https://explore.testnet.tempo.xyz/tx/",
142
143
  avgBlockTime: 0.5
143
144
  // ~500ms finality
145
+ },
146
+ // ============ BNB Chain Testnet ============
147
+ bnb_testnet: {
148
+ name: "BNB Testnet",
149
+ chainId: 97,
150
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
151
+ tokens: {
152
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
153
+ // Using official Binance-Peg testnet tokens
154
+ USDC: {
155
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
156
+ // Testnet USDC
157
+ decimals: 18,
158
+ symbol: "USDC",
159
+ eip712Name: "USD Coin"
160
+ },
161
+ USDT: {
162
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
163
+ // Testnet USDT
164
+ decimals: 18,
165
+ symbol: "USDT",
166
+ eip712Name: "Tether USD"
167
+ }
168
+ },
169
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
170
+ explorer: "https://testnet.bscscan.com/address/",
171
+ explorerTx: "https://testnet.bscscan.com/tx/",
172
+ avgBlockTime: 3,
173
+ // BNB-specific: requires approval for pay-for-success flow
174
+ requiresApproval: true
175
+ },
176
+ // ============ BNB Chain Mainnet ============
177
+ bnb: {
178
+ name: "BNB Smart Chain",
179
+ chainId: 56,
180
+ rpc: "https://bsc-dataseed.binance.org",
181
+ tokens: {
182
+ // Note: BNB uses 18 decimals for stablecoins
183
+ USDC: {
184
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
185
+ decimals: 18,
186
+ symbol: "USDC",
187
+ eip712Name: "USD Coin"
188
+ },
189
+ USDT: {
190
+ address: "0x55d398326f99059fF775485246999027B3197955",
191
+ decimals: 18,
192
+ symbol: "USDT",
193
+ eip712Name: "Tether USD"
194
+ }
195
+ },
196
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
197
+ explorer: "https://bscscan.com/address/",
198
+ explorerTx: "https://bscscan.com/tx/",
199
+ avgBlockTime: 3,
200
+ // BNB-specific: requires approval for pay-for-success flow
201
+ requiresApproval: true
144
202
  }
145
203
  };
146
204
  function getChain(name) {
@@ -151,6 +209,372 @@ function getChain(name) {
151
209
  return config;
152
210
  }
153
211
 
212
+ // src/wallet/solana.ts
213
+ init_esm_shims();
214
+ import { Keypair, PublicKey as PublicKey2, LAMPORTS_PER_SOL } from "@solana/web3.js";
215
+ import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
216
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
217
+ import { join } from "path";
218
+ import { homedir } from "os";
219
+ import bs58 from "bs58";
220
+
221
+ // src/chains/solana.ts
222
+ init_esm_shims();
223
+ import { Connection, PublicKey } from "@solana/web3.js";
224
+ var SOLANA_CHAINS = {
225
+ solana: {
226
+ name: "Solana Mainnet",
227
+ cluster: "mainnet-beta",
228
+ rpc: "https://api.mainnet-beta.solana.com",
229
+ explorer: "https://solscan.io/account/",
230
+ explorerTx: "https://solscan.io/tx/",
231
+ tokens: {
232
+ USDC: {
233
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
234
+ // Circle official USDC
235
+ decimals: 6
236
+ }
237
+ }
238
+ },
239
+ solana_devnet: {
240
+ name: "Solana Devnet",
241
+ cluster: "devnet",
242
+ rpc: "https://api.devnet.solana.com",
243
+ explorer: "https://solscan.io/account/",
244
+ explorerTx: "https://solscan.io/tx/",
245
+ tokens: {
246
+ USDC: {
247
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
248
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
249
+ decimals: 6
250
+ }
251
+ }
252
+ }
253
+ };
254
+ function getSolanaConnection(chain) {
255
+ const config = SOLANA_CHAINS[chain];
256
+ return new Connection(config.rpc, "confirmed");
257
+ }
258
+ function getUSDCMint(chain) {
259
+ return new PublicKey(SOLANA_CHAINS[chain].tokens.USDC.mint);
260
+ }
261
+
262
+ // src/wallet/solana.ts
263
+ var DEFAULT_CONFIG_DIR = join(homedir(), ".moltspay");
264
+ var SOLANA_WALLET_FILE = "wallet-solana.json";
265
+ function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
266
+ return join(configDir, SOLANA_WALLET_FILE);
267
+ }
268
+ function solanaWalletExists(configDir = DEFAULT_CONFIG_DIR) {
269
+ return existsSync(getSolanaWalletPath(configDir));
270
+ }
271
+ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
272
+ const walletPath = getSolanaWalletPath(configDir);
273
+ if (!existsSync(walletPath)) {
274
+ return null;
275
+ }
276
+ try {
277
+ const data = JSON.parse(readFileSync(walletPath, "utf-8"));
278
+ const secretKey = bs58.decode(data.secretKey);
279
+ return Keypair.fromSecretKey(secretKey);
280
+ } catch (error) {
281
+ console.error("Failed to load Solana wallet:", error);
282
+ return null;
283
+ }
284
+ }
285
+ function createSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
286
+ if (!existsSync(configDir)) {
287
+ mkdirSync(configDir, { recursive: true });
288
+ }
289
+ const keypair = Keypair.generate();
290
+ const data = {
291
+ publicKey: keypair.publicKey.toBase58(),
292
+ secretKey: bs58.encode(keypair.secretKey),
293
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
294
+ };
295
+ const walletPath = getSolanaWalletPath(configDir);
296
+ writeFileSync(walletPath, JSON.stringify(data, null, 2));
297
+ return keypair;
298
+ }
299
+ function getSolanaAddress(configDir = DEFAULT_CONFIG_DIR) {
300
+ const wallet = loadSolanaWallet(configDir);
301
+ return wallet?.publicKey.toBase58() || null;
302
+ }
303
+ async function getSolanaBalance(address, chain) {
304
+ const connection = getSolanaConnection(chain);
305
+ const pubkey = new PublicKey2(address);
306
+ const balance = await connection.getBalance(pubkey);
307
+ return balance / LAMPORTS_PER_SOL;
308
+ }
309
+ async function getSolanaUSDCBalance(address, chain) {
310
+ const connection = getSolanaConnection(chain);
311
+ const owner = new PublicKey2(address);
312
+ const mint = getUSDCMint(chain);
313
+ try {
314
+ const ata = await getAssociatedTokenAddress(mint, owner);
315
+ const account = await getAccount(connection, ata);
316
+ return Number(account.amount) / 1e6;
317
+ } catch (error) {
318
+ if (error.name === "TokenAccountNotFoundError" || error.message?.includes("could not find account")) {
319
+ return 0;
320
+ }
321
+ throw error;
322
+ }
323
+ }
324
+ async function getSolanaBalances(address, chain) {
325
+ const [sol, usdc] = await Promise.all([
326
+ getSolanaBalance(address, chain),
327
+ getSolanaUSDCBalance(address, chain)
328
+ ]);
329
+ return { sol, usdc };
330
+ }
331
+ function isValidSolanaAddress(address) {
332
+ try {
333
+ new PublicKey2(address);
334
+ return true;
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+
340
+ // src/facilitators/solana.ts
341
+ init_esm_shims();
342
+ import {
343
+ Connection as Connection3,
344
+ PublicKey as PublicKey3,
345
+ Transaction,
346
+ VersionedTransaction
347
+ } from "@solana/web3.js";
348
+ import {
349
+ getAssociatedTokenAddress as getAssociatedTokenAddress2,
350
+ createTransferCheckedInstruction,
351
+ getAccount as getAccount2,
352
+ createAssociatedTokenAccountInstruction
353
+ } from "@solana/spl-token";
354
+
355
+ // src/facilitators/interface.ts
356
+ init_esm_shims();
357
+ var BaseFacilitator = class {
358
+ supportsNetwork(network) {
359
+ return this.supportedNetworks.includes(network);
360
+ }
361
+ };
362
+
363
+ // src/facilitators/solana.ts
364
+ var SolanaFacilitator = class extends BaseFacilitator {
365
+ name = "solana";
366
+ displayName = "Solana Direct";
367
+ supportedNetworks = ["solana:mainnet", "solana:devnet"];
368
+ connections = /* @__PURE__ */ new Map();
369
+ feePayerKeypair;
370
+ constructor(config) {
371
+ super();
372
+ this.feePayerKeypair = config?.feePayerKeypair;
373
+ for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
374
+ this.connections.set(
375
+ chain,
376
+ new Connection3(config2.rpc, "confirmed")
377
+ );
378
+ }
379
+ if (this.feePayerKeypair) {
380
+ console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
381
+ }
382
+ }
383
+ /**
384
+ * Get fee payer public key (for gasless transactions)
385
+ */
386
+ getFeePayerPubkey() {
387
+ return this.feePayerKeypair?.publicKey.toBase58() || null;
388
+ }
389
+ getConnection(chain) {
390
+ const conn = this.connections.get(chain);
391
+ if (!conn) {
392
+ throw new Error(`No connection for chain: ${chain}`);
393
+ }
394
+ return conn;
395
+ }
396
+ /**
397
+ * Convert our chain name to network identifier
398
+ */
399
+ static chainToNetwork(chain) {
400
+ return chain === "solana" ? "solana:mainnet" : "solana:devnet";
401
+ }
402
+ /**
403
+ * Convert network identifier to chain name
404
+ */
405
+ static networkToChain(network) {
406
+ if (network === "solana:mainnet") return "solana";
407
+ if (network === "solana:devnet") return "solana_devnet";
408
+ return null;
409
+ }
410
+ async healthCheck() {
411
+ const start = Date.now();
412
+ try {
413
+ const conn = this.getConnection("solana_devnet");
414
+ await conn.getSlot();
415
+ return {
416
+ healthy: true,
417
+ latencyMs: Date.now() - start
418
+ };
419
+ } catch (error) {
420
+ return {
421
+ healthy: false,
422
+ error: error.message
423
+ };
424
+ }
425
+ }
426
+ /**
427
+ * Verify a Solana payment
428
+ *
429
+ * Checks:
430
+ * 1. Transaction is valid and properly signed
431
+ * 2. Transfer instruction matches expected amount and recipient
432
+ */
433
+ async verify(paymentPayload, requirements) {
434
+ try {
435
+ const solanaPayload = paymentPayload.payload;
436
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
437
+ return { valid: false, error: "Missing signed transaction" };
438
+ }
439
+ const chain = solanaPayload.chain || "solana_devnet";
440
+ const chainConfig = SOLANA_CHAINS[chain];
441
+ if (!chainConfig) {
442
+ return { valid: false, error: `Invalid chain: ${chain}` };
443
+ }
444
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
445
+ let tx;
446
+ try {
447
+ tx = Transaction.from(txBuffer);
448
+ } catch {
449
+ tx = VersionedTransaction.deserialize(txBuffer);
450
+ }
451
+ if (tx instanceof Transaction) {
452
+ const hasAnySignature = tx.signatures.some(
453
+ (sig) => sig.signature && !sig.signature.every((b) => b === 0)
454
+ );
455
+ if (!hasAnySignature) {
456
+ return { valid: false, error: "Transaction not signed" };
457
+ }
458
+ }
459
+ const expectedAmount = BigInt(requirements.amount);
460
+ const expectedRecipient = new PublicKey3(requirements.payTo);
461
+ return {
462
+ valid: true,
463
+ details: {
464
+ chain,
465
+ sender: solanaPayload.sender,
466
+ recipient: requirements.payTo,
467
+ amount: requirements.amount
468
+ }
469
+ };
470
+ } catch (error) {
471
+ return { valid: false, error: error.message };
472
+ }
473
+ }
474
+ /**
475
+ * Settle a Solana payment
476
+ *
477
+ * Submits the signed transaction to the network.
478
+ * In gasless mode, adds fee payer signature before submitting.
479
+ */
480
+ async settle(paymentPayload, requirements) {
481
+ try {
482
+ const solanaPayload = paymentPayload.payload;
483
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
484
+ return { success: false, error: "Missing signed transaction" };
485
+ }
486
+ const chain = solanaPayload.chain || "solana_devnet";
487
+ const connection = this.getConnection(chain);
488
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
489
+ let txToSend;
490
+ try {
491
+ const tx = Transaction.from(txBuffer);
492
+ if (this.feePayerKeypair && tx.feePayer) {
493
+ const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
494
+ const txFeePayer = tx.feePayer.toBase58();
495
+ if (txFeePayer === feePayerPubkey) {
496
+ console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
497
+ tx.partialSign(this.feePayerKeypair);
498
+ }
499
+ }
500
+ txToSend = tx.serialize();
501
+ } catch (e) {
502
+ txToSend = txBuffer;
503
+ }
504
+ const signature = await connection.sendRawTransaction(txToSend, {
505
+ skipPreflight: false,
506
+ preflightCommitment: "confirmed"
507
+ });
508
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
509
+ if (confirmation.value.err) {
510
+ return {
511
+ success: false,
512
+ error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
513
+ transaction: signature
514
+ };
515
+ }
516
+ return {
517
+ success: true,
518
+ transaction: signature,
519
+ status: "confirmed"
520
+ };
521
+ } catch (error) {
522
+ return { success: false, error: error.message };
523
+ }
524
+ }
525
+ supportsNetwork(network) {
526
+ return this.supportedNetworks.includes(network);
527
+ }
528
+ };
529
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
530
+ const chainConfig = SOLANA_CHAINS[chain];
531
+ const connection = new Connection3(chainConfig.rpc, "confirmed");
532
+ const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
533
+ const actualFeePayer = feePayerPubkey || senderPubkey;
534
+ const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
535
+ const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
536
+ const transaction = new Transaction();
537
+ try {
538
+ await getAccount2(connection, recipientATA);
539
+ } catch {
540
+ transaction.add(
541
+ createAssociatedTokenAccountInstruction(
542
+ actualFeePayer,
543
+ // payer (fee payer in gasless mode)
544
+ recipientATA,
545
+ // ata to create
546
+ recipientPubkey,
547
+ // owner
548
+ mint
549
+ // mint
550
+ )
551
+ );
552
+ }
553
+ transaction.add(
554
+ createTransferCheckedInstruction(
555
+ senderATA,
556
+ // source
557
+ mint,
558
+ // mint
559
+ recipientATA,
560
+ // destination
561
+ senderPubkey,
562
+ // owner (sender still authorizes the transfer)
563
+ amount,
564
+ // amount
565
+ chainConfig.tokens.USDC.decimals
566
+ // decimals
567
+ )
568
+ );
569
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
570
+ transaction.recentBlockhash = blockhash;
571
+ transaction.feePayer = actualFeePayer;
572
+ return transaction;
573
+ }
574
+
575
+ // src/client/index.ts
576
+ import { PublicKey as PublicKey4 } from "@solana/web3.js";
577
+
154
578
  // src/client/types.ts
155
579
  init_esm_shims();
156
580
 
@@ -173,7 +597,7 @@ var MoltsPayClient = class {
173
597
  todaySpending = 0;
174
598
  lastSpendingReset = 0;
175
599
  constructor(options = {}) {
176
- this.configDir = options.configDir || join(homedir(), ".moltspay");
600
+ this.configDir = options.configDir || join2(homedir2(), ".moltspay");
177
601
  this.config = this.loadConfig();
178
602
  this.walletData = this.loadWallet();
179
603
  this.loadSpending();
@@ -193,6 +617,12 @@ var MoltsPayClient = class {
193
617
  get address() {
194
618
  return this.wallet?.address || null;
195
619
  }
620
+ /**
621
+ * Get wallet instance (for direct operations like approvals)
622
+ */
623
+ getWallet() {
624
+ return this.wallet;
625
+ }
196
626
  /**
197
627
  * Get current config
198
628
  */
@@ -246,11 +676,26 @@ var MoltsPayClient = class {
246
676
  throw new Error("Client not initialized. Run: npx moltspay init");
247
677
  }
248
678
  console.log(`[MoltsPay] Requesting service: ${service}`);
249
- const requestBody = { service, params };
679
+ let executeUrl = `${serverUrl}/execute`;
680
+ try {
681
+ const services = await this.getServices(serverUrl);
682
+ const svc = services.services?.find((s) => s.id === service);
683
+ if (svc?.endpoint) {
684
+ executeUrl = `${serverUrl}${svc.endpoint}`;
685
+ console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
686
+ }
687
+ } catch {
688
+ }
689
+ let requestBody;
690
+ if (options.rawData) {
691
+ requestBody = { service, ...params };
692
+ } else {
693
+ requestBody = { service, params };
694
+ }
250
695
  if (options.chain) {
251
696
  requestBody.chain = options.chain;
252
697
  }
253
- const initialRes = await fetch(`${serverUrl}/execute`, {
698
+ const initialRes = await fetch(executeUrl, {
254
699
  method: "POST",
255
700
  headers: { "Content-Type": "application/json" },
256
701
  body: JSON.stringify(requestBody)
@@ -262,9 +707,14 @@ var MoltsPayClient = class {
262
707
  }
263
708
  throw new Error(data.error || "Unexpected response");
264
709
  }
710
+ const wwwAuthHeader = initialRes.headers.get("www-authenticate");
265
711
  const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
712
+ if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
713
+ console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
714
+ return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
715
+ }
266
716
  if (!paymentRequiredHeader) {
267
- throw new Error("Missing x-payment-required header");
717
+ throw new Error("Missing payment header (x-payment-required or www-authenticate)");
268
718
  }
269
719
  let requirements;
270
720
  try {
@@ -281,17 +731,22 @@ var MoltsPayClient = class {
281
731
  throw new Error("Invalid x-payment-required header");
282
732
  }
283
733
  const networkToChainName = (network2) => {
734
+ if (network2 === "solana:mainnet") return "solana";
735
+ if (network2 === "solana:devnet") return "solana_devnet";
284
736
  const match = network2.match(/^eip155:(\d+)$/);
285
737
  if (!match) return null;
286
738
  const chainId = parseInt(match[1]);
287
739
  if (chainId === 8453) return "base";
288
740
  if (chainId === 137) return "polygon";
289
741
  if (chainId === 84532) return "base_sepolia";
742
+ if (chainId === 42431) return "tempo_moderato";
743
+ if (chainId === 56) return "bnb";
744
+ if (chainId === 97) return "bnb_testnet";
290
745
  return null;
291
746
  };
292
747
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
293
- let chainName;
294
748
  const userSpecifiedChain = options.chain;
749
+ let selectedChain;
295
750
  if (userSpecifiedChain) {
296
751
  if (!serverChains.includes(userSpecifiedChain)) {
297
752
  throw new Error(
@@ -299,17 +754,27 @@ var MoltsPayClient = class {
299
754
  Server accepts: ${serverChains.join(", ")}`
300
755
  );
301
756
  }
302
- chainName = userSpecifiedChain;
757
+ selectedChain = userSpecifiedChain;
303
758
  } else {
304
759
  if (serverChains.length === 1 && serverChains[0] === "base") {
305
- chainName = "base";
760
+ selectedChain = "base";
306
761
  } else {
307
762
  throw new Error(
308
763
  `Server accepts: ${serverChains.join(", ")}
309
- Please specify: --chain base, --chain polygon, or --chain base_sepolia`
764
+ Please specify: --chain <chain_name>`
310
765
  );
311
766
  }
312
767
  }
768
+ if (selectedChain === "solana" || selectedChain === "solana_devnet") {
769
+ const solanaChain = selectedChain;
770
+ const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
771
+ const req2 = requirements.find((r) => r.network === network2);
772
+ if (!req2) {
773
+ throw new Error(`Failed to find payment requirement for ${selectedChain}`);
774
+ }
775
+ return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
776
+ }
777
+ const chainName = selectedChain;
313
778
  const chain = getChain(chainName);
314
779
  const network = `eip155:${chain.chainId}`;
315
780
  const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
@@ -344,6 +809,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
344
809
  } else {
345
810
  console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
346
811
  }
812
+ if (chainName === "bnb" || chainName === "bnb_testnet") {
813
+ console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
814
+ const payTo2 = req.payTo || req.resource;
815
+ if (!payTo2) {
816
+ throw new Error("Missing payTo address in payment requirements");
817
+ }
818
+ const bnbSpender = req.extra?.bnbSpender;
819
+ if (!bnbSpender) {
820
+ throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
821
+ }
822
+ return await this.handleBNBPayment(executeUrl, service, params, {
823
+ to: payTo2,
824
+ amount,
825
+ token,
826
+ chainName,
827
+ chain,
828
+ spender: bnbSpender
829
+ }, options);
830
+ }
347
831
  const payTo = req.payTo || req.resource;
348
832
  if (!payTo) {
349
833
  throw new Error("Missing payTo address in payment requirements");
@@ -373,11 +857,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
373
857
  };
374
858
  const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
375
859
  console.log(`[MoltsPay] Sending request with payment...`);
376
- const paidRequestBody = { service, params };
860
+ const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
377
861
  if (options.chain) {
378
862
  paidRequestBody.chain = options.chain;
379
863
  }
380
- const paidRes = await fetch(`${serverUrl}/execute`, {
864
+ const paidRes = await fetch(executeUrl, {
381
865
  method: "POST",
382
866
  headers: {
383
867
  "Content-Type": "application/json",
@@ -391,7 +875,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
391
875
  }
392
876
  this.recordSpending(amount);
393
877
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
394
- return result.result;
878
+ return result.result || result;
879
+ }
880
+ /**
881
+ * Handle MPP (Machine Payments Protocol) payment flow
882
+ * Called when pay() detects WWW-Authenticate header in 402 response
883
+ */
884
+ async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
885
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
886
+ const { createWalletClient, createPublicClient, http } = await import("viem");
887
+ const { tempoModerato } = await import("viem/chains");
888
+ const { Actions } = await import("viem/tempo");
889
+ const privateKey = this.walletData.privateKey;
890
+ const account = privateKeyToAccount2(privateKey);
891
+ console.log(`[MoltsPay] Using MPP protocol on Tempo`);
892
+ console.log(`[MoltsPay] Account: ${account.address}`);
893
+ const parseAuthParam = (header, key) => {
894
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
895
+ return match ? match[1] : null;
896
+ };
897
+ const challengeId = parseAuthParam(wwwAuthHeader, "id");
898
+ const method = parseAuthParam(wwwAuthHeader, "method");
899
+ const realm = parseAuthParam(wwwAuthHeader, "realm");
900
+ const requestB64 = parseAuthParam(wwwAuthHeader, "request");
901
+ if (method !== "tempo") {
902
+ throw new Error(`Unsupported payment method: ${method}`);
903
+ }
904
+ if (!requestB64) {
905
+ throw new Error("Missing request in WWW-Authenticate");
906
+ }
907
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
908
+ const paymentRequest = JSON.parse(requestJson);
909
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
910
+ const chainId = methodDetails?.chainId || 42431;
911
+ const amountDisplay = Number(amount) / 1e6;
912
+ console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
913
+ this.checkLimits(amountDisplay);
914
+ console.log(`[MoltsPay] Sending transaction on Tempo...`);
915
+ const tempoChain = { ...tempoModerato, feeToken: currency };
916
+ const publicClient = createPublicClient({
917
+ chain: tempoChain,
918
+ transport: http("https://rpc.moderato.tempo.xyz")
919
+ });
920
+ const walletClient = createWalletClient({
921
+ account,
922
+ chain: tempoChain,
923
+ transport: http("https://rpc.moderato.tempo.xyz")
924
+ });
925
+ const txHash = await Actions.token.transfer(walletClient, {
926
+ to: recipient,
927
+ amount: BigInt(amount),
928
+ token: currency
929
+ });
930
+ console.log(`[MoltsPay] Transaction: ${txHash}`);
931
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
932
+ console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
933
+ const credential = {
934
+ challenge: {
935
+ id: challengeId,
936
+ realm,
937
+ method: "tempo",
938
+ intent: "charge",
939
+ request: paymentRequest
940
+ },
941
+ payload: { hash: txHash, type: "hash" },
942
+ source: `did:pkh:eip155:${chainId}:${account.address}`
943
+ };
944
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
945
+ const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
946
+ const paidRes = await fetch(executeUrl, {
947
+ method: "POST",
948
+ headers: {
949
+ "Content-Type": "application/json",
950
+ "Authorization": `Payment ${credentialB64}`
951
+ },
952
+ body: JSON.stringify(retryBody)
953
+ });
954
+ const result = await paidRes.json();
955
+ if (!paidRes.ok) {
956
+ throw new Error(result.error || "Payment verification failed");
957
+ }
958
+ this.recordSpending(amountDisplay);
959
+ console.log(`[MoltsPay] Success!`);
960
+ return result.result || result;
961
+ }
962
+ /**
963
+ * Handle BNB Chain payment flow (pre-approval + intent signature)
964
+ *
965
+ * Flow:
966
+ * 1. Check client has approved server wallet (done via `moltspay init`)
967
+ * 2. Sign EIP-712 payment intent (no gas, just signature)
968
+ * 3. Send intent to server
969
+ * 4. Server executes service
970
+ * 5. Server calls transferFrom if successful (pay-for-success)
971
+ */
972
+ async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
973
+ const { to, amount, token, chainName, chain, spender } = paymentDetails;
974
+ const tokenConfig = chain.tokens[token];
975
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
976
+ const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
977
+ const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
978
+ if (allowance < amountWeiCheck) {
979
+ const nativeBalance = await provider.getBalance(this.wallet.address);
980
+ const minGasBalance = ethers.parseEther("0.0005");
981
+ if (nativeBalance < minGasBalance) {
982
+ const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
983
+ const isTestnet = chainName === "bnb_testnet";
984
+ if (isTestnet) {
985
+ throw new Error(
986
+ `\u274C Insufficient tBNB for approval transaction
987
+
988
+ Current tBNB: ${nativeBNB}
989
+ Required: ~0.001 tBNB
990
+
991
+ Get testnet tokens: npx moltspay faucet --chain bnb_testnet
992
+ (Gives USDC + tBNB for gas)`
993
+ );
994
+ } else {
995
+ throw new Error(
996
+ `\u274C Insufficient BNB for approval transaction
997
+
998
+ Current BNB: ${nativeBNB}
999
+ Required: ~0.001 BNB (~$0.60)
1000
+
1001
+ To get BNB:
1002
+ \u2022 Withdraw from Binance/exchange to your wallet
1003
+ \u2022 Most exchanges include BNB dust with withdrawals
1004
+
1005
+ After funding, run:
1006
+ npx moltspay approve --chain ${chainName} --spender ${spender}`
1007
+ );
1008
+ }
1009
+ }
1010
+ throw new Error(
1011
+ `Insufficient allowance for ${spender.slice(0, 10)}...
1012
+ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1013
+ );
1014
+ }
1015
+ const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
1016
+ const intent = {
1017
+ from: this.wallet.address,
1018
+ to,
1019
+ amount: amountWei,
1020
+ token: tokenConfig.address,
1021
+ service,
1022
+ nonce: Date.now(),
1023
+ // Use timestamp as nonce for simplicity
1024
+ deadline: Date.now() + 36e5
1025
+ // 1 hour
1026
+ };
1027
+ const domain = {
1028
+ name: "MoltsPay",
1029
+ version: "1",
1030
+ chainId: chain.chainId
1031
+ };
1032
+ const types = {
1033
+ PaymentIntent: [
1034
+ { name: "from", type: "address" },
1035
+ { name: "to", type: "address" },
1036
+ { name: "amount", type: "uint256" },
1037
+ { name: "token", type: "address" },
1038
+ { name: "service", type: "string" },
1039
+ { name: "nonce", type: "uint256" },
1040
+ { name: "deadline", type: "uint256" }
1041
+ ]
1042
+ };
1043
+ console.log(`[MoltsPay] Signing BNB payment intent...`);
1044
+ const signature = await this.wallet.signTypedData(domain, types, intent);
1045
+ const network = `eip155:${chain.chainId}`;
1046
+ const payload = {
1047
+ x402Version: 2,
1048
+ scheme: "exact",
1049
+ network,
1050
+ payload: {
1051
+ intent: {
1052
+ ...intent,
1053
+ signature
1054
+ },
1055
+ chainId: chain.chainId
1056
+ },
1057
+ accepted: {
1058
+ scheme: "exact",
1059
+ network,
1060
+ asset: tokenConfig.address,
1061
+ amount: amountWei,
1062
+ payTo: to,
1063
+ maxTimeoutSeconds: 300
1064
+ }
1065
+ };
1066
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
1067
+ console.log(`[MoltsPay] Sending BNB payment request...`);
1068
+ const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
1069
+ const paidRes = await fetch(executeUrl, {
1070
+ method: "POST",
1071
+ headers: {
1072
+ "Content-Type": "application/json",
1073
+ "X-Payment": paymentHeader
1074
+ },
1075
+ body: JSON.stringify(bnbRequestBody)
1076
+ });
1077
+ const result = await paidRes.json();
1078
+ if (!paidRes.ok) {
1079
+ throw new Error(result.error || "BNB payment failed");
1080
+ }
1081
+ this.recordSpending(amount);
1082
+ console.log(`[MoltsPay] Success! BNB payment settled.`);
1083
+ return result.result || result;
1084
+ }
1085
+ /**
1086
+ * Handle Solana payment flow
1087
+ *
1088
+ * Solana uses SPL token transfers with pay-for-success model:
1089
+ * 1. Client creates and signs a transfer transaction
1090
+ * 2. Server submits the transaction after service completes
1091
+ */
1092
+ async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
1093
+ const solanaWallet = loadSolanaWallet(this.configDir);
1094
+ if (!solanaWallet) {
1095
+ throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
1096
+ }
1097
+ const amount = Number(requirements.amount);
1098
+ const amountUSDC = amount / 1e6;
1099
+ this.checkLimits(amountUSDC);
1100
+ console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
1101
+ if (!requirements.payTo) {
1102
+ throw new Error("Missing payTo address in payment requirements");
1103
+ }
1104
+ const solanaFeePayer = requirements.extra?.solanaFeePayer;
1105
+ const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
1106
+ if (feePayerPubkey) {
1107
+ console.log(`[MoltsPay] Gasless mode: server pays fees`);
1108
+ }
1109
+ const recipientPubkey = new PublicKey4(requirements.payTo);
1110
+ const transaction = await createSolanaPaymentTransaction(
1111
+ solanaWallet.publicKey,
1112
+ recipientPubkey,
1113
+ BigInt(amount),
1114
+ chain,
1115
+ feePayerPubkey
1116
+ // Optional fee payer for gasless mode
1117
+ );
1118
+ if (feePayerPubkey) {
1119
+ transaction.partialSign(solanaWallet);
1120
+ } else {
1121
+ transaction.sign(solanaWallet);
1122
+ }
1123
+ const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
1124
+ console.log(`[MoltsPay] Transaction signed, sending to server...`);
1125
+ const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
1126
+ const payload = {
1127
+ x402Version: 2,
1128
+ scheme: "exact",
1129
+ network,
1130
+ payload: {
1131
+ signedTransaction: signedTx,
1132
+ sender: solanaWallet.publicKey.toBase58(),
1133
+ chain
1134
+ },
1135
+ accepted: {
1136
+ scheme: "exact",
1137
+ network,
1138
+ asset: requirements.asset,
1139
+ amount: requirements.amount,
1140
+ payTo: requirements.payTo,
1141
+ maxTimeoutSeconds: 300
1142
+ }
1143
+ };
1144
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
1145
+ const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
1146
+ const paidRes = await fetch(executeUrl, {
1147
+ method: "POST",
1148
+ headers: {
1149
+ "Content-Type": "application/json",
1150
+ "X-Payment": paymentHeader
1151
+ },
1152
+ body: JSON.stringify(solanaRequestBody)
1153
+ });
1154
+ const result = await paidRes.json();
1155
+ if (!paidRes.ok) {
1156
+ throw new Error(result.error || "Solana payment failed");
1157
+ }
1158
+ this.recordSpending(amountUSDC);
1159
+ console.log(`[MoltsPay] Success! Solana payment settled.`);
1160
+ if (result.payment?.transaction) {
1161
+ const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
1162
+ console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
1163
+ }
1164
+ return result.result || result;
1165
+ }
1166
+ /**
1167
+ * Check ERC20 allowance for a spender
1168
+ */
1169
+ async checkAllowance(tokenAddress, spender, provider) {
1170
+ const contract = new ethers.Contract(
1171
+ tokenAddress,
1172
+ ["function allowance(address owner, address spender) view returns (uint256)"],
1173
+ provider
1174
+ );
1175
+ return await contract.allowance(this.wallet.address, spender);
395
1176
  }
396
1177
  /**
397
1178
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
@@ -463,26 +1244,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
463
1244
  }
464
1245
  // --- Config & Wallet Management ---
465
1246
  loadConfig() {
466
- const configPath = join(this.configDir, "config.json");
467
- if (existsSync(configPath)) {
468
- const content = readFileSync(configPath, "utf-8");
1247
+ const configPath = join2(this.configDir, "config.json");
1248
+ if (existsSync2(configPath)) {
1249
+ const content = readFileSync2(configPath, "utf-8");
469
1250
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
470
1251
  }
471
1252
  return { ...DEFAULT_CONFIG };
472
1253
  }
473
1254
  saveConfig() {
474
- mkdirSync(this.configDir, { recursive: true });
475
- const configPath = join(this.configDir, "config.json");
476
- writeFileSync(configPath, JSON.stringify(this.config, null, 2));
1255
+ mkdirSync2(this.configDir, { recursive: true });
1256
+ const configPath = join2(this.configDir, "config.json");
1257
+ writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
477
1258
  }
478
1259
  /**
479
1260
  * Load spending data from disk
480
1261
  */
481
1262
  loadSpending() {
482
- const spendingPath = join(this.configDir, "spending.json");
483
- if (existsSync(spendingPath)) {
1263
+ const spendingPath = join2(this.configDir, "spending.json");
1264
+ if (existsSync2(spendingPath)) {
484
1265
  try {
485
- const data = JSON.parse(readFileSync(spendingPath, "utf-8"));
1266
+ const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
486
1267
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
487
1268
  if (data.date && data.date === today) {
488
1269
  this.todaySpending = data.amount || 0;
@@ -501,18 +1282,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
501
1282
  * Save spending data to disk
502
1283
  */
503
1284
  saveSpending() {
504
- mkdirSync(this.configDir, { recursive: true });
505
- const spendingPath = join(this.configDir, "spending.json");
1285
+ mkdirSync2(this.configDir, { recursive: true });
1286
+ const spendingPath = join2(this.configDir, "spending.json");
506
1287
  const data = {
507
1288
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
508
1289
  amount: this.todaySpending,
509
1290
  updatedAt: Date.now()
510
1291
  };
511
- writeFileSync(spendingPath, JSON.stringify(data, null, 2));
1292
+ writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
512
1293
  }
513
1294
  loadWallet() {
514
- const walletPath = join(this.configDir, "wallet.json");
515
- if (existsSync(walletPath)) {
1295
+ const walletPath = join2(this.configDir, "wallet.json");
1296
+ if (existsSync2(walletPath)) {
516
1297
  try {
517
1298
  const stats = statSync(walletPath);
518
1299
  const mode = stats.mode & 511;
@@ -523,7 +1304,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
523
1304
  }
524
1305
  } catch (err) {
525
1306
  }
526
- const content = readFileSync(walletPath, "utf-8");
1307
+ const content = readFileSync2(walletPath, "utf-8");
527
1308
  return JSON.parse(content);
528
1309
  }
529
1310
  return null;
@@ -532,15 +1313,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
532
1313
  * Initialize a new wallet (called by CLI)
533
1314
  */
534
1315
  static init(configDir, options) {
535
- mkdirSync(configDir, { recursive: true });
1316
+ mkdirSync2(configDir, { recursive: true });
536
1317
  const wallet = Wallet.createRandom();
537
1318
  const walletData = {
538
1319
  address: wallet.address,
539
1320
  privateKey: wallet.privateKey,
540
1321
  createdAt: Date.now()
541
1322
  };
542
- const walletPath = join(configDir, "wallet.json");
543
- writeFileSync(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1323
+ const walletPath = join2(configDir, "wallet.json");
1324
+ writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
544
1325
  const config = {
545
1326
  chain: options.chain,
546
1327
  limits: {
@@ -548,8 +1329,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
548
1329
  maxPerDay: options.maxPerDay
549
1330
  }
550
1331
  };
551
- const configPath = join(configDir, "config.json");
552
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1332
+ const configPath = join2(configDir, "config.json");
1333
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
553
1334
  return { address: wallet.address, configDir };
554
1335
  }
555
1336
  /**
@@ -585,7 +1366,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
585
1366
  if (!this.wallet) {
586
1367
  throw new Error("Client not initialized");
587
1368
  }
588
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1369
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
589
1370
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
590
1371
  const results = {};
591
1372
  const tempoTokens = {
@@ -656,12 +1437,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
656
1437
  if (!this.wallet || !this.walletData) {
657
1438
  throw new Error("Client not initialized. Run: npx moltspay init");
658
1439
  }
659
- const { privateKeyToAccount } = await import("viem/accounts");
1440
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
660
1441
  const { createWalletClient, createPublicClient, http } = await import("viem");
661
1442
  const { tempoModerato } = await import("viem/chains");
662
1443
  const { Actions } = await import("viem/tempo");
663
1444
  const privateKey = this.walletData.privateKey;
664
- const account = privateKeyToAccount(privateKey);
1445
+ const account = privateKeyToAccount2(privateKey);
665
1446
  console.log(`[MoltsPay] Making MPP request to: ${url}`);
666
1447
  console.log(`[MoltsPay] Using account: ${account.address}`);
667
1448
  const initResponse = await fetch(url, {
@@ -758,24 +1539,16 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
758
1539
 
759
1540
  // src/server/index.ts
760
1541
  init_esm_shims();
761
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
1542
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
762
1543
  import { createServer } from "http";
763
1544
  import * as path3 from "path";
764
1545
 
765
1546
  // src/facilitators/index.ts
766
1547
  init_esm_shims();
767
1548
 
768
- // src/facilitators/interface.ts
769
- init_esm_shims();
770
- var BaseFacilitator = class {
771
- supportsNetwork(network) {
772
- return this.supportedNetworks.includes(network);
773
- }
774
- };
775
-
776
1549
  // src/facilitators/cdp.ts
777
1550
  init_esm_shims();
778
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1551
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
779
1552
  import * as path2 from "path";
780
1553
  var X402_VERSION2 = 2;
781
1554
  var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
@@ -786,9 +1559,9 @@ function loadEnvFile() {
786
1559
  path2.join(process.env.HOME || "", ".moltspay", ".env")
787
1560
  ];
788
1561
  for (const envPath of envPaths) {
789
- if (existsSync2(envPath)) {
1562
+ if (existsSync3(envPath)) {
790
1563
  try {
791
- const content = readFileSync2(envPath, "utf-8");
1564
+ const content = readFileSync3(envPath, "utf-8");
792
1565
  for (const line of content.split("\n")) {
793
1566
  const trimmed = line.trim();
794
1567
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -1024,18 +1797,280 @@ var TempoFacilitator = class extends BaseFacilitator {
1024
1797
  if (chainId !== 42431) {
1025
1798
  return { healthy: false, error: `Wrong chainId: ${chainId}` };
1026
1799
  }
1027
- return { healthy: true, latencyMs: Date.now() - start };
1800
+ return { healthy: true, latencyMs: Date.now() - start };
1801
+ } catch (error) {
1802
+ return { healthy: false, error: String(error) };
1803
+ }
1804
+ }
1805
+ async verify(paymentPayload, requirements) {
1806
+ try {
1807
+ const tempoPayload = paymentPayload.payload;
1808
+ if (!tempoPayload?.txHash) {
1809
+ return { valid: false, error: "Missing txHash in payment payload" };
1810
+ }
1811
+ const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
1812
+ if (!receipt) {
1813
+ return { valid: false, error: "Transaction not found" };
1814
+ }
1815
+ if (receipt.status !== "0x1") {
1816
+ return { valid: false, error: "Transaction failed" };
1817
+ }
1818
+ const transferLog = receipt.logs.find(
1819
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
1820
+ );
1821
+ if (!transferLog) {
1822
+ return { valid: false, error: "No Transfer event found" };
1823
+ }
1824
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
1825
+ const expectedTo = requirements.payTo.toLowerCase();
1826
+ if (toAddress !== expectedTo) {
1827
+ return {
1828
+ valid: false,
1829
+ error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
1830
+ };
1831
+ }
1832
+ const amount = BigInt(transferLog.data);
1833
+ const expectedAmount = BigInt(requirements.amount);
1834
+ if (amount < expectedAmount) {
1835
+ return {
1836
+ valid: false,
1837
+ error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
1838
+ };
1839
+ }
1840
+ const tokenAddress = transferLog.address.toLowerCase();
1841
+ const expectedToken = requirements.asset.toLowerCase();
1842
+ if (tokenAddress !== expectedToken) {
1843
+ return {
1844
+ valid: false,
1845
+ error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
1846
+ };
1847
+ }
1848
+ return {
1849
+ valid: true,
1850
+ details: {
1851
+ txHash: tempoPayload.txHash,
1852
+ from: "0x" + transferLog.topics[1].slice(26),
1853
+ to: toAddress,
1854
+ amount: amount.toString(),
1855
+ token: tokenAddress
1856
+ }
1857
+ };
1858
+ } catch (error) {
1859
+ return { valid: false, error: `Verification failed: ${error}` };
1860
+ }
1861
+ }
1862
+ async settle(paymentPayload, requirements) {
1863
+ const verifyResult = await this.verify(paymentPayload, requirements);
1864
+ if (!verifyResult.valid) {
1865
+ return { success: false, error: verifyResult.error };
1866
+ }
1867
+ const tempoPayload = paymentPayload.payload;
1868
+ return {
1869
+ success: true,
1870
+ transaction: tempoPayload.txHash,
1871
+ status: "settled"
1872
+ };
1873
+ }
1874
+ async getTransactionReceipt(txHash) {
1875
+ const response = await fetch(this.rpcUrl, {
1876
+ method: "POST",
1877
+ headers: { "Content-Type": "application/json" },
1878
+ body: JSON.stringify({
1879
+ jsonrpc: "2.0",
1880
+ method: "eth_getTransactionReceipt",
1881
+ params: [txHash],
1882
+ id: 1
1883
+ })
1884
+ });
1885
+ const data = await response.json();
1886
+ return data.result;
1887
+ }
1888
+ };
1889
+
1890
+ // src/facilitators/bnb.ts
1891
+ init_esm_shims();
1892
+ import { privateKeyToAccount } from "viem/accounts";
1893
+ var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
1894
+ var EIP712_DOMAIN = {
1895
+ name: "MoltsPay",
1896
+ version: "1"
1897
+ };
1898
+ var INTENT_TYPES = {
1899
+ PaymentIntent: [
1900
+ { name: "from", type: "address" },
1901
+ { name: "to", type: "address" },
1902
+ { name: "amount", type: "uint256" },
1903
+ { name: "token", type: "address" },
1904
+ { name: "service", type: "string" },
1905
+ { name: "nonce", type: "uint256" },
1906
+ { name: "deadline", type: "uint256" }
1907
+ ]
1908
+ };
1909
+ var BNBFacilitator = class extends BaseFacilitator {
1910
+ name = "bnb";
1911
+ displayName = "BNB Smart Chain";
1912
+ supportedNetworks = ["eip155:56", "eip155:97"];
1913
+ // Mainnet + Testnet
1914
+ serverPrivateKey;
1915
+ spenderAddress = null;
1916
+ chainConfigs;
1917
+ constructor(serverPrivateKey) {
1918
+ super();
1919
+ this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
1920
+ if (this.serverPrivateKey) {
1921
+ const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
1922
+ const account = privateKeyToAccount(key);
1923
+ this.spenderAddress = account.address;
1924
+ }
1925
+ this.chainConfigs = {
1926
+ 56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
1927
+ 97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
1928
+ };
1929
+ }
1930
+ async healthCheck() {
1931
+ const start = Date.now();
1932
+ try {
1933
+ const response = await fetch(this.chainConfigs[56].rpc, {
1934
+ method: "POST",
1935
+ headers: { "Content-Type": "application/json" },
1936
+ body: JSON.stringify({
1937
+ jsonrpc: "2.0",
1938
+ method: "eth_chainId",
1939
+ params: [],
1940
+ id: 1
1941
+ })
1942
+ });
1943
+ const data = await response.json();
1944
+ const chainId = parseInt(data.result, 16);
1945
+ if (chainId !== 56) {
1946
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
1947
+ }
1948
+ return { healthy: true, latencyMs: Date.now() - start };
1949
+ } catch (error) {
1950
+ return { healthy: false, error: String(error) };
1951
+ }
1952
+ }
1953
+ /**
1954
+ * Verify a payment intent signature (before service execution)
1955
+ *
1956
+ * This verifies:
1957
+ * 1. Signature is valid for the intent
1958
+ * 2. Client has approved server wallet
1959
+ * 3. Client has sufficient balance
1960
+ * 4. Intent hasn't expired
1961
+ */
1962
+ async verify(paymentPayload, requirements) {
1963
+ try {
1964
+ const bnbPayload = paymentPayload.payload;
1965
+ if (!bnbPayload?.intent) {
1966
+ return { valid: false, error: "Missing intent in payment payload" };
1967
+ }
1968
+ const { intent, chainId } = bnbPayload;
1969
+ const config = this.chainConfigs[chainId];
1970
+ if (!config) {
1971
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
1972
+ }
1973
+ if (intent.deadline < Date.now()) {
1974
+ return { valid: false, error: "Intent expired" };
1975
+ }
1976
+ const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
1977
+ if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
1978
+ return { valid: false, error: "Invalid signature" };
1979
+ }
1980
+ if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
1981
+ return { valid: false, error: `Wrong recipient: ${intent.to}` };
1982
+ }
1983
+ if (BigInt(intent.amount) < BigInt(requirements.amount)) {
1984
+ return { valid: false, error: `Insufficient amount: ${intent.amount}` };
1985
+ }
1986
+ if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
1987
+ return { valid: false, error: `Wrong token: ${intent.token}` };
1988
+ }
1989
+ const serverAddress = await this.getServerAddress();
1990
+ const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
1991
+ if (BigInt(allowance) < BigInt(intent.amount)) {
1992
+ return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
1993
+ }
1994
+ const balance = await this.getBalance(intent.from, intent.token, config.rpc);
1995
+ if (BigInt(balance) < BigInt(intent.amount)) {
1996
+ return { valid: false, error: "Insufficient balance" };
1997
+ }
1998
+ return {
1999
+ valid: true,
2000
+ details: {
2001
+ from: intent.from,
2002
+ to: intent.to,
2003
+ amount: intent.amount,
2004
+ token: intent.token,
2005
+ service: intent.service,
2006
+ nonce: intent.nonce,
2007
+ deadline: intent.deadline
2008
+ }
2009
+ };
2010
+ } catch (error) {
2011
+ return { valid: false, error: `Verification failed: ${error}` };
2012
+ }
2013
+ }
2014
+ /**
2015
+ * Settle a payment by executing transferFrom
2016
+ *
2017
+ * This is called AFTER the service has been successfully delivered.
2018
+ * Server pays gas, transfers tokens from client to provider.
2019
+ */
2020
+ async settle(paymentPayload, requirements) {
2021
+ if (!this.serverPrivateKey) {
2022
+ return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
2023
+ }
2024
+ try {
2025
+ const verifyResult = await this.verify(paymentPayload, requirements);
2026
+ if (!verifyResult.valid) {
2027
+ return { success: false, error: verifyResult.error };
2028
+ }
2029
+ const bnbPayload = paymentPayload.payload;
2030
+ const { intent, chainId } = bnbPayload;
2031
+ const config = this.chainConfigs[chainId];
2032
+ const txHash = await this.executeTransferFrom(
2033
+ intent.from,
2034
+ intent.to,
2035
+ intent.amount,
2036
+ intent.token,
2037
+ config.rpc
2038
+ );
2039
+ return {
2040
+ success: true,
2041
+ transaction: txHash,
2042
+ status: "settled"
2043
+ };
1028
2044
  } catch (error) {
1029
- return { healthy: false, error: String(error) };
2045
+ return { success: false, error: `Settlement failed: ${error}` };
1030
2046
  }
1031
2047
  }
1032
- async verify(paymentPayload, requirements) {
2048
+ /**
2049
+ * Check if client has approved the server wallet
2050
+ */
2051
+ async checkApproval(clientAddress, token, chainId) {
2052
+ const config = this.chainConfigs[chainId];
2053
+ if (!config) {
2054
+ throw new Error(`Unsupported chainId: ${chainId}`);
2055
+ }
2056
+ const serverAddress = await this.getServerAddress();
2057
+ const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
2058
+ const minAllowance = BigInt("1000000000000000000000");
2059
+ return {
2060
+ approved: BigInt(allowance) >= minAllowance,
2061
+ allowance
2062
+ };
2063
+ }
2064
+ /**
2065
+ * Verify a completed transaction (for checking past payments)
2066
+ */
2067
+ async verifyTransaction(txHash, expected, chainId) {
2068
+ const config = this.chainConfigs[chainId];
2069
+ if (!config) {
2070
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
2071
+ }
1033
2072
  try {
1034
- const tempoPayload = paymentPayload.payload;
1035
- if (!tempoPayload?.txHash) {
1036
- return { valid: false, error: "Missing txHash in payment payload" };
1037
- }
1038
- const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
2073
+ const receipt = await this.getTransactionReceipt(txHash, config.rpc);
1039
2074
  if (!receipt) {
1040
2075
  return { valid: false, error: "Transaction not found" };
1041
2076
  }
@@ -1043,63 +2078,117 @@ var TempoFacilitator = class extends BaseFacilitator {
1043
2078
  return { valid: false, error: "Transaction failed" };
1044
2079
  }
1045
2080
  const transferLog = receipt.logs.find(
1046
- (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
2081
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
1047
2082
  );
1048
2083
  if (!transferLog) {
1049
2084
  return { valid: false, error: "No Transfer event found" };
1050
2085
  }
1051
2086
  const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
1052
- const expectedTo = requirements.payTo.toLowerCase();
1053
- if (toAddress !== expectedTo) {
1054
- return {
1055
- valid: false,
1056
- error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
1057
- };
2087
+ if (toAddress !== expected.to.toLowerCase()) {
2088
+ return { valid: false, error: `Wrong recipient: ${toAddress}` };
1058
2089
  }
1059
2090
  const amount = BigInt(transferLog.data);
1060
- const expectedAmount = BigInt(requirements.amount);
1061
- if (amount < expectedAmount) {
1062
- return {
1063
- valid: false,
1064
- error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
1065
- };
1066
- }
1067
- const tokenAddress = transferLog.address.toLowerCase();
1068
- const expectedToken = requirements.asset.toLowerCase();
1069
- if (tokenAddress !== expectedToken) {
1070
- return {
1071
- valid: false,
1072
- error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
1073
- };
2091
+ if (amount < BigInt(expected.amount)) {
2092
+ return { valid: false, error: `Insufficient amount: ${amount}` };
1074
2093
  }
1075
2094
  return {
1076
2095
  valid: true,
1077
2096
  details: {
1078
- txHash: tempoPayload.txHash,
2097
+ txHash,
1079
2098
  from: "0x" + transferLog.topics[1].slice(26),
1080
2099
  to: toAddress,
1081
2100
  amount: amount.toString(),
1082
- token: tokenAddress
2101
+ token: transferLog.address
1083
2102
  }
1084
2103
  };
1085
2104
  } catch (error) {
1086
2105
  return { valid: false, error: `Verification failed: ${error}` };
1087
2106
  }
1088
2107
  }
1089
- async settle(paymentPayload, requirements) {
1090
- const verifyResult = await this.verify(paymentPayload, requirements);
1091
- if (!verifyResult.valid) {
1092
- return { success: false, error: verifyResult.error };
1093
- }
1094
- const tempoPayload = paymentPayload.payload;
1095
- return {
1096
- success: true,
1097
- transaction: tempoPayload.txHash,
1098
- status: "settled"
2108
+ // ==================== Private Methods ====================
2109
+ /**
2110
+ * Get the server's spender address (public, for 402 responses)
2111
+ * Returns cached value computed at construction time.
2112
+ */
2113
+ getSpenderAddress() {
2114
+ return this.spenderAddress;
2115
+ }
2116
+ async getServerAddress() {
2117
+ const { ethers: ethers3 } = await import("ethers");
2118
+ const wallet = new ethers3.Wallet(this.serverPrivateKey);
2119
+ return wallet.address;
2120
+ }
2121
+ async recoverIntentSigner(intent, chainId) {
2122
+ const { ethers: ethers3 } = await import("ethers");
2123
+ const domain = {
2124
+ ...EIP712_DOMAIN,
2125
+ chainId
2126
+ };
2127
+ const message = {
2128
+ from: intent.from,
2129
+ to: intent.to,
2130
+ amount: intent.amount,
2131
+ token: intent.token,
2132
+ service: intent.service,
2133
+ nonce: intent.nonce,
2134
+ deadline: intent.deadline
1099
2135
  };
2136
+ const recoveredAddress = ethers3.verifyTypedData(
2137
+ domain,
2138
+ INTENT_TYPES,
2139
+ message,
2140
+ intent.signature
2141
+ );
2142
+ return recoveredAddress;
1100
2143
  }
1101
- async getTransactionReceipt(txHash) {
1102
- const response = await fetch(this.rpcUrl, {
2144
+ async getAllowance(owner, spender, token, rpcUrl) {
2145
+ const selector = "0xdd62ed3e";
2146
+ const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
2147
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
2148
+ const data = selector + ownerPadded + spenderPadded;
2149
+ const response = await fetch(rpcUrl, {
2150
+ method: "POST",
2151
+ headers: { "Content-Type": "application/json" },
2152
+ body: JSON.stringify({
2153
+ jsonrpc: "2.0",
2154
+ method: "eth_call",
2155
+ params: [{ to: token, data }, "latest"],
2156
+ id: 1
2157
+ })
2158
+ });
2159
+ const result = await response.json();
2160
+ return result.result || "0x0";
2161
+ }
2162
+ async getBalance(account, token, rpcUrl) {
2163
+ const selector = "0x70a08231";
2164
+ const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
2165
+ const data = selector + accountPadded;
2166
+ const response = await fetch(rpcUrl, {
2167
+ method: "POST",
2168
+ headers: { "Content-Type": "application/json" },
2169
+ body: JSON.stringify({
2170
+ jsonrpc: "2.0",
2171
+ method: "eth_call",
2172
+ params: [{ to: token, data }, "latest"],
2173
+ id: 1
2174
+ })
2175
+ });
2176
+ const result = await response.json();
2177
+ return result.result || "0x0";
2178
+ }
2179
+ async executeTransferFrom(from, to, amount, token, rpcUrl) {
2180
+ const { ethers: ethers3 } = await import("ethers");
2181
+ const provider = new ethers3.JsonRpcProvider(rpcUrl);
2182
+ const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
2183
+ const tokenContract = new ethers3.Contract(token, [
2184
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
2185
+ ], wallet);
2186
+ const tx = await tokenContract.transferFrom(from, to, amount);
2187
+ const receipt = await tx.wait();
2188
+ return receipt.hash;
2189
+ }
2190
+ async getTransactionReceipt(txHash, rpcUrl) {
2191
+ const response = await fetch(rpcUrl, {
1103
2192
  method: "POST",
1104
2193
  headers: { "Content-Type": "application/json" },
1105
2194
  body: JSON.stringify({
@@ -1116,6 +2205,8 @@ var TempoFacilitator = class extends BaseFacilitator {
1116
2205
 
1117
2206
  // src/facilitators/registry.ts
1118
2207
  init_esm_shims();
2208
+ import { Keypair as Keypair4 } from "@solana/web3.js";
2209
+ import bs582 from "bs58";
1119
2210
  var FacilitatorRegistry = class {
1120
2211
  factories = /* @__PURE__ */ new Map();
1121
2212
  instances = /* @__PURE__ */ new Map();
@@ -1124,7 +2215,20 @@ var FacilitatorRegistry = class {
1124
2215
  constructor(selection) {
1125
2216
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
1126
2217
  this.registerFactory("tempo", () => new TempoFacilitator());
1127
- this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
2218
+ this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
2219
+ this.registerFactory("solana", (config) => {
2220
+ let feePayerKeypair;
2221
+ const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
2222
+ if (feePayerKey) {
2223
+ try {
2224
+ feePayerKeypair = Keypair4.fromSecretKey(bs582.decode(feePayerKey));
2225
+ } catch (e) {
2226
+ console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
2227
+ }
2228
+ }
2229
+ return new SolanaFacilitator({ feePayerKeypair });
2230
+ });
2231
+ this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
1128
2232
  }
1129
2233
  /**
1130
2234
  * Register a new facilitator factory
@@ -1371,14 +2475,40 @@ var TOKEN_ADDRESSES = {
1371
2475
  // pathUSD
1372
2476
  USDT: "0x20c0000000000000000000000000000000000001"
1373
2477
  // alphaUSD
2478
+ },
2479
+ // BNB Smart Chain mainnet
2480
+ "eip155:56": {
2481
+ USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
2482
+ USDT: "0x55d398326f99059fF775485246999027B3197955"
2483
+ },
2484
+ // BNB Smart Chain testnet
2485
+ "eip155:97": {
2486
+ USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
2487
+ USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
2488
+ },
2489
+ // Solana networks use mint addresses (SPL tokens)
2490
+ "solana:mainnet": {
2491
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
2492
+ // Circle USDC
2493
+ },
2494
+ "solana:devnet": {
2495
+ USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
2496
+ // Devnet USDC
1374
2497
  }
1375
2498
  };
1376
2499
  var CHAIN_TO_NETWORK = {
1377
2500
  "base": "eip155:8453",
1378
2501
  "base_sepolia": "eip155:84532",
1379
2502
  "polygon": "eip155:137",
1380
- "tempo_moderato": "eip155:42431"
2503
+ "tempo_moderato": "eip155:42431",
2504
+ "bnb": "eip155:56",
2505
+ "bnb_testnet": "eip155:97",
2506
+ "solana": "solana:mainnet",
2507
+ "solana_devnet": "solana:devnet"
1381
2508
  };
2509
+ function isSolanaNetwork(network) {
2510
+ return network.startsWith("solana:");
2511
+ }
1382
2512
  var TOKEN_DOMAINS = {
1383
2513
  // Base mainnet
1384
2514
  "eip155:8453": {
@@ -1400,6 +2530,16 @@ var TOKEN_DOMAINS = {
1400
2530
  "eip155:42431": {
1401
2531
  USDC: { name: "pathUSD", version: "1" },
1402
2532
  USDT: { name: "alphaUSD", version: "1" }
2533
+ },
2534
+ // BNB Smart Chain mainnet
2535
+ "eip155:56": {
2536
+ USDC: { name: "USD Coin", version: "1" },
2537
+ USDT: { name: "Tether USD", version: "1" }
2538
+ },
2539
+ // BNB Smart Chain testnet
2540
+ "eip155:97": {
2541
+ USDC: { name: "USD Coin", version: "1" },
2542
+ USDT: { name: "Tether USD", version: "1" }
1403
2543
  }
1404
2544
  };
1405
2545
  function getTokenDomain(network, token) {
@@ -1415,9 +2555,9 @@ function loadEnvFile2() {
1415
2555
  path3.join(process.env.HOME || "", ".moltspay", ".env")
1416
2556
  ];
1417
2557
  for (const envPath of envPaths) {
1418
- if (existsSync3(envPath)) {
2558
+ if (existsSync4(envPath)) {
1419
2559
  try {
1420
- const content = readFileSync3(envPath, "utf-8");
2560
+ const content = readFileSync4(envPath, "utf-8");
1421
2561
  for (const line of content.split("\n")) {
1422
2562
  const trimmed = line.trim();
1423
2563
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -1448,7 +2588,7 @@ var MoltsPayServer = class {
1448
2588
  useMainnet;
1449
2589
  constructor(servicesPath, options = {}) {
1450
2590
  loadEnvFile2();
1451
- const content = readFileSync3(servicesPath, "utf-8");
2591
+ const content = readFileSync4(servicesPath, "utf-8");
1452
2592
  this.manifest = JSON.parse(content);
1453
2593
  this.options = {
1454
2594
  port: options.port || 3e3,
@@ -1457,7 +2597,7 @@ var MoltsPayServer = class {
1457
2597
  };
1458
2598
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
1459
2599
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
1460
- const defaultFallback = ["tempo"];
2600
+ const defaultFallback = ["tempo", "bnb", "solana"];
1461
2601
  const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
1462
2602
  const facilitatorConfig = options.facilitators || {
1463
2603
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
@@ -1500,12 +2640,20 @@ var MoltsPayServer = class {
1500
2640
  */
1501
2641
  getProviderChains() {
1502
2642
  const provider = this.manifest.provider;
2643
+ const getWalletForChain = (chainName, explicitWallet) => {
2644
+ if (explicitWallet) return explicitWallet;
2645
+ if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
2646
+ return provider.solana_wallet;
2647
+ }
2648
+ return provider.wallet;
2649
+ };
1503
2650
  if (provider.chains && provider.chains.length > 0) {
1504
2651
  return provider.chains.map((c) => {
1505
2652
  const chainName = typeof c === "string" ? c : c.chain;
2653
+ const explicitWallet = typeof c === "object" ? c.wallet : null;
1506
2654
  return {
1507
2655
  network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
1508
- wallet: (typeof c === "object" ? c.wallet : null) || provider.wallet,
2656
+ wallet: getWalletForChain(chainName, explicitWallet || void 0),
1509
2657
  tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
1510
2658
  };
1511
2659
  });
@@ -1514,7 +2662,7 @@ var MoltsPayServer = class {
1514
2662
  const network = CHAIN_TO_NETWORK[chain] || this.networkId;
1515
2663
  return [{
1516
2664
  network,
1517
- wallet: provider.wallet,
2665
+ wallet: getWalletForChain(chain),
1518
2666
  tokens: ["USDC"]
1519
2667
  }];
1520
2668
  }
@@ -1585,7 +2733,8 @@ var MoltsPayServer = class {
1585
2733
  }
1586
2734
  const body = await this.readBody(req);
1587
2735
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1588
- return await this.handleProxy(body, paymentHeader, res);
2736
+ const authHeader = req.headers[MPP_AUTH_HEADER];
2737
+ return await this.handleProxy(body, paymentHeader, authHeader, res);
1589
2738
  }
1590
2739
  const servicePath = url.pathname.replace(/^\//, "");
1591
2740
  const skill = this.skills.get(servicePath);
@@ -1622,7 +2771,9 @@ var MoltsPayServer = class {
1622
2771
  name: this.manifest.provider.name,
1623
2772
  description: this.manifest.provider.description,
1624
2773
  wallet: this.manifest.provider.wallet,
1625
- chain: this.manifest.provider.chain || "base"
2774
+ chain: this.manifest.provider.chain || "base",
2775
+ solana_wallet: this.manifest.provider.solana_wallet,
2776
+ chains: this.manifest.provider.chains
1626
2777
  },
1627
2778
  services,
1628
2779
  endpoints: {
@@ -1735,6 +2886,21 @@ var MoltsPayServer = class {
1735
2886
  });
1736
2887
  }
1737
2888
  console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
2889
+ const isSolana = isSolanaNetwork(paymentNetwork);
2890
+ let settlement = null;
2891
+ if (isSolana) {
2892
+ console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
2893
+ try {
2894
+ settlement = await this.registry.settle(payment, requirements);
2895
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2896
+ } catch (err) {
2897
+ console.error("[MoltsPay] Solana settlement failed:", err.message);
2898
+ return this.sendJson(res, 402, {
2899
+ error: "Payment settlement failed",
2900
+ message: err.message
2901
+ });
2902
+ }
2903
+ }
1738
2904
  const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
1739
2905
  console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
1740
2906
  let result;
@@ -1749,16 +2915,19 @@ var MoltsPayServer = class {
1749
2915
  console.error("[MoltsPay] Skill execution failed:", err.message);
1750
2916
  return this.sendJson(res, 500, {
1751
2917
  error: "Service execution failed",
1752
- message: err.message
2918
+ message: err.message,
2919
+ paymentSettled: isSolana ? true : false,
2920
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
1753
2921
  });
1754
2922
  }
1755
- console.log(`[MoltsPay] Skill succeeded, settling payment...`);
1756
- let settlement = null;
1757
- try {
1758
- settlement = await this.registry.settle(payment, requirements);
1759
- console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
1760
- } catch (err) {
1761
- console.error("[MoltsPay] Settlement failed:", err.message);
2923
+ if (!isSolana) {
2924
+ console.log(`[MoltsPay] Skill succeeded, settling payment...`);
2925
+ try {
2926
+ settlement = await this.registry.settle(payment, requirements);
2927
+ console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2928
+ } catch (err) {
2929
+ console.error("[MoltsPay] Settlement failed:", err.message);
2930
+ }
1762
2931
  }
1763
2932
  const responseHeaders = {};
1764
2933
  if (settlement?.success) {
@@ -2034,7 +3203,7 @@ var MoltsPayServer = class {
2034
3203
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
2035
3204
  const tokenAddress = tokenAddresses[selectedToken];
2036
3205
  const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
2037
- return {
3206
+ const requirements = {
2038
3207
  scheme: "exact",
2039
3208
  network: selectedNetwork,
2040
3209
  asset: tokenAddress,
@@ -2043,6 +3212,27 @@ var MoltsPayServer = class {
2043
3212
  maxTimeoutSeconds: 300,
2044
3213
  extra: tokenDomain
2045
3214
  };
3215
+ if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
3216
+ const solanaFacilitator = this.registry.get("solana");
3217
+ const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
3218
+ if (feePayerPubkey) {
3219
+ requirements.extra = {
3220
+ ...requirements.extra || {},
3221
+ solanaFeePayer: feePayerPubkey
3222
+ };
3223
+ }
3224
+ }
3225
+ if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
3226
+ const bnbFacilitator = this.registry.get("bnb");
3227
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
3228
+ if (spenderAddress) {
3229
+ requirements.extra = {
3230
+ ...requirements.extra || {},
3231
+ bnbSpender: spenderAddress
3232
+ };
3233
+ }
3234
+ }
3235
+ return requirements;
2046
3236
  }
2047
3237
  /**
2048
3238
  * Detect which token is being used in the payment
@@ -2095,8 +3285,10 @@ var MoltsPayServer = class {
2095
3285
  isProxyAllowed(clientIP) {
2096
3286
  const allowedIPs = process.env.PROXY_ALLOWED_IPS?.split(",").map((ip) => ip.trim()) || [];
2097
3287
  if (allowedIPs.length === 0) {
2098
- console.log(`[MoltsPay] /proxy denied: no PROXY_ALLOWED_IPS configured`);
2099
- return false;
3288
+ return true;
3289
+ }
3290
+ if (allowedIPs.includes("*")) {
3291
+ return true;
2100
3292
  }
2101
3293
  const normalizedIP = clientIP === "::1" ? "127.0.0.1" : clientIP.replace("::ffff:", "");
2102
3294
  const allowed = allowedIPs.includes(normalizedIP) || allowedIPs.includes(clientIP);
@@ -2108,31 +3300,42 @@ var MoltsPayServer = class {
2108
3300
  /**
2109
3301
  * POST /proxy - Handle payment for external services (moltspay-creators)
2110
3302
  *
2111
- * This endpoint allows other services to delegate x402 payment handling.
3303
+ * This endpoint allows other services to delegate x402/MPP payment handling.
2112
3304
  * It does NOT execute any skill - just handles payment verification/settlement.
2113
3305
  *
2114
3306
  * Request body:
2115
3307
  * { wallet, amount, currency, chain, memo, serviceId, description }
2116
3308
  *
2117
- * Without X-Payment header: returns 402 with payment requirements
2118
- * With X-Payment header: verifies payment and returns result
3309
+ * For x402 (base, polygon, base_sepolia):
3310
+ * Without X-Payment header: returns 402 with X-Payment-Required
3311
+ * With X-Payment header: verifies payment via CDP
3312
+ *
3313
+ * For MPP (tempo_moderato):
3314
+ * Without Authorization header: returns 402 with WWW-Authenticate
3315
+ * With Authorization: Payment header: verifies tx on Tempo chain
2119
3316
  */
2120
- async handleProxy(body, paymentHeader, res) {
3317
+ async handleProxy(body, paymentHeader, authHeader, res) {
2121
3318
  const { wallet, amount, currency, chain, memo, serviceId, description } = body;
2122
3319
  if (!wallet || !amount) {
2123
3320
  return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
2124
3321
  }
2125
- if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) {
2126
- return this.sendJson(res, 400, { error: "Invalid wallet address format" });
3322
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
3323
+ if (chain && !supportedChains.includes(chain)) {
3324
+ return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
3325
+ }
3326
+ const isSolanaChain = chain === "solana" || chain === "solana_devnet";
3327
+ const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
3328
+ const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
3329
+ if (isSolanaChain && !isValidSolanaAddress2) {
3330
+ return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
3331
+ }
3332
+ if (!isSolanaChain && !isValidEvmAddress) {
3333
+ return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
2127
3334
  }
2128
3335
  const amountNum = parseFloat(amount);
2129
3336
  if (isNaN(amountNum) || amountNum <= 0) {
2130
3337
  return this.sendJson(res, 400, { error: "Invalid amount" });
2131
3338
  }
2132
- const supportedChains = ["base", "polygon", "base_sepolia"];
2133
- if (chain && !supportedChains.includes(chain)) {
2134
- return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
2135
- }
2136
3339
  const proxyConfig = {
2137
3340
  id: serviceId || "proxy",
2138
3341
  name: description || "Proxy Payment",
@@ -2144,6 +3347,9 @@ var MoltsPayServer = class {
2144
3347
  input: {},
2145
3348
  output: {}
2146
3349
  };
3350
+ if (chain === "tempo_moderato") {
3351
+ return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
3352
+ }
2147
3353
  const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
2148
3354
  if (!paymentHeader) {
2149
3355
  return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
@@ -2155,37 +3361,225 @@ var MoltsPayServer = class {
2155
3361
  } catch {
2156
3362
  return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
2157
3363
  }
2158
- if (payment.x402Version !== X402_VERSION3) {
2159
- return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
3364
+ if (payment.x402Version !== X402_VERSION3) {
3365
+ return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
3366
+ }
3367
+ const scheme = payment.accepted?.scheme || payment.scheme;
3368
+ const network = payment.accepted?.network || payment.network;
3369
+ if (scheme !== "exact") {
3370
+ return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
3371
+ }
3372
+ const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
3373
+ if (network !== expectedNetwork) {
3374
+ return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
3375
+ }
3376
+ console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
3377
+ const verifyResult = await this.registry.verify(payment, requirements);
3378
+ if (!verifyResult.valid) {
3379
+ return this.sendJson(res, 402, {
3380
+ success: false,
3381
+ error: `Payment verification failed: ${verifyResult.error}`,
3382
+ facilitator: verifyResult.facilitator
3383
+ });
3384
+ }
3385
+ console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
3386
+ const { execute, service, params } = body;
3387
+ if (execute && service) {
3388
+ const skill = this.skills.get(service);
3389
+ if (!skill) {
3390
+ console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
3391
+ return this.sendJson(res, 404, {
3392
+ success: false,
3393
+ paymentSettled: false,
3394
+ error: `Service not found: ${service}`
3395
+ });
3396
+ }
3397
+ const isSolana = isSolanaNetwork(network);
3398
+ let settlement2 = null;
3399
+ if (isSolana) {
3400
+ console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
3401
+ try {
3402
+ settlement2 = await this.registry.settle(payment, requirements);
3403
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
3404
+ if (!settlement2.success) {
3405
+ console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
3406
+ return this.sendJson(res, 402, {
3407
+ success: false,
3408
+ paymentSettled: false,
3409
+ error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
3410
+ });
3411
+ }
3412
+ } catch (err) {
3413
+ console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
3414
+ return this.sendJson(res, 402, {
3415
+ success: false,
3416
+ paymentSettled: false,
3417
+ error: `Payment settlement failed: ${err.message}`
3418
+ });
3419
+ }
3420
+ } else {
3421
+ console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
3422
+ }
3423
+ const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
3424
+ let result;
3425
+ try {
3426
+ result = await Promise.race([
3427
+ skill.handler(params || {}),
3428
+ new Promise(
3429
+ (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
3430
+ )
3431
+ ]);
3432
+ console.log(`[MoltsPay] /proxy: Skill succeeded`);
3433
+ } catch (err) {
3434
+ console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
3435
+ return this.sendJson(res, 500, {
3436
+ success: false,
3437
+ paymentSettled: isSolana ? true : false,
3438
+ error: `Service execution failed: ${err.message}`,
3439
+ note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
3440
+ });
3441
+ }
3442
+ if (!isSolana) {
3443
+ console.log(`[MoltsPay] /proxy: Settling payment...`);
3444
+ try {
3445
+ settlement2 = await this.registry.settle(payment, requirements);
3446
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
3447
+ } catch (err) {
3448
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
3449
+ return this.sendJson(res, 200, {
3450
+ success: true,
3451
+ verified: true,
3452
+ settled: false,
3453
+ settlementError: err.message,
3454
+ from: payment.payload?.authorization?.from,
3455
+ paidTo: wallet,
3456
+ amount: amountNum,
3457
+ currency: currency || "USDC",
3458
+ memo,
3459
+ result
3460
+ });
3461
+ }
3462
+ }
3463
+ return this.sendJson(res, 200, {
3464
+ success: true,
3465
+ verified: true,
3466
+ settled: settlement2?.success || false,
3467
+ txHash: settlement2?.transaction,
3468
+ from: payment.payload?.authorization?.from,
3469
+ paidTo: wallet,
3470
+ amount: amountNum,
3471
+ currency: currency || "USDC",
3472
+ facilitator: settlement2?.facilitator,
3473
+ memo,
3474
+ result
3475
+ });
3476
+ }
3477
+ console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
3478
+ let settlement = null;
3479
+ try {
3480
+ settlement = await this.registry.settle(payment, requirements);
3481
+ console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
3482
+ } catch (err) {
3483
+ console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
3484
+ return this.sendJson(res, 500, {
3485
+ success: false,
3486
+ error: `Settlement failed: ${err.message}`
3487
+ });
3488
+ }
3489
+ this.sendJson(res, 200, {
3490
+ success: true,
3491
+ verified: true,
3492
+ settled: settlement?.success || false,
3493
+ txHash: settlement?.transaction,
3494
+ from: payment.payload?.authorization?.from,
3495
+ // Buyer's wallet address
3496
+ paidTo: wallet,
3497
+ amount: amountNum,
3498
+ currency: currency || "USDC",
3499
+ facilitator: settlement?.facilitator,
3500
+ memo
3501
+ });
3502
+ }
3503
+ /**
3504
+ * Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
3505
+ */
3506
+ async handleProxyMPP(body, config, authHeader, res) {
3507
+ const { wallet, amount, memo, serviceId } = body;
3508
+ const amountNum = parseFloat(amount);
3509
+ const amountInUnits = Math.floor(amountNum * 1e6).toString();
3510
+ if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
3511
+ const challengeId = this.generateChallengeId();
3512
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
3513
+ const mppRequest = {
3514
+ amount: amountInUnits,
3515
+ currency: tokenAddress,
3516
+ methodDetails: {
3517
+ chainId: 42431,
3518
+ feePayer: true
3519
+ },
3520
+ recipient: wallet
3521
+ };
3522
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
3523
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
3524
+ const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
3525
+ res.writeHead(402, {
3526
+ "Content-Type": "application/problem+json",
3527
+ [MPP_WWW_AUTH_HEADER]: wwwAuth
3528
+ });
3529
+ res.end(JSON.stringify({
3530
+ type: "https://paymentauth.org/problems/payment-required",
3531
+ title: "Payment Required",
3532
+ status: 402,
3533
+ detail: `Payment is required (${config.name}).`,
3534
+ service: serviceId || "proxy",
3535
+ price: amountNum,
3536
+ currency: "USDC"
3537
+ }, null, 2));
3538
+ return;
3539
+ }
3540
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
3541
+ if (!credentialMatch) {
3542
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
2160
3543
  }
2161
- const scheme = payment.accepted?.scheme || payment.scheme;
2162
- const network = payment.accepted?.network || payment.network;
2163
- if (scheme !== "exact") {
2164
- return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
3544
+ let mppCredential;
3545
+ try {
3546
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
3547
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
3548
+ mppCredential = JSON.parse(decoded);
3549
+ } catch (err) {
3550
+ console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
3551
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
2165
3552
  }
2166
- const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
2167
- if (network !== expectedNetwork) {
2168
- return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
3553
+ let txHash;
3554
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
3555
+ txHash = mppCredential.payload.hash;
3556
+ } else {
3557
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
2169
3558
  }
2170
- console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
2171
- const verifyResult = await this.registry.verify(payment, requirements);
2172
- if (!verifyResult.valid) {
3559
+ console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
3560
+ const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
3561
+ const paymentPayload = {
3562
+ x402Version: X402_VERSION3,
3563
+ scheme: "exact",
3564
+ network: "eip155:42431",
3565
+ payload: { txHash, chainId: 42431 }
3566
+ };
3567
+ const verification = await this.registry.verify(paymentPayload, requirements);
3568
+ if (!verification.valid) {
2173
3569
  return this.sendJson(res, 402, {
2174
- success: false,
2175
- error: `Payment verification failed: ${verifyResult.error}`,
2176
- facilitator: verifyResult.facilitator
3570
+ error: `Payment verification failed: ${verification.error}`
2177
3571
  });
2178
3572
  }
2179
- console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
3573
+ console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
2180
3574
  const { execute, service, params } = body;
2181
3575
  if (execute && service) {
2182
- console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
3576
+ console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
2183
3577
  const skill = this.skills.get(service);
2184
3578
  if (!skill) {
2185
- console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
2186
3579
  return this.sendJson(res, 404, {
2187
3580
  success: false,
2188
- paymentSettled: false,
3581
+ paymentSettled: true,
3582
+ // Payment already happened on Tempo
2189
3583
  error: `Service not found: ${service}`
2190
3584
  });
2191
3585
  }
@@ -2198,73 +3592,36 @@ var MoltsPayServer = class {
2198
3592
  (_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
2199
3593
  )
2200
3594
  ]);
2201
- console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
2202
3595
  } catch (err) {
2203
- console.error(`[MoltsPay] /proxy: Skill failed: ${err.message} - NOT settling`);
3596
+ console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
2204
3597
  return this.sendJson(res, 500, {
2205
3598
  success: false,
2206
- paymentSettled: false,
3599
+ paymentSettled: true,
2207
3600
  error: `Service execution failed: ${err.message}`
2208
3601
  });
2209
3602
  }
2210
- let settlement2 = null;
2211
- try {
2212
- settlement2 = await this.registry.settle(payment, requirements);
2213
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
2214
- } catch (err) {
2215
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2216
- return this.sendJson(res, 200, {
2217
- success: true,
2218
- verified: true,
2219
- settled: false,
2220
- settlementError: err.message,
2221
- from: payment.payload?.authorization?.from,
2222
- // Buyer's wallet address
2223
- paidTo: wallet,
2224
- amount: amountNum,
2225
- currency: currency || "USDC",
2226
- memo,
2227
- result
2228
- });
2229
- }
2230
3603
  return this.sendJson(res, 200, {
2231
3604
  success: true,
2232
3605
  verified: true,
2233
- settled: settlement2?.success || false,
2234
- txHash: settlement2?.transaction,
2235
- from: payment.payload?.authorization?.from,
2236
- // Buyer's wallet address
3606
+ txHash,
3607
+ chain: "tempo_moderato",
2237
3608
  paidTo: wallet,
2238
3609
  amount: amountNum,
2239
- currency: currency || "USDC",
2240
- facilitator: settlement2?.facilitator,
3610
+ currency: "USDC",
3611
+ facilitator: verification.facilitator,
2241
3612
  memo,
2242
3613
  result
2243
3614
  });
2244
3615
  }
2245
- console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
2246
- let settlement = null;
2247
- try {
2248
- settlement = await this.registry.settle(payment, requirements);
2249
- console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
2250
- } catch (err) {
2251
- console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
2252
- return this.sendJson(res, 500, {
2253
- success: false,
2254
- error: `Settlement failed: ${err.message}`
2255
- });
2256
- }
2257
3616
  this.sendJson(res, 200, {
2258
3617
  success: true,
2259
3618
  verified: true,
2260
- settled: settlement?.success || false,
2261
- txHash: settlement?.transaction,
2262
- from: payment.payload?.authorization?.from,
2263
- // Buyer's wallet address
3619
+ txHash,
3620
+ chain: "tempo_moderato",
2264
3621
  paidTo: wallet,
2265
3622
  amount: amountNum,
2266
- currency: currency || "USDC",
2267
- facilitator: settlement?.facilitator,
3623
+ currency: "USDC",
3624
+ facilitator: verification.facilitator,
2268
3625
  memo
2269
3626
  });
2270
3627
  }
@@ -2279,7 +3636,7 @@ var MoltsPayServer = class {
2279
3636
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
2280
3637
  const tokenAddress = tokenAddresses[selectedToken];
2281
3638
  const tokenDomain = getTokenDomain(networkId, selectedToken);
2282
- return {
3639
+ const requirements = {
2283
3640
  scheme: "exact",
2284
3641
  network: networkId,
2285
3642
  asset: tokenAddress,
@@ -2289,6 +3646,17 @@ var MoltsPayServer = class {
2289
3646
  maxTimeoutSeconds: 300,
2290
3647
  extra: tokenDomain
2291
3648
  };
3649
+ if (networkId === "eip155:56" || networkId === "eip155:97") {
3650
+ const bnbFacilitator = this.registry.get("bnb");
3651
+ const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
3652
+ if (spenderAddress) {
3653
+ requirements.extra = {
3654
+ ...requirements.extra || {},
3655
+ bnbSpender: spenderAddress
3656
+ };
3657
+ }
3658
+ }
3659
+ return requirements;
2292
3660
  }
2293
3661
  /**
2294
3662
  * Return 402 with x402 payment requirements for proxy endpoint
@@ -2338,14 +3706,14 @@ if (!globalThis.crypto) {
2338
3706
  }
2339
3707
  function getVersion() {
2340
3708
  const locations = [
2341
- join4(__dirname, "../../package.json"),
2342
- join4(__dirname, "../package.json"),
2343
- join4(process.cwd(), "node_modules/moltspay/package.json")
3709
+ join5(__dirname, "../../package.json"),
3710
+ join5(__dirname, "../package.json"),
3711
+ join5(process.cwd(), "node_modules/moltspay/package.json")
2344
3712
  ];
2345
3713
  for (const loc of locations) {
2346
3714
  try {
2347
- if (existsSync4(loc)) {
2348
- const pkg = JSON.parse(readFileSync4(loc, "utf-8"));
3715
+ if (existsSync5(loc)) {
3716
+ const pkg = JSON.parse(readFileSync5(loc, "utf-8"));
2349
3717
  if (pkg.name === "moltspay") return pkg.version;
2350
3718
  }
2351
3719
  } catch {
@@ -2353,11 +3721,94 @@ function getVersion() {
2353
3721
  }
2354
3722
  return "0.0.0";
2355
3723
  }
3724
+ var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
3725
+ var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
3726
+ var ERC20_APPROVE_ABI = [
3727
+ "function approve(address spender, uint256 amount) returns (bool)",
3728
+ "function allowance(address owner, address spender) view returns (uint256)"
3729
+ ];
3730
+ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
3731
+ const chainConfig = CHAINS[chain];
3732
+ const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
3733
+ const wallet = client.getWallet();
3734
+ if (!wallet) {
3735
+ console.log(" \u274C No wallet found");
3736
+ return;
3737
+ }
3738
+ const signer = wallet.connect(provider);
3739
+ console.log(` Spender: ${spenderAddress}`);
3740
+ let bnbBalance = await provider.getBalance(wallet.address);
3741
+ const minGasRequired = ethers2.parseEther("0.0005");
3742
+ if (bnbBalance < minGasRequired) {
3743
+ if (sponsorGas && BNB_SPONSOR_KEY) {
3744
+ console.log(" \u23F3 Sponsoring BNB gas for approvals...");
3745
+ try {
3746
+ const sponsorWallet = new ethers2.Wallet(BNB_SPONSOR_KEY, provider);
3747
+ const tx = await sponsorWallet.sendTransaction({
3748
+ to: wallet.address,
3749
+ value: ethers2.parseEther("0.001")
3750
+ });
3751
+ await tx.wait();
3752
+ console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
3753
+ bnbBalance = await provider.getBalance(wallet.address);
3754
+ } catch (err) {
3755
+ console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
3756
+ console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
3757
+ return;
3758
+ }
3759
+ } else {
3760
+ console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
3761
+ console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
3762
+ console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
3763
+ return;
3764
+ }
3765
+ }
3766
+ for (const tokenSymbol of ["USDT", "USDC"]) {
3767
+ const tokenConfig = chainConfig.tokens[tokenSymbol];
3768
+ const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
3769
+ const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
3770
+ if (allowance > 0n) {
3771
+ console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
3772
+ continue;
3773
+ }
3774
+ console.log(` \u23F3 Approving ${tokenSymbol}...`);
3775
+ try {
3776
+ const tx = await tokenContract.approve(spenderAddress, ethers2.MaxUint256);
3777
+ await tx.wait();
3778
+ console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
3779
+ } catch (err) {
3780
+ console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
3781
+ }
3782
+ }
3783
+ console.log("");
3784
+ }
3785
+ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
3786
+ const chainConfig = CHAINS[chain];
3787
+ const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
3788
+ let spenderAddress = null;
3789
+ try {
3790
+ const walletPath = join5(configDir, "wallet.json");
3791
+ const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
3792
+ spenderAddress = walletData.approvals?.[chain] || null;
3793
+ } catch {
3794
+ }
3795
+ const result = { usdt: false, usdc: false, spender: spenderAddress };
3796
+ if (!spenderAddress) {
3797
+ return result;
3798
+ }
3799
+ for (const tokenSymbol of ["USDT", "USDC"]) {
3800
+ const tokenConfig = chainConfig.tokens[tokenSymbol];
3801
+ const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
3802
+ const allowance = await tokenContract.allowance(address, spenderAddress);
3803
+ result[tokenSymbol.toLowerCase()] = allowance > 0n;
3804
+ }
3805
+ return result;
3806
+ }
2356
3807
  var program = new Command();
2357
- var DEFAULT_CONFIG_DIR = join4(homedir2(), ".moltspay");
2358
- var PID_FILE = join4(DEFAULT_CONFIG_DIR, "server.pid");
2359
- if (!existsSync4(DEFAULT_CONFIG_DIR)) {
2360
- mkdirSync2(DEFAULT_CONFIG_DIR, { recursive: true });
3808
+ var DEFAULT_CONFIG_DIR2 = join5(homedir3(), ".moltspay");
3809
+ var PID_FILE = join5(DEFAULT_CONFIG_DIR2, "server.pid");
3810
+ if (!existsSync5(DEFAULT_CONFIG_DIR2)) {
3811
+ mkdirSync3(DEFAULT_CONFIG_DIR2, { recursive: true });
2361
3812
  }
2362
3813
  function prompt(question) {
2363
3814
  const rl = readline.createInterface({
@@ -2372,19 +3823,49 @@ function prompt(question) {
2372
3823
  });
2373
3824
  }
2374
3825
  program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
2375
- program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
2376
- console.log("\n\u{1F510} MoltsPay Client Setup\n");
2377
- if (existsSync4(join4(options.configDir, "wallet.json"))) {
2378
- console.log('\u26A0\uFE0F Already initialized. Use "moltspay config" to update settings.');
2379
- console.log(` Config dir: ${options.configDir}`);
2380
- return;
2381
- }
3826
+ program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2382
3827
  let chain = options.chain;
2383
- const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
3828
+ const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
3829
+ const supportedSolanaChains = ["solana", "solana_devnet"];
3830
+ const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
2384
3831
  if (!supportedChains.includes(chain)) {
2385
3832
  console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
2386
3833
  process.exit(1);
2387
3834
  }
3835
+ if (supportedSolanaChains.includes(chain)) {
3836
+ console.log("\n\u{1F7E3} Solana Wallet Setup\n");
3837
+ if (solanaWalletExists(options.configDir)) {
3838
+ const existingAddress = getSolanaAddress(options.configDir);
3839
+ console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
3840
+ console.log(` Config dir: ${options.configDir}`);
3841
+ return;
3842
+ }
3843
+ console.log("Creating Solana wallet...");
3844
+ const keypair = createSolanaWallet(options.configDir);
3845
+ const address = keypair.publicKey.toBase58();
3846
+ console.log(`
3847
+ \u2705 Solana wallet created: ${address}`);
3848
+ console.log(`
3849
+ \u{1F4C1} Config saved to: ${join5(options.configDir, "wallet-solana.json")}`);
3850
+ console.log(`
3851
+ \u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
3852
+ console.log(` This file contains your private key!
3853
+ `);
3854
+ if (chain === "solana_devnet") {
3855
+ console.log("\u{1F4A1} Get testnet tokens:");
3856
+ console.log(" npx moltspay faucet --chain solana_devnet\n");
3857
+ } else {
3858
+ console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
3859
+ `);
3860
+ }
3861
+ return;
3862
+ }
3863
+ console.log("\n\u{1F510} MoltsPay Client Setup\n");
3864
+ if (existsSync5(join5(options.configDir, "wallet.json"))) {
3865
+ console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
3866
+ console.log(` Config dir: ${options.configDir}`);
3867
+ return;
3868
+ }
2388
3869
  let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
2389
3870
  let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
2390
3871
  if (!maxPerTx) {
@@ -2406,13 +3887,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
2406
3887
  console.log(`
2407
3888
  \u{1F4C1} Config saved to: ${result.configDir}`);
2408
3889
  console.log(`
2409
- \u26A0\uFE0F IMPORTANT: Back up ${join4(result.configDir, "wallet.json")}`);
3890
+ \u26A0\uFE0F IMPORTANT: Back up ${join5(result.configDir, "wallet.json")}`);
2410
3891
  console.log(` This file contains your private key!
2411
3892
  `);
3893
+ if (chain === "bnb" || chain === "bnb_testnet") {
3894
+ console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
3895
+ console.log(" \u2139\uFE0F Using default spender. For other services, run:");
3896
+ console.log(` npx moltspay approve --chain ${chain} --spender <address>
3897
+ `);
3898
+ const client = new MoltsPayClient({ configDir: options.configDir });
3899
+ await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
3900
+ }
2412
3901
  console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
2413
3902
  `);
2414
3903
  });
2415
- program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
3904
+ program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2416
3905
  const client = new MoltsPayClient({ configDir: options.configDir });
2417
3906
  if (!client.isInitialized) {
2418
3907
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -2447,25 +3936,40 @@ program.command("config").description("Update MoltsPay settings").option("--max-
2447
3936
  }
2448
3937
  }
2449
3938
  });
2450
- program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, or base_sepolia)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (amountStr, options) => {
3939
+ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, solana, base_sepolia, bnb, or bnb_testnet)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (amountStr, options) => {
2451
3940
  const client = new MoltsPayClient({ configDir: options.configDir });
2452
- if (!client.isInitialized) {
2453
- console.log("\u274C Not initialized. Run: npx moltspay init");
2454
- return;
2455
- }
2456
3941
  const amount = parseFloat(amountStr);
2457
3942
  if (isNaN(amount) || amount < 5) {
2458
3943
  console.log("\u274C Minimum $5.");
2459
3944
  return;
2460
3945
  }
2461
3946
  const chain = options.chain?.toLowerCase() || "base";
2462
- if (!["base", "polygon", "base_sepolia"].includes(chain)) {
2463
- console.log("\u274C Invalid chain. Use: base, polygon, or base_sepolia");
3947
+ if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
3948
+ console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
2464
3949
  return;
2465
3950
  }
3951
+ let walletAddress;
3952
+ if (chain === "solana") {
3953
+ const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
3954
+ if (!solanaWallet) {
3955
+ console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
3956
+ return;
3957
+ }
3958
+ walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
3959
+ if (!walletAddress) {
3960
+ console.log("\u274C Could not get Solana wallet address.");
3961
+ return;
3962
+ }
3963
+ } else {
3964
+ if (!client.isInitialized) {
3965
+ console.log("\u274C Not initialized. Run: npx moltspay init");
3966
+ return;
3967
+ }
3968
+ walletAddress = client.address;
3969
+ }
2466
3970
  if (chain === "base_sepolia") {
2467
3971
  console.log("\n\u{1F9EA} Testnet Funding\n");
2468
- console.log(` Wallet: ${client.address}`);
3972
+ console.log(` Wallet: ${walletAddress}`);
2469
3973
  console.log(` Chain: Base Sepolia (testnet)
2470
3974
  `);
2471
3975
  console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
@@ -2473,9 +3977,36 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
2473
3977
  console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
2474
3978
  return;
2475
3979
  }
3980
+ if (chain === "bnb_testnet") {
3981
+ console.log("\n\u{1F9EA} BNB Testnet Funding\n");
3982
+ console.log(` Wallet: ${walletAddress}`);
3983
+ console.log(` Chain: BNB Testnet
3984
+ `);
3985
+ console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
3986
+ console.log(" npx moltspay faucet --chain bnb_testnet\n");
3987
+ console.log(" This gives you:\n");
3988
+ console.log(" \u2022 1 USDC (testnet) for payments");
3989
+ console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
3990
+ return;
3991
+ }
3992
+ if (chain === "bnb") {
3993
+ console.log("\n\u{1F4CB} BNB Chain Funding\n");
3994
+ console.log(` Wallet: ${walletAddress}
3995
+ `);
3996
+ console.log(" To use MoltsPay on BNB Chain, you need:\n");
3997
+ console.log(" 1. USDC for payments");
3998
+ console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
3999
+ console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
4000
+ console.log(" \u2192 First approval transaction requires gas");
4001
+ console.log(" \u2192 After approval, all payments are gasless\n");
4002
+ console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
4003
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4004
+ console.log(" After funding, check status: npx moltspay status\n");
4005
+ return;
4006
+ }
2476
4007
  console.log("\n\u{1F4B3} Fund your agent wallet\n");
2477
- console.log(` Wallet: ${client.address}`);
2478
- console.log(` Chain: ${chain}`);
4008
+ console.log(` Wallet: ${walletAddress}`);
4009
+ console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
2479
4010
  console.log(` Amount: $${amount.toFixed(2)}
2480
4011
  `);
2481
4012
  try {
@@ -2484,7 +4015,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
2484
4015
  method: "POST",
2485
4016
  headers: { "Content-Type": "application/json" },
2486
4017
  body: JSON.stringify({
2487
- address: client.address,
4018
+ address: walletAddress,
2488
4019
  amount,
2489
4020
  chain
2490
4021
  })
@@ -2502,11 +4033,92 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
2502
4033
  console.log(`\u274C ${error.message}`);
2503
4034
  }
2504
4035
  });
2505
- program.command("faucet").description("Request testnet tokens from faucet (Base Sepolia or Tempo Moderato)").option("--chain <chain>", "Chain to get tokens on (base_sepolia or tempo_moderato)", "base_sepolia").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
4036
+ program.command("approve").description("Approve a spender address for BNB chain payments").requiredOption("--spender <address>", "Spender address to approve (from server 402 response)").option("--chain <chain>", "BNB chain (bnb or bnb_testnet)", "bnb_testnet").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
4037
+ const chain = options.chain;
4038
+ if (chain !== "bnb" && chain !== "bnb_testnet") {
4039
+ console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
4040
+ return;
4041
+ }
4042
+ if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
4043
+ console.log("\u274C Invalid spender address format");
4044
+ return;
4045
+ }
4046
+ const client = new MoltsPayClient({ configDir: options.configDir });
4047
+ if (!client.isInitialized) {
4048
+ console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
4049
+ return;
4050
+ }
4051
+ console.log(`
4052
+ \u{1F510} Approving spender for ${chain}...
4053
+ `);
4054
+ await setupBNBApprovals(client, chain, options.spender, false);
4055
+ const walletPath = join5(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
4056
+ try {
4057
+ const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
4058
+ walletData.approvals = walletData.approvals || {};
4059
+ walletData.approvals[chain] = options.spender;
4060
+ writeFileSync3(walletPath, JSON.stringify(walletData, null, 2));
4061
+ console.log(`\u2705 Approval complete! Spender saved for ${chain}.
4062
+ `);
4063
+ } catch (err) {
4064
+ console.log("\u2705 Approval complete!\n");
4065
+ console.log("\u26A0\uFE0F Could not save spender to wallet config");
4066
+ }
4067
+ });
4068
+ program.command("faucet").description("Request testnet tokens from faucet (Base Sepolia, Tempo Moderato, BNB Testnet, or Solana Devnet)").option("--chain <chain>", "Chain to get tokens on (base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet)", "base_sepolia").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2506
4069
  let address = options.address;
2507
4070
  const chain = options.chain?.toLowerCase() || "base_sepolia";
2508
- if (!["base_sepolia", "tempo_moderato"].includes(chain)) {
2509
- console.log("\u274C Invalid chain. Use: base_sepolia or tempo_moderato");
4071
+ if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
4072
+ console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
4073
+ return;
4074
+ }
4075
+ if (chain === "solana_devnet") {
4076
+ if (!address) {
4077
+ address = getSolanaAddress(options.configDir);
4078
+ if (!address) {
4079
+ console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
4080
+ return;
4081
+ }
4082
+ }
4083
+ if (!isValidSolanaAddress(address)) {
4084
+ console.log("\u274C Invalid Solana address");
4085
+ return;
4086
+ }
4087
+ console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
4088
+ console.log(` Address: ${address}
4089
+ `);
4090
+ let usdcSuccess = false;
4091
+ try {
4092
+ console.log(" \u23F3 Requesting 1 USDC from faucet...");
4093
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
4094
+ const response = await fetch(FAUCET_API, {
4095
+ method: "POST",
4096
+ headers: { "Content-Type": "application/json" },
4097
+ body: JSON.stringify({ address, chain: "solana_devnet" })
4098
+ });
4099
+ const result = await response.json();
4100
+ if (!response.ok) {
4101
+ console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
4102
+ if (result.hint) console.log(` ${result.hint}`);
4103
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
4104
+ } else {
4105
+ console.log(` \u2705 Received ${result.amount} USDC!`);
4106
+ console.log(` Transaction: ${result.explorer}`);
4107
+ if (result.faucet_balance) {
4108
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
4109
+ }
4110
+ usdcSuccess = true;
4111
+ }
4112
+ } catch (error) {
4113
+ console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
4114
+ }
4115
+ console.log("");
4116
+ if (usdcSuccess) {
4117
+ console.log("\u{1F4A1} Check your balance:");
4118
+ console.log(" npx moltspay status\n");
4119
+ } else {
4120
+ console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
4121
+ }
2510
4122
  return;
2511
4123
  }
2512
4124
  if (!address) {
@@ -2554,6 +4166,46 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
2554
4166
  console.log(`\u274C ${error.message}`);
2555
4167
  console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
2556
4168
  }
4169
+ } else if (chain === "bnb_testnet") {
4170
+ console.log(` Requesting 1 USDC on BNB Testnet...`);
4171
+ console.log(` Address: ${address}
4172
+ `);
4173
+ try {
4174
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
4175
+ const response = await fetch(FAUCET_API, {
4176
+ method: "POST",
4177
+ headers: { "Content-Type": "application/json" },
4178
+ body: JSON.stringify({ address, chain: "bnb_testnet" })
4179
+ });
4180
+ const result = await response.json();
4181
+ if (!response.ok) {
4182
+ console.log(`\u274C ${result.error || "Request failed"}`);
4183
+ if (result.hint) console.log(` ${result.hint}`);
4184
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
4185
+ console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
4186
+ console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
4187
+ console.log(` 2. Select "Peggy Tokens" -> USDC`);
4188
+ console.log(` 3. Enter: ${address}
4189
+ `);
4190
+ return;
4191
+ }
4192
+ console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
4193
+ `);
4194
+ console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
4195
+ if (result.faucet_balance) {
4196
+ console.log(` Faucet balance: ${result.faucet_balance} USDC`);
4197
+ }
4198
+ console.log("\n\u{1F4A1} Now you can test BNB payments:");
4199
+ console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
4200
+ `);
4201
+ } catch (error) {
4202
+ console.log(`\u274C ${error.message}`);
4203
+ console.log("\n\u{1F4A1} Get tokens manually:");
4204
+ console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
4205
+ console.log(` 2. Select "Peggy Tokens" -> USDC`);
4206
+ console.log(` 3. Enter: ${address}
4207
+ `);
4208
+ }
2557
4209
  } else {
2558
4210
  console.log(` Requesting 1 USDC on Base Sepolia...`);
2559
4211
  console.log(` Address: ${address}
@@ -2563,7 +4215,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
2563
4215
  const response = await fetch(FAUCET_API, {
2564
4216
  method: "POST",
2565
4217
  headers: { "Content-Type": "application/json" },
2566
- body: JSON.stringify({ address })
4218
+ body: JSON.stringify({ address, chain: "base_sepolia" })
2567
4219
  });
2568
4220
  const result = await response.json();
2569
4221
  if (!response.ok) {
@@ -2586,7 +4238,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
2586
4238
  }
2587
4239
  }
2588
4240
  });
2589
- program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).option("--json", "Output as JSON").action(async (options) => {
4241
+ program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).option("--json", "Output as JSON").action(async (options) => {
2590
4242
  const client = new MoltsPayClient({ configDir: options.configDir });
2591
4243
  if (!client.isInitialized) {
2592
4244
  if (options.json) {
@@ -2603,12 +4255,31 @@ program.command("status").description("Show wallet status and balance").option("
2603
4255
  } catch (err) {
2604
4256
  console.error("Warning: Could not fetch balances:", err.message);
2605
4257
  }
4258
+ const solanaAddress = getSolanaAddress(options.configDir);
4259
+ let solanaBalances = {};
4260
+ if (solanaAddress) {
4261
+ try {
4262
+ solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
4263
+ } catch {
4264
+ }
4265
+ try {
4266
+ solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
4267
+ } catch {
4268
+ }
4269
+ }
2606
4270
  if (options.json) {
2607
- console.log(JSON.stringify({
4271
+ const output = {
2608
4272
  address: client.address,
2609
4273
  balances: allBalances,
2610
4274
  limits: config.limits
2611
- }, null, 2));
4275
+ };
4276
+ if (solanaAddress) {
4277
+ output.solana = {
4278
+ address: solanaAddress,
4279
+ balances: solanaBalances
4280
+ };
4281
+ }
4282
+ console.log(JSON.stringify(output, null, 2));
2612
4283
  } else {
2613
4284
  console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
2614
4285
  console.log(` Address: ${client.address}`);
@@ -2632,18 +4303,90 @@ program.command("status").description("Show wallet status and balance").option("
2632
4303
  console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
2633
4304
  console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
2634
4305
  console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
4306
+ } else if (chainName === "bnb" || chainName === "bnb_testnet") {
4307
+ const bnbBalance = balance.native;
4308
+ const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
4309
+ console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
2635
4310
  } else {
2636
4311
  console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
2637
4312
  }
2638
4313
  }
4314
+ const address = client.address;
4315
+ let bnbApprovalStatus = null;
4316
+ let bnbTestnetApprovalStatus = null;
4317
+ try {
4318
+ if (allBalances["bnb"]) {
4319
+ bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
4320
+ }
4321
+ if (allBalances["bnb_testnet"]) {
4322
+ bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
4323
+ }
4324
+ } catch {
4325
+ }
4326
+ if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
4327
+ console.log("");
4328
+ console.log(" BNB Approvals (pay-for-success):");
4329
+ if (bnbApprovalStatus) {
4330
+ if (!bnbApprovalStatus.spender) {
4331
+ console.log(" BNB: \u26A0\uFE0F No spender configured");
4332
+ console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
4333
+ } else {
4334
+ const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
4335
+ const tokens = [
4336
+ bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
4337
+ bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
4338
+ ].join(", ");
4339
+ console.log(` BNB: ${status} ${tokens}`);
4340
+ const bnbNative = allBalances["bnb"]?.native || 0;
4341
+ if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
4342
+ console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
4343
+ }
4344
+ }
4345
+ }
4346
+ if (bnbTestnetApprovalStatus) {
4347
+ if (!bnbTestnetApprovalStatus.spender) {
4348
+ console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
4349
+ console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
4350
+ } else {
4351
+ const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
4352
+ const tokens = [
4353
+ bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
4354
+ bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
4355
+ ].join(", ");
4356
+ console.log(` BNB Testnet: ${status} ${tokens}`);
4357
+ const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
4358
+ if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
4359
+ console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
4360
+ }
4361
+ }
4362
+ }
4363
+ }
2639
4364
  console.log("");
2640
4365
  console.log(" Spending Limits:");
2641
4366
  console.log(` Per Transaction: $${config.limits.maxPerTx}`);
2642
4367
  console.log(` Daily: $${config.limits.maxPerDay}`);
4368
+ const solanaAddress2 = getSolanaAddress(options.configDir);
4369
+ if (solanaAddress2) {
4370
+ console.log("");
4371
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4372
+ console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
4373
+ try {
4374
+ const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
4375
+ console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
4376
+ } catch (err) {
4377
+ console.log(` Devnet: (unable to fetch)`);
4378
+ }
4379
+ try {
4380
+ const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
4381
+ console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
4382
+ } catch (err) {
4383
+ console.log(` Mainnet: (unable to fetch)`);
4384
+ }
4385
+ }
2643
4386
  console.log("");
2644
4387
  }
2645
4388
  });
2646
- program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
4389
+ program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
2647
4390
  const client = new MoltsPayClient({ configDir: options.configDir });
2648
4391
  if (!client.isInitialized) {
2649
4392
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -2950,14 +4693,14 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2950
4693
  let manifestPath;
2951
4694
  let skillDir;
2952
4695
  let isSkillDir = false;
2953
- if (existsSync4(join4(resolvedPath, "moltspay.services.json"))) {
2954
- manifestPath = join4(resolvedPath, "moltspay.services.json");
4696
+ if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
4697
+ manifestPath = join5(resolvedPath, "moltspay.services.json");
2955
4698
  skillDir = resolvedPath;
2956
4699
  isSkillDir = true;
2957
- } else if (existsSync4(resolvedPath) && resolvedPath.endsWith(".json")) {
4700
+ } else if (existsSync5(resolvedPath) && resolvedPath.endsWith(".json")) {
2958
4701
  manifestPath = resolvedPath;
2959
4702
  skillDir = dirname(resolvedPath);
2960
- } else if (existsSync4(resolvedPath)) {
4703
+ } else if (existsSync5(resolvedPath)) {
2961
4704
  console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
2962
4705
  continue;
2963
4706
  } else {
@@ -2966,25 +4709,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
2966
4709
  }
2967
4710
  console.log(`\u{1F4E6} Loading: ${manifestPath}`);
2968
4711
  try {
2969
- const manifestContent = JSON.parse(readFileSync4(manifestPath, "utf-8"));
4712
+ const manifestContent = JSON.parse(readFileSync5(manifestPath, "utf-8"));
2970
4713
  if (!provider) {
2971
4714
  provider = manifestContent.provider;
2972
4715
  }
2973
4716
  let skillModule = null;
2974
4717
  if (isSkillDir) {
2975
4718
  let entryPoint = "index.js";
2976
- const pkgJsonPath = join4(skillDir, "package.json");
2977
- if (existsSync4(pkgJsonPath)) {
4719
+ const pkgJsonPath = join5(skillDir, "package.json");
4720
+ if (existsSync5(pkgJsonPath)) {
2978
4721
  try {
2979
- const pkgJson = JSON.parse(readFileSync4(pkgJsonPath, "utf-8"));
4722
+ const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
2980
4723
  if (pkgJson.main) {
2981
4724
  entryPoint = pkgJson.main;
2982
4725
  }
2983
4726
  } catch {
2984
4727
  }
2985
4728
  }
2986
- const modulePath = join4(skillDir, entryPoint);
2987
- if (existsSync4(modulePath)) {
4729
+ const modulePath = join5(skillDir, entryPoint);
4730
+ if (existsSync5(modulePath)) {
2988
4731
  try {
2989
4732
  skillModule = await import(modulePath);
2990
4733
  console.log(` \u2705 Loaded module: ${modulePath}`);
@@ -3062,8 +4805,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
3062
4805
  provider,
3063
4806
  services: allServices
3064
4807
  };
3065
- const tempManifestPath = join4(DEFAULT_CONFIG_DIR, "combined-manifest.json");
3066
- writeFileSync2(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
4808
+ const tempManifestPath = join5(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
4809
+ writeFileSync3(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
3067
4810
  console.log(`
3068
4811
  \u{1F4CB} Combined manifest: ${allServices.length} services`);
3069
4812
  console.log(` Provider: ${provider.name}`);
@@ -3076,12 +4819,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
3076
4819
  server.skill(serviceId, handler);
3077
4820
  }
3078
4821
  const pidData = { pid: process.pid, port, paths: allPaths };
3079
- writeFileSync2(PID_FILE, JSON.stringify(pidData, null, 2));
4822
+ writeFileSync3(PID_FILE, JSON.stringify(pidData, null, 2));
3080
4823
  server.listen(port);
3081
4824
  const cleanup = () => {
3082
4825
  try {
3083
- if (existsSync4(PID_FILE)) unlinkSync(PID_FILE);
3084
- if (existsSync4(tempManifestPath)) unlinkSync(tempManifestPath);
4826
+ if (existsSync5(PID_FILE)) unlinkSync(PID_FILE);
4827
+ if (existsSync5(tempManifestPath)) unlinkSync(tempManifestPath);
3085
4828
  } catch {
3086
4829
  }
3087
4830
  };
@@ -3102,12 +4845,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
3102
4845
  }
3103
4846
  });
3104
4847
  program.command("stop").description("Stop the running MoltsPay server").action(async () => {
3105
- if (!existsSync4(PID_FILE)) {
4848
+ if (!existsSync5(PID_FILE)) {
3106
4849
  console.log("\u274C No running server found (no PID file)");
3107
4850
  process.exit(1);
3108
4851
  }
3109
4852
  try {
3110
- const pidData = JSON.parse(readFileSync4(PID_FILE, "utf-8"));
4853
+ const pidData = JSON.parse(readFileSync5(PID_FILE, "utf-8"));
3111
4854
  const { pid, port, manifest } = pidData;
3112
4855
  console.log(`
3113
4856
  \u{1F6D1} Stopping MoltsPay Server
@@ -3132,7 +4875,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
3132
4875
  process.kill(pid, "SIGKILL");
3133
4876
  } catch {
3134
4877
  }
3135
- if (existsSync4(PID_FILE)) {
4878
+ if (existsSync5(PID_FILE)) {
3136
4879
  unlinkSync(PID_FILE);
3137
4880
  }
3138
4881
  console.log("\u2705 Server stopped\n");
@@ -3141,14 +4884,23 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
3141
4884
  process.exit(1);
3142
4885
  }
3143
4886
  });
3144
- program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, or tempo_moderato).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
4887
+ program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
3145
4888
  const client = new MoltsPayClient({ configDir: options.configDir });
3146
4889
  if (!client.isInitialized) {
3147
4890
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
3148
4891
  process.exit(1);
3149
4892
  }
3150
4893
  let params = {};
3151
- if (paramsJson) {
4894
+ let useRawData = false;
4895
+ if (options.data) {
4896
+ try {
4897
+ params = JSON.parse(options.data);
4898
+ useRawData = true;
4899
+ } catch {
4900
+ console.error("\u274C Invalid JSON in --data flag");
4901
+ process.exit(1);
4902
+ }
4903
+ } else if (paramsJson) {
3152
4904
  try {
3153
4905
  params = JSON.parse(paramsJson);
3154
4906
  } catch {
@@ -3156,24 +4908,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
3156
4908
  process.exit(1);
3157
4909
  }
3158
4910
  }
3159
- if (options.prompt) params.prompt = options.prompt;
4911
+ if (!useRawData && options.prompt) params.prompt = options.prompt;
3160
4912
  if (options.image) {
3161
4913
  const imagePath = options.image;
3162
4914
  if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
3163
4915
  params.image_url = imagePath;
3164
4916
  } else {
3165
4917
  const filePath = resolve(imagePath);
3166
- if (!existsSync4(filePath)) {
4918
+ if (!existsSync5(filePath)) {
3167
4919
  console.error(`\u274C Image file not found: ${filePath}`);
3168
4920
  process.exit(1);
3169
4921
  }
3170
- const imageData = readFileSync4(filePath);
4922
+ const imageData = readFileSync5(filePath);
3171
4923
  params.image_base64 = imageData.toString("base64");
3172
4924
  }
3173
4925
  }
4926
+ const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
3174
4927
  const chain = options.chain?.toLowerCase();
3175
- if (chain && !["base", "polygon", "base_sepolia", "tempo_moderato"].includes(chain)) {
3176
- console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon, base_sepolia, tempo_moderato`);
4928
+ if (chain && !supportedPayChains.includes(chain)) {
4929
+ console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
3177
4930
  process.exit(1);
3178
4931
  }
3179
4932
  const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
@@ -3196,7 +4949,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
3196
4949
  `);
3197
4950
  console.log(` Server: ${server}`);
3198
4951
  console.log(` Service: ${service}`);
3199
- console.log(` Prompt: ${params.prompt}`);
4952
+ if (useRawData) {
4953
+ console.log(` Data: ${JSON.stringify(params).slice(0, 50)}${JSON.stringify(params).length > 50 ? "..." : ""}`);
4954
+ } else {
4955
+ console.log(` Prompt: ${params.prompt}`);
4956
+ }
3200
4957
  if (imageDisplay) console.log(` Image: ${imageDisplay}`);
3201
4958
  console.log(` Chain: ${chain || "(auto)"}`);
3202
4959
  console.log(` Token: ${token}`);
@@ -3204,22 +4961,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
3204
4961
  console.log("");
3205
4962
  }
3206
4963
  try {
3207
- let result;
3208
- if (chain === "tempo_moderato") {
3209
- if (!options.json) {
3210
- console.log(" Protocol: MPP (Machine Payments Protocol)");
3211
- console.log("");
3212
- }
3213
- const mppUrl = server.includes(service) ? server : `${server}/${service}`;
3214
- result = await client.payWithMPP(mppUrl, {
3215
- body: params
3216
- });
3217
- } else {
3218
- result = await client.pay(server, service, params, {
3219
- token,
3220
- chain
3221
- });
3222
- }
4964
+ const result = await client.pay(server, service, params, {
4965
+ token,
4966
+ chain,
4967
+ rawData: useRawData
4968
+ });
3223
4969
  if (options.json) {
3224
4970
  console.log(JSON.stringify(result));
3225
4971
  } else {
@@ -3239,9 +4985,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
3239
4985
  program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
3240
4986
  const resolvedPath = resolve(inputPath);
3241
4987
  let manifestPath;
3242
- if (existsSync4(join4(resolvedPath, "moltspay.services.json"))) {
3243
- manifestPath = join4(resolvedPath, "moltspay.services.json");
3244
- } else if (resolvedPath.endsWith(".json") && existsSync4(resolvedPath)) {
4988
+ if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
4989
+ manifestPath = join5(resolvedPath, "moltspay.services.json");
4990
+ } else if (resolvedPath.endsWith(".json") && existsSync5(resolvedPath)) {
3245
4991
  manifestPath = resolvedPath;
3246
4992
  } else {
3247
4993
  console.error(`\u274C Not found: ${resolvedPath}`);
@@ -3251,7 +4997,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
3251
4997
  \u{1F4CB} Validating: ${manifestPath}
3252
4998
  `);
3253
4999
  try {
3254
- const content = JSON.parse(readFileSync4(manifestPath, "utf-8"));
5000
+ const content = JSON.parse(readFileSync5(manifestPath, "utf-8"));
3255
5001
  const errors = [];
3256
5002
  if (!content.provider) {
3257
5003
  errors.push("Missing required field: provider");