moltspay 0.7.1 → 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.
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,38 @@ 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
370
  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
- }
371
+ var X402_VERSION2 = 2;
372
+ var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
373
+ var PAYMENT_HEADER2 = "x-payment";
408
374
  var MoltsPayServer = class {
409
375
  manifest;
410
376
  skills = /* @__PURE__ */ new Map();
411
- charges = /* @__PURE__ */ new Map();
412
377
  options;
378
+ provider = null;
379
+ wallet = null;
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
385
  host: options.host || "0.0.0.0",
419
- chargeExpirySecs: options.chargeExpirySecs || 300
420
- // 5 minutes
386
+ privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
421
387
  };
388
+ if (this.options.privateKey) {
389
+ try {
390
+ const chain = getChain(this.manifest.provider.chain);
391
+ this.provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
392
+ this.wallet = new import_ethers2.ethers.Wallet(this.options.privateKey, this.provider);
393
+ console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
394
+ } catch (err) {
395
+ console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
396
+ }
397
+ }
422
398
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
423
399
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
424
- console.log(`[MoltsPay] Wallet: ${this.manifest.provider.wallet}`);
400
+ console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
401
+ console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
425
402
  }
426
403
  /**
427
404
  * Register a skill handler for a service
@@ -448,10 +425,8 @@ var MoltsPayServer = class {
448
425
  server.listen(p, this.options.host, () => {
449
426
  console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
450
427
  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`);
428
+ console.log(` GET /services - List available services`);
429
+ console.log(` POST /execute - Execute service (x402 payment)`);
455
430
  });
456
431
  }
457
432
  async handleRequest(req, res) {
@@ -460,7 +435,8 @@ var MoltsPayServer = class {
460
435
  const method = req.method || "GET";
461
436
  res.setHeader("Access-Control-Allow-Origin", "*");
462
437
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
463
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
438
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
439
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
464
440
  if (method === "OPTIONS") {
465
441
  res.writeHead(204);
466
442
  res.end();
@@ -470,17 +446,10 @@ var MoltsPayServer = class {
470
446
  if (method === "GET" && path === "/services") {
471
447
  return this.handleGetServices(res);
472
448
  }
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") {
449
+ if (method === "POST" && path === "/execute") {
478
450
  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);
451
+ const paymentHeader = req.headers[PAYMENT_HEADER2];
452
+ return this.handleExecute(body, paymentHeader, res);
484
453
  }
485
454
  this.sendJson(res, 404, { error: "Not found" });
486
455
  } catch (err) {
@@ -492,6 +461,7 @@ var MoltsPayServer = class {
492
461
  * GET /services - List available services
493
462
  */
494
463
  handleGetServices(res) {
464
+ const chain = getChain(this.manifest.provider.chain);
495
465
  const services = this.manifest.services.map((s) => ({
496
466
  id: s.id,
497
467
  name: s.name,
@@ -504,14 +474,20 @@ var MoltsPayServer = class {
504
474
  }));
505
475
  this.sendJson(res, 200, {
506
476
  provider: this.manifest.provider,
507
- services
477
+ services,
478
+ x402: {
479
+ version: X402_VERSION2,
480
+ network: `eip155:${chain.chainId}`,
481
+ schemes: ["exact"]
482
+ }
508
483
  });
509
484
  }
510
485
  /**
511
- * POST /pay - Create payment request
486
+ * POST /execute - Execute service with x402 payment
512
487
  * Body: { service: string, params: object }
488
+ * Header: X-Payment (optional - if missing, returns 402)
513
489
  */
514
- handlePay(body, res) {
490
+ async handleExecute(body, paymentHeader, res) {
515
491
  const { service, params } = body;
516
492
  if (!service) {
517
493
  return this.sendJson(res, 400, { error: "Missing service" });
@@ -525,113 +501,129 @@ var MoltsPayServer = class {
525
501
  return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
526
502
  }
527
503
  }
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
553
- });
554
- }
555
- /**
556
- * POST /verify - Verify payment and execute skill
557
- * Body: { chargeId: string, txHash: string }
558
- */
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
- });
504
+ if (!paymentHeader) {
505
+ return this.sendPaymentRequired(skill.config, res);
577
506
  }
