moltspay 0.7.2 → 0.8.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.
@@ -69,11 +69,15 @@ interface MoltsPayServerOptions {
69
69
  port?: number;
70
70
  host?: string;
71
71
  chargeExpirySecs?: number;
72
+ /** Private key for claiming payments (can also use MOLTSPAY_PRIVATE_KEY env) */
73
+ privateKey?: string;
72
74
  }
73
75
 
74
76
  /**
75
77
  * MoltsPay Server - Payment infrastructure for AI Agents
76
78
  *
79
+ * Uses x402 protocol for gasless, pay-for-success payments.
80
+ *
77
81
  * Usage:
78
82
  * const server = new MoltsPayServer('./moltspay.services.json');
79
83
  * server.skill('text-to-video', async (params) => { ... });
@@ -83,8 +87,9 @@ interface MoltsPayServerOptions {
83
87
  declare class MoltsPayServer {
84
88
  private manifest;
85
89
  private skills;
86
- private charges;
87
90
  private options;
91
+ private provider;
92
+ private wallet;
88
93
  constructor(servicesPath: string, options?: MoltsPayServerOptions);
89
94
  /**
90
95
  * Register a skill handler for a service
@@ -100,19 +105,23 @@ declare class MoltsPayServer {
100
105
  */
101
106
  private handleGetServices;
102
107
  /**
103
- * POST /pay - Create payment request
108
+ * POST /execute - Execute service with x402 payment
104
109
  * Body: { service: string, params: object }
110
+ * Header: X-Payment (optional - if missing, returns 402)
111
+ */
112
+ private handleExecute;
113
+ /**
114
+ * Return 402 with x402 payment requirements
105
115
  */
106
- private handlePay;
116
+ private sendPaymentRequired;
107
117
  /**
108
- * POST /verify - Verify payment and execute skill
109
- * Body: { chargeId: string, txHash: string }
118
+ * Validate x402 payment payload
110
119
  */
111
- private handleVerify;
120
+ private validatePayment;
112
121
  /**
113
- * GET /status/:chargeId - Check charge status
122
+ * Claim payment using transferWithAuthorization
114
123
  */
115
- private handleStatus;
124
+ private claimPayment;
116
125
  private readBody;
117
126
  private sendJson;
118
127
  }
@@ -69,11 +69,15 @@ interface MoltsPayServerOptions {
69
69
  port?: number;
70
70
  host?: string;
71
71
  chargeExpirySecs?: number;
72
+ /** Private key for claiming payments (can also use MOLTSPAY_PRIVATE_KEY env) */
73
+ privateKey?: string;
72
74
  }
73
75
 
74
76
  /**
75
77
  * MoltsPay Server - Payment infrastructure for AI Agents
76
78
  *
79
+ * Uses x402 protocol for gasless, pay-for-success payments.
80
+ *
77
81
  * Usage:
78
82
  * const server = new MoltsPayServer('./moltspay.services.json');
79
83
  * server.skill('text-to-video', async (params) => { ... });
@@ -83,8 +87,9 @@ interface MoltsPayServerOptions {
83
87
  declare class MoltsPayServer {
84
88
  private manifest;
85
89
  private skills;
86
- private charges;
87
90
  private options;
91
+ private provider;
92
+ private wallet;
88
93
  constructor(servicesPath: string, options?: MoltsPayServerOptions);
89
94
  /**
90
95
  * Register a skill handler for a service
@@ -100,19 +105,23 @@ declare class MoltsPayServer {
100
105
  */
101
106
  private handleGetServices;
102
107
  /**
103
- * POST /pay - Create payment request
108
+ * POST /execute - Execute service with x402 payment
104
109
  * Body: { service: string, params: object }
110
+ * Header: X-Payment (optional - if missing, returns 402)
111
+ */
112
+ private handleExecute;
113
+ /**
114
+ * Return 402 with x402 payment requirements
105
115
  */
106
- private handlePay;
116
+ private sendPaymentRequired;
107
117
  /**
108
- * POST /verify - Verify payment and execute skill
109
- * Body: { chargeId: string, txHash: string }
118
+ * Validate x402 payment payload
110
119
  */
