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.
@@ -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,38 @@ 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
+ import { ethers as ethers2 } from "ethers";
348
+ var X402_VERSION2 = 2;
349
+ var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
350
+ var PAYMENT_HEADER2 = "x-payment";
385
351
  var MoltsPayServer = class {
386
352
  manifest;
387
353
  skills = /* @__PURE__ */ new Map();
388
- charges = /* @__PURE__ */ new Map();
389
354
  options;
355
+ provider = null;
356
+ wallet = null;
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
362
  host: options.host || "0.0.0.0",
396
- chargeExpirySecs: options.chargeExpirySecs || 300
397
- // 5 minutes
363
+ privateKey: options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY
398
364
  };
365
+ if (this.options.privateKey) {
366
+ try {
367
+ const chain = getChain(this.manifest.provider.chain);
368
+ this.provider = new ethers2.JsonRpcProvider(chain.rpc);
369
+ this.wallet = new ethers2.Wallet(this.options.privateKey, this.provider);
370
+ console.log(`[MoltsPay] Payment wallet: ${this.wallet.address}`);
371
+ } catch (err) {
372
+ console.warn("[MoltsPay] Warning: Could not initialize wallet for payment claims");
373
+ }
374
+ }
399
375
  console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
400
376
  console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
401
- console.log(`[MoltsPay] Wallet: ${this.manifest.provider.wallet}`);
377
+ console.log(`[MoltsPay] Receive wallet: ${this.manifest.provider.wallet}`);
378
+ console.log(`[MoltsPay] Protocol: x402 (gasless, pay-for-success)`);
402
379
  }
403
380
  /**
404
381
  * Register a skill handler for a service
@@ -425,10 +402,8 @@ var MoltsPayServer = class {
425
402
  server.listen(p, this.options.host, () => {
426
403
  console.log(`[MoltsPay] Server listening on http://${this.options.host}:${p}`);
427
404
  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`);
405
+ console.log(` GET /services - List available services`);
406
+ console.log(` POST /execute - Execute service (x402 payment)`);
432
407
  });
433
408
  }
434
409
  async handleRequest(req, res) {
@@ -437,7 +412,8 @@ var MoltsPayServer = class {
437
412
  const method = req.method || "GET";
438
413
  res.setHeader("Access-Control-Allow-Origin", "*");
439
414
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
440
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
415
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
416
+ res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
441
417
  if (method === "OPTIONS") {
442
418
  res.writeHead(204);
443
419
  res.end();
@@ -447,17 +423,10 @@ var MoltsPayServer = class {
447
423
  if (method === "GET" && path === "/services") {
448
424
  return this.handleGetServices(res);
449
425
  }
450
- if (method === "POST" && path === "/pay") {
426
+ if (method === "POST" && path === "/execute") {
451
427
  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);
428
+ const paymentHeader = req.headers[PAYMENT_HEADER2];
429
+ return this.handleExecute(body, paymentHeader, res);
461
430
  }
462
431
  this.sendJson(res, 404, { error: "Not found" });
463
432
  } catch (err) {
@@ -469,6 +438,7 @@ var MoltsPayServer = class {
469
438
  * GET /services - List available services
470
439
  */
471
440
  handleGetServices(res) {
441
+ const chain = getChain(this.manifest.provider.chain);
472
442
  const services = this.manifest.services.map((s) => ({
473
443
  id: s.id,
474
444
  name: s.name,
@@ -481,14 +451,20 @@ var MoltsPayServer = class {
481
451
  }));
482
452
  this.sendJson(res, 200, {
483
453
  provider: this.manifest.provider,
484
- services
454
+ services,
455
+ x402: {
456
+ version: X402_VERSION2,
457
+ network: `eip155:${chain.chainId}`,
458
+ schemes: ["exact"]
459
+ }
485
460
  });
486
461
  }
487
462
  /**
488
- * POST /pay - Create payment request
463
+ * POST /execute - Execute service with x402 payment
489
464
  * Body: { service: string, params: object }
465
+ * Header: X-Payment (optional - if missing, returns 402)
490
466
  */
491
- handlePay(body, res) {
467
+ async handleExecute(body, paymentHeader, res) {
492
468
  const { service, params } = body;
493
469
  if (!service) {
494
470
  return this.sendJson(res, 400, { error: "Missing service" });
@@ -502,113 +478,129 @@ var MoltsPayServer = class {
502
478
  return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
503
479
  }
504
480
  }
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
530
- });
531
- }
532
- /**
533
- * POST /verify - Verify payment and execute skill
534
- * Body: { chargeId: string, txHash: string }
535
- */
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
- });
481
+ if (!paymentHeader) {
482
+ return this.sendPaymentRequired(skill.config, res);
554
483
  }
