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
@@ -7,14 +7,19 @@ var __esm = (fn, res) => function __init() {
7
7
  // node_modules/tsup/assets/esm_shims.js
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
10
+ var getFilename, getDirname, __dirname;
10
11
  var init_esm_shims = __esm({
11
12
  "node_modules/tsup/assets/esm_shims.js"() {
12
13
  "use strict";
14
+ getFilename = () => fileURLToPath(import.meta.url);
15
+ getDirname = () => path.dirname(getFilename());
16
+ __dirname = /* @__PURE__ */ getDirname();
13
17
  }
14
18
  });
15
19
 
16
20
  // src/cli/index.ts
17
21
  init_esm_shims();
22
+ import { webcrypto } from "crypto";
18
23
  import { Command } from "commander";
19
24
  import { homedir as homedir2 } from "os";
20
25
  import { join as join4, dirname, resolve } from "path";
@@ -40,12 +45,15 @@ var CHAINS = {
40
45
  USDC: {
41
46
  address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
42
47
  decimals: 6,
43
- symbol: "USDC"
48
+ symbol: "USDC",
49
+ eip712Name: "USD Coin"
50
+ // EIP-712 domain name
44
51
  },
45
52
  USDT: {
46
53
  address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
47
54
  decimals: 6,
48
- symbol: "USDT"
55
+ symbol: "USDT",
56
+ eip712Name: "Tether USD"
49
57
  }
50
58
  },
51
59
  usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -62,12 +70,15 @@ var CHAINS = {
62
70
  USDC: {
63
71
  address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
64
72
  decimals: 6,
65
- symbol: "USDC"
73
+ symbol: "USDC",
74
+ eip712Name: "USD Coin"
66
75
  },
67
76
  USDT: {
68
77
  address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
69
78
  decimals: 6,
70
- symbol: "USDT"
79
+ symbol: "USDT",
80
+ eip712Name: "(PoS) Tether USD"
81
+ // Polygon uses this name
71
82
  }
72
83
  },
73
84
  usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
@@ -75,27 +86,6 @@ var CHAINS = {
75
86
  explorerTx: "https://polygonscan.com/tx/",
76
87
  avgBlockTime: 2
77
88
  },
78
- ethereum: {
79
- name: "Ethereum",
80
- chainId: 1,
81
- rpc: "https://eth.llamarpc.com",
82
- tokens: {
83
- USDC: {
84
- address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
85
- decimals: 6,
86
- symbol: "USDC"
87
- },
88
- USDT: {
89
- address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
90
- decimals: 6,
91
- symbol: "USDT"
92
- }
93
- },
94
- usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
95
- explorer: "https://etherscan.io/address/",
96
- explorerTx: "https://etherscan.io/tx/",
97
- avgBlockTime: 12
98
- },
99
89
  // ============ Testnet ============
100
90
  base_sepolia: {
101
91
  name: "Base Sepolia",
@@ -105,13 +95,17 @@ var CHAINS = {
105
95
  USDC: {
106
96
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
107
97
  decimals: 6,
108
- symbol: "USDC"
98
+ symbol: "USDC",
99
+ eip712Name: "USDC"
100
+ // Testnet USDC uses 'USDC' not 'USD Coin'
109
101
  },
110
102
  USDT: {
111
103
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
112
104
  // Same as USDC on testnet (no official USDT)
113
105
  decimals: 6,
114
- symbol: "USDT"
106
+ symbol: "USDT",
107
+ eip712Name: "USDC"
108
+ // Uses same contract as USDC
115
109
  }
116
110
  },
117
111
  usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
@@ -119,27 +113,34 @@ var CHAINS = {
119
113
  explorerTx: "https://sepolia.basescan.org/tx/",
120
114
  avgBlockTime: 2
121
115
  },
122
- sepolia: {
123
- name: "Sepolia",
124
- chainId: 11155111,
125
- rpc: "https://rpc.sepolia.org",
116
+ // ============ Tempo Testnet (Moderato) ============
117
+ tempo_moderato: {
118
+ name: "Tempo Moderato",
119
+ chainId: 42431,
120
+ rpc: "https://rpc.moderato.tempo.xyz",
126
121
  tokens: {
122
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
123
+ // Note: Tempo uses USD as native gas token, not ETH
127
124
  USDC: {
128
- address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
125
+ address: "0x20c0000000000000000000000000000000000000",
126
+ // pathUSD - primary testnet stablecoin
129
127
  decimals: 6,
130
- symbol: "USDC"
128
+ symbol: "USDC",
129
+ eip712Name: "pathUSD"
131
130
  },
132
131
  USDT: {
133
- address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
134
- // Same as USDC on testnet
132
+ address: "0x20c0000000000000000000000000000000000001",
133
+ // alphaUSD
135
134
  decimals: 6,
136
- symbol: "USDT"
135
+ symbol: "USDT",
136
+ eip712Name: "alphaUSD"
137
137
  }
138
138
  },
139
- usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
140
- explorer: "https://sepolia.etherscan.io/address/",
141
- explorerTx: "https://sepolia.etherscan.io/tx/",
142
- avgBlockTime: 12
139
+ usdc: "0x20c0000000000000000000000000000000000000",
140
+ explorer: "https://explore.testnet.tempo.xyz/address/",
141
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
142
+ avgBlockTime: 0.5
143
+ // ~500ms finality
143
144
  }
144
145
  };
145
146
  function getChain(name) {
@@ -305,7 +306,7 @@ Server accepts: ${serverChains.join(", ")}`
305
306
  } else {
306
307
  throw new Error(
307
308
  `Server accepts: ${serverChains.join(", ")}
308
- Please specify: --chain base or --chain polygon`
309
+ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
309
310
  );
310
311
  }
311
312
  }
@@ -347,13 +348,19 @@ Please specify: --chain base or --chain polygon`
347
348
  if (!payTo) {
348
349
  throw new Error("Missing payTo address in payment requirements");
349
350
  }
350
- const authorization = await this.signEIP3009(payTo, amount, chain, token);
351
+ const domainOverride = req.extra && typeof req.extra === "object" && req.extra.name ? { name: req.extra.name, version: req.extra.version || "2" } : void 0;
352
+ const authorization = await this.signEIP3009(payTo, amount, chain, token, domainOverride);
351
353
  const tokenConfig = chain.tokens[token];
352
- const tokenName = token === "USDC" ? "USD Coin" : "Tether USD";
354
+ const extra = req.extra && typeof req.extra === "object" ? req.extra : {
355
+ name: tokenConfig.eip712Name || "USD Coin",
356
+ version: "2"
357
+ };
353
358
  const payload = {
354
359
  x402Version: X402_VERSION,
360
+ scheme: "exact",
361
+ network,
355
362
  payload: authorization,
356
- // v2 requires 'accepted' field with the requirements being fulfilled
363
+ // { authorization: {...}, signature: "0x..." }
357
364
  accepted: {
358
365
  scheme: "exact",
359
366
  network,
@@ -361,7 +368,7 @@ Please specify: --chain base or --chain polygon`
361
368
  amount: amountRaw,
362
369
  payTo,
363
370
  maxTimeoutSeconds: req.maxTimeoutSeconds || 300,
364
- extra: req.extra || { name: tokenName, version: "2" }
371
+ extra
365
372
  }
366
373
  };
367
374
  const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
@@ -391,7 +398,7 @@ Please specify: --chain base or --chain polygon`
391
398
  * This only signs - no on-chain transaction, no gas needed.
392
399
  * Supports both USDC and USDT.
393
400
  */
394
- async signEIP3009(to, amount, chain, token = "USDC") {
401
+ async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
395
402
  const validAfter = 0;
396
403
  const validBefore = Math.floor(Date.now() / 1e3) + 3600;
397
404
  const nonce = ethers.hexlify(ethers.randomBytes(32));
@@ -405,10 +412,11 @@ Please specify: --chain base or --chain polygon`
405
412
  validBefore: validBefore.toString(),
406
413
  nonce
407
414
  };
408
- const tokenName = token === "USDC" ? "USD Coin" : "Tether USD";
415
+ const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
416
+ const tokenVersion = domainOverride?.version || "2";
409
417
  const domain = {
410
418
  name: tokenName,
411
- version: "2",
419
+ version: tokenVersion,
412
420
  chainId: chain.chainId,
413
421
  verifyingContract: tokenConfig.address
414
422
  };
@@ -571,30 +579,59 @@ Please specify: --chain base or --chain polygon`
571
579
  };
572
580
  }
573
581
  /**
574
- * Get wallet balances on all supported chains (Base + Polygon)
582
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
575
583
  */
576
584
  async getAllBalances() {
577
585
  if (!this.wallet) {
578
586
  throw new Error("Client not initialized");
579
587
  }
580
- const supportedChains = ["base", "polygon"];
588
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
581
589
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
582
590
  const results = {};
591
+ const tempoTokens = {
592
+ pathUSD: "0x20c0000000000000000000000000000000000000",
593
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
594
+ betaUSD: "0x20c0000000000000000000000000000000000002",
595
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
596
+ };
583
597
  await Promise.all(
584
598
  supportedChains.map(async (chainName) => {
585
599
  try {
586
600
  const chain = getChain(chainName);
587
601
  const provider = new ethers.JsonRpcProvider(chain.rpc);
588
- const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
589
- provider.getBalance(this.wallet.address),
590
- new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
591
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
592
- ]);
593
- results[chainName] = {
594
- usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
595
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
596
- native: parseFloat(ethers.formatEther(nativeBalance))
597
- };
602
+ if (chainName === "tempo_moderato") {
603
+ const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
604
+ provider.getBalance(this.wallet.address),
605
+ new ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
606
+ new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
607
+ new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
608
+ new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
609
+ ]);
610
+ results[chainName] = {
611
+ usdc: parseFloat(ethers.formatUnits(pathUSD, 6)),
612
+ // pathUSD as default USDC
613
+ usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
614
+ // alphaUSD as default USDT
615
+ native: parseFloat(ethers.formatEther(nativeBalance)),
616
+ tempo: {
617
+ pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
618
+ alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
619
+ betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
620
+ thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
621
+ }
622
+ };
623
+ } else {
624
+ const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
625
+ provider.getBalance(this.wallet.address),
626
+ new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
627
+ new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
628
+ ]);
629
+ results[chainName] = {
630
+ usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
631
+ usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
632
+ native: parseFloat(ethers.formatEther(nativeBalance))
633
+ };
634
+ }
598
635
  } catch (err) {
599
636
  results[chainName] = { usdc: 0, usdt: 0, native: 0 };
600
637
  }
@@ -602,6 +639,121 @@ Please specify: --chain base or --chain polygon`
602
639
  );
603
640
  return results;
604
641
  }
642
+ /**
643
+ * Pay for a service using MPP (Machine Payments Protocol)
644
+ *
645
+ * This implements the MPP flow manually for EOA wallets:
646
+ * 1. Request service → get 402 with WWW-Authenticate
647
+ * 2. Parse payment requirements
648
+ * 3. Execute transfer on Tempo chain
649
+ * 4. Retry with transaction hash as credential
650
+ *
651
+ * @param url - Full URL of the MPP-enabled endpoint
652
+ * @param options - Request options (body, headers)
653
+ * @returns Response from the service
654
+ */
655
+ async payWithMPP(url, options = {}) {
656
+ if (!this.wallet || !this.walletData) {
657
+ throw new Error("Client not initialized. Run: npx moltspay init");
658
+ }
659
+ const { privateKeyToAccount } = await import("viem/accounts");
660
+ const { createWalletClient, createPublicClient, http } = await import("viem");
661
+ const { tempoModerato } = await import("viem/chains");
662
+ const { Actions } = await import("viem/tempo");
663
+ const privateKey = this.walletData.privateKey;
664
+ const account = privateKeyToAccount(privateKey);
665
+ console.log(`[MoltsPay] Making MPP request to: ${url}`);
666
+ console.log(`[MoltsPay] Using account: ${account.address}`);
667
+ const initResponse = await fetch(url, {
668
+ method: "POST",
669
+ headers: {
670
+ "Content-Type": "application/json",
671
+ ...options.headers
672
+ },
673
+ body: options.body ? JSON.stringify(options.body) : void 0
674
+ });
675
+ if (initResponse.status !== 402) {
676
+ if (initResponse.ok) {
677
+ return initResponse.json();
678
+ }
679
+ const errorText = await initResponse.text();
680
+ throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
681
+ }
682
+ const wwwAuth = initResponse.headers.get("www-authenticate");
683
+ if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
684
+ throw new Error("No WWW-Authenticate Payment challenge in 402 response");
685
+ }
686
+ console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
687
+ const parseAuthParam = (header, key) => {
688
+ const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
689
+ return match ? match[1] : null;
690
+ };
691
+ const challengeId = parseAuthParam(wwwAuth, "id");
692
+ const method = parseAuthParam(wwwAuth, "method");
693
+ const realm = parseAuthParam(wwwAuth, "realm");
694
+ const requestB64 = parseAuthParam(wwwAuth, "request");
695
+ if (method !== "tempo") {
696
+ throw new Error(`Unsupported payment method: ${method}`);
697
+ }
698
+ if (!requestB64) {
699
+ throw new Error("Missing request in WWW-Authenticate");
700
+ }
701
+ const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
702
+ const paymentRequest = JSON.parse(requestJson);
703
+ console.log(`[MoltsPay] Payment request:`, paymentRequest);
704
+ const { amount, currency, recipient, methodDetails } = paymentRequest;
705
+ const chainId = methodDetails?.chainId || 42431;
706
+ console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
707
+ console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
708
+ const tempoChain = { ...tempoModerato, feeToken: currency };
709
+ const publicClient = createPublicClient({
710
+ chain: tempoChain,
711
+ transport: http("https://rpc.moderato.tempo.xyz")
712
+ });
713
+ const walletClient = createWalletClient({
714
+ account,
715
+ chain: tempoChain,
716
+ transport: http("https://rpc.moderato.tempo.xyz")
717
+ });
718
+ const txHash = await Actions.token.transfer(walletClient, {
719
+ to: recipient,
720
+ amount: BigInt(amount),
721
+ token: currency
722
+ });
723
+ console.log(`[MoltsPay] Transaction sent: ${txHash}`);
724
+ console.log(`[MoltsPay] Waiting for confirmation...`);
725
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
726
+ console.log(`[MoltsPay] Transaction confirmed!`);
727
+ const challenge = {
728
+ id: challengeId,
729
+ realm,
730
+ method: "tempo",
731
+ intent: "charge",
732
+ request: paymentRequest
733
+ };
734
+ const credential = {
735
+ challenge,
736
+ payload: { hash: txHash, type: "hash" },
737
+ source: `did:pkh:eip155:${chainId}:${account.address}`
738
+ };
739
+ const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
740
+ console.log(`[MoltsPay] Retrying with payment credential...`);
741
+ const paidResponse = await fetch(url, {
742
+ method: "POST",
743
+ headers: {
744
+ "Content-Type": "application/json",
745
+ "Authorization": `Payment ${credentialB64}`,
746
+ ...options.headers
747
+ },
748
+ body: options.body ? JSON.stringify(options.body) : void 0
749
+ });
750
+ if (!paidResponse.ok) {
751
+ const errorText = await paidResponse.text();
752
+ throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
753
+ }
754
+ console.log(`[MoltsPay] Payment verified! Service completed.`);
755
+ return paidResponse.json();
756
+ }
605
757
  };
606
758
 
607
759
  // src/server/index.ts
@@ -626,8 +778,8 @@ init_esm_shims();
626
778
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
627
779
  import * as path2 from "path";
628
780
  var X402_VERSION2 = 2;
629
- var CDP_MAINNET_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
630
- var CDP_TESTNET_URL = "https://www.x402.org/facilitator";
781
+ var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
782
+ var TESTNET_CHAIN_IDS = [84532];
631
783
  function loadEnvFile() {
632
784
  const envPaths = [
633
785
  path2.join(process.cwd(), ".env"),
@@ -662,31 +814,33 @@ var CDPFacilitator = class extends BaseFacilitator {
662
814
  displayName = "Coinbase CDP";
663
815
  supportedNetworks;
664
816
  endpoint;
665
- useMainnet;
666
817
  apiKeyId;
667
818
  apiKeySecret;
668
819
  constructor(config = {}) {
669
820
  super();
670
821
  loadEnvFile();
671
- this.useMainnet = config.useMainnet ?? process.env.USE_MAINNET?.toLowerCase() === "true";
672
822
  this.apiKeyId = config.apiKeyId || process.env.CDP_API_KEY_ID;
673
823
  this.apiKeySecret = config.apiKeySecret || process.env.CDP_API_KEY_SECRET;
674
- this.endpoint = this.useMainnet ? CDP_MAINNET_URL : CDP_TESTNET_URL;
675
- this.supportedNetworks = this.useMainnet ? ["eip155:8453", "eip155:137"] : ["eip155:8453", "eip155:84532", "eip155:137"];
676
- if (this.useMainnet && (!this.apiKeyId || !this.apiKeySecret)) {
677
- console.warn("[CDPFacilitator] WARNING: Mainnet mode but missing CDP credentials!");
678
- console.warn("[CDPFacilitator] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET");
824
+ this.endpoint = CDP_URL;
825
+ this.supportedNetworks = [
826
+ "eip155:8453",
827
+ // Base mainnet
828
+ "eip155:137",
829
+ // Polygon mainnet
830
+ "eip155:84532"
831
+ // Base Sepolia (testnet)
832
+ ];
833
+ if (!this.apiKeyId || !this.apiKeySecret) {
834
+ console.warn("[CDPFacilitator] WARNING: Missing CDP credentials!");
835
+ console.warn("[CDPFacilitator] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in ~/.moltspay/.env");
679
836
  }
680
837
  }
681
838
  /**
682
839
  * Get auth headers for CDP API requests
683
840
  */
684
841
  async getAuthHeaders(method, urlPath, body) {
685
- if (!this.useMainnet) {
686
- return {};
687
- }
688
842
  if (!this.apiKeyId || !this.apiKeySecret) {
689
- throw new Error("CDP credentials required for mainnet");
843
+ throw new Error("CDP credentials required. Set CDP_API_KEY_ID and CDP_API_KEY_SECRET");
690
844
  }
691
845
  try {
692
846
  const { getAuthHeaders } = await import("@coinbase/cdp-sdk/auth");
@@ -738,23 +892,23 @@ var CDPFacilitator = class extends BaseFacilitator {
738
892
  paymentPayload,
739
893
  paymentRequirements: requirements
740
894
  };
895
+ console.log("[CDP Verify] Payload:", JSON.stringify(paymentPayload, null, 2));
896
+ const authHeaders = await this.getAuthHeaders(
897
+ "POST",
898
+ "/platform/v2/x402/verify",
899
+ requestBody
900
+ );
741
901
  const headers = {
742
- "Content-Type": "application/json"
902
+ "Content-Type": "application/json",
903
+ ...authHeaders
743
904
  };
744
- if (this.useMainnet) {
745
- const authHeaders = await this.getAuthHeaders(
746
- "POST",
747
- "/platform/v2/x402/verify",
748
- requestBody
749
- );
750
- Object.assign(headers, authHeaders);
751
- }
752
905
  const response = await fetch(`${this.endpoint}/verify`, {
753
906
  method: "POST",
754
907
  headers,
755
908
  body: JSON.stringify(requestBody)
756
909
  });
757
910
  const result = await response.json();
911
+ console.log("[CDP Verify] Response:", response.status, JSON.stringify(result));
758
912
  if (!response.ok || !result.isValid) {
759
913
  return {
760
914
  valid: false,
@@ -780,17 +934,15 @@ var CDPFacilitator = class extends BaseFacilitator {
780
934
  paymentPayload,
781
935
  paymentRequirements: requirements
782
936
  };
937
+ const authHeaders = await this.getAuthHeaders(
938
+ "POST",
939
+ "/platform/v2/x402/settle",
940
+ requestBody
941
+ );
783
942
  const headers = {
784
- "Content-Type": "application/json"
943
+ "Content-Type": "application/json",
944
+ ...authHeaders
785
945
  };
786
- if (this.useMainnet) {
787
- const authHeaders = await this.getAuthHeaders(
788
- "POST",
789
- "/platform/v2/x402/settle",
790
- requestBody
791
- );
792
- Object.assign(headers, authHeaders);
793
- }
794
946
  const response = await fetch(`${this.endpoint}/settle`, {
795
947
  method: "POST",
796
948
  headers,
@@ -825,13 +977,140 @@ var CDPFacilitator = class extends BaseFacilitator {
825
977
  freeQuota: 1e3
826
978
  };
827
979
  }
980
+ /**
981
+ * Check if a chain ID is testnet
982
+ */
983
+ static isTestnet(chainId) {
984
+ return TESTNET_CHAIN_IDS.includes(chainId);
985
+ }
828
986
  /**
829
987
  * Get configuration summary (for logging)
830
988
  */
831
989
  getConfigSummary() {
832
- const mode = this.useMainnet ? "mainnet" : "testnet";
833
990
  const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
834
- return `CDP Facilitator (${mode}, credentials: ${hasCredentials ? "yes" : "no"})`;
991
+ const networks = this.supportedNetworks.join(", ");
992
+ return `CDP Facilitator (networks: ${networks}, credentials: ${hasCredentials ? "yes" : "no"})`;
993
+ }
994
+ };
995
+
996
+ // src/facilitators/tempo.ts
997
+ init_esm_shims();
998
+ var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
999
+ var TempoFacilitator = class extends BaseFacilitator {
1000
+ name = "tempo";
1001
+ displayName = "Tempo Testnet";
1002
+ supportedNetworks = ["eip155:42431"];
1003
+ // Tempo Moderato
1004
+ rpcUrl;
1005
+ constructor() {
1006
+ super();
1007
+ this.rpcUrl = CHAINS.tempo_moderato.rpc;
1008
+ }
1009
+ async healthCheck() {
1010
+ const start = Date.now();
1011
+ try {
1012
+ const response = await fetch(this.rpcUrl, {
1013
+ method: "POST",
1014
+ headers: { "Content-Type": "application/json" },
1015
+ body: JSON.stringify({
1016
+ jsonrpc: "2.0",
1017
+ method: "eth_chainId",
1018
+ params: [],
1019
+ id: 1
1020
+ })
1021
+ });
1022
+ const data = await response.json();
1023
+ const chainId = parseInt(data.result, 16);
1024
+ if (chainId !== 42431) {
1025
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
1026
+ }
1027
+ return { healthy: true, latencyMs: Date.now() - start };
1028
+ } catch (error) {
1029
+ return { healthy: false, error: String(error) };
1030
+ }
1031
+ }
1032
+ async verify(paymentPayload, requirements) {
1033
+ 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);
1039
+ if (!receipt) {
1040
+ return { valid: false, error: "Transaction not found" };
1041
+ }
1042
+ if (receipt.status !== "0x1") {
1043
+ return { valid: false, error: "Transaction failed" };
1044
+ }
1045
+ const transferLog = receipt.logs.find(
1046
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
1047
+ );
1048
+ if (!transferLog) {
1049
+ return { valid: false, error: "No Transfer event found" };
1050
+ }
1051
+ 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
+ };
1058
+ }
1059
+ 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
+ };
1074
+ }
1075
+ return {
1076
+ valid: true,
1077
+ details: {
1078
+ txHash: tempoPayload.txHash,
1079
+ from: "0x" + transferLog.topics[1].slice(26),
1080
+ to: toAddress,
1081
+ amount: amount.toString(),
1082
+ token: tokenAddress
1083
+ }
1084
+ };
1085
+ } catch (error) {
1086
+ return { valid: false, error: `Verification failed: ${error}` };
1087
+ }
1088
+ }
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"
1099
+ };
1100
+ }
1101
+ async getTransactionReceipt(txHash) {
1102
+ const response = await fetch(this.rpcUrl, {
1103
+ method: "POST",
1104
+ headers: { "Content-Type": "application/json" },
1105
+ body: JSON.stringify({
1106
+ jsonrpc: "2.0",
1107
+ method: "eth_getTransactionReceipt",
1108
+ params: [txHash],
1109
+ id: 1
1110
+ })
1111
+ });
1112
+ const data = await response.json();
1113
+ return data.result;
835
1114
  }
836
1115
  };
837
1116
 
@@ -844,7 +1123,8 @@ var FacilitatorRegistry = class {
844
1123
  roundRobinIndex = 0;
845
1124
  constructor(selection) {
846
1125
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
847
- this.selection = selection || { primary: "cdp", strategy: "failover" };
1126
+ this.registerFactory("tempo", () => new TempoFacilitator());
1127
+ this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
848
1128
  }
849
1129
  /**
850
1130
  * Register a new facilitator factory
@@ -1068,6 +1348,9 @@ var X402_VERSION3 = 2;
1068
1348
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
1069
1349
  var PAYMENT_HEADER2 = "x-payment";
1070
1350
  var PAYMENT_RESPONSE_HEADER = "x-payment-response";
1351
+ var MPP_AUTH_HEADER = "authorization";
1352
+ var MPP_WWW_AUTH_HEADER = "www-authenticate";
1353
+ var MPP_RECEIPT_HEADER = "payment-receipt";
1071
1354
  var TOKEN_ADDRESSES = {
1072
1355
  "eip155:8453": {
1073
1356
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -1081,17 +1364,48 @@ var TOKEN_ADDRESSES = {
1081
1364
  "eip155:137": {
1082
1365
  USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
1083
1366
  USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
1367
+ },
1368
+ "eip155:42431": {
1369
+ // Tempo Moderato testnet - TIP-20 stablecoins
1370
+ USDC: "0x20c0000000000000000000000000000000000000",
1371
+ // pathUSD
1372
+ USDT: "0x20c0000000000000000000000000000000000001"
1373
+ // alphaUSD
1084
1374
  }
1085
1375
  };
1086
1376
  var CHAIN_TO_NETWORK = {
1087
1377
  "base": "eip155:8453",
1088
1378
  "base_sepolia": "eip155:84532",
1089
- "polygon": "eip155:137"
1379
+ "polygon": "eip155:137",
1380
+ "tempo_moderato": "eip155:42431"
1090
1381
  };
1091
1382
  var TOKEN_DOMAINS = {
1092
- USDC: { name: "USD Coin", version: "2" },
1093
- USDT: { name: "Tether USD", version: "2" }
1383
+ // Base mainnet
1384
+ "eip155:8453": {
1385
+ USDC: { name: "USD Coin", version: "2" },
1386
+ USDT: { name: "Tether USD", version: "2" }
1387
+ },
1388
+ // Base Sepolia testnet - USDC uses 'USDC' not 'USD Coin'
1389
+ "eip155:84532": {
1390
+ USDC: { name: "USDC", version: "2" },
1391
+ USDT: { name: "USDC", version: "2" }
1392
+ // Same contract as USDC on testnet
1393
+ },
1394
+ // Polygon mainnet
1395
+ "eip155:137": {
1396
+ USDC: { name: "USD Coin", version: "2" },
1397
+ USDT: { name: "(PoS) Tether USD", version: "2" }
1398
+ },
1399
+ // Tempo Moderato testnet - TIP-20 stablecoins
1400
+ "eip155:42431": {
1401
+ USDC: { name: "pathUSD", version: "1" },
1402
+ USDT: { name: "alphaUSD", version: "1" }
1403
+ }
1094
1404
  };
1405
+ function getTokenDomain(network, token) {
1406
+ const networkDomains = TOKEN_DOMAINS[network] || TOKEN_DOMAINS["eip155:8453"];
1407
+ return networkDomains[token] || { name: "USD Coin", version: "2" };
1408
+ }
1095
1409
  function getAcceptedCurrencies(config) {
1096
1410
  return config.acceptedCurrencies ?? [config.currency];
1097
1411
  }
@@ -1143,9 +1457,11 @@ var MoltsPayServer = class {
1143
1457
  };
1144
1458
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
1145
1459
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
1460
+ const defaultFallback = ["tempo"];
1461
+ const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
1146
1462
  const facilitatorConfig = options.facilitators || {
1147
1463
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
1148
- fallback: process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean),
1464
+ fallback: envFallback || defaultFallback,
1149
1465
  strategy: process.env.FACILITATOR_STRATEGY || "failover",
1150
1466
  config: {
1151
1467
  cdp: { useMainnet: this.useMainnet }
@@ -1185,11 +1501,14 @@ var MoltsPayServer = class {
1185
1501
  getProviderChains() {
1186
1502
  const provider = this.manifest.provider;
1187
1503
  if (provider.chains && provider.chains.length > 0) {
1188
- return provider.chains.map((c) => ({
1189
- network: c.network || CHAIN_TO_NETWORK[c.chain] || "eip155:8453",
1190
- wallet: c.wallet || provider.wallet,
1191
- tokens: c.tokens || ["USDC"]
1192
- }));
1504
+ return provider.chains.map((c) => {
1505
+ const chainName = typeof c === "string" ? c : c.chain;
1506
+ return {
1507
+ network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
1508
+ wallet: (typeof c === "object" ? c.wallet : null) || provider.wallet,
1509
+ tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
1510
+ };
1511
+ });
1193
1512
  }
1194
1513
  const chain = provider.chain || "base";
1195
1514
  const network = CHAIN_TO_NETWORK[chain] || this.networkId;
@@ -1236,8 +1555,8 @@ var MoltsPayServer = class {
1236
1555
  async handleRequest(req, res) {
1237
1556
  res.setHeader("Access-Control-Allow-Origin", "*");
1238
1557
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1239
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
1240
- res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
1558
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
1559
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
1241
1560
  if (req.method === "OPTIONS") {
1242
1561
  res.writeHead(204);
1243
1562
  res.end();
@@ -1268,6 +1587,14 @@ var MoltsPayServer = class {
1268
1587
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1269
1588
  return await this.handleProxy(body, paymentHeader, res);
1270
1589
  }
1590
+ const servicePath = url.pathname.replace(/^\//, "");
1591
+ const skill = this.skills.get(servicePath);
1592
+ if (skill && (req.method === "POST" || req.method === "GET")) {
1593
+ const body = req.method === "POST" ? await this.readBody(req) : {};
1594
+ const authHeader = req.headers[MPP_AUTH_HEADER];
1595
+ const x402Header = req.headers[PAYMENT_HEADER2];
1596
+ return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
1597
+ }
1271
1598
  this.sendJson(res, 404, { error: "Not found" });
1272
1599
  } catch (err) {
1273
1600
  console.error("[MoltsPay] Error:", err);
@@ -1399,7 +1726,7 @@ var MoltsPayServer = class {
1399
1726
  const paymentNetwork = payment.accepted?.network || payment.network || this.networkId;
1400
1727
  const paymentWallet = this.getWalletForNetwork(paymentNetwork);
1401
1728
  const requirements = this.buildPaymentRequirements(skill.config, paymentNetwork, paymentWallet, paymentToken);
1402
- console.log(`[MoltsPay] Verifying payment...`);
1729
+ console.log(`[MoltsPay] Verifying payment on ${paymentNetwork}...`);
1403
1730
  const verifyResult = await this.registry.verify(payment, requirements);
1404
1731
  if (!verifyResult.valid) {
1405
1732
  return this.sendJson(res, 402, {
@@ -1451,6 +1778,187 @@ var MoltsPayServer = class {
1451
1778
  payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
1452
1779
  }, responseHeaders);
1453
1780
  }
1781
+ /**
1782
+ * Handle MPP (Machine Payments Protocol) request
1783
+ * Supports both x402 and MPP protocols on service endpoints
1784
+ */
1785
+ async handleMPPRequest(skill, body, authHeader, x402Header, res) {
1786
+ const config = skill.config;
1787
+ const params = body || {};
1788
+ if (x402Header) {
1789
+ return await this.handleExecute({ service: config.id, params }, x402Header, res);
1790
+ }
1791
+ if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
1792
+ return await this.handleMPPPayment(skill, params, authHeader, res);
1793
+ }
1794
+ return this.sendMPPPaymentRequired(config, res);
1795
+ }
1796
+ /**
1797
+ * Handle MPP payment verification and service execution
1798
+ */
1799
+ async handleMPPPayment(skill, params, authHeader, res) {
1800
+ const config = skill.config;
1801
+ const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
1802
+ if (!credentialMatch) {
1803
+ return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
1804
+ }
1805
+ let mppCredential;
1806
+ try {
1807
+ const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
1808
+ const decoded = Buffer.from(base64, "base64").toString("utf-8");
1809
+ mppCredential = JSON.parse(decoded);
1810
+ } catch (err) {
1811
+ console.error("[MoltsPay] Failed to parse MPP credential:", err);
1812
+ return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
1813
+ }
1814
+ let txHash;
1815
+ if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
1816
+ txHash = mppCredential.payload.hash;
1817
+ } else if (mppCredential.payload?.type === "transaction") {
1818
+ return this.sendJson(res, 400, {
1819
+ error: "Transaction type not supported. Please use push mode (hash type)."
1820
+ });
1821
+ }
1822
+ if (!txHash) {
1823
+ return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
1824
+ }
1825
+ let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
1826
+ if (!chainId && mppCredential.source) {
1827
+ const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
1828
+ if (chainMatch) chainId = parseInt(chainMatch[1], 10);
1829
+ }
1830
+ chainId = chainId || 42431;
1831
+ const network = `eip155:${chainId}`;
1832
+ if (!this.isNetworkAccepted(network)) {
1833
+ return this.sendJson(res, 402, {
1834
+ error: `Network not accepted: ${network}`
1835
+ });
1836
+ }
1837
+ const requirements = this.buildPaymentRequirements(
1838
+ config,
1839
+ network,
1840
+ this.getWalletForNetwork(network),
1841
+ "USDC"
1842
+ );
1843
+ const paymentPayload = {
1844
+ x402Version: X402_VERSION3,
1845
+ scheme: "exact",
1846
+ network,
1847
+ payload: {
1848
+ txHash,
1849
+ chainId
1850
+ }
1851
+ };
1852
+ console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
1853
+ const verification = await this.registry.verify(paymentPayload, requirements);
1854
+ if (!verification.valid) {
1855
+ return this.sendJson(res, 402, {
1856
+ error: `Payment verification failed: ${verification.error}`
1857
+ });
1858
+ }
1859
+ console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
1860
+ let result;
1861
+ try {
1862
+ result = await skill.handler(params);
1863
+ } catch (err) {
1864
+ console.error(`[MoltsPay] Skill execution error:`, err);
1865
+ return this.sendJson(res, 500, {
1866
+ error: `Service execution failed: ${err.message}`
1867
+ });
1868
+ }
1869
+ const receipt = {
1870
+ success: true,
1871
+ txHash,
1872
+ network,
1873
+ facilitator: verification.facilitator
1874
+ };
1875
+ const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
1876
+ res.writeHead(200, {
1877
+ "Content-Type": "application/json",
1878
+ [MPP_RECEIPT_HEADER]: receiptEncoded
1879
+ });
1880
+ res.end(JSON.stringify({
1881
+ success: true,
1882
+ result,
1883
+ payment: {
1884
+ txHash,
1885
+ status: "verified",
1886
+ facilitator: verification.facilitator
1887
+ }
1888
+ }, null, 2));
1889
+ }
1890
+ /**
1891
+ * Return 402 with both x402 and MPP payment requirements
1892
+ */
1893
+ sendMPPPaymentRequired(config, res) {
1894
+ const acceptedTokens = getAcceptedCurrencies(config);
1895
+ const providerChains = this.getProviderChains();
1896
+ const accepts = [];
1897
+ for (const chainConfig of providerChains) {
1898
+ for (const token of acceptedTokens) {
1899
+ if (chainConfig.tokens.includes(token)) {
1900
+ accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
1901
+ }
1902
+ }
1903
+ }
1904
+ const x402PaymentRequired = {
1905
+ x402Version: X402_VERSION3,
1906
+ accepts,
1907
+ acceptedCurrencies: acceptedTokens,
1908
+ resource: {
1909
+ url: `/${config.id}`,
1910
+ description: `${config.name} - $${config.price} ${config.currency}`
1911
+ }
1912
+ };
1913
+ const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
1914
+ const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
1915
+ let mppWwwAuth = "";
1916
+ if (tempoChain) {
1917
+ const challengeId = this.generateChallengeId();
1918
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
1919
+ const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
1920
+ const mppRequest = {
1921
+ amount: amountInUnits,
1922
+ currency: tokenAddress,
1923
+ methodDetails: {
1924
+ chainId: 42431,
1925
+ feePayer: true
1926
+ },
1927
+ recipient: tempoChain.wallet
1928
+ };
1929
+ const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
1930
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
1931
+ mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
1932
+ }
1933
+ const headers = {
1934
+ "Content-Type": "application/problem+json",
1935
+ [PAYMENT_REQUIRED_HEADER2]: x402Encoded
1936
+ };
1937
+ if (mppWwwAuth) {
1938
+ headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
1939
+ }
1940
+ res.writeHead(402, headers);
1941
+ res.end(JSON.stringify({
1942
+ type: "https://paymentauth.org/problems/payment-required",
1943
+ title: "Payment Required",
1944
+ status: 402,
1945
+ detail: `Payment is required (${config.name}).`,
1946
+ service: config.id,
1947
+ price: config.price,
1948
+ currency: config.currency,
1949
+ acceptedCurrencies: acceptedTokens
1950
+ }, null, 2));
1951
+ }
1952
+ /**
1953
+ * Generate a unique challenge ID for MPP
1954
+ */
1955
+ generateChallengeId() {
1956
+ const bytes = new Uint8Array(24);
1957
+ for (let i = 0; i < bytes.length; i++) {
1958
+ bytes[i] = Math.floor(Math.random() * 256);
1959
+ }
1960
+ return Buffer.from(bytes).toString("base64url");
1961
+ }
1454
1962
  /**
1455
1963
  * Return 402 with x402 payment requirements (v2 format)
1456
1964
  * Includes requirements for all chains and all accepted currencies
@@ -1525,7 +2033,7 @@ var MoltsPayServer = class {
1525
2033
  const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
1526
2034
  const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
1527
2035
  const tokenAddress = tokenAddresses[selectedToken];
1528
- const tokenDomain = TOKEN_DOMAINS[selectedToken] || TOKEN_DOMAINS.USDC;
2036
+ const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
1529
2037
  return {
1530
2038
  scheme: "exact",
1531
2039
  network: selectedNetwork,
@@ -1770,7 +2278,7 @@ var MoltsPayServer = class {
1770
2278
  const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
1771
2279
  const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
1772
2280
  const tokenAddress = tokenAddresses[selectedToken];
1773
- const tokenDomain = TOKEN_DOMAINS[selectedToken] || TOKEN_DOMAINS.USDC;
2281
+ const tokenDomain = getTokenDomain(networkId, selectedToken);
1774
2282
  return {
1775
2283
  scheme: "exact",
1776
2284
  network: networkId,
@@ -1825,6 +2333,26 @@ async function printQRCode(url) {
1825
2333
 
1826
2334
  // src/cli/index.ts
1827
2335
  import * as readline from "readline";
2336
+ if (!globalThis.crypto) {
2337
+ globalThis.crypto = webcrypto;
2338
+ }
2339
+ function getVersion() {
2340
+ const locations = [
2341
+ join4(__dirname, "../../package.json"),
2342
+ join4(__dirname, "../package.json"),
2343
+ join4(process.cwd(), "node_modules/moltspay/package.json")
2344
+ ];
2345
+ for (const loc of locations) {
2346
+ try {
2347
+ if (existsSync4(loc)) {
2348
+ const pkg = JSON.parse(readFileSync4(loc, "utf-8"));
2349
+ if (pkg.name === "moltspay") return pkg.version;
2350
+ }
2351
+ } catch {
2352
+ }
2353
+ }
2354
+ return "0.0.0";
2355
+ }
1828
2356
  var program = new Command();
1829
2357
  var DEFAULT_CONFIG_DIR = join4(homedir2(), ".moltspay");
1830
2358
  var PID_FILE = join4(DEFAULT_CONFIG_DIR, "server.pid");
@@ -1843,7 +2371,7 @@ function prompt(question) {
1843
2371
  });
1844
2372
  });
1845
2373
  }
1846
- program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version("1.0.0");
2374
+ program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
1847
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) => {
1848
2376
  console.log("\n\u{1F510} MoltsPay Client Setup\n");
1849
2377
  if (existsSync4(join4(options.configDir, "wallet.json"))) {
@@ -1852,6 +2380,11 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
1852
2380
  return;
1853
2381
  }
1854
2382
  let chain = options.chain;
2383
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
2384
+ if (!supportedChains.includes(chain)) {
2385
+ console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
2386
+ process.exit(1);
2387
+ }
1855
2388
  let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
1856
2389
  let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
1857
2390
  if (!maxPerTx) {
@@ -1914,7 +2447,7 @@ program.command("config").description("Update MoltsPay settings").option("--max-
1914
2447
  }
1915
2448
  }
1916
2449
  });
1917
- 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) => {
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) => {
1918
2451
  const client = new MoltsPayClient({ configDir: options.configDir });
1919
2452
  if (!client.isInitialized) {
1920
2453
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -1926,8 +2459,18 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1926
2459
  return;
1927
2460
  }
1928
2461
  const chain = options.chain?.toLowerCase() || "base";
1929
- if (!["base", "polygon"].includes(chain)) {
1930
- console.log("\u274C Invalid chain. Use: base or polygon");
2462
+ if (!["base", "polygon", "base_sepolia"].includes(chain)) {
2463
+ console.log("\u274C Invalid chain. Use: base, polygon, or base_sepolia");
2464
+ return;
2465
+ }
2466
+ if (chain === "base_sepolia") {
2467
+ console.log("\n\u{1F9EA} Testnet Funding\n");
2468
+ console.log(` Wallet: ${client.address}`);
2469
+ console.log(` Chain: Base Sepolia (testnet)
2470
+ `);
2471
+ console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
2472
+ console.log(" npx moltspay faucet\n");
2473
+ console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
1931
2474
  return;
1932
2475
  }
1933
2476
  console.log("\n\u{1F4B3} Fund your agent wallet\n");
@@ -1959,6 +2502,90 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1959
2502
  console.log(`\u274C ${error.message}`);
1960
2503
  }
1961
2504
  });
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) => {
2506
+ let address = options.address;
2507
+ 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");
2510
+ return;
2511
+ }
2512
+ if (!address) {
2513
+ const client = new MoltsPayClient({ configDir: options.configDir });
2514
+ if (client.isInitialized) {
2515
+ address = client.address;
2516
+ } else {
2517
+ console.log('\u274C No wallet found. Either run "npx moltspay init" or provide --address');
2518
+ return;
2519
+ }
2520
+ }
2521
+ if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
2522
+ console.log("\u274C Invalid Ethereum address");
2523
+ return;
2524
+ }
2525
+ console.log("\n\u{1F6B0} MoltsPay Testnet Faucet\n");
2526
+ if (chain === "tempo_moderato") {
2527
+ console.log(` Requesting testnet tokens on Tempo Moderato...`);
2528
+ console.log(` Address: ${address}
2529
+ `);
2530
+ try {
2531
+ const TEMPO_FAUCET_API = "https://docs.tempo.xyz/api/faucet";
2532
+ const response = await fetch(TEMPO_FAUCET_API, {
2533
+ method: "POST",
2534
+ headers: { "Content-Type": "application/json" },
2535
+ body: JSON.stringify({ address })
2536
+ });
2537
+ const result = await response.json();
2538
+ if (response.ok && result.data && result.data.length > 0) {
2539
+ console.log(`\u2705 Received testnet tokens!
2540
+ `);
2541
+ console.log(` Tokens: pathUSD, AlphaUSD, BetaUSD, ThetaUSD (1M each)`);
2542
+ console.log(` Transactions:`);
2543
+ for (const tx of result.data) {
2544
+ console.log(` https://explore.testnet.tempo.xyz/tx/${tx.hash}`);
2545
+ }
2546
+ console.log("\n\u{1F4A1} Use these tokens to test MPP payments:");
2547
+ console.log(` npx moltspay pay <service-url> <service-id> --chain tempo_moderato
2548
+ `);
2549
+ } else {
2550
+ console.log(`\u274C ${result.error || "Faucet request failed"}`);
2551
+ console.log("\n Try again later or use Tempo Wallet: https://wallet.tempo.xyz\n");
2552
+ }
2553
+ } catch (error) {
2554
+ console.log(`\u274C ${error.message}`);
2555
+ console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
2556
+ }
2557
+ } else {
2558
+ console.log(` Requesting 1 USDC on Base Sepolia...`);
2559
+ console.log(` Address: ${address}
2560
+ `);
2561
+ try {
2562
+ const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
2563
+ const response = await fetch(FAUCET_API, {
2564
+ method: "POST",
2565
+ headers: { "Content-Type": "application/json" },
2566
+ body: JSON.stringify({ address })
2567
+ });
2568
+ const result = await response.json();
2569
+ if (!response.ok) {
2570
+ console.log(`\u274C ${result.error || "Request failed"}`);
2571
+ if (result.hint) console.log(` ${result.hint}`);
2572
+ if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
2573
+ return;
2574
+ }
2575
+ console.log(`\u2705 Received ${result.amount} USDC!
2576
+ `);
2577
+ console.log(` Transaction: ${result.transaction}`);
2578
+ console.log(` Explorer: ${result.explorer}`);
2579
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
2580
+ `);
2581
+ console.log("\u{1F4A1} Use this USDC to test x402 payments:");
2582
+ console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
2583
+ `);
2584
+ } catch (error) {
2585
+ console.log(`\u274C ${error.message}`);
2586
+ }
2587
+ }
2588
+ });
1962
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) => {
1963
2590
  const client = new MoltsPayClient({ configDir: options.configDir });
1964
2591
  if (!client.isInitialized) {
@@ -1988,8 +2615,26 @@ program.command("status").description("Show wallet status and balance").option("
1988
2615
  console.log("");
1989
2616
  console.log(" Balances:");
1990
2617
  for (const [chainName, balance] of Object.entries(allBalances)) {
1991
- const chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
1992
- console.log(` ${chainLabel.padEnd(10)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
2618
+ let chainLabel;
2619
+ if (chainName === "base_sepolia") {
2620
+ chainLabel = "Base Sepolia";
2621
+ } else if (chainName === "tempo_moderato") {
2622
+ chainLabel = "Tempo Moderato";
2623
+ } else {
2624
+ chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
2625
+ }
2626
+ if (chainName === "tempo_moderato" && balance.tempo) {
2627
+ const tempo = balance.tempo;
2628
+ const nativeStr = balance.native > 1e12 ? balance.native.toExponential(2) : balance.native.toFixed(2);
2629
+ console.log(` ${chainLabel}:`);
2630
+ console.log(` Native: ${nativeStr} TEMPO (for gas)`);
2631
+ console.log(` pathUSD: ${tempo.pathUSD.toFixed(2)}`);
2632
+ console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
2633
+ console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
2634
+ console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
2635
+ } else {
2636
+ console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
2637
+ }
1993
2638
  }
1994
2639
  console.log("");
1995
2640
  console.log(" Spending Limits:");
@@ -1998,7 +2643,7 @@ program.command("status").description("Show wallet status and balance").option("
1998
2643
  console.log("");
1999
2644
  }
2000
2645
  });
2001
- 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) => {
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) => {
2002
2647
  const client = new MoltsPayClient({ configDir: options.configDir });
2003
2648
  if (!client.isInitialized) {
2004
2649
  console.log("\u274C Not initialized. Run: npx moltspay init");
@@ -2007,8 +2652,8 @@ program.command("list").description("List recent transactions").option("--days <
2007
2652
  const days = parseInt(options.days) || 7;
2008
2653
  const limit = parseInt(options.limit) || 20;
2009
2654
  const chain = options.chain?.toLowerCase() || "all";
2010
- if (!["base", "polygon", "all"].includes(chain)) {
2011
- console.log("\u274C Invalid chain. Use: base, polygon, or all");
2655
+ if (!["base", "polygon", "base_sepolia", "tempo_moderato", "all"].includes(chain)) {
2656
+ console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, tempo_moderato, or all");
2012
2657
  return;
2013
2658
  }
2014
2659
  const wallet = client.address;
@@ -2023,9 +2668,21 @@ program.command("list").description("List recent transactions").option("--days <
2023
2668
  api: "https://polygon.blockscout.com/api/v2",
2024
2669
  usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
2025
2670
  name: "Polygon"
2671
+ },
2672
+ base_sepolia: {
2673
+ api: "https://base-sepolia.blockscout.com/api/v2",
2674
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
2675
+ name: "Base Sepolia"
2676
+ },
2677
+ // Tempo explorer doesn't have public API yet
2678
+ tempo_moderato: {
2679
+ api: "",
2680
+ // No API available
2681
+ usdc: "0x20c0000000000000000000000000000000000000",
2682
+ name: "Tempo Moderato"
2026
2683
  }
2027
2684
  };
2028
- const chainsToQuery = chain === "all" ? ["base", "polygon"] : [chain];
2685
+ const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia", "tempo_moderato"] : [chain];
2029
2686
  console.log(`
2030
2687
  \u{1F4DC} Transactions (last ${days} day${days > 1 ? "s" : ""})
2031
2688
  `);
@@ -2033,27 +2690,136 @@ program.command("list").description("List recent transactions").option("--days <
2033
2690
  for (const c of chainsToQuery) {
2034
2691
  const explorer = explorers[c];
2035
2692
  try {
2036
- const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2037
- const response = await fetch(url);
2038
- const data = await response.json();
2039
- if (data.items && Array.isArray(data.items)) {
2040
- for (const tx of data.items) {
2041
- const timestamp = new Date(tx.timestamp).getTime();
2042
- if (timestamp < cutoffTime) continue;
2043
- const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2044
- const decimals = parseInt(tx.total.decimals) || 6;
2045
- allTxns.push({
2046
- chain: c,
2047
- timestamp,
2048
- type: isIncoming ? "IN" : "OUT",
2049
- amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2050
- other: isIncoming ? tx.from.hash : tx.to.hash,
2051
- hash: tx.transaction_hash
2052
- });
2693
+ if (c === "tempo_moderato") {
2694
+ const tempoTokens = [
2695
+ { address: "0x20c0000000000000000000000000000000000000", name: "pathUSD" },
2696
+ { address: "0x20c0000000000000000000000000000000000001", name: "alphaUSD" },
2697
+ { address: "0x20c0000000000000000000000000000000000002", name: "betaUSD" },
2698
+ { address: "0x20c0000000000000000000000000000000000003", name: "thetaUSD" }
2699
+ ];
2700
+ const transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
2701
+ const walletTopic = "0x000000000000000000000000" + wallet.toLowerCase().slice(2);
2702
+ let latestBlock = 0;
2703
+ for (let attempt = 0; attempt < 3; attempt++) {
2704
+ try {
2705
+ const blockRes = await fetch("https://rpc.moderato.tempo.xyz", {
2706
+ method: "POST",
2707
+ headers: { "Content-Type": "application/json" },
2708
+ body: JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 })
2709
+ });
2710
+ const blockData = await blockRes.json();
2711
+ if (blockData.result) {
2712
+ latestBlock = parseInt(blockData.result, 16);
2713
+ break;
2714
+ }
2715
+ } catch (e) {
2716
+ if (attempt === 2) throw e;
2717
+ await new Promise((r) => setTimeout(r, 500));
2718
+ }
2719
+ }
2720
+ if (latestBlock === 0) {
2721
+ console.log(" \u26A0\uFE0F Tempo Moderato: Could not get latest block");
2722
+ continue;
2723
+ }
2724
+ const maxBlocks = 1e5;
2725
+ const blocksPerDay = 172800;
2726
+ const requestedBlocks = blocksPerDay * days;
2727
+ const actualBlocks = Math.min(requestedBlocks, maxBlocks);
2728
+ const fromBlock = "0x" + Math.max(0, latestBlock - actualBlocks).toString(16);
2729
+ const toBlock = "0x" + latestBlock.toString(16);
2730
+ if (requestedBlocks > maxBlocks) {
2731
+ console.log(` \u2139\uFE0F Tempo: querying last ~14 hours (RPC limit: 100k blocks)`);
2732
+ }
2733
+ for (const token of tempoTokens) {
2734
+ try {
2735
+ const inRes = await fetch("https://rpc.moderato.tempo.xyz", {
2736
+ method: "POST",
2737
+ headers: { "Content-Type": "application/json" },
2738
+ body: JSON.stringify({
2739
+ jsonrpc: "2.0",
2740
+ method: "eth_getLogs",
2741
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, null, walletTopic] }],
2742
+ id: 1
2743
+ })
2744
+ });
2745
+ const inData = await inRes.json();
2746
+ if (inData.error) {
2747
+ console.log(` \u26A0\uFE0F ${token.name}: ${inData.error.message}`);
2748
+ continue;
2749
+ }
2750
+ if (inData.result && Array.isArray(inData.result)) {
2751
+ for (const log of inData.result) {
2752
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
2753
+ if (timestamp < cutoffTime) continue;
2754
+ const amount = parseInt(log.data, 16) / 1e6;
2755
+ const from = "0x" + log.topics[1].slice(26);
2756
+ allTxns.push({
2757
+ chain: c,
2758
+ timestamp,
2759
+ type: "IN",
2760
+ amount,
2761
+ other: from,
2762
+ hash: log.transactionHash,
2763
+ token: token.name
2764
+ });
2765
+ }
2766
+ }
2767
+ const outRes = await fetch("https://rpc.moderato.tempo.xyz", {
2768
+ method: "POST",
2769
+ headers: { "Content-Type": "application/json" },
2770
+ body: JSON.stringify({
2771
+ jsonrpc: "2.0",
2772
+ method: "eth_getLogs",
2773
+ params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, walletTopic, null] }],
2774
+ id: 1
2775
+ })
2776
+ });
2777
+ const outData = await outRes.json();
2778
+ if (outData.result && Array.isArray(outData.result)) {
2779
+ for (const log of outData.result) {
2780
+ const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
2781
+ if (timestamp < cutoffTime) continue;
2782
+ const amount = parseInt(log.data, 16) / 1e6;
2783
+ const to = "0x" + log.topics[2].slice(26);
2784
+ allTxns.push({
2785
+ chain: c,
2786
+ timestamp,
2787
+ type: "OUT",
2788
+ amount,
2789
+ other: to,
2790
+ hash: log.transactionHash,
2791
+ token: token.name
2792
+ });
2793
+ }
2794
+ }
2795
+ } catch (tokenError) {
2796
+ continue;
2797
+ }
2798
+ }
2799
+ } else {
2800
+ const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2801
+ const response = await fetch(url);
2802
+ const data = await response.json();
2803
+ if (data.items && Array.isArray(data.items)) {
2804
+ for (const tx of data.items) {
2805
+ const timestamp = new Date(tx.timestamp).getTime();
2806
+ if (timestamp < cutoffTime) continue;
2807
+ const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2808
+ const decimals = parseInt(tx.total.decimals) || 6;
2809
+ allTxns.push({
2810
+ chain: c,
2811
+ timestamp,
2812
+ type: isIncoming ? "IN" : "OUT",
2813
+ amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2814
+ other: isIncoming ? tx.from.hash : tx.to.hash,
2815
+ hash: tx.transaction_hash
2816
+ });
2817
+ }
2053
2818
  }
2054
2819
  }
2055
2820
  } catch (error) {
2056
- console.log(` \u26A0\uFE0F ${explorer.name}: API error`);
2821
+ const errMsg = error instanceof Error ? error.message : String(error);
2822
+ console.log(` \u26A0\uFE0F ${explorer.name}: ${errMsg}`);
2057
2823
  }
2058
2824
  }
2059
2825
  allTxns.sort((a, b) => b.timestamp - a.timestamp);
@@ -2066,8 +2832,12 @@ program.command("list").description("List recent transactions").option("--days <
2066
2832
  const color = tx.type === "IN" ? "\x1B[32m" : "\x1B[31m";
2067
2833
  const reset = "\x1B[0m";
2068
2834
  const date = new Date(tx.timestamp).toISOString().slice(5, 16).replace("T", " ");
2069
- const chainTag = chain === "all" ? `[${tx.chain.toUpperCase()}] ` : "";
2070
- 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}`);
2835
+ let chainLabel = tx.chain.toUpperCase();
2836
+ if (tx.chain === "tempo_moderato") chainLabel = "TEMPO";
2837
+ else if (tx.chain === "base_sepolia") chainLabel = "BASE_SEPOLIA";
2838
+ const chainTag = chain === "all" ? `[${chainLabel}] ` : "";
2839
+ const tokenName = tx.token || "USDC";
2840
+ 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}`);
2071
2841
  }
2072
2842
  const inTotal = allTxns.filter((t) => t.type === "IN").reduce((s, t) => s + t.amount, 0);
2073
2843
  const outTotal = allTxns.filter((t) => t.type === "OUT").reduce((s, t) => s + t.amount, 0);
@@ -2076,39 +2846,88 @@ program.command("list").description("List recent transactions").option("--days <
2076
2846
  `);
2077
2847
  }
2078
2848
  });