111
- private handleVerify;
120
+ private validatePayment;
112
121
  /**
113
- * GET /status/:chargeId - Check charge status
122
+ * Claim payment using transferWithAuthorization
114
123
  */
115
- private handleStatus;
124
+ private claimPayment;
116
125
  private readBody;
117
126
  private sendJson;
118
127
  }
@@ -25,8 +25,6 @@ __export(server_exports, {
25
25
  module.exports = __toCommonJS(server_exports);
26
26
  var import_fs = require("fs");
27
27
  var import_http = require("http");
28
-
29
- // src/verify/index.ts
30
28
  var import_ethers = require("ethers");
31
29
 
32
30
  // src/chains/index.ts
@@ -86,101 +84,39 @@ function getChain(name) {
86
84
  }
87
85
  return config;
88
86
  }
89
- function getChainById(chainId) {
90
- return Object.values(CHAINS).find((c) => c.chainId === chainId);
91
- }
92
-
93
- // src/verify/index.ts
94
- var TRANSFER_EVENT_TOPIC = import_ethers.ethers.id("Transfer(address,address,uint256)");
95
- async function verifyPayment(params) {
96
- const { txHash, expectedAmount, expectedTo } = params;
97
- let chain;
98
- try {
99
- if (typeof params.chain === "number") {
100
- chain = getChainById(params.chain);
101
- } else {
102
- chain = getChain(params.chain || "base");
103
- }
104
- if (!chain) {
105
- return { verified: false, error: `Unsupported chain: ${params.chain}` };
106
- }
107
- } catch (e) {
108
- return { verified: false, error: `Unsupported chain: ${params.chain}` };
109
- }
110
- try {
111
- const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
112
- const receipt = await provider.getTransactionReceipt(txHash);
113
- if (!receipt) {
114
- return { verified: false, error: "Transaction not found or not confirmed" };
115
- }
116
- if (receipt.status !== 1) {
117
- return { verified: false, error: "Transaction failed" };
118
- }
119
- const usdcAddress = chain.usdc?.toLowerCase();
120
- if (!usdcAddress) {
121
- return { verified: false, error: `Chain ${chain.name} USDC address not configured` };
122
- }
123
- for (const log of receipt.logs) {
124
- if (log.address.toLowerCase() !== usdcAddress) {
125
- continue;
126
- }
127
- if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC) {
128
- continue;
129
- }
130
- const from = "0x" + log.topics[1].slice(-40);
131
- const to = "0x" + log.topics[2].slice(-40);
132
- const amountRaw = BigInt(log.data);
133
- const amount = Number(amountRaw) / 1e6;
134
- if (expectedTo && to.toLowerCase() !== expectedTo.toLowerCase()) {
135
- continue;
136
- }
137
- if (amount < expectedAmount) {
138
- return {
139
- verified: false,
140
- error: `Insufficient amount: received ${amount} USDC, expected ${expectedAmount} USDC`,
141
- amount,
142
- from,
143
- to,
144
- txHash,
145
- blockNumber: receipt.blockNumber
146
- };
147
- }
148
- return {
149
- verified: true,
150
- amount,
151
- from,
152
- to,
153
- txHash,
154
- blockNumber: receipt.blockNumber
155
- };
156
- }
157
- return { verified: false, error: "No USDC transfer found" };
158
- } catch (e) {
159
- return { verified: false, error: e.message || String(e) };
160
- }
161
- }
162
87
 
163
88
  // src/server/index.ts
