moltspay 0.7.2 → 0.8.1

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/dist/cli/index.js CHANGED
@@ -93,11 +93,11 @@ function getChain(name) {
93
93
  }
94
94
  return config;
95
95
  }
96
- function getChainById(chainId) {
97
- return Object.values(CHAINS).find((c) => c.chainId === chainId);
98
- }
99
96
 
100
97
  // src/client/index.ts
98
+ var X402_VERSION = 2;
99
+ var PAYMENT_REQUIRED_HEADER = "x-payment-required";
100
+ var PAYMENT_HEADER = "x-payment";
101
101
  var DEFAULT_CONFIG = {
102
102
  chain: "base",
103
103
  limits: {
@@ -161,43 +161,112 @@ var MoltsPayClient = class {
161
161
  return res.json();
162
162
  }
163
163
  /**
164
- * Pay for a service and get the result
164
+ * Pay for a service and get the result (x402 protocol)
165
+ *
166
+ * This is GASLESS for the client - server pays gas to claim payment.
167
+ * This is PAY-FOR-SUCCESS - payment only claimed if service succeeds.
165
168
  */
166
169
  async pay(serverUrl, service, params) {
167
- if (!this.wallet) {
170
+ if (!this.wallet || !this.walletData) {
168
171
  throw new Error("Client not initialized. Run: npx moltspay init");
169
172
  }
170
- const payRes = await fetch(`${serverUrl}/pay`, {
173
+ console.log(`[MoltsPay] Requesting service: ${service}`);
174
+ const initialRes = await fetch(`${serverUrl}/execute`, {
171
175
  method: "POST",
172
176
  headers: { "Content-Type": "application/json" },
173
177
  body: JSON.stringify({ service, params })
174
178
  });
175
- if (payRes.status !== 402) {
176
- const err = await payRes.json();
177
- throw new Error(err.error || "Unexpected response");
178
- }
179
- const paymentReq = await payRes.json();
180
- const { payment } = paymentReq;
181
- this.checkLimits(payment.amount);
182
- console.log(`[MoltsPay] Paying $${payment.amount} ${payment.currency} to ${payment.wallet}`);
183
- const txHash = await this.executePayment(payment);
184
- console.log(`[MoltsPay] Payment tx: ${txHash}`);
185
- const verifyRes = await fetch(`${serverUrl}/verify`, {
179
+ if (initialRes.status !== 402) {
180
+ const data = await initialRes.json();
181
+ if (initialRes.ok && data.result) {
182
+ return data.result;
183
+ }
184
+ throw new Error(data.error || "Unexpected response");
185
+ }
186
+ const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
187
+ if (!paymentRequiredHeader) {
188
+ throw new Error("Missing x-payment-required header");
189
+ }
190
+ let requirements;
191
+ try {
192
+ const decoded = Buffer.from(paymentRequiredHeader, "base64").toString("utf-8");
193
+ requirements = JSON.parse(decoded);
194
+ if (!Array.isArray(requirements)) {
195
+ requirements = [requirements];
196
+ }
197
+ } catch {
198
+ throw new Error("Invalid x-payment-required header");
199
+ }
200
+ const chain = getChain(this.config.chain);
201
+ const network = `eip155:${chain.chainId}`;
202
+ const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
203
+ if (!req) {
204
+ throw new Error(`No matching payment option for ${network}`);
205
+ }
206
+ const amount = Number(req.maxAmountRequired) / 1e6;
207
+ this.checkLimits(amount);
208
+ console.log(`[MoltsPay] Signing payment: $${amount} USDC (gasless)`);
209
+ const authorization = await this.signEIP3009(req.resource, amount, chain);
210
+ const payload = {
211
+ x402Version: X402_VERSION,
212
+ scheme: "exact",
213
+ network,
214
+ payload: authorization
215
+ };
216
+ const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
217
+ console.log(`[MoltsPay] Sending request with payment...`);
218
+ const paidRes = await fetch(`${serverUrl}/execute`, {
186
219
  method: "POST",
187
- headers: { "Content-Type": "application/json" },
188
- body: JSON.stringify({
189
- chargeId: payment.chargeId,
190
- txHash
191
- })
220
+ headers: {
221
+ "Content-Type": "application/json",
222
+ [PAYMENT_HEADER]: paymentHeader
223
+ },
224
+ body: JSON.stringify({ service, params })
192
225
  });
193
- if (!verifyRes.ok) {
194
- const err = await verifyRes.json();
195
- throw new Error(err.error || "Verification failed");
226
+ const result = await paidRes.json();
227
+ if (!paidRes.ok) {
228
+ throw new Error(result.error || "Service execution failed");
196
229
  }
197
- const result = await verifyRes.json();
198
- this.recordSpending(payment.amount);
230
+ this.recordSpending(amount);
231
+ console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
199
232
  return result.result;
200
233
  }
234
+ /**
235
+ * Sign EIP-3009 transferWithAuthorization (GASLESS)
236
+ * This only signs - no on-chain transaction, no gas needed.
237
+ */
238
+ async signEIP3009(to, amount, chain) {
239
+ const validAfter = 0;
240
+ const validBefore = Math.floor(Date.now() / 1e3) + 3600;
241
+ const nonce = import_ethers.ethers.hexlify(import_ethers.ethers.randomBytes(32));
242
+ const value = BigInt(Math.floor(amount * 1e6)).toString();
243
+ const authorization = {
244
+ from: this.wallet.address,
245
+ to,
246
+ value,
247
+ validAfter: validAfter.toString(),
248
+ validBefore: validBefore.toString(),
249
+ nonce
250
+ };
251
+ const domain = {
252
+ name: "USD Coin",
253
+ version: "2",
254
+ chainId: chain.chainId,
255
+ verifyingContract: chain.usdc
256
+ };
257
+ const types = {
258
+ TransferWithAuthorization: [
259
+ { name: "from", type: "address" },
260
+ { name: "to", type: "address" },
261
+ { name: "value", type: "uint256" },
262
+ { name: "validAfter", type: "uint256" },
263
+ { name: "validBefore", type: "uint256" },
264
+ { name: "nonce", type: "bytes32" }
265
+ ]
266
+ };
267
+ const signature = await this.wallet.signTypedData(domain, types, authorization);
268
+ return { authorization, signature };
269
+ }
201
270
  /**
202
271
  * Check spending limits
203
272
  */
@@ -224,36 +293,6 @@ var MoltsPayClient = class {
224
293
  recordSpending(amount) {
225
294
  this.todaySpending += amount;
226
295
  }
227
- /**
228
- * Execute payment on-chain
229
- */
230
- async executePayment(payment) {
231
- let chain;
232
- try {
233
- chain = getChain(payment.chain);
234
- } catch {
235
- throw new Error(`Unknown chain: ${payment.chain}`);
236
- }
237
- const { ethers: ethers2 } = await import("ethers");
238
- const provider = new ethers2.JsonRpcProvider(chain.rpc);
239
- const signer = new ethers2.Wallet(this.walletData.privateKey, provider);
240
- const usdcAddress = chain.usdc;
241
- const usdcAbi = [
242
- "function transfer(address to, uint256 amount) returns (bool)",
243
- "function balanceOf(address account) view returns (uint256)"
244
- ];
245
- const usdc = new ethers2.Contract(usdcAddress, usdcAbi, signer);
246
- const amountInUnits = ethers2.parseUnits(payment.amount.toString(), 6);
247
- const balance = await usdc.balanceOf(this.wallet.address);
248
- if (balance < amountInUnits) {
249
- throw new Error(
250
- `Insufficient USDC balance: ${ethers2.formatUnits(balance, 6)} < ${payment.amount}`
251
- );
252
- }
253
- const tx = await usdc.transfer(payment.wallet, amountInUnits);
254
- const receipt = await tx.wait();
255
- return receipt.hash;
256
- }
257
296
  // --- Config & Wallet Management ---
258
297
  loadConfig() {
259
298
  const configPath = (0, import_path.join)(this.configDir, "config.json");
@@ -313,15 +352,14 @@ var MoltsPayClient = class {
313
352
  } catch {
314
353
  throw new Error(`Unknown chain: ${this.config.chain}`);
315
354
  }
316
- const { ethers: ethers2 } = await import("ethers");
317
- const provider = new ethers2.JsonRpcProvider(chain.rpc);
355
+ const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
318
356
  const nativeBalance = await provider.getBalance(this.wallet.address);
319
357
  const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
320
- const usdc = new ethers2.Contract(chain.usdc, usdcAbi, provider);
358
+ const usdc = new import_ethers.ethers.Contract(chain.usdc, usdcAbi, provider);
321
359
  const usdcBalance = await usdc.balanceOf(this.wallet.address);
322
360
  return {
323
- usdc: parseFloat(ethers2.formatUnits(usdcBalance, 6)),
324
- native: parseFloat(ethers2.formatEther(nativeBalance))
361
+ usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, 6)),
362
+ native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
325
363
  };
326
364
  }
327
365
  };
@@ -329,99 +367,29 @@ var MoltsPayClient = class {
329
367
  // src/server/index.ts
330
368
  var import_fs2 = require("fs");
331
369
  var import_http = require("http");
332
-
333
- // src/verify/index.ts
334
- var import_ethers2 = require("ethers");
335
- var TRANSFER_EVENT_TOPIC = import_ethers2.ethers.id("Transfer(address,address,uint256)");
336
- async function verifyPayment(params) {
337
- const { txHash, expectedAmount, expectedTo } = params;
338
- let chain;
339
- try {
340
- if (typeof params.chain === "number") {
341
- chain = getChainById(params.chain);
342
- } else {
343
- chain = getChain(params.chain || "base");
344
- }
345
- if (!chain) {
346
- return { verified: false, error: `Unsupported chain: ${params.chain}` };
347
- }
348
- } catch (e) {
349
- return { verified: false, error: `Unsupported chain: ${params.chain}` };
350
- }
351
- try {
352
- const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
353
- const receipt = await provider.getTransactionReceipt(txHash);
354
- if (!receipt) {
355
- return { verified: false, error: "Transaction not found or not confirmed" };
356
- }
357
- if (receipt.status !== 1) {
358
- return { verified: false, error: "Transaction failed" };
359
- }
360
- const usdcAddress = chain.usdc?.toLowerCase();
361
- if (!usdcAddress) {
362
- return { verified: false, error: `Chain ${chain.name} USDC address not configured` };
363
- }
364
- for (const log of receipt.logs) {
365
- if (log.address.toLowerCase() !== usdcAddress) {
366
- continue;
367
- }
368
- if (log.topics.length < 3 || log.topics[0] !== TRANSFER_EVENT_TOPIC) {
369
- continue;
370
- }
371
- const from = "0x" + log.topics[1].slice(-40);
372
- const to = "0x" + log.topics[2].slice(-40);
373
- const amountRaw = BigInt(log.data);
374
- const amount = Number(amountRaw) / 1e6;
375
- if (expectedTo && to.toLowerCase() !== expectedTo.toLowerCase()) {
376
- continue;
377
- }
378
- if (amount < expectedAmount) {
379
- return {
380
- verified: false,
381
- error: `Insufficient amount: received ${amount} USDC, expected ${expectedAmount} USDC`,
382
- amount,
383
- from,
384
- to,
385
- txHash,
386
- blockNumber: receipt.blockNumber
387
- };
388
- }
389
- return {
390
- verified: true,
391
- amount,
392
- from,
393
- to,
394
- txHash,
395
- blockNumber: receipt.blockNumber
396
- };
397
- }
398
- return { verified: false, error: "No USDC transfer found" };
399
- } catch (e) {
400
- return { verified: false, error: e.message || String(e) };
401
- }
402
- }
403
-
404
- // src/server/index.ts
405
- function generateChargeId() {
406
- return "ch_" + Math.random().toString(36).substring(2, 15);
407
- }
370
+ var X402_VERSION2 = 2;
371
+ var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
372
+ var PAYMENT_HEADER2 = "x-payment";
373
+ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
374
+ var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
408
375
  var MoltsPayServer = class {
409
376
  manifest;
410
377
  skills = /* @__PURE__ */ new Map();
411
- charges = /* @__PURE__ */ new Map();
412
378
  options;
379
+ facilitatorUrl;
413
380
  constructor(servicesPath, options = {}) {
414
381
  const content = (0, import_fs2.readFileSync)(servicesPath, "utf-8");
415
382
  this.manifest = JSON.parse(content);
416
383
  this.options = {
417
384
  port: options.port || 3e3,
418
- host: options.host || "0.0.0.0",
419
- chargeExpirySecs: options.chargeExpirySecs || 300
420
- // 5 minutes
385
+ host: options.host || "0.0.0.0"
421
386
  };
387
+ this.facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR_URL;
422
388
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
423
389
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
424
- console.log(`[MoltsPay] Wallet: ${this.manifest.provider.wallet}`);
390
+ console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
391
+ console.log(`[MoltsPay] Facilitator: ${this.facilitatorUrl}`);
392
+ console.log(`[MoltsPay] Protocol: x402 (gasless for both client AND server)`);
425
393
  }
426
394
  /**
427
395
  * Register a skill handler for a service
@@ -431,56 +399,45 @@ var MoltsPayServer = class {
431
399
  if (!config) {
432
400
  throw new Error(`Service '${serviceId}' not found in manifest`);
433
401
  }
434
- this.skills.set(serviceId, {
435
- id: serviceId,
436
- config,
437
- handler
438
- });
439
- console.log(`[MoltsPay] Registered skill: ${serviceId} ($${config.price} ${config.currency})`);
402
+ this.skills.set(serviceId, { id: serviceId, config, handler });
440
403
  return this;
441
404
  }
442
405
  /**
443
- * Start the server
406
+ * Start HTTP server
444
407
  */
445
408
  listen(port) {
446
- const p = port || this.options.port;
409
+ const p = port || this.options.port || 3e3;
410
+ const host = this.options.host || "0.0.0.0";
447
411
  const server = (0, import_http.createServer)((req, res) => this.handleRequest(req, res));
448
- server.listen(p, this.options.host, () => {
449
- console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
412
+ server.listen(p, host, () => {
413
+ console.log(`[MoltsPay] Server listening on http://${host}:${p}`);
450
414
  console.log(`[MoltsPay] Endpoints:`);
451
- console.log(` GET /services - List available services`);
452
- console.log(` POST /pay - Create payment & execute service`);
453
- console.log(` POST /verify - Verify payment & get result`);
454
- console.log(` GET /status/:id - Check charge status`);
415
+ console.log(` GET /services - List available services`);
416
+ console.log(` POST /execute - Execute service (x402 payment)`);
455
417
  });
456
418
  }
419
+ /**
420
+ * Handle incoming request
421
+ */
457
422
  async handleRequest(req, res) {
458
- const url = new URL(req.url || "/", `http://${req.headers.host}`);
459
- const path = url.pathname;
460
- const method = req.method || "GET";
461
423
  res.setHeader("Access-Control-Allow-Origin", "*");
462
424
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
463
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
464
- if (method === "OPTIONS") {
425
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
426
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
427
+ if (req.method === "OPTIONS") {
465
428
  res.writeHead(204);
466
429
  res.end();
467
430
  return;
468
431
  }
469
432
  try {
470
- if (method === "GET" && path === "/services") {
433
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
434
+ if (url.pathname === "/services" && req.method === "GET") {
471
435
  return this.handleGetServices(res);
472
436
  }
473
- if (method === "POST" && path === "/pay") {
474
- const body = await this.readBody(req);
475
- return this.handlePay(body, res);
476
- }
477
- if (method === "POST" && path === "/verify") {
437
+ if (url.pathname === "/execute" && req.method === "POST") {
478
438
  const body = await this.readBody(req);
479
- return this.handleVerify(body, res);
480
- }
481
- if (method === "GET" && path.startsWith("/status/")) {
482
- const chargeId = path.replace("/status/", "");
483
- return this.handleStatus(chargeId, res);
439
+ const paymentHeader = req.headers[PAYMENT_HEADER2];
440
+ return await this.handleExecute(body, paymentHeader, res);
484
441
  }
485
442
  this.sendJson(res, 404, { error: "Not found" });
486
443
  } catch (err) {
@@ -492,6 +449,7 @@ var MoltsPayServer = class {
492
449
  * GET /services - List available services
493
450
  */
494
451
  handleGetServices(res) {
452
+ const chain = getChain(this.manifest.provider.chain);
495
453
  const services = this.manifest.services.map((s) => ({
496
454
  id: s.id,
497
455
  name: s.name,
@@ -504,14 +462,21 @@ var MoltsPayServer = class {
504
462
  }));
505
463
  this.sendJson(res, 200, {
506
464
  provider: this.manifest.provider,
507
- services
465
+ services,
466
+ x402: {
467
+ version: X402_VERSION2,
468
+ network: `eip155:${chain.chainId}`,
469
+ schemes: ["exact"],
470
+ facilitator: this.facilitatorUrl
471
+ }
508
472
  });
509
473
  }
510
474
  /**
511
- * POST /pay - Create payment request
475
+ * POST /execute - Execute service with x402 payment
512
476
  * Body: { service: string, params: object }
477
+ * Header: X-Payment (optional - if missing, returns 402)
513
478
  */
514
- handlePay(body, res) {
479
+ async handleExecute(body, paymentHeader, res) {
515
480
  const { service, params } = body;
516
481
  if (!service) {
517
482
  return this.sendJson(res, 400, { error: "Missing service" });
@@ -525,113 +490,162 @@ var MoltsPayServer = class {
525
490
  return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
526
491
  }
527
492
  }
528
- const chargeId = generateChargeId();
529
- const now = Date.now();
530
- const charge = {
531
- id: chargeId,
532
- service,
533
- params: params || {},
534
- amount: skill.config.price,
535
- currency: skill.config.currency,
536
- status: "pending",
537
- createdAt: now,
538
- expiresAt: now + this.options.chargeExpirySecs * 1e3
539
- };
540
- this.charges.set(chargeId, charge);
541
- const paymentRequest = {
542
- chargeId,
543
- service,
544
- amount: charge.amount,
545
- currency: charge.currency,
546
- wallet: this.manifest.provider.wallet,
547
- chain: this.manifest.provider.chain,
548
- expiresAt: charge.expiresAt
549
- };
550
- this.sendJson(res, 402, {
551
- message: "Payment required",
552
- payment: paymentRequest
493
+ if (!paymentHeader) {
494
+ return this.sendPaymentRequired(skill.config, res);
495
+ }
496
+ let payment;
497
+ try {
498
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
499
+ payment = JSON.parse(decoded);
500
+ } catch {
501
+ return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
502
+ }
503
+ const validation = this.validatePayment(payment, skill.config);
504
+ if (!validation.valid) {
505
+ return this.sendJson(res, 402, { error: validation.error });
506
+ }
507
+ console.log(`[MoltsPay] Verifying payment with facilitator...`);
508
+ const verifyResult = await this.verifyWithFacilitator(payment, skill.config);
509
+ if (!verifyResult.valid) {
510
+ return this.sendJson(res, 402, { error: `Payment verification failed: ${verifyResult.error}` });
511
+ }
512
+ console.log(`[MoltsPay] Executing skill: ${service}`);
513
+ let result;
514
+ try {
515
+ result = await skill.handler(params || {});
516
+ } catch (err) {
517
+ console.error("[MoltsPay] Skill execution failed:", err.message);
518
+ return this.sendJson(res, 500, {
519
+ error: "Service execution failed",
520
+ message: err.message
521
+ });
522
+ }
523
+ console.log(`[MoltsPay] Skill succeeded, settling payment...`);
524
+ let settlement = null;
525
+ try {
526
+ settlement = await this.settleWithFacilitator(payment, skill.config);
527
+ console.log(`[MoltsPay] Payment settled: ${settlement.transaction || "pending"}`);
528
+ } catch (err) {
529
+ console.error("[MoltsPay] Settlement failed:", err.message);
530
+ }
531
+ const responseHeaders = {};
532
+ if (settlement) {
533
+ const responsePayload = {
534
+ success: true,
535
+ transaction: settlement.transaction,
536
+ network: payment.network
537
+ };
538
+ responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(
539
+ JSON.stringify(responsePayload)
540
+ ).toString("base64");
541
+ }
542
+ this.sendJson(res, 200, {
543
+ success: true,
544
+ result,
545
+ payment: settlement ? { transaction: settlement.transaction, status: "settled" } : { status: "pending" }
546
+ }, responseHeaders);
547
+ }
548
+ /**
549
+ * Return 402 with x402 payment requirements
550
+ */
551
+ sendPaymentRequired(config, res) {
552
+ const chain = getChain(this.manifest.provider.chain);
553
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
554
+ const requirements = [{
555
+ scheme: "exact",
556
+ network: `eip155:${chain.chainId}`,
557
+ maxAmountRequired: amountInUnits,
558
+ resource: this.manifest.provider.wallet,
559
+ description: `${config.name} - $${config.price} ${config.currency}`,
560
+ // Include facilitator info for client
561
+ extra: JSON.stringify({ facilitator: this.facilitatorUrl })
562
+ }];
563
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
564
+ res.writeHead(402, {
565
+ "Content-Type": "application/json",
566
+ [PAYMENT_REQUIRED_HEADER2]: encoded
553
567
  });
568
+ res.end(JSON.stringify({
569
+ error: "Payment required",
570
+ message: `Service requires $${config.price} ${config.currency}`,
571
+ x402: requirements[0]
572
+ }, null, 2));
554
573
  }
555
574
  /**
556
- * POST /verify - Verify payment and execute skill
557
- * Body: { chargeId: string, txHash: string }
575
+ * Basic payment validation (before calling facilitator)
558
576
  */
559
- async handleVerify(body, res) {
560
- const { chargeId, txHash } = body;
561
- if (!chargeId || !txHash) {
562
- return this.sendJson(res, 400, { error: "Missing chargeId or txHash" });
563
- }
564
- const charge = this.charges.get(chargeId);
565
- if (!charge) {
566
- return this.sendJson(res, 404, { error: "Charge not found" });
567
- }
568
- if (Date.now() > charge.expiresAt) {
569
- charge.status = "expired";
570
- return this.sendJson(res, 400, { error: "Charge expired" });
571
- }
572
- if (charge.status === "completed") {
573
- return this.sendJson(res, 200, {
574
- status: "completed",
575
- result: charge.result
576
- });
577
+ validatePayment(payment, config) {
578
+ if (payment.x402Version !== X402_VERSION2) {
579
+ return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
577
580
  }
581
+ if (payment.scheme !== "exact") {
582
+ return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
583
+ }
584
+ const chain = getChain(this.manifest.provider.chain);
585
+ const expectedNetwork = `eip155:${chain.chainId}`;
586
+ if (payment.network !== expectedNetwork) {
587
+ return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
588
+ }
589
+ return { valid: true };
590
+ }
591
+ /**
592
+ * Verify payment with facilitator
593
+ */
594
+ async verifyWithFacilitator(payment, config) {
578
595
  try {
579
- const verification = await verifyPayment({
580
- txHash,
581
- expectedTo: this.manifest.provider.wallet,
582
- expectedAmount: charge.amount,
583
- chain: this.manifest.provider.chain
596
+ const chain = getChain(this.manifest.provider.chain);
597
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
598
+ const requirements = {
599
+ scheme: "exact",
600
+ network: `eip155:${chain.chainId}`,
601
+ maxAmountRequired: amountInUnits,
602
+ resource: this.manifest.provider.wallet
603
+ };
604
+ const response = await fetch(`${this.facilitatorUrl}/verify`, {
605
+ method: "POST",
606
+ headers: { "Content-Type": "application/json" },
607
+ body: JSON.stringify({
608
+ paymentPayload: payment,
609
+ paymentRequirements: requirements
610
+ })
584
611
  });
585
- if (!verification.verified) {
586
- charge.status = "failed";
587
- return this.sendJson(res, 400, {
588
- error: "Payment verification failed",
589
- reason: verification.error
590
- });
612
+ const result = await response.json();
613
+ if (!response.ok || !result.isValid) {
614
+ return { valid: false, error: result.invalidReason || "Verification failed" };
591
615
  }
592
- charge.status = "paid";
593
- charge.txHash = txHash;
594
- charge.paidAt = Date.now();
595
- const skill = this.skills.get(charge.service);
596
- console.log(`[MoltsPay] Executing skill: ${charge.service}`);
597
- const result = await skill.handler(charge.params);
598
- charge.status = "completed";
599
- charge.result = result;
600
- charge.completedAt = Date.now();
601
- this.sendJson(res, 200, {
602
- status: "completed",
603
- chargeId,
604
- txHash,
605
- result
606
- });
616
+ return { valid: true };
607
617
  } catch (err) {
608
- console.error("[MoltsPay] Skill execution error:", err);
609
- charge.status = "failed";
610
- this.sendJson(res, 500, {
611
- error: "Skill execution failed",
612
- message: err.message
613
- });
618
+ return { valid: false, error: `Facilitator error: ${err.message}` };
614
619
  }
615
620
  }
616
621
  /**
617
- * GET /status/:chargeId - Check charge status
622
+ * Settle payment with facilitator (execute on-chain transfer)
618
623
  */
619
- handleStatus(chargeId, res) {
620
- const charge = this.charges.get(chargeId);
621
- if (!charge) {
622
- return this.sendJson(res, 404, { error: "Charge not found" });
623
- }
624
- this.sendJson(res, 200, {
625
- chargeId: charge.id,
626
- service: charge.service,
627
- amount: charge.amount,
628
- currency: charge.currency,
629
- status: charge.status,
630
- txHash: charge.txHash,
631
- result: charge.status === "completed" ? charge.result : void 0,
632
- createdAt: charge.createdAt,
633
- expiresAt: charge.expiresAt
624
+ async settleWithFacilitator(payment, config) {
625
+ const chain = getChain(this.manifest.provider.chain);
626
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
627
+ const requirements = {
628
+ scheme: "exact",
629
+ network: `eip155:${chain.chainId}`,
630
+ maxAmountRequired: amountInUnits,
631
+ resource: this.manifest.provider.wallet
632
+ };
633
+ const response = await fetch(`${this.facilitatorUrl}/settle`, {
634
+ method: "POST",
635
+ headers: { "Content-Type": "application/json" },
636
+ body: JSON.stringify({
637
+ paymentPayload: payment,
638
+ paymentRequirements: requirements
639
+ })
634
640
  });
641
+ const result = await response.json();
642
+ if (!response.ok) {
643
+ throw new Error(result.error || "Settlement failed");
644
+ }
645
+ return {
646
+ transaction: result.transaction,
647
+ status: result.status || "settled"
648
+ };
635
649
  }
636
650
  async readBody(req) {
637
651
  return new Promise((resolve2, reject) => {
@@ -647,8 +661,12 @@ var MoltsPayServer = class {
647
661
  req.on("error", reject);
648
662
  });
649
663
  }
650
- sendJson(res, status, data) {
651
- res.writeHead(status, { "Content-Type": "application/json" });
664
+ sendJson(res, status, data, extraHeaders) {
665
+ const headers = { "Content-Type": "application/json" };
666
+ if (extraHeaders) {
667
+ Object.assign(headers, extraHeaders);
668
+ }
669
+ res.writeHead(status, headers);
652
670
  res.end(JSON.stringify(data, null, 2));
653
671
  }
654
672
  };
@@ -809,7 +827,7 @@ program.command("services <url>").description("List services from a provider").o
809
827
  console.error("\u274C Error:", err.message);
810
828
  }
811
829
  });
812
- 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").action(async (manifest, options) => {
830
+ 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) => {
813
831
  const manifestPath = (0, import_path2.resolve)(manifest);
814
832
  if (!(0, import_fs3.existsSync)(manifestPath)) {
815
833
  console.error(`\u274C Manifest not found: ${manifestPath}`);
@@ -817,14 +835,15 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
817
835
  }
818
836
  const port = parseInt(options.port, 10);
819
837
  const host = options.host;
838
+ const facilitatorUrl = options.facilitator;
820
839
  console.log(`
821
- \u{1F680} Starting MoltsPay Server
840
+ \u{1F680} Starting MoltsPay Server (x402 protocol)
822
841
  `);
823
842
  console.log(` Manifest: ${manifestPath}`);
824
843
  console.log(` Port: ${port}`);
825
844
  console.log("");
826
845
  try {
827
- const server = new MoltsPayServer(manifestPath, { port, host });
846
+ const server = new MoltsPayServer(manifestPath, { port, host, facilitatorUrl });
828
847
  const manifestContent = await import("fs").then(
829
848
  (fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
830
849
  );