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