507
+ let payment;
578
508
  try {
579
- const verification = await verifyPayment({
580
- txHash,
581
- expectedTo: this.manifest.provider.wallet,
582
- expectedAmount: charge.amount,
583
- chain: this.manifest.provider.chain
584
- });
585
- if (!verification.verified) {
586
- charge.status = "failed";
587
- return this.sendJson(res, 400, {
588
- error: "Payment verification failed",
589
- reason: verification.error
590
- });
591
- }
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
- });
509
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
510
+ payment = JSON.parse(decoded);
511
+ } catch {
512
+ return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
513
+ }
514
+ const validation = this.validatePayment(payment, skill.config);
515
+ if (!validation.valid) {
516
+ return this.sendJson(res, 402, { error: validation.error });
517
+ }
518
+ console.log(`[MoltsPay] Executing skill: ${service}`);
519
+ let result;
520
+ try {
521
+ result = await skill.handler(params || {});
607
522
  } catch (err) {
608
- console.error("[MoltsPay] Skill execution error:", err);
609
- charge.status = "failed";
610
- this.sendJson(res, 500, {
611
- error: "Skill execution failed",
523
+ console.error("[MoltsPay] Skill execution failed:", err.message);
524
+ return this.sendJson(res, 500, {
525
+ error: "Service execution failed",
612
526
  message: err.message
613
527
  });
614
528
  }
529
+ console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
530
+ let txHash = null;
531
+ try {
532
+ txHash = await this.claimPayment(payment);
533
+ console.log(`[MoltsPay] Payment claimed: ${txHash}`);
534
+ } catch (err) {
535
+ console.error("[MoltsPay] Payment claim failed:", err.message);
536
+ }
537
+ this.sendJson(res, 200, {
538
+ success: true,
539
+ result,
540
+ payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
541
+ });
615
542
  }
616
543
  /**
617
- * GET /status/:chargeId - Check charge status
544
+ * Return 402 with x402 payment requirements
618
545
  */
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
546
+ sendPaymentRequired(config, res) {
547
+ const chain = getChain(this.manifest.provider.chain);
548
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
549
+ const requirements = [{
550
+ scheme: "exact",
551
+ network: `eip155:${chain.chainId}`,
552
+ maxAmountRequired: amountInUnits,
553
+ resource: this.manifest.provider.wallet,
554
+ description: `${config.name} - $${config.price} ${config.currency}`
555
+ }];
556
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
557
+ res.writeHead(402, {
558
+ "Content-Type": "application/json",
559
+ [PAYMENT_REQUIRED_HEADER2]: encoded
634
560
  });
561
+ res.end(JSON.stringify({
562
+ error: "Payment required",
563
+ message: `Service requires $${config.price} ${config.currency}`,
564
+ x402: requirements[0]
565
+ }, null, 2));
566
+ }
567
+ /**
568
+ * Validate x402 payment payload
569
+ */
570
+ validatePayment(payment, config) {
571
+ if (payment.x402Version !== X402_VERSION2) {
572
+ return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
573
+ }
574
+ if (payment.scheme !== "exact") {
575
+ return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
576
+ }
577
+ const chain = getChain(this.manifest.provider.chain);
578
+ const expectedNetwork = `eip155:${chain.chainId}`;
579
+ if (payment.network !== expectedNetwork) {
580
+ return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
581
+ }
582
+ const auth = payment.payload.authorization;
583
+ if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
584
+ return { valid: false, error: "Payment recipient mismatch" };
585
+ }
586
+ const amount = Number(auth.value) / 1e6;
587
+ if (amount < config.price) {
588
+ return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
589
+ }
590
+ const now = Math.floor(Date.now() / 1e3);
591
+ if (Number(auth.validBefore) < now) {
592
+ return { valid: false, error: "Payment authorization expired" };
593
+ }
594
+ if (Number(auth.validAfter) > now) {
595
+ return { valid: false, error: "Payment authorization not yet valid" };
596
+ }
597
+ return { valid: true };
598
+ }
599
+ /**
600
+ * Claim payment using transferWithAuthorization
601
+ */
602
+ async claimPayment(payment) {
603
+ if (!this.wallet || !this.provider) {
604
+ throw new Error("Wallet not configured for payment claims");
605
+ }
606
+ const chain = getChain(this.manifest.provider.chain);
607
+ const auth = payment.payload.authorization;
608
+ const sig = payment.payload.signature;
609
+ const { r, s, v } = import_ethers2.ethers.Signature.from(sig);
610
+ const usdcAbi = [
611
+ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
612
+ ];
613
+ const usdc = new import_ethers2.ethers.Contract(chain.usdc, usdcAbi, this.wallet);
614
+ const tx = await usdc.transferWithAuthorization(
615
+ auth.from,
616
+ auth.to,
617
+ auth.value,
618
+ auth.validAfter,
619
+ auth.validBefore,
620
+ auth.nonce,
621
+ v,
622
+ r,
623
+ s
624
+ );
625
+ const receipt = await tx.wait();
626
+ return receipt.hash;
635
627
  }
