moltspay 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +161 -17
  2. package/dist/cdp/index.d.mts +1 -1
  3. package/dist/cdp/index.d.ts +1 -1
  4. package/dist/cdp/index.js +60 -30408
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +44 -30400
  7. package/dist/cdp/index.mjs.map +1 -1
  8. package/dist/cdp-DeohBe1o.d.ts +66 -0
  9. package/dist/cdp-p_eHuQpb.d.mts +66 -0
  10. package/dist/chains/index.d.mts +1 -1
  11. package/dist/chains/index.d.ts +1 -1
  12. package/dist/chains/index.js +36 -40
  13. package/dist/chains/index.js.map +1 -1
  14. package/dist/chains/index.mjs +36 -40
  15. package/dist/chains/index.mjs.map +1 -1
  16. package/dist/cli/index.js +997 -174
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/index.mjs +1001 -174
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.d.mts +27 -4
  21. package/dist/client/index.d.ts +27 -4
  22. package/dist/client/index.js +217 -60
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +207 -60
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/facilitators/index.d.mts +15 -47
  27. package/dist/facilitators/index.d.ts +15 -47
  28. package/dist/facilitators/index.js +273 -34
  29. package/dist/facilitators/index.js.map +1 -1
  30. package/dist/facilitators/index.mjs +272 -34
  31. package/dist/facilitators/index.mjs.map +1 -1
  32. package/dist/{index-B3v8IWjM.d.mts → index-On9ZaGDW.d.mts} +2 -1
  33. package/dist/{index-B3v8IWjM.d.ts → index-On9ZaGDW.d.ts} +2 -1
  34. package/dist/index.d.mts +2 -2
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +792 -30657
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +782 -30657
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/server/index.d.mts +17 -0
  41. package/dist/server/index.d.ts +17 -0
  42. package/dist/server/index.js +513 -48
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +513 -48
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/verify/index.d.mts +1 -1
  47. package/dist/verify/index.d.ts +1 -1
  48. package/dist/verify/index.js +36 -40
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs +36 -40
  51. package/dist/verify/index.mjs.map +1 -1
  52. package/dist/wallet/index.d.mts +1 -1
  53. package/dist/wallet/index.d.ts +1 -1
  54. package/dist/wallet/index.js +36 -40
  55. package/dist/wallet/index.js.map +1 -1
  56. package/dist/wallet/index.mjs +36 -40
  57. package/dist/wallet/index.mjs.map +1 -1
  58. package/package.json +5 -2