164
- function generateChargeId() {
165
- return "ch_" + Math.random().toString(36).substring(2, 15);
166
- }
89
+ var X402_VERSION = 2;
90
+ var PAYMENT_REQUIRED_HEADER = "x-payment-required";
91
+ var PAYMENT_HEADER = "x-payment";
167
92
  var MoltsPayServer = class {
168
93
  manifest;
169
94
  skills = /* @__PURE__ */ new Map();
170
- charges = /* @__PURE__ */ new Map();
171
95
  options;
96
+ provider = null;
97
+ wallet = null;
172
98
  constructor(servicesPath, options = {}) {
173
99
  const content = (0, import_fs.readFileSync)(servicesPath, "utf-8");
174
100
  this.manifest = JSON.parse(content);
175
101
  this.options = {
176
102
  port: options.port || 3e3,
177
103
  host: options.host || "0.0.0.0",
178
- chargeExpirySecs: options.chargeExpirySecs || 300
179
- // 5 minutes
104
+ privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
180
105
  };
106
+ if (this.options.privateKey) {
107
+ try {
108
+ const chain = getChain(this.manifest.provider.chain);
109
+ this.provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
110
+ this.wallet = new import_ethers.ethers.Wallet(this.options.privateKey, this.provider);
111
+ console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
112
+ } catch (err) {
113
+ console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
114
+ }
115
+ }
181
116
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
182
117
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
183
- console.log(`[MoltsPay] Wallet: ${this.manifest.provider.wallet}`);
118
+ console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
119
+ console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
184
120
  }
185
121
  /**
186
122
  * Register a skill handler for a service
@@ -207,10 +143,8 @@ var MoltsPayServer = class {
207
143
  server.listen(p, this.options.host, () => {
208
144
  console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
209
145
  console.log(`[MoltsPay] Endpoints:`);
210
- console.log(` GET /services - List available services`);
211
- console.log(` POST /pay - Create payment & execute service`);
212
- console.log(` POST /verify - Verify payment & get result`);
213
- console.log(` GET /status/:id - Check charge status`);
146
+ console.log(` GET /services - List available services`);
147
+ console.log(` POST /execute - Execute service (x402 payment)`);
214
148
  });
215
149
  }
216
150
  async handleRequest(req, res) {
@@ -219,7 +153,8 @@ var MoltsPayServer = class {
219
153
  const method = req.method || "GET";
220
154
  res.setHeader("Access-Control-Allow-Origin", "*");
221
155
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
222
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
156
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
157
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
223
158
  if (method === "OPTIONS") {
224
159
  res.writeHead(204);
225
160
  res.end();
@@ -229,17 +164,10 @@ var MoltsPayServer = class {
229
164
  if (method === "GET" && path === "/services") {
230
165
  return this.handleGetServices(res);
231
166
  }
232
- if (method === "POST" && path === "/pay") {
233
- const body = await this.readBody(req);
234
- return this.handlePay(body, res);
235
- }
236
- if (method === "POST" && path === "/verify") {
167
+ if (method === "POST" && path === "/execute") {
237
168
  const body = await this.readBody(req);
238
- return this.handleVerify(body, res);
239
- }
240
- if (method === "GET" && path.startsWith("/status/")) {
241
- const chargeId = path.replace("/status/", "");
242
- return this.handleStatus(chargeId, res);
169
+ const paymentHeader = req.headers[PAYMENT_HEADER];
170
+ return this.handleExecute(body, paymentHeader, res);
243
171
  }
244
172
  this.sendJson(res, 404, { error: "Not found" });
245
173
  } catch (err) {
@@ -251,6 +179,7 @@ var MoltsPayServer = class {
251
179
  * GET /services - List available services
252
180
  */
253
181
  handleGetServices(res) {
182
+ const chain = getChain(this.manifest.provider.chain);
254
183
  const services = this.manifest.services.map((s) => ({
255
184
  id: s.id,
256
185
  name: s.name,
@@ -263,14 +192,20 @@ var MoltsPayServer = class {
263
192
  }));
264
193
  this.sendJson(res, 200, {
265
194
  provider: this.manifest.provider,
266
- services
195
+ services,
196
+ x402: {
197
+ version: X402_VERSION,
198
+ network: `eip155:${chain.chainId}`,
199
+ schemes: ["exact"]
200
+ }
267
201
  });
268
202
  }
269
203
  /**
270
- * POST /pay - Create payment request
204
+ * POST /execute - Execute service with x402 payment
271
205
  * Body: { service: string, params: object }
206
+ * Header: X-Payment (optional - if missing, returns 402)
272
207
  */
273
- handlePay(body, res) {
208
+ async handleExecute(body, paymentHeader, res) {
274
209
  const { service, params } = body;
275
210
  if (!service) {
276
211
  return this.sendJson(res, 400, { error: "Missing service" });
@@ -284,113 +219,129 @@ var MoltsPayServer = class {
284
219
  return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
285
220
  }
286
221
  }
287
- const chargeId = generateChargeId();
288
- const now = Date.now();
289
- const charge = {
290
- id: chargeId,
291
- service,
292
- params: params || {},
293
- amount: skill.config.price,
294
- currency: skill.config.currency,
295
- status: "pending",
296
- createdAt: now,
297
- expiresAt: now + this.options.chargeExpirySecs * 1e3
298
- };
299
- this.charges.set(chargeId, charge);
300
- const paymentRequest = {
301
- chargeId,
302
- service,
303
- amount: charge.amount,
304
- currency: charge.currency,
305
- wallet: this.manifest.provider.wallet,
306
- chain: this.manifest.provider.chain,
307
- expiresAt: charge.expiresAt
308
- };
309
- this.sendJson(res, 402, {
310
- message: "Payment required",
311
- payment: paymentRequest
222
+ if (!paymentHeader) {
223
+ return this.sendPaymentRequired(skill.config, res);
224
+ }
225
+ let payment;
226
+ try {
227
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
228
+ payment = JSON.parse(decoded);
229
+ } catch {
230
+ return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
231
+ }
232
+ const validation = this.validatePayment(payment, skill.config);
233
+ if (!validation.valid) {
234
+ return this.sendJson(res, 402, { error: validation.error });
235
+ }
236
+ console.log(`[MoltsPay] Executing skill: ${service}`);
237
+ let result;
238
+ try {
239
+ result = await skill.handler(params || {});
240
+ } catch (err) {
241
+ console.error("[MoltsPay] Skill execution failed:", err.message);
242
+ return this.sendJson(res, 500, {
243
+ error: "Service execution failed",
244
+ message: err.message
245
+ });
246
+ }
247
+ console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
248
+ let txHash = null;
249
+ try {
250
+ txHash = await this.claimPayment(payment);
251
+ console.log(`[MoltsPay] Payment claimed: ${txHash}`);
252
+ } catch (err) {
253
+ console.error("[MoltsPay] Payment claim failed:", err.message);
254
+ }
255
+ this.sendJson(res, 200, {
256
+ success: true,
257
+ result,
258
+ payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
312
259
  });
313
260
  }
314
261
  /**
315
- * POST /verify - Verify payment and execute skill
316
- * Body: { chargeId: string, txHash: string }
262
+ * Return 402 with x402 payment requirements
317
263
  */
318
- async handleVerify(body, res) {
319
- const { chargeId, txHash } = body;
320
- if (!chargeId || !txHash) {
321
- return this.sendJson(res, 400, { error: "Missing chargeId or txHash" });
264
+ sendPaymentRequired(config, res) {
265
+ const chain = getChain(this.manifest.provider.chain);
266
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
267
+ const requirements = [{
268
+ scheme: "exact",
269
+ network: `eip155:${chain.chainId}`,
270
+ maxAmountRequired: amountInUnits,
271
+ resource: this.manifest.provider.wallet,
272
+ description: `${config.name} - $${config.price} ${config.currency}`
273
+ }];
274
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
275
+ res.writeHead(402, {
276
+ "Content-Type": "application/json",
277
+ [PAYMENT_REQUIRED_HEADER]: encoded
278
+ });
279
+ res.end(JSON.stringify({
280
+ error: "Payment required",
281
+ message: `Service requires $${config.price} ${config.currency}`,
282
+ x402: requirements[0]
283
+ }, null, 2));
284
+ }
285
+ /**
286
+ * Validate x402 payment payload
287
+ */
288
+ validatePayment(payment, config) {
289
+ if (payment.x402Version !== X402_VERSION) {
290
+ return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
322
291
  }
323
- const charge = this.charges.get(chargeId);
324
- if (!charge) {
325
- return this.sendJson(res, 404, { error: "Charge not found" });
292
+ if (payment.scheme !== "exact") {
293
+ return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
326
294
  }
327
- if (Date.now() > charge.expiresAt) {
328
- charge.status = "expired";
329
- return this.sendJson(res, 400, { error: "Charge expired" });
295
+ const chain = getChain(this.manifest.provider.chain);
296
+ const expectedNetwork = `eip155:${chain.chainId}`;
297
+ if (payment.network !== expectedNetwork) {
298
+ return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
330
299
  }
331
- if (charge.status === "completed") {
332
- return this.sendJson(res, 200, {
333
- status: "completed",
334
- result: charge.result
335
- });
300
+ const auth = payment.payload.authorization;
301
+ if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
302
+ return { valid: false, error: "Payment recipient mismatch" };
336
303
  }
337
- try {
338
- const verification = await verifyPayment({
339
- txHash,
340
- expectedTo: this.manifest.provider.wallet,
341
- expectedAmount: charge.amount,
342
- chain: this.manifest.provider.chain
343
- });
344
- if (!verification.verified) {
345
- charge.status = "failed";
346
- return this.sendJson(res, 400, {
347
- error: "Payment verification failed",
348
- reason: verification.error
349
- });
350
- }
351
- charge.status = "paid";
352
- charge.txHash = txHash;
353
- charge.paidAt = Date.now();
354
- const skill = this.skills.get(charge.service);
355
- console.log(`[MoltsPay] Executing skill: ${charge.service}`);
356
- const result = await skill.handler(charge.params);
357
- charge.status = "completed";
358
- charge.result = result;
359
- charge.completedAt = Date.now();
360
- this.sendJson(res, 200, {
361
- status: "completed",
362
- chargeId,
363
- txHash,
364
- result
365
- });
366
- } catch (err) {
367
- console.error("[MoltsPay] Skill execution error:", err);
368
- charge.status = "failed";
369
- this.sendJson(res, 500, {
370
- error: "Skill execution failed",
371
- message: err.message
372
- });
304
+ const amount = Number(auth.value) / 1e6;
305
+ if (amount < config.price) {
306
+ return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
307
+ }
308
+ const now = Math.floor(Date.now() / 1e3);
309
+ if (Number(auth.validBefore) < now) {
310
+ return { valid: false, error: "Payment authorization expired" };
373
311
  }
312
+ if (Number(auth.validAfter) > now) {
313
+ return { valid: false, error: "Payment authorization not yet valid" };
314
+ }
315
+ return { valid: true };
374
316
  }
375
317
  /**
376
- * GET /status/:chargeId - Check charge status
318
+ * Claim payment using transferWithAuthorization
377
319
  */
378
- handleStatus(chargeId, res) {
379
- const charge = this.charges.get(chargeId);
380
- if (!charge) {
381
- return this.sendJson(res, 404, { error: "Charge not found" });
320
+ async claimPayment(payment) {
321
+ if (!this.wallet || !this.provider) {
322
+ throw new Error("Wallet not configured for payment claims");
382
323
  }
383
- this.sendJson(res, 200, {
384
- chargeId: charge.id,
385
- service: charge.service,
386
- amount: charge.amount,
387
- currency: charge.currency,
388
- status: charge.status,
389
- txHash: charge.txHash,
390
- result: charge.status === "completed" ? charge.result : void 0,
391
- createdAt: charge.createdAt,
392
- expiresAt: charge.expiresAt
393
- });
324
+ const chain = getChain(this.manifest.provider.chain);
325
+ const auth = payment.payload.authorization;
326
+ const sig = payment.payload.signature;
327
+ const { r, s, v } = import_ethers.ethers.Signature.from(sig);
328
+ const usdcAbi = [
329
+ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
330
+ ];
331
+ const usdc = new import_ethers.ethers.Contract(chain.usdc, usdcAbi, this.wallet);
332
+ const tx = await usdc.transferWithAuthorization(
333
+ auth.from,
334
+ auth.to,
335
+ auth.value,
336
+ auth.validAfter,
337
+ auth.validBefore,
338
+ auth.nonce,
339
+ v,
340
+ r,
341
+ s
342
+ );
343
+ const receipt = await tx.wait();
344
+ return receipt.hash;
394
345
  }
395
346
  async readBody(req) {
396
347
  return new Promise((resolve, reject) => {