moltspay 0.8.11 → 0.8.13

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/README.md CHANGED
@@ -330,9 +330,67 @@ Client Agent Server Facilitator (Coinbase
330
330
  - ❌ NO ETH/gas needed
331
331
  - ❌ NO API credentials needed
332
332
 
333
+ ## Server Setup (Selling Services)
334
+
335
+ To run a MoltsPay server that accepts x402 payments:
336
+
337
+ ### 1. Configure CDP Credentials (Required for Mainnet)
338
+
339
+ ```bash
340
+ # Copy the example config
341
+ cp .env.example ~/.moltspay/.env
342
+
343
+ # Edit with your CDP credentials (get from https://portal.cdp.coinbase.com/)
344
+ nano ~/.moltspay/.env
345
+ ```
346
+
347
+ ```env
348
+ # ~/.moltspay/.env
349
+ USE_MAINNET=true
350
+ CDP_API_KEY_ID=your-key-id
351
+ CDP_API_KEY_SECRET=your-secret
352
+ ```
353
+
354
+ **Important:** Server does NOT need a private key! The x402 facilitator handles all on-chain settlement.
355
+
356
+ ### 2. Create Services Manifest
357
+
358
+ ```json
359
+ {
360
+ "provider": {
361
+ "name": "My AI Service",
362
+ "wallet": "0xYOUR_WALLET_ADDRESS",
363
+ "chain": "base"
364
+ },
365
+ "services": [
366
+ {
367
+ "id": "my-service",
368
+ "name": "My Service",
369
+ "price": 0.99,
370
+ "currency": "USDC",
371
+ "command": "./handlers/my-service.sh",
372
+ "input": { "prompt": { "type": "string", "required": true } },
373
+ "output": { "result": { "type": "string" } }
374
+ }
375
+ ]
376
+ }
377
+ ```
378
+
379
+ ### 3. Start Server
380
+
381
+ ```bash
382
+ npx moltspay start ./moltspay.services.json --port 3000
383
+ ```
384
+
385
+ The server will:
386
+ - Return 402 with payment requirements for unpaid requests
387
+ - Verify signatures with CDP facilitator
388
+ - Execute service only after payment verification
389
+ - Settle payment on-chain (facilitator pays gas)
390
+
333
391
  ## CDP Wallet Support (Optional, Advanced)
334
392
 
335
- CDP wallet is an **optional alternative** for cases where you want Coinbase to host the wallet. Most users should use the simple local wallet above.
393
+ CDP wallet is an **optional alternative** for **client agents** who want Coinbase to host their wallet. Most users should use the simple local wallet above.
336
394
 
337
395
  ```bash
338
396
  # Only if you have CDP credentials and want hosted wallet
@@ -445,8 +503,33 @@ const result = await permit.executePermitAndTransfer(
445
503
  # Install globally
446
504
  npm install -g moltspay
447
505
 
448
- # Get balance
449
- moltspay balance --chain base
506
+ # === Client Commands ===
507
+
508
+ # Initialize wallet (first time setup)
509
+ npx moltspay init --chain base
510
+
511
+ # Check wallet status and balance
512
+ npx moltspay status
513
+
514
+ # Update spending limits
515
+ npx moltspay config # Interactive
516
+ npx moltspay config --max-per-tx 50 --max-per-day 500 # Direct
517
+
518
+ # Pay for a service
519
+ npx moltspay pay http://server:3000 service-id --prompt "your prompt"
520
+
521
+ # List services from a provider
522
+ npx moltspay services http://server:3000
523
+
524
+ # === Server Commands ===
525
+
526
+ # Start server from services manifest
527
+ npx moltspay start ./moltspay.services.json --port 3000
528
+
529
+ # Stop running server
530
+ npx moltspay stop
531
+
532
+ # === Legacy Commands ===
450
533
 
451
534
  # Generate invoice
452
535
  moltspay invoice --order order_123 --amount 2.0 --service video
package/dist/cli/index.js CHANGED
@@ -116,6 +116,7 @@ var MoltsPayClient = class {
116
116
  this.configDir = options.configDir || (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
117
117
  this.config = this.loadConfig();
118
118
  this.walletData = this.loadWallet();
119
+ this.loadSpending();
119
120
  if (this.walletData) {
120
121
  this.wallet = new import_ethers.Wallet(this.walletData.privateKey);
121
122
  }
@@ -300,6 +301,7 @@ var MoltsPayClient = class {
300
301
  if (today > this.lastSpendingReset) {
301
302
  this.todaySpending = 0;
302
303
  this.lastSpendingReset = today;
304
+ this.saveSpending();
303
305
  }
304
306
  if (this.todaySpending + amount > this.config.limits.maxPerDay) {
305
307
  throw new Error(
@@ -308,10 +310,11 @@ var MoltsPayClient = class {
308
310
  }
309
311
  }
310
312
  /**
311
- * Record spending
313
+ * Record spending and persist to disk
312
314
  */
313
315
  recordSpending(amount) {
314
316
  this.todaySpending += amount;
317
+ this.saveSpending();
315
318
  }
316
319
  // --- Config & Wallet Management ---
317
320
  loadConfig() {
@@ -327,9 +330,54 @@ var MoltsPayClient = class {
327
330
  const configPath = (0, import_path.join)(this.configDir, "config.json");
328
331
  (0, import_fs.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
329
332
  }
333
+ /**
334
+ * Load spending data from disk
335
+ */
336
+ loadSpending() {
337
+ const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
338
+ if ((0, import_fs.existsSync)(spendingPath)) {
339
+ try {
340
+ const data = JSON.parse((0, import_fs.readFileSync)(spendingPath, "utf-8"));
341
+ const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
342
+ if (data.date && data.date === today) {
343
+ this.todaySpending = data.amount || 0;
344
+ this.lastSpendingReset = data.date;
345
+ } else {
346
+ this.todaySpending = 0;
347
+ this.lastSpendingReset = today;
348
+ }
349
+ } catch {
350
+ this.todaySpending = 0;
351
+ this.lastSpendingReset = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
352
+ }
353
+ }
354
+ }
355
+ /**
356
+ * Save spending data to disk
357
+ */
358
+ saveSpending() {
359
+ (0, import_fs.mkdirSync)(this.configDir, { recursive: true });
360
+ const spendingPath = (0, import_path.join)(this.configDir, "spending.json");
361
+ const data = {
362
+ date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
363
+ amount: this.todaySpending,
364
+ updatedAt: Date.now()
365
+ };
366
+ (0, import_fs.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
367
+ }
330
368
  loadWallet() {
331
369
  const walletPath = (0, import_path.join)(this.configDir, "wallet.json");
332
370
  if ((0, import_fs.existsSync)(walletPath)) {
371
+ try {
372
+ const stats = (0, import_fs.statSync)(walletPath);
373
+ const mode = stats.mode & 511;
374
+ if (mode !== 384) {
375
+ console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
376
+ console.warn(`[MoltsPay] Fixing permissions to 0600...`);
377
+ (0, import_fs.chmodSync)(walletPath, 384);
378
+ }
379
+ } catch (err) {
380
+ }
333
381
  const content = (0, import_fs.readFileSync)(walletPath, "utf-8");
334
382
  return JSON.parse(content);
335
383
  }
@@ -347,7 +395,7 @@ var MoltsPayClient = class {
347
395
  createdAt: Date.now()
348
396
  };
349
397
  const walletPath = (0, import_path.join)(configDir, "wallet.json");
350
- (0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
398
+ (0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
351
399
  const config = {
352
400
  chain: options.chain,
353
401
  limits: {
@@ -963,75 +1011,141 @@ program.command("services <url>").description("List services from a provider").o
963
1011
  console.error("\u274C Error:", err.message);
964
1012
  }
965
1013
  });
966
- 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) => {
967
- const manifestPath = (0, import_path2.resolve)(manifest);
968
- if (!(0, import_fs3.existsSync)(manifestPath)) {
969
- console.error(`\u274C Manifest not found: ${manifestPath}`);
970
- process.exit(1);
971
- }
1014
+ program.command("start <paths...>").description("Start MoltsPay server from skill directories or manifest files").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 (paths, options) => {
972
1015
  const port = parseInt(options.port, 10);
973
1016
  const host = options.host;
974
1017
  const facilitatorUrl = options.facilitator;
1018
+ const allPaths = paths.flatMap((p) => p.split(",").map((s) => s.trim())).filter(Boolean);
975
1019
  console.log(`
976
1020
  \u{1F680} Starting MoltsPay Server (x402 protocol)
977
1021
  `);
978
- console.log(` Manifest: ${manifestPath}`);
979
- console.log(` Port: ${port}`);
980
- console.log("");
981
- try {
982
- const server = new MoltsPayServer(manifestPath, { port, host, facilitatorUrl });
983
- const manifestContent = await import("fs").then(
984
- (fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
985
- );
986
- for (const service of manifestContent.services) {
987
- if (service.command) {
988
- const workdir = (0, import_path2.dirname)(manifestPath);
989
- server.skill(service.id, async (params) => {
990
- return new Promise((resolve2, reject) => {
991
- const proc = (0, import_child_process.spawn)("sh", ["-c", service.command], {
992
- cwd: workdir,
993
- stdio: ["pipe", "pipe", "pipe"]
994
- });
995
- let stdout = "";
996
- let stderr = "";
997
- proc.stdout.on("data", (data) => {
998
- stdout += data.toString();
999
- });
1000
- proc.stderr.on("data", (data) => {
1001
- stderr += data.toString();
1002
- process.stderr.write(data);
1003
- });
1004
- proc.stdin.write(JSON.stringify(params));
1005
- proc.stdin.end();
1006
- proc.on("close", (code) => {
1007
- if (code !== 0) {
1008
- reject(new Error(`Command failed (exit ${code}): ${stderr || "Unknown error"}`));
1009
- return;
1010
- }
1011
- try {
1012
- const result = JSON.parse(stdout.trim());
1013
- resolve2(result);
1014
- } catch {
1015
- resolve2({ output: stdout.trim() });
1016
- }
1017
- });
1018
- proc.on("error", (err) => {
1019
- reject(new Error(`Failed to spawn command: ${err.message}`));
1022
+ const allServices = [];
1023
+ const handlers = /* @__PURE__ */ new Map();
1024
+ let provider = null;
1025
+ for (const inputPath of allPaths) {
1026
+ const resolvedPath = (0, import_path2.resolve)(inputPath);
1027
+ let manifestPath;
1028
+ let skillDir;
1029
+ let isSkillDir = false;
1030
+ if ((0, import_fs3.existsSync)((0, import_path2.join)(resolvedPath, "moltspay.services.json"))) {
1031
+ manifestPath = (0, import_path2.join)(resolvedPath, "moltspay.services.json");
1032
+ skillDir = resolvedPath;
1033
+ isSkillDir = true;
1034
+ } else if ((0, import_fs3.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
1035
+ manifestPath = resolvedPath;
1036
+ skillDir = (0, import_path2.dirname)(resolvedPath);
1037
+ } else if ((0, import_fs3.existsSync)(resolvedPath)) {
1038
+ console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
1039
+ continue;
1040
+ } else {
1041
+ console.error(`\u274C Path not found: ${resolvedPath}`);
1042
+ continue;
1043
+ }
1044
+ console.log(`\u{1F4E6} Loading: ${manifestPath}`);
1045
+ try {
1046
+ const manifestContent = JSON.parse((0, import_fs3.readFileSync)(manifestPath, "utf-8"));
1047
+ if (!provider) {
1048
+ provider = manifestContent.provider;
1049
+ }
1050
+ let skillModule = null;
1051
+ if (isSkillDir) {
1052
+ const indexPath = (0, import_path2.join)(skillDir, "index.js");
1053
+ if ((0, import_fs3.existsSync)(indexPath)) {
1054
+ try {
1055
+ skillModule = await import(indexPath);
1056
+ console.log(` \u2705 Loaded module: ${indexPath}`);
1057
+ } catch (err) {
1058
+ console.error(` \u26A0\uFE0F Failed to load module: ${err.message}`);
1059
+ }
1060
+ }
1061
+ }
1062
+ for (const service of manifestContent.services) {
1063
+ allServices.push(service);
1064
+ if (service.function && skillModule) {
1065
+ const fn = skillModule[service.function] || skillModule.default?.[service.function];
1066
+ if (fn && typeof fn === "function") {
1067
+ handlers.set(service.id, fn);
1068
+ console.log(` \u2705 ${service.id} \u2192 ${service.function}()`);
1069
+ } else {
1070
+ console.error(` \u274C Function '${service.function}' not found in index.js`);
1071
+ }
1072
+ } else if (service.command) {
1073
+ const workdir = skillDir;
1074
+ handlers.set(service.id, async (params) => {
1075
+ return new Promise((resolvePromise, reject) => {
1076
+ const proc = (0, import_child_process.spawn)("sh", ["-c", service.command], {
1077
+ cwd: workdir,
1078
+ stdio: ["pipe", "pipe", "pipe"]
1079
+ });
1080
+ let stdout = "";
1081
+ let stderr = "";
1082
+ proc.stdout.on("data", (data) => {
1083
+ stdout += data.toString();
1084
+ });
1085
+ proc.stderr.on("data", (data) => {
1086
+ stderr += data.toString();
1087
+ process.stderr.write(data);
1088
+ });
1089
+ proc.stdin.write(JSON.stringify(params));
1090
+ proc.stdin.end();
1091
+ proc.on("close", (code) => {
1092
+ if (code !== 0) {
1093
+ reject(new Error(`Command failed (exit ${code}): ${stderr || "Unknown error"}`));
1094
+ return;
1095
+ }
1096
+ try {
1097
+ resolvePromise(JSON.parse(stdout.trim()));
1098
+ } catch {
1099
+ resolvePromise({ output: stdout.trim() });
1100
+ }
1101
+ });
1102
+ proc.on("error", (err) => {
1103
+ reject(new Error(`Failed to spawn command: ${err.message}`));
1104
+ });
1020
1105
  });
1021
1106
  });
1022
- });
1107
+ console.log(` \u2705 ${service.id} \u2192 command`);
1108
+ } else {
1109
+ console.warn(` \u26A0\uFE0F ${service.id}: no function or command defined`);
1110
+ }
1023
1111
  }
1112
+ } catch (err) {
1113
+ console.error(`\u274C Failed to load ${manifestPath}: ${err.message}`);
1114
+ continue;
1115
+ }
1116
+ }
1117
+ if (allServices.length === 0) {
1118
+ console.error("\n\u274C No services loaded. Exiting.");
1119
+ process.exit(1);
1120
+ }
1121
+ if (!provider) {
1122
+ console.error("\n\u274C No provider config found. Exiting.");
1123
+ process.exit(1);
1124
+ }
1125
+ const combinedManifest = {
1126
+ provider,
1127
+ services: allServices
1128
+ };
1129
+ const tempManifestPath = (0, import_path2.join)(DEFAULT_CONFIG_DIR, "combined-manifest.json");
1130
+ (0, import_fs3.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
1131
+ console.log(`
1132
+ \u{1F4CB} Combined manifest: ${allServices.length} services`);
1133
+ console.log(` Provider: ${provider.name}`);
1134
+ console.log(` Wallet: ${provider.wallet}`);
1135
+ console.log(` Port: ${port}`);
1136
+ console.log("");
1137
+ try {
1138
+ const server = new MoltsPayServer(tempManifestPath, { port, host, facilitatorUrl });
1139
+ for (const [serviceId, handler] of handlers) {
1140
+ server.skill(serviceId, handler);
1024
1141
  }
1025
- const pidData = { pid: process.pid, port, manifest: manifestPath };
1142
+ const pidData = { pid: process.pid, port, paths: allPaths };
1026
1143
  (0, import_fs3.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
1027
- console.log(` PID file: ${PID_FILE}`);
1028
- console.log("");
1029
1144
  server.listen(port);
1030
1145
  const cleanup = () => {
1031
1146
  try {
1032
- if ((0, import_fs3.existsSync)(PID_FILE)) {
1033
- (0, import_fs3.unlinkSync)(PID_FILE);
1034
- }
1147
+ if ((0, import_fs3.existsSync)(PID_FILE)) (0, import_fs3.unlinkSync)(PID_FILE);
1148
+ if ((0, import_fs3.existsSync)(tempManifestPath)) (0, import_fs3.unlinkSync)(tempManifestPath);
1035
1149
  } catch {
1036
1150
  }
1037
1151
  };