package/dist/cli/index.js CHANGED
@@ -35,6 +35,7 @@ var init_cjs_shims = __esm({
35
35
 
36
36
  // src/cli/index.ts
37
37
  init_cjs_shims();
38
+ var import_crypto = require("crypto");
38
39
  var import_commander = require("commander");
39
40
  var import_os2 = require("os");
40
41
  var import_path2 = require("path");
@@ -60,12 +61,15 @@ var CHAINS = {
60
61
  USDC: {
61
62
  address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
62
63
  decimals: 6,
63
- symbol: "USDC"
64
+ symbol: "USDC",
65
+ eip712Name: "USD Coin"
66
+ // EIP-712 domain name
64
67
  },
65
68
  USDT: {
66
69
  address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
67
70
  decimals: 6,
68
- symbol: "USDT"
71
+ symbol: "USDT",
72
+ eip712Name: "Tether USD"
69
73
  }
70
74
  },
71
75
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -82,12 +86,15 @@ var CHAINS = {
82
86
  USDC: {
83
87
  address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
84
88
  decimals: 6,
85
- symbol: "USDC"
89
+ symbol: "USDC",
90
+ eip712Name: "USD Coin"
86
91
  },
87
92
  USDT: {
88
93
  address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
89
94
  decimals: 6,
90
- symbol: "USDT"
95
+ symbol: "USDT",
96
+ eip712Name: "(PoS) Tether USD"
97
+ // Polygon uses this name
91
98
  }
92
99
  },
93
100
  usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
@@ -95,27 +102,6 @@ var CHAINS = {
95
102
  explorerTx: "https://polygonscan.com/tx/",
96
103
  avgBlockTime: 2
97
104
  },
98
- ethereum: {
99
- name: "Ethereum",
100
- chainId: 1,
101
- rpc: "https://eth.llamarpc.com",
102
- tokens: {
103
- USDC: {
104
- address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
105
- decimals: 6,
106
- symbol: "USDC"
107
- },
108
- USDT: {
109
- address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
110
- decimals: 6,
111
- symbol: "USDT"
112
- }
113
- },
114
- usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
115
- explorer: "https://etherscan.io/address/",
116
- explorerTx: "https://etherscan.io/tx/",
117
- avgBlockTime: 12
118
- },
119
105
  // ============ Testnet ============
120
106
  base_sepolia: {
121
107
  name: "Base Sepolia",
@@ -125,13 +111,17 @@ var CHAINS = {
125
111
  USDC: {
126
112
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
127
113
  decimals: 6,
128
- symbol: "USDC"
114
+ symbol: "USDC",
115
+ eip712Name: "USDC"
116
+ // Testnet USDC uses 'USDC' not 'USD Coin'
129
117
  },
130
118
  USDT: {
131
119
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
132
120
  // Same as USDC on testnet (no official USDT)
133
121
  decimals: 6,
134
- symbol: "USDT"
122
+ symbol: "USDT",
123
+ eip712Name: "USDC"
124
+ // Uses same contract as USDC
135
125
  }
136
126
  },
137
127
  usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
@@ -139,27 +129,34 @@ var CHAINS = {
139
129
  explorerTx: "https://sepolia.basescan.org/tx/",
140
130
  avgBlockTime: 2
141
131
  },
142
- sepolia: {
143
- name: "Sepolia",
144
- chainId: 11155111,
145
- rpc: "https://rpc.sepolia.org",
132
+ // ============ Tempo Testnet (Moderato) ============
133
+ tempo_moderato: {
134
+ name: "Tempo Moderato",
135
+ chainId: 42431,
136
+ rpc: "https://rpc.moderato.tempo.xyz",
146
137
  tokens: {
138
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
139
+ // Note: Tempo uses USD as native gas token, not ETH
147
140
  USDC: {
148
- address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
141
+ address: "0x20c0000000000000000000000000000000000000",
142
+ // pathUSD - primary testnet stablecoin
149
143
  decimals: 6,
150
- symbol: "USDC"
144
+ symbol: "USDC",
145
+ eip712Name: "pathUSD"
151
146
  },
152
147
  USDT: {
153
- address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
154
- // Same as USDC on testnet
148
+ address: "0x20c0000000000000000000000000000000000001",
149
+ // alphaUSD
155
150
  decimals: 6,
156
- symbol: "USDT"
151
+ symbol: "USDT",
152
+ eip712Name: "alphaUSD"
157
153
  }
158
154
  },
159
- usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
160
- explorer: "https://sepolia.etherscan.io/address/",
161
- explorerTx: "https://sepolia.etherscan.io/tx/",
162
- avgBlockTime: 12
155
+ usdc: "0x20c0000000000000000000000000000000000000",
156
+ explorer: "https://explore.testnet.tempo.xyz/address/",
157
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
158
+ avgBlockTime: 0.5
159
+ // ~500ms finality
163
160
  }
164
161
  };
165
162
  function getChain(name) {
@@ -325,7 +322,7 @@ Server accepts: ${serverChains.join(", ")}`
325
322
  } else {
326
323
  throw new Error(
327
324
  `Server accepts: ${serverChains.join(", ")}
328
- Please specify: --chain base or --chain polygon`
325
+ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
329
326
  );
330
327
  }
331
328
  }
@@ -367,13 +364,19 @@ Please specify: --chain base or --chain polygon`
367
364
  if (!payTo) {
368
365
  throw new Error("Missing payTo address in payment requirements");
369
366
  }
370
- const authorization = await this.signEIP3009(payTo, amount, chain, token);
367
+ const domainOverride = req.extra && typeof req.extra === "object" && req.extra.name ? { name: req.extra.name, version: req.extra.version || "2" } : void 0;
368
+ const authorization = await this.signEIP3009(payTo, amount, chain, token, domainOverride);
371
369
  const tokenConfig = chain.tokens[token];
372
- const tokenName = token === "USDC" ? "USD Coin" : "Tether USD";
370
+ const extra = req.extra && typeof req.extra === "object" ? req.extra : {
371
+ name: tokenConfig.eip712Name || "USD Coin",
372
+ version: "2"
373
+ };
373
374
  const payload = {
374
375
  x402Version: X402_VERSION,
376
+ scheme: "exact",
377
+ network,
375
378
  payload: authorization,
376
- // v2 requires 'accepted' field with the requirements being fulfilled
379
+ // { authorization: {...}, signature: "0x..." }
377
380
  accepted: {
378
381
  scheme: "exact",
379
382
  network,
@@ -381,7 +384,7 @@ Please specify: --chain base or --chain polygon`
381
384
  amount: amountRaw,
382
385
  payTo,
383
386
  maxTimeoutSeconds: req.maxTimeoutSeconds || 300,
384
- extra: req.extra || { name: tokenName, version: "2" }
387
+ extra
385
388
  }
386
389
  };
387
390
  const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
@@ -411,7 +414,7 @@ Please specify: --chain base or --chain polygon`
411
414
  * This only signs - no on-chain transaction, no gas needed.
412
415
  * Supports both USDC and USDT.
413
416
  */
414
- async signEIP3009(to, amount, chain, token = "USDC") {
417
+ async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
415
418
  const validAfter = 0;
416
419
  const validBefore = Math.floor(Date.now() / 1e3) + 3600;
417
420
  const nonce = import_ethers.ethers.hexlify(import_ethers.ethers.randomBytes(32));
@@ -425,10 +428,11 @@ Please specify: --chain base or --chain polygon`
425
428
  validBefore: validBefore.toString(),
426
429
  nonce
427
430
  };
428
- const tokenName = token === "USDC" ? "USD Coin" : "Tether USD";
431
+ const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
432
+ const tokenVersion = domainOverride?.version || "2";
429
433
  const domain = {
430
434
  name: tokenName,
431
- version: "2",
435
+ version: tokenVersion,
432
436
  chainId: chain.chainId,
433
437
  verifyingContract: tokenConfig.address
434
438
  };
@@ -591,30 +595,59 @@ Please specify: --chain base or --chain polygon`
591
595
  };
592
596
  }
593
597
  /**
594
- * Get wallet balances on all supported chains (Base + Polygon)
598
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
595
599
  */
596
600
  async getAllBalances() {
597
601
  if (!this.wallet) {
598
602
  throw new Error("Client not initialized");
599
603
  }
600
- const supportedChains = ["base", "polygon"];
604
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
601
605
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
602
606
  const results = {};
607
+ const tempoTokens = {
608
+ pathUSD: "0x20c0000000000000000000000000000000000000",
609
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
610
+ betaUSD: "0x20c0000000000000000000000000000000000002",
611
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
612
+ };
603
613
  await Promise.all(
604
614
  supportedChains.map(async (chainName) => {
605
615
  try {
606
616
  const chain = getChain(chainName);
607
617
  const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
608
- const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
609
- provider.getBalance(this.wallet.address),
610
- new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
611
- new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
612
- ]);
613
- results[chainName] = {
614
- usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
615
- usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
616
- native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
617
- };
618
+ if (chainName === "tempo_moderato") {
619
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
620
+ provider.getBalance(this.wallet.address),
621
+ new import_ethers.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
622
+ new import_ethers.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
623
+ new import_ethers.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
624
+ new import_ethers.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
625
+ ]);
626
+ results[chainName] = {
627
+ usdc: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
628
+ // pathUSD as default USDC
629
+ usdt: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
630
+ // alphaUSD as default USDT
631
+ native: parseFloat(import_ethers.ethers.formatEther(nativeBalance)),
632
+ tempo: {
633
+ pathUSD: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
634
+ alphaUSD: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
635
+ betaUSD: parseFloat(import_ethers.ethers.formatUnits(betaUSD, 6)),
636
+ thetaUSD: parseFloat(import_ethers.ethers.formatUnits(thetaUSD, 6))
637
+ }
638
+ };
639
+ } else {
640
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
641
+ provider.getBalance(this.wallet.address),
642
+ new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
643
+ new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
644
+ ]);
645
+ results[chainName] = {
646
+ usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
647
+ usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
648
+ native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
649
+ };
650
+ }
618
651
  } catch (err) {
619
652
  results[chainName] = { usdc: 0, usdt: 0, native: 0 };
620
653
  }
@@ -622,6 +655,121 @@ Please specify: --chain base or --chain polygon`
622
655
  );
623
656
  return results;
624
657
  }
658
+ /**
659
+ * Pay for a service using MPP (Machine Payments Protocol)
660
+ *
661
+ * This implements the MPP flow manually for EOA wallets:
662
+ * 1. Request service → get 402 with WWW-Authenticate
663
+ * 2. Parse payment requirements
664
+ * 3. Execute transfer on Tempo chain
665
+ * 4. Retry with transaction hash as credential
666
+ *
667
+ * @param url - Full URL of the MPP-enabled endpoint
668
+ * @param options - Request options (body, headers)
669
+ * @returns Response from the service
670
+ */
671
+ async payWithMPP(url, options = {}) {
672
+ if (!this.wallet || !this.walletData) {
673
+ throw new Error("Client not initialized. Run: npx moltspay init");
674
+ }
675
+ const { privateKeyToAccount } = await import("viem/accounts");
676
+ const { createWalletClient, createPublicClient, http } = await import("viem");
677
+ const { tempoModerato } = await import("viem/chains");
678
+ const { Actions } = await import("viem/tempo");
679
+ const privateKey = this.walletData.privateKey;
680
+ const account = privateKeyToAccount(privateKey);
681
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
682
+ console.log(`[MoltsPay] Using account: ${account.address}`);
683
+ const initResponse = await fetch(url, {
684
+ method: "POST",
685
+ headers: {
686
+ "Content-Type": "application/json",
687
+ ...options.headers
688
+ },
689
+ body: options.body ? JSON.stringify(options.body) : void 0
690
+ });
691
+ if (initResponse.status !== 402) {
692
+ if (initResponse.ok) {
693
+ return initResponse.json();
694
+ }
695
+ const errorText = await initResponse.text();
696
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
697
+ }
698
+ const wwwAuth = initResponse.headers.get("www-authenticate");
699
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
700
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
701
+ }
702
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
703
+ const parseAuthParam = (header, key) => {
704
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
705
+ return match ? match[1] : null;
706
+ };
707
+ const challengeId = parseAuthParam(wwwAuth, "id");
708
+ const method = parseAuthParam(wwwAuth, "method");
709
+ const realm = parseAuthParam(wwwAuth, "realm");
710
+ const requestB64 = parseAuthParam(wwwAuth, "request");
711
+ if (method !== "tempo") {
712
+ throw new Error(`Unsupported payment method: ${method}`);
713
+ }
714
+ if (!requestB64) {
715
+ throw new Error("Missing request in WWW-Authenticate");
716
+ }
717
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
718
+ const paymentRequest = JSON.parse(requestJson);
719
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
720
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
721
+ const chainId = methodDetails?.chainId || 42431;
722
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
723
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
724
+ const tempoChain = { ...tempoModerato, feeToken: currency };
725
+ const publicClient = createPublicClient({
726
+ chain: tempoChain,
727
+ transport: http("https://rpc.moderato.tempo.xyz")
728
+ });
729
+ const walletClient = createWalletClient({
730
+ account,
731
+ chain: tempoChain,
732
+ transport: http("https://rpc.moderato.tempo.xyz")
733
+ });
734
+ const txHash = await Actions.token.transfer(walletClient, {
735
+ to: recipient,
736
+ amount: BigInt(amount),
737
+ token: currency
738
+ });
739
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
740
+ console.log(`[MoltsPay] Waiting for confirmation...`);
741
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
742
+ console.log(`[MoltsPay] Transaction confirmed!`);
743
+ const challenge = {
744
+ id: challengeId,
745
+ realm,
746
+ method: "tempo",
747
+ intent: "charge",
748
+ request: paymentRequest
749
+ };
750
+ const credential = {
751
+ challenge,
752
+ payload: { hash: txHash, type: "hash" },
753
+ source: `did:pkh:eip155:${chainId}:${account.address}`
754
+ };
755
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
756
+ console.log(`[MoltsPay] Retrying with payment credential...`);
757
+ const paidResponse = await fetch(url, {
758
+ method: "POST",
759
+ headers: {
760
+ "Content-Type": "application/json",
761
+ "Authorization": `Payment ${credentialB64}`,
762
+ ...options.headers
763
+ },
764
+ body: options.body ? JSON.stringify(options.body) : void 0
765
+ });
766
+ if (!paidResponse.ok) {
767
+ const errorText = await paidResponse.text();
768
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
769
+ }
770
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
771
+ return paidResponse.json();
772
+ }
625
773
  };
626
774
 
627
775
  // src/server/index.ts
@@ -646,8 +794,8 @@ init_cjs_shims();
646
794
  var import_fs2 = require("fs");
647
795
  var path = __toESM(require("path"));
648
796
  var X402_VERSION2 = 2;
649
- var CDP_MAINNET_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
650
- var CDP_TESTNET_URL = "https://www.x402.org/facilitator";
797
+ var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
798
+ var TESTNET_CHAIN_IDS = [84532];
651
799
  function loadEnvFile() {
652
800
  const envPaths = [
653
801
  path.join(process.cwd(), ".env"),
@@ -682,31 +830,33 @@ var CDPFacilitator = class extends BaseFacilitator {
682
830
  displayName = "Coinbase CDP";
683
831
  supportedNetworks;
684
832
  endpoint;
685
- useMainnet;
686
833
  apiKeyId;
687
834
  apiKeySecret;
688
835
  constructor(config = {}) {
689
836
  super();
690
837
  loadEnvFile();
691
- this.useMainnet = config.useMainnet ?? process.env.USE_MAINNET?.toLowerCase() === "true";
692
838
  this.apiKeyId = config.apiKeyId || process.env.CDP_API_KEY_ID;
693
839
  this.apiKeySecret = config.apiKeySecret || process.env.CDP_API_KEY_SECRET;
694
- this.endpoint = this.useMainnet ? CDP_MAINNET_URL : CDP_TESTNET_URL;
695
- this.supportedNetworks = this.useMainnet ? ["eip155:8453", "eip155:137"] : ["eip155:8453", "eip155:84532", "eip155:137"];
696
- if (this.useMainnet && (!this.apiKeyId || !this.apiKeySecret)) {
697
- console.warn("[CDPFacilitator] WARNING: Mainnet mode but missing CDP credentials!");
698
- console.warn("[CDPFacilitator] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET");
840
+ this.endpoint = CDP_URL;
841
+ this.supportedNetworks = [
842
+ "eip155:8453",
843
+ // Base mainnet
844
+ "eip155:137",
845
+ // Polygon mainnet
846
+ "eip155:84532"
847
+ // Base Sepolia (testnet)
848
+ ];
849
+ if (!this.apiKeyId || !this.apiKeySecret) {
850
+ console.warn("[CDPFacilitator] WARNING: Missing CDP credentials!");
851
+ console.warn("[CDPFacilitator] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in ~/.moltspay/.env");
699
852
  }
700
853
  }
701
854
  /**
702
855
  * Get auth headers for CDP API requests
703
856
  */
704
857
  async getAuthHeaders(method, urlPath, body) {
705
- if (!this.useMainnet) {
706
- return {};
707
- }
708
858
  if (!this.apiKeyId || !this.apiKeySecret) {
709
- throw new Error("CDP credentials required for mainnet");
859
+ throw new Error("CDP credentials required. Set CDP_API_KEY_ID and CDP_API_KEY_SECRET");
710
860
  }
711
861
  try {
712
862
  const { getAuthHeaders } = await import("@coinbase/cdp-sdk/auth");
@@ -758,23 +908,23 @@ var CDPFacilitator = class extends BaseFacilitator {
758
908
  paymentPayload,
759
909
  paymentRequirements: requirements
760
910
  };
911
+ console.log("[CDP Verify] Payload:", JSON.stringify(paymentPayload, null, 2));
912
+ const authHeaders = await this.getAuthHeaders(
913
+ "POST",
914
+ "/platform/v2/x402/verify",
915
+ requestBody
916
+ );
761
917
  const headers = {
762
- "Content-Type": "application/json"
918
+ "Content-Type": "application/json",
919
+ ...authHeaders
763
920
  };
764
- if (this.useMainnet) {
765
- const authHeaders = await this.getAuthHeaders(
766
- "POST",
767
- "/platform/v2/x402/verify",
768
- requestBody
769
- );
770
- Object.assign(headers, authHeaders);
771
- }
772
921
  const response = await fetch(`${this.endpoint}/verify`, {
773
922
  method: "POST",
774
923
  headers,
775
924
  body: JSON.stringify(requestBody)
776
925
  });
777
926
  const result = await response.json();
927
+ console.log("[CDP Verify] Response:", response.status, JSON.stringify(result));
778
928
  if (!response.ok || !result.isValid) {
779
929
  return {
780
930
  valid: false,
@@ -800,17 +950,15 @@ var CDPFacilitator = class extends BaseFacilitator {
800
950
  paymentPayload,
801
951
  paymentRequirements: requirements
802
952
  };
953
+ const authHeaders = await this.getAuthHeaders(
954
+ "POST",
955
+ "/platform/v2/x402/settle",
956
+ requestBody
957
+ );
803
958
  const headers = {
804
- "Content-Type": "application/json"
959
+ "Content-Type": "application/json",
960
+ ...authHeaders
805
961
  };
806
- if (this.useMainnet) {
807
- const authHeaders = await this.getAuthHeaders(
808
- "POST",
809
- "/platform/v2/x402/settle",
810
- requestBody
811
- );
812
- Object.assign(headers, authHeaders);
813
- }
814
962
  const response = await fetch(`${this.endpoint}/settle`, {
815
963
  method: "POST",
816
964
  headers,
@@ -845,13 +993,140 @@ var CDPFacilitator = class extends BaseFacilitator {
845
993
  freeQuota: 1e3
846
994
  };
847
995
  }
996
+ /**
997
+ * Check if a chain ID is testnet
998
+ */
999
+ static isTestnet(chainId) {
1000
+ return TESTNET_CHAIN_IDS.includes(chainId);
1001
+ }
848
1002
  /**
849
1003
  * Get configuration summary (for logging)
850
1004
  */
851
1005
  getConfigSummary() {
852
- const mode = this.useMainnet ? "mainnet" : "testnet";
853
1006
  const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
854
- return `CDP Facilitator (${mode}, credentials: ${hasCredentials ? "yes" : "no"})`;
1007
+ const networks = this.supportedNetworks.join(", ");
1008
+ return `CDP Facilitator (networks: ${networks}, credentials: ${hasCredentials ? "yes" : "no"})`;
1009
+ }
1010
+ };
1011
+
1012
+ // src/facilitators/tempo.ts
1013
+ init_cjs_shims();
1014
+ var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
1015
+ var TempoFacilitator = class extends BaseFacilitator {
1016
+ name = "tempo";
1017
+ displayName = "Tempo Testnet";
1018
+ supportedNetworks = ["eip155:42431"];
1019
+ // Tempo Moderato
1020
+ rpcUrl;
1021
+ constructor() {
1022
+ super();
1023
+ this.rpcUrl = CHAINS.tempo_moderato.rpc;
1024
+ }
1025
+ async healthCheck() {
1026
+ const start = Date.now();
1027
+ try {
1028
+ const response = await fetch(this.rpcUrl, {
1029
+ method: "POST",
1030
+ headers: { "Content-Type": "application/json" },
1031
+ body: JSON.stringify({
1032
+ jsonrpc: "2.0",
1033
+ method: "eth_chainId",
1034
+ params: [],
1035
+ id: 1
1036
+ })
1037
+ });
1038
+ const data = await response.json();
1039
+ const chainId = parseInt(data.result, 16);
1040
+ if (chainId !== 42431) {
1041
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
1042
+ }
1043
+ return { healthy: true, latencyMs: Date.now() - start };
1044
+ } catch (error) {
1045
+ return { healthy: false, error: String(error) };
1046
+ }
1047
+ }
1048
+ async verify(paymentPayload, requirements) {
1049
+ try {
1050
+ const tempoPayload = paymentPayload.payload;
1051
+ if (!tempoPayload?.txHash) {
1052
+ return { valid: false, error: "Missing txHash in payment payload" };
1053
+ }
1054
+ const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
1055
+ if (!receipt) {
1056
+ return { valid: false, error: "Transaction not found" };
1057
+ }
1058
+ if (receipt.status !== "0x1") {
1059
+ return { valid: false, error: "Transaction failed" };
1060
+ }
1061
+ const transferLog = receipt.logs.find(
1062
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
1063
+ );
1064
+ if (!transferLog) {
1065
+ return { valid: false, error: "No Transfer event found" };
1066
+ }
1067
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
1068
+ const expectedTo = requirements.payTo.toLowerCase();
1069
+ if (toAddress !== expectedTo) {
1070
+ return {
1071
+ valid: false,
1072
+ error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
1073
+ };
1074
+ }
1075
+ const amount = BigInt(transferLog.data);
1076
+ const expectedAmount = BigInt(requirements.amount);
1077
+ if (amount < expectedAmount) {
1078
+ return {
1079
+ valid: false,
1080
+ error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
1081
+ };
1082
+ }
1083
+ const tokenAddress = transferLog.address.toLowerCase();
1084
+ const expectedToken = requirements.asset.toLowerCase();
1085
+ if (tokenAddress !== expectedToken) {
1086
+ return {
1087
+ valid: false,
1088
+ error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
1089
+ };
1090
+ }
1091
+ return {
1092
+ valid: true,
1093
+ details: {
1094
+ txHash: tempoPayload.txHash,
1095
+ from: "0x" + transferLog.topics[1].slice(26),
1096
+ to: toAddress,
1097
+ amount: amount.toString(),
1098
+ token: tokenAddress
1099
+ }
1100
+ };
1101
+ } catch (error) {
1102
+ return { valid: false, error: `Verification failed: ${error}` };
1103
+ }
1104
+ }
1105
+ async settle(paymentPayload, requirements) {
1106
+ const verifyResult = await this.verify(paymentPayload, requirements);
1107
+ if (!verifyResult.valid) {
1108
+ return { success: false, error: verifyResult.error };
1109
+ }
1110
+ const tempoPayload = paymentPayload.payload;
1111
+ return {
1112
+ success: true,
1113
+ transaction: tempoPayload.txHash,
1114
+ status: "settled"
1115
+ };
1116
+ }
1117
+ async getTransactionReceipt(txHash) {
1118
+ const response = await fetch(this.rpcUrl, {
1119
+ method: "POST",
1120
+ headers: { "Content-Type": "application/json" },
1121
+ body: JSON.stringify({
1122
+ jsonrpc: "2.0",
1123
+ method: "eth_getTransactionReceipt",
1124
+ params: [txHash],
1125
+ id: 1
1126
+ })
1127
+ });
1128
+ const data = await response.json();
1129
+ return data.result;
855
1130
  }
856
1131
  };
857
1132
 
@@ -864,7 +1139,8 @@ var FacilitatorRegistry = class {
864
1139
  roundRobinIndex = 0;
865
1140
  constructor(selection) {
866
1141
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
867
- this.selection = selection || { primary: "cdp", strategy: "failover" };
1142
+ this.registerFactory("tempo", () => new TempoFacilitator());
1143
+ this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
868
1144
  }
869
1145
  /**
870
1146
  * Register a new facilitator factory
@@ -1088,6 +1364,9 @@ var X402_VERSION3 = 2;
1088
1364
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
1089
1365
  var PAYMENT_HEADER2 = "x-payment";
1090
1366
  var PAYMENT_RESPONSE_HEADER = "x-payment-response";
1367
+ var MPP_AUTH_HEADER = "authorization";
1368
+ var MPP_WWW_AUTH_HEADER = "www-authenticate";
1369
+ var MPP_RECEIPT_HEADER = "payment-receipt";
1091
1370
  var TOKEN_ADDRESSES = {
1092
1371
  "eip155:8453": {
1093
1372
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -1101,17 +1380,48 @@ var TOKEN_ADDRESSES = {
1101
1380
  "eip155:137": {
1102
1381
  USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
1103
1382
  USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
1383
+ },
1384
+ "eip155:42431": {
1385
+ // Tempo Moderato testnet - TIP-20 stablecoins
1386
+ USDC: "0x20c0000000000000000000000000000000000000",
1387
+ // pathUSD
1388
+ USDT: "0x20c0000000000000000000000000000000000001"
1389
+ // alphaUSD
1104
1390
  }
1105
1391
  };
1106
1392
  var CHAIN_TO_NETWORK = {
1107
1393
  "base": "eip155:8453",
1108
1394
  "base_sepolia": "eip155:84532",
1109
- "polygon": "eip155:137"
1395
+ "polygon": "eip155:137",
1396
+ "tempo_moderato": "eip155:42431"
1110
1397
  };
1111
1398
  var TOKEN_DOMAINS = {
1112
- USDC: { name: "USD Coin", version: "2" },
1113
- USDT: { name: "Tether USD", version: "2" }
1399
+ // Base mainnet
1400
+ "eip155:8453": {
1401
+ USDC: { name: "USD Coin", version: "2" },
1402
+ USDT: { name: "Tether USD", version: "2" }
1403
+ },
1404
+ // Base Sepolia testnet - USDC uses 'USDC' not 'USD Coin'
1405
+ "eip155:84532": {
1406
+ USDC: { name: "USDC", version: "2" },
1407
+ USDT: { name: "USDC", version: "2" }
1408
+ // Same contract as USDC on testnet
1409
+ },
1410
+ // Polygon mainnet
1411
+ "eip155:137": {
1412
+ USDC: { name: "USD Coin", version: "2" },
1413
+ USDT: { name: "(PoS) Tether USD", version: "2" }
1414
+ },
1415
+ // Tempo Moderato testnet - TIP-20 stablecoins
1416
+ "eip155:42431": {
1417
+ USDC: { name: "pathUSD", version: "1" },
1418
+ USDT: { name: "alphaUSD", version: "1" }
1419
+ }
1114
1420
  };
1421
+ function getTokenDomain(network, token) {
1422
+ const networkDomains = TOKEN_DOMAINS[network] || TOKEN_DOMAINS["eip155:8453"];
1423
+ return networkDomains[token] || { name: "USD Coin", version: "2" };
1424
+ }
1115
1425
  function getAcceptedCurrencies(config) {
1116
1426
  return config.acceptedCurrencies ?? [config.currency];
1117
1427
  }
@@ -1163,9 +1473,11 @@ var MoltsPayServer = class {
1163
1473
  };
1164
1474
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
1165
1475
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
1476
+ const defaultFallback = ["tempo"];
1477
+ const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
1166
1478
  const facilitatorConfig = options.facilitators || {
1167
1479
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
1168
- fallback: process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean),
1480
+ fallback: envFallback || defaultFallback,
1169
1481
  strategy: process.env.FACILITATOR_STRATEGY || "failover",
1170
1482
  config: {
1171
1483
  cdp: { useMainnet: this.useMainnet }
@@ -1205,11 +1517,14 @@ var MoltsPayServer = class {
1205
1517
  getProviderChains() {
1206
1518
  const provider = this.manifest.provider;
1207
1519
  if (provider.chains && provider.chains.length > 0) {
1208
- return provider.chains.map((c) => ({
1209
- network: c.network || CHAIN_TO_NETWORK[c.chain] || "eip155:8453",
1210
- wallet: c.wallet || provider.wallet,
1211
- tokens: c.tokens || ["USDC"]
1212
- }));
1520
+ return provider.chains.map((c) => {
1521
+ const chainName = typeof c === "string" ? c : c.chain;
1522
+ return {
1523
+ network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
1524
+ wallet: (typeof c === "object" ? c.wallet : null) || provider.wallet,
1525
+ tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
1526
+ };
1527
+ });
1213
1528
  }
1214
1529
  const chain = provider.chain || "base";
1215
1530
  const network = CHAIN_TO_NETWORK[chain] || this.networkId;
@@ -1256,8 +1571,8 @@ var MoltsPayServer = class {
1256
1571
  async handleRequest(req, res) {
1257
1572
  res.setHeader("Access-Control-Allow-Origin", "*");
1258
1573
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1259
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
1260
- res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
1574
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
1575
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
1261
1576
  if (req.method === "OPTIONS") {
1262
1577
  res.writeHead(204);
1263
1578
  res.end();
@@ -1288,6 +1603,14 @@ var MoltsPayServer = class {
1288
1603
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1289
1604
  return await this.handleProxy(body, paymentHeader, res);
1290
1605
  }
1606
+ const servicePath = url.pathname.replace(/^\//, "");
1607
+ const skill = this.skills.get(servicePath);
1608
+ if (skill && (req.method === "POST" || req.method === "GET")) {
1609
+ const body = req.method === "POST" ? await this.readBody(req) : {};
1610
+ const authHeader = req.headers[MPP_AUTH_HEADER];
1611
+ const x402Header = req.headers[PAYMENT_HEADER2];
1612
+ return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
1613
+ }
1291
1614
  this.sendJson(res, 404, { error: "Not found" });
1292
1615
  } catch (err) {
1293
1616
  console.error("[MoltsPay] Error:", err);
@@ -1419,7 +1742,7 @@ var MoltsPayServer = class {
1419
1742
  const paymentNetwork = payment.accepted?.network || payment.network || this.networkId;
1420
1743
  const paymentWallet = this.getWalletForNetwork(paymentNetwork);
1421
1744
  const requirements = this.buildPaymentRequirements(skill.config, paymentNetwork, paymentWallet, paymentToken);
1422
- console.log(`[MoltsPay] Verifying payment...`);
1745
+ console.log(`[MoltsPay] Verifying payment on ${paymentNetwork}...`);
1423
1746
  const verifyResult = await this.registry.verify(payment, requirements);
1424
1747
  if (!verifyResult.valid) {
1425
1748
  return this.sendJson(res, 402, {
@@ -1471,6 +1794,187 @@ var MoltsPayServer = class {
1471
1794
  payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
1472
1795
  }, responseHeaders);
1473
1796
  }
1797
+ /**
1798
+ * Handle MPP (Machine Payments Protocol) request
1799
+ * Supports both x402 and MPP protocols on service endpoints
1800
+ */
1801
+ async handleMPPRequest(skill, body, authHeader, x402Header, res) {
1802
+ const config = skill.config;
1803
+ const params = body || {};
1804
+ if (x402Header) {
1805
+ return await this.handleExecute({ service: config.id, params }, x402Header, res);
1806
+ }
1807
+ if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
1808
+ return await this.handleMPPPayment(skill, params, authHeader, res);
1809
+ }
1810
+ return this.sendMPPPaymentRequired(config, res);
1811
+ }
1812
+ /**
1813
+ * Handle MPP payment verification and service execution
1814
+ */
1815
+ async handleMPPPayment(skill, params, authHeader, res) {
1816
+ const config = skill.config;
1817
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
1818
+ if (!credentialMatch) {
1819
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
1820
+ }
1821
+ let mppCredential;
1822
+ try {
1823
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
1824
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
1825
+ mppCredential = JSON.parse(decoded);
1826
+ } catch (err) {
1827
+ console.error("[MoltsPay] Failed to parse MPP credential:", err);
1828
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
1829
+ }
1830
+ let txHash;
1831
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
1832
+ txHash = mppCredential.payload.hash;
1833
+ } else if (mppCredential.payload?.type === "transaction") {
1834
+ return this.sendJson(res, 400, {
1835
+ error: "Transaction type not supported. Please use push mode (hash type)."
1836
+ });
1837
+ }
1838
+ if (!txHash) {
1839
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
1840
+ }
1841
+ let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
1842
+ if (!chainId && mppCredential.source) {
1843
+ const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
1844
+ if (chainMatch) chainId = parseInt(chainMatch[1], 10);
1845
+ }
1846
+ chainId = chainId || 42431;
1847
+ const network = `eip155:${chainId}`;
1848
+ if (!this.isNetworkAccepted(network)) {
1849
+ return this.sendJson(res, 402, {
1850
+ error: `Network not accepted: ${network}`
1851
+ });
1852
+ }
1853
+ const requirements = this.buildPaymentRequirements(
1854
+ config,
1855
+ network,
1856
+ this.getWalletForNetwork(network),
1857
+ "USDC"
1858
+ );
1859
+ const paymentPayload = {
1860
+ x402Version: X402_VERSION3,
1861
+ scheme: "exact",
1862
+ network,
1863
+ payload: {
1864
+ txHash,
1865
+ chainId
1866
+ }
1867
+ };
1868
+ console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
1869
+ const verification = await this.registry.verify(paymentPayload, requirements);
1870
+ if (!verification.valid) {
1871
+ return this.sendJson(res, 402, {
1872
+ error: `Payment verification failed: ${verification.error}`
1873
+ });
1874
+ }
1875
+ console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
1876
+ let result;
1877
+ try {
1878
+ result = await skill.handler(params);
1879
+ } catch (err) {
1880
+ console.error(`[MoltsPay] Skill execution error:`, err);
1881
+ return this.sendJson(res, 500, {
1882
+ error: `Service execution failed: ${err.message}`
1883
+ });
1884
+ }
1885
+ const receipt = {
1886
+ success: true,
1887
+ txHash,
1888
+ network,
1889
+ facilitator: verification.facilitator
1890
+ };
1891
+ const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
1892
+ res.writeHead(200, {
1893
+ "Content-Type": "application/json",
1894
+ [MPP_RECEIPT_HEADER]: receiptEncoded
1895
+ });
1896
+ res.end(JSON.stringify({
1897
+ success: true,
1898
+ result,
1899
+ payment: {
1900
+ txHash,
1901
+ status: "verified",
1902
+ facilitator: verification.facilitator
1903
+ }
1904
+ }, null, 2));
1905
+ }
1906
+ /**
1907
+ * Return 402 with both x402 and MPP payment requirements
1908
+ */
1909
+ sendMPPPaymentRequired(config, res) {
1910
+ const acceptedTokens = getAcceptedCurrencies(config);
1911
+ const providerChains = this.getProviderChains();
1912
+ const accepts = [];
1913
+ for (const chainConfig of providerChains) {
1914
+ for (const token of acceptedTokens) {
1915
+ if (chainConfig.tokens.includes(token)) {
1916
+ accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
1917
+ }
1918
+ }
1919
+ }
1920
+ const x402PaymentRequired = {
1921
+ x402Version: X402_VERSION3,
1922
+ accepts,
1923
+ acceptedCurrencies: acceptedTokens,
1924
+ resource: {
1925
+ url: `/${config.id}`,
1926
+ description: `${config.name} - $${config.price} ${config.currency}`
1927
+ }
1928
+ };
1929
+ const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
1930
+ const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
1931
+ let mppWwwAuth = "";
1932
+ if (tempoChain) {
1933
+ const challengeId = this.generateChallengeId();
1934
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
1935
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
1936
+ const mppRequest = {
1937
+ amount: amountInUnits,
1938
+ currency: tokenAddress,
1939
+ methodDetails: {
1940
+ chainId: 42431,
1941
+ feePayer: true
1942
+ },
1943
+ recipient: tempoChain.wallet
1944
+ };
1945
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
1946
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
1947
+ mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
1948
+ }
1949
+ const headers = {
1950
+ "Content-Type": "application/problem+json",
1951
+ [PAYMENT_REQUIRED_HEADER2]: x402Encoded
1952
+ };
1953
+ if (mppWwwAuth) {
1954
+ headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
1955
+ }
1956
+ res.writeHead(402, headers);
1957
+ res.end(JSON.stringify({
1958
+ type: "https://paymentauth.org/problems/payment-required",
1959
+ title: "Payment Required",
1960
+ status: 402,
1961
+ detail: `Payment is required (${config.name}).`,
1962
+ service: config.id,
1963
+ price: config.price,
1964
+ currency: config.currency,
1965
+ acceptedCurrencies: acceptedTokens
1966
+ }, null, 2));
1967
+ }
1968
+ /**
1969
+ * Generate a unique challenge ID for MPP
1970
+ */
1971
+ generateChallengeId() {
1972
+ const bytes = new Uint8Array(24);
1973
+ for (let i = 0; i < bytes.length; i++) {
1974
+ bytes[i] = Math.floor(Math.random() * 256);
1975
+ }
1976
+ return Buffer.from(bytes).toString("base64url");
1977
+ }
1474
1978
  /**
1475
1979
  * Return 402 with x402 payment requirements (v2 format)
1476
1980
  * Includes requirements for all chains and all accepted currencies
@@ -1545,7 +2049,7 @@ var MoltsPayServer = class {
1545
2049
  const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
1546
2050
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
1547
2051
  const tokenAddress = tokenAddresses[selectedToken];
1548
- const tokenDomain = TOKEN_DOMAINS[selectedToken] || TOKEN_DOMAINS.USDC;
2052
+ const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
1549
2053
  return {
1550
2054
  scheme: "exact",
1551
2055
  network: selectedNetwork,
@@ -1790,7 +2294,7 @@ var MoltsPayServer = class {
1790
2294
  const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
1791
2295
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1792
2296
  const tokenAddress = tokenAddresses[selectedToken];
1793
- const tokenDomain = TOKEN_DOMAINS[selectedToken] || TOKEN_DOMAINS.USDC;
2297
+ const tokenDomain = getTokenDomain(networkId, selectedToken);
1794
2298
  return {
1795
2299
  scheme: "exact",
1796
2300
  network: networkId,
@@ -1845,6 +2349,26 @@ async function printQRCode(url) {
1845
2349
 
1846
2350
  // src/cli/index.ts
1847
2351
  var readline = __toESM(require("readline"));
2352
+ if (!globalThis.crypto) {
2353
+ globalThis.crypto = import_crypto.webcrypto;
2354
+ }
2355
+ function getVersion() {
2356
+ const locations = [
2357
+ (0, import_path2.join)(__dirname, "../../package.json"),
2358
+ (0, import_path2.join)(__dirname, "../package.json"),
2359
+ (0, import_path2.join)(process.cwd(), "node_modules/moltspay/package.json")
2360
+ ];
2361
+ for (const loc of locations) {
2362
+ try {
2363
+ if ((0, import_fs4.existsSync)(loc)) {
2364
+ const pkg = JSON.parse((0, import_fs4.readFileSync)(loc, "utf-8"));
2365
+ if (pkg.name === "moltspay") return pkg.version;
2366
+ }
2367
+ } catch {
2368
+ }
2369
+ }
2370
+ return "0.0.0";
2371
+ }
1848
2372
  var program = new import_commander.Command();
1849
2373
  var DEFAULT_CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
1850
2374
  var PID_FILE = (0, import_path2.join)(DEFAULT_CONFIG_DIR, "server.pid");
@@ -1863,7 +2387,7 @@ function prompt(question) {
1863
2387
  });
1864
2388
  });
1865
2389
  }
1866
- program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version("1.0.0");
2390
+ program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
1867
2391
  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) => {
1868
2392
  console.log("\n\u{1F510} MoltsPay Client Setup\n");
1869
2393
  if ((0, import_fs4.existsSync)((0, import_path2.join)(options.configDir, "wallet.json"))) {
@@ -1872,6 +2396,11 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
1872
2396
  return;
1873
2397
  }
1874
2398
  let chain = options.chain;
2399
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
2400
+ if (!supportedChains.includes(chain)) {
2401
+ console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
2402
+ process.exit(1);
2403
+ }
1875
2404
  let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
1876
2405
  let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
1877
2406
  if (!maxPerTx) {
@@ -1934,7 +2463,7 @@ program.command("config").description("Update MoltsPay settings").option("--max-
1934
2463
  }
1935
2464
  }
1936
2465
  });
1937
- program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base or polygon)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (amountStr, options) => {
2466
+ 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) => {
1938
2467
  const client = new MoltsPayClient({ configDir: options.configDir });
1939
2468
  if (!client.isInitialized) {
1940
2469
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -1946,8 +2475,18 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1946
2475
  return;
1947
2476
  }
1948
2477
  const chain = options.chain?.toLowerCase() || "base";
1949
- if (!["base", "polygon"].includes(chain)) {
1950
- console.log("\u274C Invalid chain. Use: base or polygon");
2478
+ if (!["base", "polygon", "base_sepolia"].includes(chain)) {
2479
+ console.log("\u274C Invalid chain. Use: base, polygon, or base_sepolia");
2480
+ return;
2481
+ }
2482
+ if (chain === "base_sepolia") {
2483
+ console.log("\n\u{1F9EA} Testnet Funding\n");
2484
+ console.log(` Wallet: ${client.address}`);
2485
+ console.log(` Chain: Base Sepolia (testnet)
2486
+ `);
2487
+ console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
2488
+ console.log(" npx moltspay faucet\n");
2489
+ console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
1951
2490
  return;
1952
2491
  }
1953
2492
  console.log("\n\u{1F4B3} Fund your agent wallet\n");
@@ -1979,6 +2518,90 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1979
2518
  console.log(`\u274C ${error.message}`);
1980
2519
  }
1981
2520
  });
2521
+ 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) => {
2522
+ let address = options.address;
2523
+ const chain = options.chain?.toLowerCase() || "base_sepolia";
2524
+ if (!["base_sepolia", "tempo_moderato"].includes(chain)) {
2525
+ console.log("\u274C Invalid chain. Use: base_sepolia or tempo_moderato");
2526
+ return;
2527
+ }
2528
+ if (!address) {
2529
+ const client = new MoltsPayClient({ configDir: options.configDir });
2530
+ if (client.isInitialized) {
2531
+ address = client.address;
2532
+ } else {
2533
+ console.log('\u274C No wallet found. Either run "npx moltspay init" or provide --address');
2534
+ return;
2535
+ }
2536
+ }
2537
+ if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
2538
+ console.log("\u274C Invalid Ethereum address");
2539
+ return;
2540
+ }
2541
+ console.log("\n\u{1F6B0} MoltsPay Testnet Faucet\n");
2542
+ if (chain === "tempo_moderato") {
2543
+ console.log(` Requesting testnet tokens on Tempo Moderato...`);
2544
+ console.log(` Address: ${address}
2545
+ `);
2546
+ try {
2547
+ const TEMPO_FAUCET_API = "https://docs.tempo.xyz/api/faucet";
2548
+ const response = await fetch(TEMPO_FAUCET_API, {
2549
+ method: "POST",
2550
+ headers: { "Content-Type": "application/json" },
2551
+ body: JSON.stringify({ address })
2552
+ });
2553
+ const result = await response.json();
2554
+ if (response.ok && result.data && result.data.length > 0) {
2555
+ console.log(`\u2705 Received testnet tokens!
2556
+ `);
2557
+ console.log(` Tokens: pathUSD, AlphaUSD, BetaUSD, ThetaUSD (1M each)`);
2558
+ console.log(` Transactions:`);
2559
+ for (const tx of result.data) {
2560
+ console.log(` https://explore.testnet.tempo.xyz/tx/${tx.hash}`);
2561
+ }
2562
+ console.log("\n\u{1F4A1} Use these tokens to test MPP payments:");
2563
+ console.log(` npx moltspay pay <service-url> <service-id> --chain tempo_moderato
2564
+ `);
2565
+ } else {
2566
+ console.log(`\u274C ${result.error || "Faucet request failed"}`);
2567
+ console.log("\n Try again later or use Tempo Wallet: https://wallet.tempo.xyz\n");
2568
+ }
2569
+ } catch (error) {
2570
+ console.log(`\u274C ${error.message}`);
2571
+ console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
2572
+ }
2573
+ } else {
2574
+ console.log(` Requesting 1 USDC on Base Sepolia...`);
2575
+ console.log(` Address: ${address}
2576
+ `);
2577
+ try {
2578
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
2579
+ const response = await fetch(FAUCET_API, {
2580
+ method: "POST",
2581
+ headers: { "Content-Type": "application/json" },
2582
+ body: JSON.stringify({ address })
2583
+ });
2584
+ const result = await response.json();
2585
+ if (!response.ok) {
2586
+ console.log(`\u274C ${result.error || "Request failed"}`);
2587
+ if (result.hint) console.log(` ${result.hint}`);
2588
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
2589
+ return;
2590
+ }
2591
+ console.log(`\u2705 Received ${result.amount} USDC!
2592
+ `);
2593
+ console.log(` Transaction: ${result.transaction}`);
2594
+ console.log(` Explorer: ${result.explorer}`);
2595
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
2596
+ `);
2597
+ console.log("\u{1F4A1} Use this USDC to test x402 payments:");
2598
+ console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
2599
+ `);
2600
+ } catch (error) {
2601
+ console.log(`\u274C ${error.message}`);
2602
+ }
2603
+ }
2604
+ });
1982
2605
  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) => {
1983
2606
  const client = new MoltsPayClient({ configDir: options.configDir });
1984
2607
  if (!client.isInitialized) {
@@ -2008,8 +2631,26 @@ program.command("status").description("Show wallet status and balance").option("
2008
2631
  console.log("");
2009
2632
  console.log(" Balances:");
2010
2633
  for (const [chainName, balance] of Object.entries(allBalances)) {
2011
- const chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
2012
- console.log(` ${chainLabel.padEnd(10)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
2634
+ let chainLabel;
2635
+ if (chainName === "base_sepolia") {
2636
+ chainLabel = "Base Sepolia";
2637
+ } else if (chainName === "tempo_moderato") {
2638
+ chainLabel = "Tempo Moderato";
2639
+ } else {
2640
+ chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
2641
+ }
2642
+ if (chainName === "tempo_moderato" && balance.tempo) {
2643
+ const tempo = balance.tempo;
2644
+ const nativeStr = balance.native > 1e12 ? balance.native.toExponential(2) : balance.native.toFixed(2);
2645
+ console.log(` ${chainLabel}:`);
2646
+ console.log(` Native: ${nativeStr} TEMPO (for gas)`);
2647
+ console.log(` pathUSD: ${tempo.pathUSD.toFixed(2)}`);
2648
+ console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
2649
+ console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
2650
+ console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
2651
+ } else {
2652
+ console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
2653
+ }
2013
2654
  }
2014
2655
  console.log("");
2015
2656
  console.log(" Spending Limits:");
@@ -2018,7 +2659,7 @@ program.command("status").description("Show wallet status and balance").option("
2018
2659
  console.log("");
2019
2660
  }
2020
2661
  });
2021
- 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, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
2662
+ 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) => {
2022
2663
  const client = new MoltsPayClient({ configDir: options.configDir });
2023
2664
  if (!client.isInitialized) {
2024
2665
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -2027,8 +2668,8 @@ program.command("list").description("List recent transactions").option("--days <
2027
2668
  const days = parseInt(options.days) || 7;
2028
2669
  const limit = parseInt(options.limit) || 20;
2029
2670
  const chain = options.chain?.toLowerCase() || "all";
2030
- if (!["base", "polygon", "all"].includes(chain)) {
2031
- console.log("\u274C Invalid chain. Use: base, polygon, or all");
2671
+ if (!["base", "polygon", "base_sepolia", "tempo_moderato", "all"].includes(chain)) {
2672
+ console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, tempo_moderato, or all");
2032
2673
  return;
2033
2674
  }
2034
2675
  const wallet = client.address;
@@ -2043,9 +2684,21 @@ program.command("list").description("List recent transactions").option("--days <
2043
2684
  api: "https://polygon.blockscout.com/api/v2",
2044
2685
  usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
2045
2686
  name: "Polygon"
2687
+ },
2688
+ base_sepolia: {
2689
+ api: "https://base-sepolia.blockscout.com/api/v2",
2690
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
2691
+ name: "Base Sepolia"
2692
+ },
2693
+ // Tempo explorer doesn't have public API yet
2694
+ tempo_moderato: {
2695
+ api: "",
2696
+ // No API available
2697
+ usdc: "0x20c0000000000000000000000000000000000000",
2698
+ name: "Tempo Moderato"
2046
2699
  }
2047
2700
  };
2048
- const chainsToQuery = chain === "all" ? ["base", "polygon"] : [chain];
2701
+ const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia", "tempo_moderato"] : [chain];
2049
2702
  console.log(`
2050
2703
  \u{1F4DC} Transactions (last ${days} day${days > 1 ? "s" : ""})
2051
2704
  `);
@@ -2053,27 +2706,136 @@ program.command("list").description("List recent transactions").option("--days <
2053
2706
  for (const c of chainsToQuery) {
2054
2707
  const explorer = explorers[c];
2055
2708
  try {
2056
- const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2057
- const response = await fetch(url);
2058
- const data = await response.json();
2059
- if (data.items && Array.isArray(data.items)) {
2060
- for (const tx of data.items) {
2061
- const timestamp = new Date(tx.timestamp).getTime();
2062
- if (timestamp < cutoffTime) continue;
2063
- const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2064
- const decimals = parseInt(tx.total.decimals) || 6;
2065
- allTxns.push({
2066
- chain: c,
2067
- timestamp,
2068
- type: isIncoming ? "IN" : "OUT",
2069
- amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2070
- other: isIncoming ? tx.from.hash : tx.to.hash,
2071
- hash: tx.transaction_hash
2072
- });
2709
+ if (c === "tempo_moderato") {
2710
+ const tempoTokens = [
2711
+ { address: "0x20c0000000000000000000000000000000000000", name: "pathUSD" },
2712
+ { address: "0x20c0000000000000000000000000000000000001", name: "alphaUSD" },
2713
+ { address: "0x20c0000000000000000000000000000000000002", name: "betaUSD" },
2714
+ { address: "0x20c0000000000000000000000000000000000003", name: "thetaUSD" }
2715
+ ];
2716
+ const transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
2717
+ const walletTopic = "0x000000000000000000000000" + wallet.toLowerCase().slice(2);
2718
+ let latestBlock = 0;
2719
+ for (let attempt = 0; attempt < 3; attempt++) {
2720
+ try {
2721
+ const blockRes = await fetch("https://rpc.moderato.tempo.xyz", {
2722
+ method: "POST",
2723
+ headers: { "Content-Type": "application/json" },
2724
+ body: JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 })
2725
+ });
2726
+ const blockData = await blockRes.json();
2727
+ if (blockData.result) {
2728
+ latestBlock = parseInt(blockData.result, 16);
2729
+ break;
2730
+ }
2731
+ } catch (e) {
2732
+ if (attempt === 2) throw e;
2733
+ await new Promise((r) => setTimeout(r, 500));
2734
+ }
2735
+ }
2736
+ if (latestBlock === 0) {
2737
+ console.log(" \u26A0\uFE0F Tempo Moderato: Could not get latest block");
2738
+ continue;
2739
+ }
2740
+ const maxBlocks = 1e5;
2741
+ const blocksPerDay = 172800;
2742
+ const requestedBlocks = blocksPerDay * days;
2743
+ const actualBlocks = Math.min(requestedBlocks, maxBlocks);
2744
+ const fromBlock = "0x" + Math.max(0, latestBlock - actualBlocks).toString(16);
2745
+ const toBlock = "0x" + latestBlock.toString(16);
2746
+ if (requestedBlocks > maxBlocks) {
2747
+ console.log(` \u2139\uFE0F Tempo: querying last ~14 hours (RPC limit: 100k blocks)`);
2748
+ }
2749
+ for (const token of tempoTokens) {
2750
+ try {
2751
+ const inRes = await fetch("https://rpc.moderato.tempo.xyz", {
2752
+ method: "POST",
2753
+ headers: { "Content-Type": "application/json" },
2754
+ body: JSON.stringify({
2755
+ jsonrpc: "2.0",
2756
+ method: "eth_getLogs",
2757
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, null, walletTopic] }],
2758
+ id: 1
2759
+ })
2760
+ });
2761
+ const inData = await inRes.json();
2762
+ if (inData.error) {
2763
+ console.log(` \u26A0\uFE0F ${token.name}: ${inData.error.message}`);
2764
+ continue;
2765
+ }
2766
+ if (inData.result && Array.isArray(inData.result)) {
2767
+ for (const log of inData.result) {
2768
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
2769
+ if (timestamp < cutoffTime) continue;
2770
+ const amount = parseInt(log.data, 16) / 1e6;
2771
+ const from = "0x" + log.topics[1].slice(26);
2772
+ allTxns.push({
2773
+ chain: c,
2774
+ timestamp,
2775
+ type: "IN",
2776
+ amount,
2777
+ other: from,
2778
+ hash: log.transactionHash,
2779
+ token: token.name
2780
+ });
2781
+ }
2782
+ }
2783
+ const outRes = await fetch("https://rpc.moderato.tempo.xyz", {
2784
+ method: "POST",
2785
+ headers: { "Content-Type": "application/json" },
2786
+ body: JSON.stringify({
2787
+ jsonrpc: "2.0",
2788
+ method: "eth_getLogs",
2789
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, walletTopic, null] }],
2790
+ id: 1
2791
+ })
2792
+ });
2793
+ const outData = await outRes.json();
2794
+ if (outData.result && Array.isArray(outData.result)) {
2795
+ for (const log of outData.result) {
2796
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
2797
+ if (timestamp < cutoffTime) continue;
2798
+ const amount = parseInt(log.data, 16) / 1e6;
2799
+ const to = "0x" + log.topics[2].slice(26);
2800
+ allTxns.push({
2801
+ chain: c,
2802
+ timestamp,
2803
+ type: "OUT",
2804
+ amount,
2805
+ other: to,
2806
+ hash: log.transactionHash,
2807
+ token: token.name
2808
+ });
2809
+ }
2810
+ }
2811
+ } catch (tokenError) {
2812
+ continue;
2813
+ }
2814
+ }
2815
+ } else {
2816
+ const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2817
+ const response = await fetch(url);
2818
+ const data = await response.json();
2819
+ if (data.items && Array.isArray(data.items)) {
2820
+ for (const tx of data.items) {
2821
+ const timestamp = new Date(tx.timestamp).getTime();
2822
+ if (timestamp < cutoffTime) continue;
2823
+ const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2824
+ const decimals = parseInt(tx.total.decimals) || 6;
2825
+ allTxns.push({
2826
+ chain: c,
2827
+ timestamp,
2828
+ type: isIncoming ? "IN" : "OUT",
2829
+ amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2830
+ other: isIncoming ? tx.from.hash : tx.to.hash,
2831
+ hash: tx.transaction_hash
2832
+ });
2833
+ }
2073
2834
  }
2074
2835
  }
2075
2836
  } catch (error) {
2076
- console.log(` \u26A0\uFE0F ${explorer.name}: API error`);
2837
+ const errMsg = error instanceof Error ? error.message : String(error);
2838
+ console.log(` \u26A0\uFE0F ${explorer.name}: ${errMsg}`);
2077
2839
  }
2078
2840
  }
2079
2841
  allTxns.sort((a, b) => b.timestamp - a.timestamp);
@@ -2086,8 +2848,12 @@ program.command("list").description("List recent transactions").option("--days <
2086
2848
  const color = tx.type === "IN" ? "\x1B[32m" : "\x1B[31m";
2087
2849
  const reset = "\x1B[0m";
2088
2850
  const date = new Date(tx.timestamp).toISOString().slice(5, 16).replace("T", " ");
2089
- const chainTag = chain === "all" ? `[${tx.chain.toUpperCase()}] ` : "";
2090
- console.log(` ${color}${sign}${tx.amount.toFixed(2)} USDC${reset} | ${chainTag}${tx.type === "IN" ? "from" : "to"} ${tx.other.slice(0, 10)}...${tx.other.slice(-4)} | ${date}`);
2851
+ let chainLabel = tx.chain.toUpperCase();
2852
+ if (tx.chain === "tempo_moderato") chainLabel = "TEMPO";
2853
+ else if (tx.chain === "base_sepolia") chainLabel = "BASE_SEPOLIA";
2854
+ const chainTag = chain === "all" ? `[${chainLabel}] ` : "";
2855
+ const tokenName = tx.token || "USDC";
2856
+ console.log(` ${color}${sign}${tx.amount.toFixed(2)} ${tokenName}${reset} | ${chainTag}${tx.type === "IN" ? "from" : "to"} ${tx.other.slice(0, 10)}...${tx.other.slice(-4)} | ${date}`);
2091
2857
  }
2092
2858
  const inTotal = allTxns.filter((t) => t.type === "IN").reduce((s, t) => s + t.amount, 0);
2093
2859
  const outTotal = allTxns.filter((t) => t.type === "OUT").reduce((s, t) => s + t.amount, 0);
@@ -2096,39 +2862,88 @@ program.command("list").description("List recent transactions").option("--days <
2096
2862
  `);
2097
2863
  }
2098
2864
  });
2099
- program.command("services <url>").description("List services from a provider").option("--json", "Output as JSON").action(async (url, options) => {
2865
+ program.command("services [url]").description("List services from registry or a specific provider").option("-q, --query <keyword>", "Search by keyword (name, description, tags)").option("--max-price <price>", "Maximum price in USD").option("--type <type>", "Filter by type: api_service | file_download").option("--tag <tag>", "Filter by tag").option("--json", "Output as JSON").action(async (url, options) => {
2866
+ const MOLTSPAY_REGISTRY = "https://moltspay.com";
2100
2867
  try {
2101
- const client = new MoltsPayClient();
2102
- const services = await client.getServices(url);
2868
+ let services;
2869
+ let isRegistry = false;
2870
+ if (url) {
2871
+ const client = new MoltsPayClient();
2872
+ services = await client.getServices(url);
2873
+ } else {
2874
+ isRegistry = true;
2875
+ const params = new URLSearchParams();
2876
+ if (options.query) params.set("q", options.query);
2877
+ if (options.maxPrice) params.set("maxPrice", options.maxPrice);
2878
+ if (options.type) params.set("type", options.type);
2879
+ if (options.tag) params.set("tag", options.tag);
2880
+ const queryString = params.toString();
2881
+ const registryUrl = `${MOLTSPAY_REGISTRY}/registry/services${queryString ? "?" + queryString : ""}`;
2882
+ const res = await fetch(registryUrl);
2883
+ if (!res.ok) {
2884
+ throw new Error(`Registry request failed: ${res.status}`);
2885
+ }
2886
+ services = await res.json();
2887
+ }
2103
2888
  if (options.json) {
2104
2889
  console.log(JSON.stringify(services, null, 2));
2105
2890
  } else {
2106
- if (services.provider) {
2107
- console.log(`
2108
- \u{1F3EA} ${services.provider.name}
2891
+ const serviceList = services.services || [];
2892
+ if (isRegistry) {
2893
+ if (options.query) {
2894
+ console.log(`
2895
+ \u{1F50D} Search: "${options.query}" (${serviceList.length} results)
2896
+ `);
2897
+ } else {
2898
+ const filters = [];
2899
+ if (options.maxPrice) filters.push(`max $${options.maxPrice}`);
2900
+ if (options.type) filters.push(options.type);
2901
+ if (options.tag) filters.push(`#${options.tag}`);
2902
+ const filterStr = filters.length > 0 ? ` (${filters.join(", ")})` : "";
2903
+ console.log(`
2904
+ \u{1F50D} MoltsPay Registry${filterStr} - ${serviceList.length} services
2109
2905
  `);
2110
- console.log(` ${services.provider.description || ""}`);
2111
- console.log(` Wallet: ${services.provider.wallet}`);
2112
- const chains = services.provider.chains ? Array.isArray(services.provider.chains) ? services.provider.chains.map((c) => typeof c === "string" ? c : c.chain).join(", ") : services.provider.chains : services.provider.chain || "base";
2113
- console.log(` Chains: ${chains}`);
2906
+ }
2907
+ for (const svc of serviceList) {
2908
+ const name = (svc.name || svc.id).slice(0, 30).padEnd(30);
2909
+ const price = `$${svc.price}`.padEnd(8);
2910
+ const type = (svc.type || "unknown").padEnd(14);
2911
+ const provider = `@${svc.provider?.username || "unknown"}`;
2912
+ console.log(` ${name} ${price} ${type} ${provider}`);
2913
+ }
2914
+ if (serviceList.length > 0) {
2915
+ console.log(`
2916
+ \u{1F4A1} Use: moltspay pay <provider-url> <service-id>
2917
+ `);
2918
+ }
2114
2919
  } else {
2115
- console.log(`
2116
- \u{1F3EA} MoltsPay Service Registry
2920
+ if (services.provider) {
2921
+ console.log(`
2922
+ \u{1F3EA} ${services.provider.name}
2923
+ `);
2924
+ console.log(` ${services.provider.description || ""}`);
2925
+ console.log(` Wallet: ${services.provider.wallet}`);
2926
+ const chains = services.provider.chains ? Array.isArray(services.provider.chains) ? services.provider.chains.map((c) => typeof c === "string" ? c : c.chain).join(", ") : services.provider.chains : services.provider.chain || "base";
2927
+ console.log(` Chains: ${chains}`);
2928
+ } else {
2929
+ console.log(`
2930
+ \u{1F3EA} Provider Services
2117
2931
  `);
2118
- console.log(` ${services.services?.length || 0} services available`);
2119
- }
2120
- console.log("\n\u{1F4E6} Services:\n");
2121
- for (const svc of services.services) {
2122
- const status = svc.available !== false ? "\u2705" : "\u274C";
2123
- console.log(` ${status} ${svc.id || svc.name}`);
2124
- console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2125
- if (svc.description) {
2126
- console.log(` ${svc.description}`);
2932
+ console.log(` ${serviceList.length} services available`);
2127
2933
  }
2128
- if (svc.provider && !services.provider) {
2129
- console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
2934
+ console.log("\n\u{1F4E6} Services:\n");
2935
+ for (const svc of serviceList) {
2936
+ const status = svc.available !== false ? "\u2705" : "\u274C";
2937
+ console.log(` ${status} ${svc.id || svc.name}`);
2938
+ console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2939
+ if (svc.description) {
2940
+ console.log(` ${svc.description}`);
2941
+ }
2942
+ if (svc.provider && !services.provider) {
2943
+ console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
2944
+ }
2945
+ console.log("");
2130
2946
  }
2131
- console.log("");
2132
2947
  }
2133
2948
  }
2134
2949
  } catch (err) {
@@ -2342,8 +3157,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2342
3157
  process.exit(1);
2343
3158
  }
2344
3159
  });
2345
- 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 or polygon). Required if server accepts multiple chains.").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
2346
- const client = new MoltsPayClient();
3160
+ 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) => {
3161
+ const client = new MoltsPayClient({ configDir: options.configDir });
2347
3162
  if (!client.isInitialized) {
2348
3163
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
2349
3164
  process.exit(1);
@@ -2372,13 +3187,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2372
3187
  params.image_base64 = imageData.toString("base64");
2373
3188
  }
2374
3189
  }
2375
- if (!params.prompt) {
2376
- console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
2377
- process.exit(1);
2378
- }
2379
3190
  const chain = options.chain?.toLowerCase();
2380
- if (chain && !["base", "polygon"].includes(chain)) {
2381
- console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon`);
3191
+ if (chain && !["base", "polygon", "base_sepolia", "tempo_moderato"].includes(chain)) {
3192
+ console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon, base_sepolia, tempo_moderato`);
2382
3193
  process.exit(1);
2383
3194
  }
2384
3195
  const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
@@ -2409,10 +3220,22 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2409
3220
  console.log("");
2410
3221
  }
2411
3222
  try {
2412
- const result = await client.pay(server, service, params, {
2413
- token,
2414
- chain
2415
- });
3223
+ let result;
3224
+ if (chain === "tempo_moderato") {
3225
+ if (!options.json) {
3226
+ console.log(" Protocol: MPP (Machine Payments Protocol)");
3227
+ console.log("");
3228
+ }
3229
+ const mppUrl = server.includes(service) ? server : `${server}/${service}`;
3230
+ result = await client.payWithMPP(mppUrl, {
3231
+ body: params
3232
+ });
3233
+ } else {
3234
+ result = await client.pay(server, service, params, {
3235
+ token,
3236
+ chain
3237
+ });
3238
+ }
2416
3239
  if (options.json) {
2417
3240
  console.log(JSON.stringify(result));
2418
3241
  } else {