484
+ let payment;
555
485
  try {
556
- const verification = await verifyPayment({
557
- txHash,
558
- expectedTo: this.manifest.provider.wallet,
559
- expectedAmount: charge.amount,
560
- chain: this.manifest.provider.chain
561
- });
562
- if (!verification.verified) {
563
- charge.status = "failed";
564
- return this.sendJson(res, 400, {
565
- error: "Payment verification failed",
566
- reason: verification.error
567
- });
568
- }
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
- });
486
+ const decoded = Buffer.from(paymentHeader, "base64").toString("utf-8");
487
+ payment = JSON.parse(decoded);
488
+ } catch {
489
+ return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
490
+ }
491
+ const validation = this.validatePayment(payment, skill.config);
492
+ if (!validation.valid) {
493
+ return this.sendJson(res, 402, { error: validation.error });
494
+ }
495
+ console.log(`[MoltsPay] Executing skill: ${service}`);
496
+ let result;
497
+ try {
498
+ result = await skill.handler(params || {});
584
499
  } catch (err) {
585
- console.error("[MoltsPay] Skill execution error:", err);
586
- charge.status = "failed";
587
- this.sendJson(res, 500, {
588
- error: "Skill execution failed",
500
+ console.error("[MoltsPay] Skill execution failed:", err.message);
501
+ return this.sendJson(res, 500, {
502
+ error: "Service execution failed",
589
503
  message: err.message
590
504
  });
591
505
  }
506
+ console.log(`[MoltsPay] Skill succeeded, claiming payment...`);
507
+ let txHash = null;
508
+ try {
509
+ txHash = await this.claimPayment(payment);
510
+ console.log(`[MoltsPay] Payment claimed: ${txHash}`);
511
+ } catch (err) {
512
+ console.error("[MoltsPay] Payment claim failed:", err.message);
513
+ }
514
+ this.sendJson(res, 200, {
515
+ success: true,
516
+ result,
517
+ payment: txHash ? { txHash, status: "claimed" } : { status: "pending" }
518
+ });
592
519
  }
593
520
  /**
594
- * GET /status/:chargeId - Check charge status
521
+ * Return 402 with x402 payment requirements
595
522
  */
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
523
+ sendPaymentRequired(config, res) {
524
+ const chain = getChain(this.manifest.provider.chain);
525
+ const amountInUnits = Math.floor(config.price * 1e6).toString();
526
+ const requirements = [{
527
+ scheme: "exact",
528
+ network: `eip155:${chain.chainId}`,
529
+ maxAmountRequired: amountInUnits,
530
+ resource: this.manifest.provider.wallet,
531
+ description: `${config.name} - $${config.price} ${config.currency}`
532
+ }];
533
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
534
+ res.writeHead(402, {
535
+ "Content-Type": "application/json",
536
+ [PAYMENT_REQUIRED_HEADER2]: encoded
611
537
  });
538
+ res.end(JSON.stringify({
539
+ error: "Payment required",
540
+ message: `Service requires $${config.price} ${config.currency}`,
541
+ x402: requirements[0]
542
+ }, null, 2));
543
+ }
544
+ /**
545
+ * Validate x402 payment payload
546
+ */
547
+ validatePayment(payment, config) {
548
+ if (payment.x402Version !== X402_VERSION2) {
549
+ return { valid: false, error: `Unsupported x402 version: ${payment.x402Version}` };
550
+ }
551
+ if (payment.scheme !== "exact") {
552
+ return { valid: false, error: `Unsupported scheme: ${payment.scheme}` };
553
+ }
554
+ const chain = getChain(this.manifest.provider.chain);
555
+ const expectedNetwork = `eip155:${chain.chainId}`;
556
+ if (payment.network !== expectedNetwork) {
557
+ return { valid: false, error: `Network mismatch: expected ${expectedNetwork}` };
558
+ }
559
+ const auth = payment.payload.authorization;
560
+ if (auth.to.toLowerCase() !== this.manifest.provider.wallet.toLowerCase()) {
561
+ return { valid: false, error: "Payment recipient mismatch" };
562
+ }
563
+ const amount = Number(auth.value) / 1e6;
564
+ if (amount < config.price) {
565
+ return { valid: false, error: `Insufficient amount: $${amount} < $${config.price}` };
566
+ }
567
+ const now = Math.floor(Date.now() / 1e3);
568
+ if (Number(auth.validBefore) < now) {
569
+ return { valid: false, error: "Payment authorization expired" };
570
+ }
571
+ if (Number(auth.validAfter) > now) {
572
+ return { valid: false, error: "Payment authorization not yet valid" };
573
+ }
574
+ return { valid: true };
575
+ }
576
+ /**
577
+ * Claim payment using transferWithAuthorization
578
+ */
579
+ async claimPayment(payment) {
580
+ if (!this.wallet || !this.provider) {
581
+ throw new Error("Wallet not configured for payment claims");
582
+ }
583
+ const chain = getChain(this.manifest.provider.chain);
584
+ const auth = payment.payload.authorization;
585
+ const sig = payment.payload.signature;
586
+ const { r, s, v } = ethers2.Signature.from(sig);
587
+ const usdcAbi = [
588
+ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
589
+ ];
590
+ const usdc = new ethers2.Contract(chain.usdc, usdcAbi, this.wallet);
591
+ const tx = await usdc.transferWithAuthorization(
592
+ auth.from,
593
+ auth.to,
594
+ auth.value,
595
+ auth.validAfter,
596
+ auth.validBefore,
597
+ auth.nonce,
598
+ v,
599
+ r,
600
+ s
601
+ );
602
+ const receipt = await tx.wait();
603
+ return receipt.hash;
612
604
  }
613
605
  async readBody(req) {
614
606
  return new Promise((resolve2, reject) => {
@@ -786,7 +778,7 @@ program.command("services <url>").description("List services from a provider").o
786
778
  console.error("\u274C Error:", err.message);
787
779
  }
788
780
  });
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) => {
781
+ 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) => {
790
782
  const manifestPath = resolve(manifest);
791
783
  if (!existsSync2(manifestPath)) {
792
784
  console.error(`\u274C Manifest not found: ${manifestPath}`);
@@ -794,14 +786,16 @@ program.command("start <manifest>").description("Start MoltsPay server from serv
794
786
  }
795
787
  const port = parseInt(options.port, 10);
796
788
  const host = options.host;
789
+ const privateKey = options.privateKey || process.env.MOLTSPAY_PRIVATE_KEY;
797
790
  console.log(`
798
- \u{1F680} Starting MoltsPay Server
791
+ \u{1F680} Starting MoltsPay Server (x402 protocol)
799
792
  `);
800
793
  console.log(` Manifest: ${manifestPath}`);
801
794
  console.log(` Port: ${port}`);
795
+ console.log(` Payment claims: ${privateKey ? "enabled" : "disabled (no private key)"}`);
802
796
  console.log("");
803
797
  try {
804
- const server = new MoltsPayServer(manifestPath, { port, host });
798
+ const server = new MoltsPayServer(manifestPath, { port, host, privateKey });
805
799
  const manifestContent = await import("fs").then(
806
800
  (fs) => JSON.parse(fs.readFileSync(manifestPath, "utf-8"))
807
801
  );
@@ -913,7 +907,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
913
907
  process.exit(1);
914
908
  }
915
909
  });
