moltspay 1.2.1 → 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 +89 -14
  2. package/dist/cdp/index.d.mts +1 -1
  3. package/dist/cdp/index.d.ts +1 -1
  4. package/dist/cdp/index.js +53 -30368
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +37 -30360
  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 +29 -0
  13. package/dist/chains/index.js.map +1 -1
  14. package/dist/chains/index.mjs +29 -0
  15. package/dist/chains/index.mjs.map +1 -1
  16. package/dist/cli/index.js +863 -109
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/index.mjs +867 -109
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.d.mts +25 -2
  21. package/dist/client/index.d.ts +25 -2
  22. package/dist/client/index.js +195 -12
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +185 -12
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/facilitators/index.d.mts +15 -53
  27. package/dist/facilitators/index.d.ts +15 -53
  28. package/dist/facilitators/index.js +234 -1
  29. package/dist/facilitators/index.js.map +1 -1
  30. package/dist/facilitators/index.mjs +233 -1
  31. package/dist/facilitators/index.mjs.map +1 -1
  32. package/dist/{index-DgJPZMBG.d.mts → index-On9ZaGDW.d.mts} +1 -1
  33. package/dist/{index-DgJPZMBG.d.ts → index-On9ZaGDW.d.ts} +1 -1
  34. package/dist/index.d.mts +2 -2
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +718 -30584
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +711 -30587
  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 +443 -5
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +443 -5
  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 +29 -0
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs +29 -0
  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 +29 -0
  55. package/dist/wallet/index.js.map +1 -1
  56. package/dist/wallet/index.mjs +29 -0
  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";
@@ -107,6 +112,35 @@ var CHAINS = {
107
112
  explorer: "https://sepolia.basescan.org/address/",
108
113
  explorerTx: "https://sepolia.basescan.org/tx/",
109
114
  avgBlockTime: 2
115
+ },
116
+ // ============ Tempo Testnet (Moderato) ============
117
+ tempo_moderato: {
118
+ name: "Tempo Moderato",
119
+ chainId: 42431,
120
+ rpc: "https://rpc.moderato.tempo.xyz",
121
+ tokens: {
122
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
123
+ // Note: Tempo uses USD as native gas token, not ETH
124
+ USDC: {
125
+ address: "0x20c0000000000000000000000000000000000000",
126
+ // pathUSD - primary testnet stablecoin
127
+ decimals: 6,
128
+ symbol: "USDC",
129
+ eip712Name: "pathUSD"
130
+ },
131
+ USDT: {
132
+ address: "0x20c0000000000000000000000000000000000001",
133
+ // alphaUSD
134
+ decimals: 6,
135
+ symbol: "USDT",
136
+ eip712Name: "alphaUSD"
137
+ }
138
+ },
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
110
144
  }
111
145
  };
112
146
  function getChain(name) {
@@ -545,30 +579,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
545
579
  };
546
580
  }
547
581
  /**
548
- * Get wallet balances on all supported chains (Base + Polygon)
582
+ * Get wallet balances on all supported chains (Base + Polygon + Tempo)
549
583
  */
550
584
  async getAllBalances() {
551
585
  if (!this.wallet) {
552
586
  throw new Error("Client not initialized");
553
587
  }
554
- const supportedChains = ["base", "polygon", "base_sepolia"];
588
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
555
589
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
556
590
  const results = {};
591
+ const tempoTokens = {
592
+ pathUSD: "0x20c0000000000000000000000000000000000000",
593
+ alphaUSD: "0x20c0000000000000000000000000000000000001",
594
+ betaUSD: "0x20c0000000000000000000000000000000000002",
595
+ thetaUSD: "0x20c0000000000000000000000000000000000003"
596
+ };
557
597
  await Promise.all(
558
598
  supportedChains.map(async (chainName) => {
559
599
  try {
560
600
  const chain = getChain(chainName);
561
601
  const provider = new ethers.JsonRpcProvider(chain.rpc);
562
- const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
563
- provider.getBalance(this.wallet.address),
564
- new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
565
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
566
- ]);
567
- results[chainName] = {
568
- usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
569
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
570
- native: parseFloat(ethers.formatEther(nativeBalance))
571
- };
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
+ }
572
635
  } catch (err) {
573
636
  results[chainName] = { usdc: 0, usdt: 0, native: 0 };
574
637
  }
@@ -576,6 +639,121 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
576
639
  );