636
628
  async readBody(req) {
637
629
  return new Promise((resolve2, reject) => {
@@ -809,7 +801,7 @@ program.command("services <url>").description("List services from a provider").o
809
801
  console.error("\u274C Error:", err.message);
810
802
  }
811
803
  });
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) => {
804
+ 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("--private-key <key>", "Private key for claiming payments (or use MOLTSPAY_PRIVATE_KEY env)").action(async (manifest, options) => {
813
805
  const manifestPath = (0, import_path2.resolve)(manifest);
814
806
  if (!(0, import_fs3.existsSync)(manifestPath)) {
815
807
  console.error(`\u274C Manifest not found: ${manifestPath}`);
@@ -817,14 +809,16 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
817
809
  }
818
810
  const port = parseInt(options.port, 10);
819
811
  const host = options.host;
812
+ const privateKey = options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY;
820
813
  console.log(`
821
- \u{1F680} Starting MoltsPay Server
814
+ \u{1F680} Starting MoltsPay Server (x402 protocol)
822
815
  `);
823
816
  console.log(` Manifest: ${manifestPath}`);
824
817
  console.log(` Port: ${port}`);
818
+ console.log(` Payment claims: ${privateKey ? "enabled" : "disabled (no private key)"}`);
825
819
  console.log("");
826
820
  try {
827
- const server = new MoltsPayServer(manifestPath, { port, host });
821
+ const server = new MoltsPayServer(manifestPath, { port, host, privateKey });
828
822
  const manifestContent = await import("fs").then(
829
823
  (fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
830
824
  );
@@ -936,7 +930,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
936
930
  process.exit(1);
937
931
  }
938
932
  });
939
- program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <url>", "Image URL (for image-to-video)").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
933
+ program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
940
934
  const client = new MoltsPayClient();
941
935
  if (!client.isInitialized) {
942
936
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
@@ -952,11 +946,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
952
946
  }
953
947
  }
954
948
  if (options.prompt) params.prompt = options.prompt;
955
- if (options.image) params.image_url = options.image;
949
+ if (options.image) {
950
+ const imagePath = options.image;
951
+ if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
952
+ params.image_url = imagePath;
953
+ } else {
954
+ const filePath = (0, import_path2.resolve)(imagePath);
955
+ if (!(0, import_fs3.existsSync)(filePath)) {
956
+ console.error(`\u274C Image file not found: ${filePath}`);
957
+ process.exit(1);
958
+ }
959
+ const imageData = (0, import_fs3.readFileSync)(filePath);
960
+ params.image_base64 = imageData.toString("base64");
961
+ }
962
+ }
956
963
  if (!params.prompt) {
957
964
  console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
958
965
  process.exit(1);
959
966
  }
967
+ const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
960
968
  if (!options.json) {
961
969
  console.log(`
962
970
  \u{1F4B3} MoltsPay - Paying for service
@@ -964,7 +972,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
964
972
  console.log(` Server: ${server}`);
965
973
  console.log(` Service: ${service}`);
966
974
  console.log(` Prompt: ${params.prompt}`);
967
- if (params.image_url) console.log(` Image: ${params.image_url}`);
975
+ if (imageDisplay) console.log(` Image: ${imageDisplay}`);
968
976
  console.log(` Wallet: ${client.address}`);
969
977
  console.log("");
970
978
  }