moltspay 0.8.10 → 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 +86 -3
- package/dist/cli/index.js +178 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +179 -62
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +9 -1
- package/dist/client/index.d.ts +9 -1
- package/dist/client/index.js +50 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +51 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/index.js +57 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -7
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +2 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +7 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +7 -4
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
#
|
|
449
|
-
|
|
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: {
|
|
@@ -659,10 +707,14 @@ var MoltsPayServer = class {
|
|
|
659
707
|
*/
|
|
660
708
|
sendPaymentRequired(config, res) {
|
|
661
709
|
const requirements = this.buildPaymentRequirements(config);
|
|
662
|
-
requirements.description = `${config.name} - $${config.price} ${config.currency}`;
|
|
663
710
|
const paymentRequired = {
|
|
664
711
|
x402Version: X402_VERSION2,
|
|
665
|
-
accepts: [requirements]
|
|
712
|
+
accepts: [requirements],
|
|
713
|
+
resource: {
|
|
714
|
+
url: `/execute?service=${config.id}`,
|
|
715
|
+
description: `${config.name} - $${config.price} ${config.currency}`,
|
|
716
|
+
mimeType: "application/json"
|
|
717
|
+
}
|
|
666
718
|
};
|
|
667
719
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
668
720
|
res.writeHead(402, {
|
|
@@ -701,9 +753,8 @@ var MoltsPayServer = class {
|
|
|
701
753
|
return {
|
|
702
754
|
scheme: "exact",
|
|
703
755
|
network: this.networkId,
|
|
704
|
-
maxAmountRequired: amountInUnits,
|
|
705
|
-
amount: amountInUnits,
|
|
706
756
|
asset: usdcAddress,
|
|
757
|
+
amount: amountInUnits,
|
|
707
758
|
payTo: this.manifest.provider.wallet,
|
|
708
759
|
maxTimeoutSeconds: 300,
|
|
709
760
|
extra: USDC_DOMAIN
|
|
@@ -960,75 +1011,141 @@ program.command("services <url>").description("List services from a provider").o
|
|
|
960
1011
|
console.error("\u274C Error:", err.message);
|
|
961
1012
|
}
|
|
962
1013
|
});
|
|
963
|
-
program.command("start <
|
|
964
|
-
const manifestPath = (0, import_path2.resolve)(manifest);
|
|
965
|
-
if (!(0, import_fs3.existsSync)(manifestPath)) {
|
|
966
|
-
console.error(`\u274C Manifest not found: ${manifestPath}`);
|
|
967
|
-
process.exit(1);
|
|
968
|
-
}
|
|
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) => {
|
|
969
1015
|
const port = parseInt(options.port, 10);
|
|
970
1016
|
const host = options.host;
|
|
971
1017
|
const facilitatorUrl = options.facilitator;
|
|
1018
|
+
const allPaths = paths.flatMap((p) => p.split(",").map((s) => s.trim())).filter(Boolean);
|
|
972
1019
|
console.log(`
|
|
973
1020
|
\u{1F680} Starting MoltsPay Server (x402 protocol)
|
|
974
1021
|
`);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
});
|
|
1017
1105
|
});
|
|
1018
1106
|
});
|
|
1019
|
-
|
|
1107
|
+
console.log(` \u2705 ${service.id} \u2192 command`);
|
|
1108
|
+
} else {
|
|
1109
|
+
console.warn(` \u26A0\uFE0F ${service.id}: no function or command defined`);
|
|
1110
|
+
}
|
|
1020
1111
|
}
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
console.error(`\u274C Failed to load ${manifestPath}: ${err.message}`);
|
|
1114
|
+
continue;
|
|
1021
1115
|
}
|
|
1022
|
-
|
|
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);
|
|
1141
|
+
}
|
|
1142
|
+
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
1023
1143
|
(0, import_fs3.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
1024
|
-
console.log(` PID file: ${PID_FILE}`);
|
|
1025
|
-
console.log("");
|
|
1026
1144
|
server.listen(port);
|
|
1027
1145
|
const cleanup = () => {
|
|
1028
1146
|
try {
|
|
1029
|
-
if ((0, import_fs3.existsSync)(PID_FILE))
|
|
1030
|
-
|
|
1031
|
-
}
|
|
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);
|
|
1032
1149
|
} catch {
|
|
1033
1150
|
}
|
|
1034
1151
|
};
|