916
- 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) => {
910
+ 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) => {
917
911
  const client = new MoltsPayClient();
918
912
  if (!client.isInitialized) {
919
913
  console.error("\u274C Wallet not initialized. Run: npx moltspay init");
@@ -929,11 +923,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
929
923
  }
930
924
  }
931
925
  if (options.prompt) params.prompt = options.prompt;
932
- if (options.image) params.image_url = options.image;
926
+ if (options.image) {
927
+ const imagePath = options.image;
928
+ if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
929
+ params.image_url = imagePath;
930
+ } else {
931
+ const filePath = resolve(imagePath);
932
+ if (!existsSync2(filePath)) {
933
+ console.error(`\u274C Image file not found: ${filePath}`);
934
+ process.exit(1);
935
+ }
936
+ const imageData = readFileSync3(filePath);
937
+ params.image_base64 = imageData.toString("base64");
938
+ }
939
+ }
933
940
  if (!params.prompt) {
934
941
  console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
935
942
  process.exit(1);
936
943
  }
944
+ const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
937
945
  if (!options.json) {
938
946
  console.log(`
939
947
  \u{1F4B3} MoltsPay - Paying for service
@@ -941,7 +949,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
941
949
  console.log(` Server: ${server}`);
942
950
  console.log(` Service: ${service}`);
943
951
  console.log(` Prompt: ${params.prompt}`);
944
- if (params.image_url) console.log(` Image: ${params.image_url}`);
952
+ if (imageDisplay) console.log(` Image: ${imageDisplay}`);
945
953
  console.log(` Wallet: ${client.address}`);
946
954
  console.log("");
947
955
  }