2079
- program.command("services <url>").description("List services from a provider").option("--json", "Output as JSON").action(async (url, options) => {
2849
+ 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) => {
2850
+ const MOLTSPAY_REGISTRY = "https://moltspay.com";
2080
2851
  try {
2081
- const client = new MoltsPayClient();
2082
- const services = await client.getServices(url);
2852
+ let services;
2853
+ let isRegistry = false;
2854
+ if (url) {
2855
+ const client = new MoltsPayClient();
2856
+ services = await client.getServices(url);
2857
+ } else {
2858
+ isRegistry = true;
2859
+ const params = new URLSearchParams();
2860
+ if (options.query) params.set("q", options.query);
2861
+ if (options.maxPrice) params.set("maxPrice", options.maxPrice);
2862
+ if (options.type) params.set("type", options.type);
2863
+ if (options.tag) params.set("tag", options.tag);
2864
+ const queryString = params.toString();
2865
+ const registryUrl = `${MOLTSPAY_REGISTRY}/registry/services${queryString ? "?" + queryString : ""}`;
2866
+ const res = await fetch(registryUrl);
2867
+ if (!res.ok) {
2868
+ throw new Error(`Registry request failed: ${res.status}`);
2869
+ }
2870
+ services = await res.json();
2871
+ }
2083
2872
  if (options.json) {
2084
2873
  console.log(JSON.stringify(services, null, 2));
2085
2874
  } else {
2086
- if (services.provider) {
2087
- console.log(`
2088
- \u{1F3EA} ${services.provider.name}
2875
+ const serviceList = services.services || [];
2876
+ if (isRegistry) {
2877
+ if (options.query) {
2878
+ console.log(`
2879
+ \u{1F50D} Search: "${options.query}" (${serviceList.length} results)
2880
+ `);
2881
+ } else {
2882
+ const filters = [];
2883
+ if (options.maxPrice) filters.push(`max $${options.maxPrice}`);
2884
+ if (options.type) filters.push(options.type);
2885
+ if (options.tag) filters.push(`#${options.tag}`);
2886
+ const filterStr = filters.length > 0 ? ` (${filters.join(", ")})` : "";
2887
+ console.log(`
2888
+ \u{1F50D} MoltsPay Registry${filterStr} - ${serviceList.length} services
2089
2889
  `);
2090
- console.log(` ${services.provider.description || ""}`);
2091
- console.log(` Wallet: ${services.provider.wallet}`);
2092
- 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";
2093
- console.log(` Chains: ${chains}`);
2890
+ }
2891
+ for (const svc of serviceList) {
2892
+ const name = (svc.name || svc.id).slice(0, 30).padEnd(30);
2893
+ const price = `$${svc.price}`.padEnd(8);
2894
+ const type = (svc.type || "unknown").padEnd(14);
2895
+ const provider = `@${svc.provider?.username || "unknown"}`;
2896
+ console.log(` ${name} ${price} ${type} ${provider}`);
2897
+ }
2898
+ if (serviceList.length > 0) {
2899
+ console.log(`
2900
+ \u{1F4A1} Use: moltspay pay <provider-url> <service-id>
2901
+ `);
2902
+ }
2094
2903
  } else {
2095
- console.log(`
2096
- \u{1F3EA} MoltsPay Service Registry
2904
+ if (services.provider) {
2905
+ console.log(`
2906
+ \u{1F3EA} ${services.provider.name}
2907
+ `);
2908
+ console.log(` ${services.provider.description || ""}`);
2909
+ console.log(` Wallet: ${services.provider.wallet}`);
2910
+ 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";
2911
+ console.log(` Chains: ${chains}`);
2912
+ } else {
2913
+ console.log(`
2914
+ \u{1F3EA} Provider Services
2097
2915
  `);
2098
- console.log(` ${services.services?.length || 0} services available`);
2099
- }
2100
- console.log("\n\u{1F4E6} Services:\n");
2101
- for (const svc of services.services) {
2102
- const status = svc.available !== false ? "\u2705" : "\u274C";
2103
- console.log(` ${status} ${svc.id || svc.name}`);
2104
- console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2105
- if (svc.description) {
2106
- console.log(` ${svc.description}`);
2916
+ console.log(` ${serviceList.length} services available`);
2107
2917
  }
2108
- if (svc.provider && !services.provider) {
2109
- console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
2918
+ console.log("\n\u{1F4E6} Services:\n");
2919
+ for (const svc of serviceList) {
2920
+ const status = svc.available !== false ? "\u2705" : "\u274C";
2921
+ console.log(` ${status} ${svc.id || svc.name}`);
2922
+ console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2923
+ if (svc.description) {
2924
+ console.log(` ${svc.description}`);
2925
+ }
2926
+ if (svc.provider && !services.provider) {
2927
+ console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
2928
+ }
2929
+ console.log("");
2110
2930
  }
2111
- console.log("");
2112
2931
  }
2113
2932
  }
2114
2933
  } catch (err) {
@@ -2322,8 +3141,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2322
3141
  process.exit(1);
2323
3142
  }
2324
3143
  });
2325
- 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) => {
2326
- const client = new MoltsPayClient();
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) => {
3145
+ const client = new MoltsPayClient({ configDir: options.configDir });
2327
3146
  if (!client.isInitialized) {
2328
3147
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
2329
3148
  process.exit(1);
@@ -2352,13 +3171,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2352
3171
  params.image_base64 = imageData.toString("base64");
2353
3172
  }
2354
3173
  }
2355
- if (!params.prompt) {
2356
- console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
2357
- process.exit(1);
2358
- }
2359
3174
  const chain = options.chain?.toLowerCase();
2360
- if (chain && !["base", "polygon"].includes(chain)) {
2361
- console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon`);
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`);
2362
3177
  process.exit(1);
2363
3178
  }
2364
3179
  const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
@@ -2389,10 +3204,22 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2389
3204
  console.log("");
2390
3205
  }
2391
3206
  try {
2392
- const result = await client.pay(server, service, params, {
2393
- token,
2394
- chain
2395
- });
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
+ }
2396
3223
  if (options.json) {
2397
3224
  console.log(JSON.stringify(result));
2398
3225
  } else {