moltspay 0.8.0 → 0.8.2

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.
package/.env.example ADDED
@@ -0,0 +1,10 @@
1
+ # MoltsPay Server Configuration
2
+ # Copy to ~/.moltspay/.env or ./.env and fill in values
3
+
4
+ # Network: true = Base mainnet, false = Base Sepolia testnet
5
+ USE_MAINNET=true
6
+
7
+ # CDP API Credentials (required for mainnet)
8
+ # Get from: https://portal.cdp.coinbase.com/
9
+ CDP_API_KEY_ID=
10
+ CDP_API_KEY_SECRET=
package/dist/cli/index.js CHANGED
@@ -367,38 +367,96 @@ var MoltsPayClient = class {
367
367
  // src/server/index.ts
368
368
  var import_fs2 = require("fs");
369
369
  var import_http = require("http");
370
- var import_ethers2 = require("ethers");
370
+ var path = __toESM(require("path"));
371
371
  var X402_VERSION2 = 2;
372
372
  var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
373
373
  var PAYMENT_HEADER2 = "x-payment";
374
+ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
375
+ var FACILITATOR_TESTNET = "https://www.x402.org/facilitator";
376
+ var FACILITATOR_MAINNET = "https://api.cdp.coinbase.com/platform/v2/x402";
377
+ function loadEnvFiles() {
378
+ try {
379
+ const dotenv = require("dotenv");
380
+ const envPaths = [
381
+ path.join(process.cwd(), ".env"),
382
+ path.join(process.env.HOME || "", ".moltspay", ".env")
383
+ ];
384
+ for (const envPath of envPaths) {
385
+ if ((0, import_fs2.existsSync)(envPath)) {
386
+ dotenv.config({ path: envPath });
387
+ console.log(`[MoltsPay] Loaded config from ${envPath}`);
388
+ break;
389
+ }
390
+ }
391
+ } catch {
392
+ }
393
+ }
394
+ function getCDPConfig() {
395
+ loadEnvFiles();
396
+ return {
397
+ useMainnet: process.env.USE_MAINNET?.toLowerCase() === "true",
398
+ apiKeyId: process.env.CDP_API_KEY_ID,
399
+ apiKeySecret: process.env.CDP_API_KEY_SECRET
400
+ };
401
+ }
402
+ async function getCDPAuthHeaders(method, urlPath, body) {
403
+ const config = getCDPConfig();
404
+ if (!config.apiKeyId || !config.apiKeySecret) {
405
+ throw new Error("CDP_API_KEY_ID and CDP_API_KEY_SECRET required for mainnet");
406
+ }
407
+ try {
408
+ const { getAuthHeaders } = await import("@coinbase/cdp-sdk/auth");
409
+ const headers = await getAuthHeaders({
410
+ apiKeyId: config.apiKeyId,
411
+ apiKeySecret: config.apiKeySecret,
412
+ requestMethod: method,
413
+ requestHost: "api.cdp.coinbase.com",
414
+ requestPath: urlPath,
415
+ requestBody: body
416
+ });
417
+ return headers;
418
+ } catch (err) {
419
+ console.error("[MoltsPay] Failed to generate CDP auth headers:", err.message);
420
+ throw err;
421
+ }
422
+ }
374
423
  var MoltsPayServer = class {
375
424
  manifest;
376
425
  skills = /* @__PURE__ */ new Map();
377
426
  options;
378
- provider = null;
379
- wallet = null;
427
+ cdpConfig;
428
+ facilitatorUrl;
429
+ networkId;
380
430
  constructor(servicesPath, options = {}) {
431
+ this.cdpConfig = getCDPConfig();
381
432
  const content = (0, import_fs2.readFileSync)(servicesPath, "utf-8");
382
433
  this.manifest = JSON.parse(content);
383
434
  this.options = {
384
435
  port: options.port || 3e3,
385
- host: options.host || "0.0.0.0",
386
- privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
436
+ host: options.host || "0.0.0.0"
387
437
  };
388
- if (this.options.privateKey) {
389
- try {
390
- const chain = getChain(this.manifest.provider.chain);
391
- this.provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
392
- this.wallet = new import_ethers2.ethers.Wallet(this.options.privateKey, this.provider);
393
- console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
394
- } catch (err) {
395
- console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
438
+ if (this.cdpConfig.useMainnet) {
439
+ if (!this.cdpConfig.apiKeyId || !this.cdpConfig.apiKeySecret) {
440
+ console.warn("[MoltsPay] WARNING: USE_MAINNET=true but CDP keys not set!");
441
+ console.warn("[MoltsPay] Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in ~/.moltspay/.env");
396
442
  }
443
+ this.facilitatorUrl = FACILITATOR_MAINNET;
444
+ this.networkId = "eip155:8453";
445
+ } else {
446
+ this.facilitatorUrl = options.facilitatorUrl || FACILITATOR_TESTNET;
447
+ this.networkId = "eip155:84532";
397
448
  }
449
+ const networkName = this.cdpConfig.useMainnet ? "Base mainnet" : "Base Sepolia (testnet)";
450
+ const facilitatorName = this.cdpConfig.useMainnet ? "CDP" : "x402.org";
398
451
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
399
452
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
400
453
  console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
401
- console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
454
+ console.log(`[MoltsPay] Network: ${this.networkId} (${networkName})`);
455
+ console.log(`[MoltsPay] Facilitator: ${facilitatorName} (${this.facilitatorUrl})`);
456
+ if (this.cdpConfig.useMainnet && this.cdpConfig.apiKeyId) {
457
+ console.log(`[MoltsPay] CDP API Key: ${this.cdpConfig.apiKeyId.slice(0, 8)}...`);
458
+ }
459
+ console.log(`[MoltsPay] Protocol: x402 (gasless for both client AND server)`);
402
460
  }
403
461
  /**
404
462
  * Register a skill handler for a service
@@ -408,48 +466,45 @@ var MoltsPayServer = class {
408
466
  if (!config) {
409
467
  throw new Error(`Service '${serviceId}' not found in manifest`);
410
468
  }
411
- this.skills.set(serviceId, {
412
- id: serviceId,
413
- config,
414
- handler
415
- });
416
- console.log(`[MoltsPay] Registered skill: ${serviceId} ($${config.price} ${config.currency})`);
469
+ this.skills.set(serviceId, { id: serviceId, config, handler });
417
470
  return this;
418
471
  }
419
472
  /**
420
- * Start the server
473
+ * Start HTTP server
421
474
  */
422
475
  listen(port) {
423
- const p = port || this.options.port;
476
+ const p = port || this.options.port || 3e3;
477
+ const host = this.options.host || "0.0.0.0";
424
478
  const server = (0, import_http.createServer)((req, res) => this.handleRequest(req, res));
425
- server.listen(p, this.options.host, () => {
426
- console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
479
+ server.listen(p, host, () => {
480
+ console.log(`[MoltsPay] Server listening on http://${host}:${p}`);
427
481
  console.log(`[MoltsPay] Endpoints:`);
428
482
  console.log(` GET /services - List available services`);
429
483
  console.log(` POST /execute - Execute service (x402 payment)`);
430
484
  });
431
485
  }
486
+ /**
487
+ * Handle incoming request
488
+ */
432
489
  async handleRequest(req, res) {
433
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
434
- const path = url.pathname;
435
- const method = req.method || "GET";
436
490
  res.setHeader("Access-Control-Allow-Origin", "*");
437
491
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
438
492
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
439
493
  res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
440
- if (method === "OPTIONS") {
494
+ if (req.method === "OPTIONS") {
441
495
  res.writeHead(204);
442
496
  res.end();
443
497
  return;
444
498
  }
445
499
  try {
446
- if (method === "GET" && path === "/services") {
500
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
501
+ if (url.pathname === "/services" && req.method === "GET") {
447
502
  return this.handleGetServices(res);
448
503
  }
449
- if (method === "POST" && path === "/execute") {
504
+ if (url.pathname === "/execute" && req.method === "POST") {
450
505
  const body = await this.readBody(req);
451
506
  const paymentHeader = req.headers[PAYMENT_HEADER2];
452
- return this.handleExecute(body, paymentHeader, res);
507
+ return await this.handleExecute(body, paymentHeader, res);
453
508
  }
454
509
  this.sendJson(res, 404, { error: "Not found" });
455
510
  } catch (err) {
@@ -461,7 +516,6 @@ var MoltsPayServer = class {
461
516
  * GET /services - List available services
462
517
  */
463
518
  handleGetServices(res) {
464
- const chain = getChain(this.manifest.provider.chain);
465
519
  const services = this.manifest.services.map((s) => ({
466
520
  id: s.id,
467
521
  name: s.name,
@@ -477,15 +531,15 @@ var MoltsPayServer = class {
477
531
  services,
478
532
  x402: {
479
533
  version: X402_VERSION2,
480
- network: `eip155:${chain.chainId}`,
481
- schemes: ["exact"]
534
+ network: this.networkId,
535
+ schemes: ["exact"],
536
+ facilitator: this.cdpConfig.useMainnet ? "cdp" : "x402.org",
537
+ mainnet: this.cdpConfig.useMainnet
482
538
  }
483
539
  });
484
540
  }
485
541
  /**
486
542
  * POST /execute - Execute service with x402 payment
487
- * Body: { service: string, params: object }
488
- * Header: X-Payment (optional - if missing, returns 402)
489
543
  */
490
544
  async handleExecute(body, paymentHeader, res) {
491
545
  const { service, params } = body;
@@ -515,6 +569,11 @@ var MoltsPayServer = class {
515
569
  if (!validation.valid) {
516
570
  return this.sendJson(res, 402, { error: validation.error });
517
571
  }
572
+ console.log(`[MoltsPay] Verifying payment with facilitator...`);
573
+ const verifyResult = await this.verifyWithFacilitator(payment, skill.config);
574
+ if (!verifyResult.valid) {
575
+ return this.sendJson(res, 402, { error: `Payment verification failed: ${verifyResult.error}` });
576
+ }
518
577
  console.log(`[MoltsPay] Executing skill: ${service}`);
519
578
  let result;
520
579
  try {
@@ -526,32 +585,46 @@ var MoltsPayServer = class {
526
585
  message: err.message
527
586
  });
528
587
  }
529
- console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
530
- let txHash = null;
588
+ console.log(`[MoltsPay] Skill succeeded, settling payment...`);
589
+ let settlement = null;
531
590
  try {
532
- txHash = await this.claimPayment(payment);
533
- console.log(`[MoltsPay] Payment claimed: ${txHash}`);
591
+ settlement = await this.settleWithFacilitator(payment, skill.config);
592
+ console.log(`[MoltsPay] Payment settled: ${settlement.transaction || "pending"}`);
534
593
  } catch (err) {
535
- console.error("[MoltsPay] Payment claim failed:", err.message);
594
+ console.error("[MoltsPay] Settlement failed:", err.message);
595
+ }
596
+ const responseHeaders = {};
597
+ if (settlement) {
598
+ const responsePayload = {
599
+ success: true,
600
+ transaction: settlement.transaction,
601
+ network: payment.network
602
+ };
603
+ responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(
604
+ JSON.stringify(responsePayload)
605
+ ).toString("base64");
536
606
  }
537
607
  this.sendJson(res, 200, {
538
608
  success: true,
539
609
  result,
540
- payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
541
- });
610
+ payment: settlement ? { transaction: settlement.transaction, status: "settled" } : { status: "pending" }
611
+ }, responseHeaders);
542
612
  }
543
613
  /**
544
614
  * Return 402 with x402 payment requirements
545
615
  */
546
616
  sendPaymentRequired(config, res) {
547
- const chain = getChain(this.manifest.provider.chain);
548
617
  const amountInUnits = Math.floor(config.price * 1e6).toString();
549
618
  const requirements = [{
550
619
  scheme: "exact",
551
- network: `eip155:${chain.chainId}`,
620
+ network: this.networkId,
552
621
  maxAmountRequired: amountInUnits,
553
622
  resource: this.manifest.provider.wallet,
554
- description: `${config.name} - $${config.price} ${config.currency}`
623
+ description: `${config.name} - $${config.price} ${config.currency}`,
624
+ extra: JSON.stringify({
625
+ facilitator: this.cdpConfig.useMainnet ? "cdp" : "x402.org",
626
+ mainnet: this.cdpConfig.useMainnet
627
+ })
555
628
  }];
556
629
  const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
557
630
  res.writeHead(402, {
@@ -565,7 +638,7 @@ var MoltsPayServer = class {
565
638
  }, null, 2));
566
639
  }
567
640
  /**
568
- * Validate x402 payment payload
641
+ * Basic payment validation
569
642
  */
570
643
  validatePayment(payment, config) {
571
644
  if (payment.x402Version !== X402_VERSION2) {
@@ -574,56 +647,89 @@ var MoltsPayServer = class {
574
647
  if (payment.scheme !== "exact") {
575
648
  return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
576
649
  }
577
- const chain = getChain(this.manifest.provider.chain);
578
- const expectedNetwork = `eip155:${chain.chainId}`;
579
- if (payment.network !== expectedNetwork) {
580
- return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
581
- }
582
- const auth = payment.payload.authorization;
583
- if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
584
- return { valid: false, error: "Payment recipient mismatch" };
585
- }
586
- const amount = Number(auth.value) / 1e6;
587
- if (amount < config.price) {
588
- return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
589
- }
590
- const now = Math.floor(Date.now() / 1e3);
591
- if (Number(auth.validBefore) < now) {
592
- return { valid: false, error: "Payment authorization expired" };
593
- }
594
- if (Number(auth.validAfter) > now) {
595
- return { valid: false, error: "Payment authorization not yet valid" };
650
+ if (payment.network !== this.networkId) {
651
+ return { valid: false, error: `Network mismatch: expected ${this.networkId}, got ${payment.network}` };
596
652
  }
597
653
  return { valid: true };
598
654
  }
599
655
  /**
600
- * Claim payment using transferWithAuthorization
656
+ * Verify payment with facilitator (testnet or CDP)
601
657
  */
602
- async claimPayment(payment) {
603
- if (!this.wallet || !this.provider) {
604
- throw new Error("Wallet not configured for payment claims");
605
- }
606
- const chain = getChain(this.manifest.provider.chain);
607
- const auth = payment.payload.authorization;
608
- const sig = payment.payload.signature;
609
- const { r, s, v } = import_ethers2.ethers.Signature.from(sig);
610
- const usdcAbi = [
611
- "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
612
- ];
613
- const usdc = new import_ethers2.ethers.Contract(chain.usdc, usdcAbi, this.wallet);
614
- const tx = await usdc.transferWithAuthorization(
615
- auth.from,
616
- auth.to,
617
- auth.value,
618
- auth.validAfter,
619
- auth.validBefore,
620
- auth.nonce,
621
- v,
622
- r,
623
- s
624
- );
625
- const receipt = await tx.wait();
626
- return receipt.hash;
658
+ async verifyWithFacilitator(payment, config) {
659
+ try {
660
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
661
+ const requirements = {
662
+ scheme: "exact",
663
+ network: this.networkId,
664
+ maxAmountRequired: amountInUnits,
665
+ resource: this.manifest.provider.wallet,
666
+ payTo: this.manifest.provider.wallet
667
+ };
668
+ const requestBody = {
669
+ paymentPayload: payment,
670
+ paymentRequirements: requirements
671
+ };
672
+ let headers = { "Content-Type": "application/json" };
673
+ if (this.cdpConfig.useMainnet) {
674
+ const authHeaders = await getCDPAuthHeaders(
675
+ "POST",
676
+ "/platform/v2/x402/verify",
677
+ requestBody
678
+ );
679
+ headers = { ...headers, ...authHeaders };
680
+ }
681
+ const response = await fetch(`${this.facilitatorUrl}/verify`, {
682
+ method: "POST",
683
+ headers,
684
+ body: JSON.stringify(requestBody)
685
+ });
686
+ const result = await response.json();
687
+ if (!response.ok || !result.isValid) {
688
+ return { valid: false, error: result.invalidReason || result.error || "Verification failed" };
689
+ }
690
+ return { valid: true };
691
+ } catch (err) {
692
+ return { valid: false, error: `Facilitator error: ${err.message}` };
693
+ }
694
+ }
695
+ /**
696
+ * Settle payment with facilitator (execute on-chain transfer)
697
+ */
698
+ async settleWithFacilitator(payment, config) {
699
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
700
+ const requirements = {
701
+ scheme: "exact",
702
+ network: this.networkId,
703
+ maxAmountRequired: amountInUnits,
704
+ resource: this.manifest.provider.wallet,
705
+ payTo: this.manifest.provider.wallet
706
+ };
707
+ const requestBody = {
708
+ paymentPayload: payment,
709
+ paymentRequirements: requirements
710
+ };
711
+ let headers = { "Content-Type": "application/json" };
712
+ if (this.cdpConfig.useMainnet) {
713
+ const authHeaders = await getCDPAuthHeaders(
714
+ "POST",
715
+ "/platform/v2/x402/settle",
716
+ requestBody
717
+ );
718
+ headers = { ...headers, ...authHeaders };
719
+ }
720
+ const response = await fetch(`${this.facilitatorUrl}/settle`, {
721
+ method: "POST",
722
+ headers,
723
+ body: JSON.stringify(requestBody)
724
+ });
725
+ const result = await response.json();
726
+ if (!response.ok || !result.success) {
727
+ throw new Error(result.error || result.errorReason || "Settlement failed");
728
+ }
729
+ return {
730
+ transaction: result.transaction,
731
+ status: result.status || "settled"
732
+ };
627
733
  }
628
734
  async readBody(req) {
629
735
  return new Promise((resolve2, reject) => {
@@ -639,8 +745,12 @@ var MoltsPayServer = class {
639
745
  req.on("error", reject);
640
746
  });
641
747
  }
642
- sendJson(res, status, data) {
643
- res.writeHead(status, { "Content-Type": "application/json" });
748
+ sendJson(res, status, data, extraHeaders) {
749
+ const headers = { "Content-Type": "application/json" };
750
+ if (extraHeaders) {
751
+ Object.assign(headers, extraHeaders);
752
+ }
753
+ res.writeHead(status, headers);
644
754
  res.end(JSON.stringify(data, null, 2));
645
755
  }
646
756
  };
@@ -801,7 +911,7 @@ program.command("services <url>").description("List services from a provider").o
801
911
  console.error("\u274C Error:", err.message);
802
912
  }
803
913
  });
804
- program.command("start <manifest>").description("Start MoltsPay server from services manifest").option("-p, --port <port>", "Port to listen on", "3000").option("--host <host>", "Host to bind", "0.0.0.0").option("--private-key <key>", "Private key for claiming payments (or use MOLTSPAY_PRIVATE_KEY env)").action(async (manifest, options) => {
914
+ program.command("start <manifest>").description("Start MoltsPay server from services manifest").option("-p, --port <port>", "Port to listen on", "3000").option("--host <host>", "Host to bind", "0.0.0.0").option("--facilitator <url>", "x402 facilitator URL (default: https://x402.org/facilitator)").action(async (manifest, options) => {
805
915
  const manifestPath = (0, import_path2.resolve)(manifest);
806
916
  if (!(0, import_fs3.existsSync)(manifestPath)) {
807
917
  console.error(`\u274C Manifest not found: ${manifestPath}`);
@@ -809,16 +919,15 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
809
919
  }
810
920
  const port = parseInt(options.port, 10);
811
921
  const host = options.host;
812
- const privateKey = options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY;
922
+ const facilitatorUrl = options.facilitator;
813
923
  console.log(`
814
924
  \u{1F680} Starting MoltsPay Server (x402 protocol)
815
925
  `);
816
926
  console.log(` Manifest: ${manifestPath}`);
817
927
  console.log(` Port: ${port}`);
818
- console.log(` Payment claims: ${privateKey ? "enabled" : "disabled (no private key)"}`);
819
928
  console.log("");
820
929
  try {
821
- const server = new MoltsPayServer(manifestPath, { port, host, privateKey });
930
+ const server = new MoltsPayServer(manifestPath, { port, host, facilitatorUrl });
822
931
  const manifestContent = await import("fs").then(
823
932
  (fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
824
933
  );