577
640
  return results;
578
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
+ }
579
757
  };
580
758
 
581
759
  // src/server/index.ts
@@ -815,6 +993,127 @@ var CDPFacilitator = class extends BaseFacilitator {
815
993
  }
816
994
  };
817
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;
1114
+ }
1115
+ };
1116
+
818
1117
  // src/facilitators/registry.ts
819
1118
  init_esm_shims();
820
1119
  var FacilitatorRegistry = class {
@@ -824,7 +1123,8 @@ var FacilitatorRegistry = class {
824
1123
  roundRobinIndex = 0;
825
1124
  constructor(selection) {
826
1125
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
827
- this.selection = selection || { primary: "cdp", strategy: "failover" };
1126
+ this.registerFactory("tempo", () => new TempoFacilitator());
1127
+ this.selection = selection || { primary: "cdp", fallback: ["tempo"], strategy: "failover" };
828
1128
  }
829
1129
  /**
830
1130
  * Register a new facilitator factory
@@ -1048,6 +1348,9 @@ var X402_VERSION3 = 2;
1048
1348
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
1049
1349
  var PAYMENT_HEADER2 = "x-payment";
1050
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";
1051
1354
  var TOKEN_ADDRESSES = {
1052
1355
  "eip155:8453": {
1053
1356
  USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -1061,12 +1364,20 @@ var TOKEN_ADDRESSES = {
1061
1364
  "eip155:137": {
1062
1365
  USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
1063
1366
  USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
1367
+ },
1368
+ "eip155:42431": {
1369
+ // Tempo Moderato testnet - TIP-20 stablecoins
1370
+ USDC: "0x20c0000000000000000000000000000000000000",
1371
+ // pathUSD
1372
+ USDT: "0x20c0000000000000000000000000000000000001"
1373
+ // alphaUSD
1064
1374
  }
1065
1375
  };
1066
1376
  var CHAIN_TO_NETWORK = {
1067
1377
  "base": "eip155:8453",
1068
1378
  "base_sepolia": "eip155:84532",
1069
- "polygon": "eip155:137"
1379
+ "polygon": "eip155:137",
1380
+ "tempo_moderato": "eip155:42431"
1070
1381
  };
1071
1382
  var TOKEN_DOMAINS = {
1072
1383
  // Base mainnet
@@ -1084,6 +1395,11 @@ var TOKEN_DOMAINS = {
1084
1395
  "eip155:137": {
1085
1396
  USDC: { name: "USD Coin", version: "2" },
1086
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" }
1087
1403
  }
1088
1404
  };
1089
1405
  function getTokenDomain(network, token) {
@@ -1141,9 +1457,11 @@ var MoltsPayServer = class {
1141
1457
  };
1142
1458
  this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
1143
1459
  this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
1460
+ const defaultFallback = ["tempo"];
1461
+ const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
1144
1462
  const facilitatorConfig = options.facilitators || {
1145
1463
  primary: process.env.FACILITATOR_PRIMARY || "cdp",
1146
- fallback: process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean),
1464
+ fallback: envFallback || defaultFallback,
1147
1465
  strategy: process.env.FACILITATOR_STRATEGY || "failover",
1148
1466
  config: {
1149
1467
  cdp: { useMainnet: this.useMainnet }
@@ -1237,8 +1555,8 @@ var MoltsPayServer = class {
1237
1555
  async handleRequest(req, res) {
1238
1556
  res.setHeader("Access-Control-Allow-Origin", "*");
1239
1557
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1240
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
1241
- 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");
1242
1560
  if (req.method === "OPTIONS") {
1243
1561
  res.writeHead(204);
1244
1562
  res.end();
@@ -1269,6 +1587,14 @@ var MoltsPayServer = class {
1269
1587
  const paymentHeader = req.headers[PAYMENT_HEADER2];
1270
1588
  return await this.handleProxy(body, paymentHeader, res);
1271
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
+ }
1272
1598
  this.sendJson(res, 404, { error: "Not found" });
1273
1599
  } catch (err) {
1274
1600
  console.error("[MoltsPay] Error:", err);
@@ -1452,6 +1778,187 @@ var MoltsPayServer = class {
1452
1778
  payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
1453
1779
  }, responseHeaders);
1454
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
+ }
1455
1962
  /**
1456
1963
  * Return 402 with x402 payment requirements (v2 format)
1457
1964
  * Includes requirements for all chains and all accepted currencies
@@ -1826,6 +2333,26 @@ async function printQRCode(url) {
1826
2333
 
1827
2334
  // src/cli/index.ts
1828
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
+ }
1829
2356
  var program = new Command();
1830
2357
  var DEFAULT_CONFIG_DIR = join4(homedir2(), ".moltspay");
1831
2358
  var PID_FILE = join4(DEFAULT_CONFIG_DIR, "server.pid");
@@ -1844,7 +2371,7 @@ function prompt(question) {
1844
2371
  });
1845
2372
  });
1846
2373
  }
1847
- 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());
1848
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) => {
1849
2376
  console.log("\n\u{1F510} MoltsPay Client Setup\n");
1850
2377
  if (existsSync4(join4(options.configDir, "wallet.json"))) {
@@ -1853,7 +2380,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
1853
2380
  return;
1854
2381
  }
1855
2382
  let chain = options.chain;
1856
- const supportedChains = ["base", "polygon", "base_sepolia"];
2383
+ const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
1857
2384
  if (!supportedChains.includes(chain)) {
1858
2385
  console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
1859
2386
  process.exit(1);
@@ -1941,11 +2468,9 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1941
2468
  console.log(` Wallet: ${client.address}`);
1942
2469
  console.log(` Chain: Base Sepolia (testnet)
1943
2470
  `);
1944
- console.log("\u{1F4DD} Get testnet USDC from these faucets:");
1945
- console.log(" \u2022 Circle Faucet: https://faucet.circle.com/");
1946
- console.log(" \u2022 Base Sepolia: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet\n");
1947
- console.log(`\u{1F4A1} Send USDC to: ${client.address}
1948
- `);
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");
1949
2474
  return;
1950
2475
  }
1951
2476
  console.log("\n\u{1F4B3} Fund your agent wallet\n");
@@ -1977,8 +2502,13 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
1977
2502
  console.log(`\u274C ${error.message}`);
1978
2503
  }
1979
2504
  });
1980
- program.command("faucet").description("Request testnet USDC from MoltsPay faucet (Base Sepolia)").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR).action(async (options) => {
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) => {
1981
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
+ }
1982
2512
  if (!address) {
1983
2513
  const client = new MoltsPayClient({ configDir: options.configDir });
1984
2514
  if (client.isInitialized) {
@@ -1993,34 +2523,67 @@ program.command("faucet").description("Request testnet USDC from MoltsPay faucet
1993
2523
  return;
1994
2524
  }
1995
2525
  console.log("\n\u{1F6B0} MoltsPay Testnet Faucet\n");
1996
- console.log(` Requesting 1 USDC on Base Sepolia...`);
1997
- console.log(` Address: ${address}
2526
+ if (chain === "tempo_moderato") {
2527
+ console.log(` Requesting testnet tokens on Tempo Moderato...`);
2528
+ console.log(` Address: ${address}
1998
2529
  `);
1999
- try {
2000
- const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
2001
- const response = await fetch(FAUCET_API, {
2002
- method: "POST",
2003
- headers: { "Content-Type": "application/json" },
2004
- body: JSON.stringify({ address })
2005
- });
2006
- const result = await response.json();
2007
- if (!response.ok) {
2008
- console.log(`\u274C ${result.error || "Request failed"}`);
2009
- if (result.hint) console.log(` ${result.hint}`);
2010
- if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
2011
- return;
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");
2012
2556
  }
2013
- console.log(`\u2705 Received ${result.amount} USDC!
2557
+ } else {
2558
+ console.log(` Requesting 1 USDC on Base Sepolia...`);
2559
+ console.log(` Address: ${address}
2014
2560
  `);
2015
- console.log(` Transaction: ${result.transaction}`);
2016
- console.log(` Explorer: ${result.explorer}`);
2017
- console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
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!
2018
2576
  `);
2019
- console.log("\u{1F4A1} Use this USDC to test x402 payments:");
2020
- console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
2577
+ console.log(` Transaction: ${result.transaction}`);
2578
+ console.log(` Explorer: ${result.explorer}`);
2579
+ console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
2021
2580
  `);
2022
- } catch (error) {
2023
- console.log(`\u274C ${error.message}`);
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
+ }
2024
2587
  }
2025
2588
  });
2026
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) => {
@@ -2052,8 +2615,26 @@ program.command("status").description("Show wallet status and balance").option("
2052
2615
  console.log("");
2053
2616
  console.log(" Balances:");
2054
2617
  for (const [chainName, balance] of Object.entries(allBalances)) {
2055
- const chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
2056
- 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
+ }
2057
2638
  }
2058
2639
  console.log("");
2059
2640
  console.log(" Spending Limits:");
@@ -2071,8 +2652,8 @@ program.command("list").description("List recent transactions").option("--days <
2071
2652
  const days = parseInt(options.days) || 7;
2072
2653
  const limit = parseInt(options.limit) || 20;
2073
2654
  const chain = options.chain?.toLowerCase() || "all";
2074
- if (!["base", "polygon", "base_sepolia", "all"].includes(chain)) {
2075
- console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, 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");
2076
2657
  return;
2077
2658
  }
2078
2659
  const wallet = client.address;
@@ -2092,9 +2673,16 @@ program.command("list").description("List recent transactions").option("--days <
2092
2673
  api: "https://base-sepolia.blockscout.com/api/v2",
2093
2674
  usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
2094
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"
2095
2683
  }
2096
2684
  };
2097
- const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia"] : [chain];
2685
+ const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia", "tempo_moderato"] : [chain];
2098
2686
  console.log(`
2099
2687
  \u{1F4DC} Transactions (last ${days} day${days > 1 ? "s" : ""})
2100
2688
  `);
@@ -2102,27 +2690,136 @@ program.command("list").description("List recent transactions").option("--days <
2102
2690
  for (const c of chainsToQuery) {
2103
2691
  const explorer = explorers[c];
2104
2692
  try {
2105
- const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
2106
- const response = await fetch(url);
2107
- const data = await response.json();
2108
- if (data.items && Array.isArray(data.items)) {
2109
- for (const tx of data.items) {
2110
- const timestamp = new Date(tx.timestamp).getTime();
2111
- if (timestamp < cutoffTime) continue;
2112
- const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
2113
- const decimals = parseInt(tx.total.decimals) || 6;
2114
- allTxns.push({
2115
- chain: c,
2116
- timestamp,
2117
- type: isIncoming ? "IN" : "OUT",
2118
- amount: parseInt(tx.total.value) / Math.pow(10, decimals),
2119
- other: isIncoming ? tx.from.hash : tx.to.hash,
2120
- hash: tx.transaction_hash
2121
- });
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
+ }
2122
2818
  }
2123
2819
  }
2124
2820
  } catch (error) {
2125
- 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}`);
2126
2823
  }
2127
2824
  }
2128
2825
  allTxns.sort((a, b) => b.timestamp - a.timestamp);
@@ -2135,8 +2832,12 @@ program.command("list").description("List recent transactions").option("--days <
2135
2832
  const color = tx.type === "IN" ? "\x1B[32m" : "\x1B[31m";
2136
2833
  const reset = "\x1B[0m";
2137
2834
  const date = new Date(tx.timestamp).toISOString().slice(5, 16).replace("T", " ");
2138
- const chainTag = chain === "all" ? `[${tx.chain.toUpperCase()}] ` : "";
2139
- 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}`);
2140
2841
  }
2141
2842
  const inTotal = allTxns.filter((t) => t.type === "IN").reduce((s, t) => s + t.amount, 0);
2142
2843
  const outTotal = allTxns.filter((t) => t.type === "OUT").reduce((s, t) => s + t.amount, 0);
@@ -2145,39 +2846,88 @@ program.command("list").description("List recent transactions").option("--days <
2145
2846
  `);
2146
2847
  }
2147
2848
  });
2148
- 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";
2149
2851
  try {
2150
- const client = new MoltsPayClient();
2151
- 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
+ }
2152
2872
  if (options.json) {
2153
2873
  console.log(JSON.stringify(services, null, 2));
2154
2874
  } else {
2155
- if (services.provider) {
2156
- console.log(`
2157
- \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)
2158
2880
  `);
2159
- console.log(` ${services.provider.description || ""}`);
2160
- console.log(` Wallet: ${services.provider.wallet}`);
2161
- 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";
2162
- console.log(` Chains: ${chains}`);
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
2889
+ `);
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
+ }
2163
2903
  } else {
2164
- console.log(`
2165
- \u{1F3EA} MoltsPay Service Registry
2904
+ if (services.provider) {
2905
+ console.log(`
2906
+ \u{1F3EA} ${services.provider.name}
2166
2907
  `);
2167
- console.log(` ${services.services?.length || 0} services available`);
2168
- }
2169
- console.log("\n\u{1F4E6} Services:\n");
2170
- for (const svc of services.services) {
2171
- const status = svc.available !== false ? "\u2705" : "\u274C";
2172
- console.log(` ${status} ${svc.id || svc.name}`);
2173
- console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
2174
- if (svc.description) {
2175
- console.log(` ${svc.description}`);
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
2915
+ `);
2916
+ console.log(` ${serviceList.length} services available`);
2176
2917
  }
2177
- if (svc.provider && !services.provider) {
2178
- 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("");
2179
2930
  }
2180
- console.log("");
2181
2931
  }
2182
2932
  }
2183
2933
  } catch (err) {
@@ -2391,8 +3141,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
2391
3141
  process.exit(1);
2392
3142
  }
2393
3143
  });
2394
- 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, or base_sepolia). Required if server accepts multiple chains.").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
2395
- 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 });
2396
3146
  if (!client.isInitialized) {
2397
3147
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
2398
3148
  process.exit(1);
@@ -2421,13 +3171,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2421
3171
  params.image_base64 = imageData.toString("base64");
2422
3172
  }
2423
3173
  }
2424
- if (!params.prompt) {
2425
- console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
2426
- process.exit(1);
2427
- }
2428
3174
  const chain = options.chain?.toLowerCase();
2429
- if (chain && !["base", "polygon", "base_sepolia"].includes(chain)) {
2430
- console.error(`\u274C Unknown chain: ${chain}. Supported: base, polygon, base_sepolia`);
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`);
2431
3177
  process.exit(1);
2432
3178
  }
2433
3179
  const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
@@ -2458,10 +3204,22 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
2458
3204
  console.log("");
2459
3205
  }
2460
3206
  try {
2461
- const result = await client.pay(server, service, params, {
2462
- token,
2463
- chain
2464
- });
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
+ }
2465
3223
  if (options.json) {
2466
3224
  console.log(JSON.stringify(result));
2467
3225
  } else {