z-zero-mcp-server 1.1.0 → 1.1.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/README.md CHANGED
@@ -52,6 +52,9 @@ Get your Passport Key at: **[clawcard.store/dashboard/agents](https://www.clawca
52
52
  | `execute_payment` | Auto-fill checkout form and execute payment |
53
53
  | `cancel_payment_token` | Cancel unused token, refund to wallet |
54
54
  | `request_human_approval` | Pause and ask human for approval |
55
+ | `set_api_key` | **(Hot-Swap)** Update the Passport Key *without restarting Claude* |
56
+ | `show_api_key_status` | Check if a Passport Key is currently loaded |
57
+ | `check_for_updates` | **(Maintenance)** Check if a new MCP version is available |
55
58
 
56
59
  ---
57
60
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export { CURRENT_MCP_VERSION } from "./version.js";
package/dist/index.js CHANGED
@@ -3,7 +3,6 @@
3
3
  // OpenClaw MCP Server (z-zero-mcp-server) v1.1.0
4
4
  // Exposes secure JIT payment tools to AI Agents via Model Context Protocol
5
5
  // Status: Connected to Z-ZERO Gateway — produces secure JIT virtual cards
6
- // WDK Mode: Set Z_ZERO_WALLET_MODE=wdk for non-custodial WDK payments
7
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
7
  if (k2 === undefined) k2 = k;
9
8
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -38,21 +37,17 @@ var __importStar = (this && this.__importStar) || (function () {
38
37
  };
39
38
  })();
40
39
  Object.defineProperty(exports, "__esModule", { value: true });
41
- const CURRENT_MCP_VERSION = "1.1.0"; // ← bump this on every release
42
- const ZZERO_VERSION_API = "https://www.clawcard.store/api/version";
40
+ exports.CURRENT_MCP_VERSION = void 0;
41
+ var version_js_1 = require("./version.js");
42
+ Object.defineProperty(exports, "CURRENT_MCP_VERSION", { enumerable: true, get: function () { return version_js_1.CURRENT_MCP_VERSION; } });
43
+ const version_js_2 = require("./version.js");
44
+ // Note: version warnings are now delivered automatically via X-MCP-Version header
45
+ // in each API call — no need for a separate check_for_updates tool.
43
46
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
44
47
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
45
48
  const zod_1 = require("zod");
46
- // ── Backend selection (resolved at startup, before server init) ──────────────
47
- // tsx / Node ESM: import() is async, so we use a synchronous pattern here.
48
- // Both backends export identical function signatures (drop-in swap).
49
- const WALLET_MODE = process.env.Z_ZERO_WALLET_MODE || "custodial";
50
- const isWDKMode = WALLET_MODE === "wdk";
51
- // Import both backends; pick which one to use based on env var.
52
- // In production build, tree-shaking will handle this. In dev (tsx), both load.
53
- const custodialBackend = __importStar(require("./api_backend.js"));
54
- const wdkBackend = __importStar(require("./wdk_backend.js"));
55
- const activeBackend = isWDKMode ? wdkBackend : custodialBackend;
49
+ // ── WDK Non-Custodial Backend (single backend, no custodial fallback) ─────────
50
+ const activeBackend = __importStar(require("./wdk_backend.js"));
56
51
  const issueTokenRemote = activeBackend.issueTokenRemote;
57
52
  const resolveTokenRemote = activeBackend.resolveTokenRemote;
58
53
  const burnTokenRemote = activeBackend.burnTokenRemote;
@@ -61,7 +56,7 @@ const refundUnderspendRemote = activeBackend.refundUnderspendRemote;
61
56
  const getBalanceRemote = activeBackend.getBalanceRemote;
62
57
  const listCardsRemote = activeBackend.listCardsRemote;
63
58
  const getDepositAddressesRemote = activeBackend.getDepositAddressesRemote;
64
- console.error(`[Z-ZERO MCP] 🚀 Wallet mode: ${WALLET_MODE.toUpperCase()}`);
59
+ console.error(`[Z-ZERO MCP] 🚀 Pure WDK Non-Custodial Mode`);
65
60
  // ────────────────────────────────────────────────────────────────────────────
66
61
  const playwright_bridge_js_1 = require("./playwright_bridge.js");
67
62
  const web3_detector_js_1 = require("./lib/web3-detector.js");
@@ -73,7 +68,7 @@ const key_store_js_1 = require("./lib/key-store.js"); // ✅ Hot-Swap support
73
68
  // ============================================================
74
69
  const server = new mcp_js_1.McpServer({
75
70
  name: "z-zero-mcp-server",
76
- version: "2.0.0",
71
+ version: version_js_2.CURRENT_MCP_VERSION,
77
72
  });
78
73
  // ============================================================
79
74
  // TOOL 1: List available cards (safe - no sensitive data)
@@ -177,57 +172,36 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
177
172
  if (data?.wdk_wallet?.address) {
178
173
  const wdkAddr = data.wdk_wallet.address;
179
174
  const balance = data.wdk_wallet.balance_usdt ?? 0;
175
+ const tronAddr = data.wdk_wallet.tron_address;
180
176
  return {
181
177
  content: [{
182
178
  type: "text",
183
179
  text: JSON.stringify({
184
180
  wallet_type: "non-custodial (WDK)",
185
- address: wdkAddr,
186
181
  balance_usdt: balance,
187
182
  supported_chains: [
188
- { chain: "Polygon", token: "USDT", address: wdkAddr },
183
+ { chain: "Ethereum", token: "USDT", address: wdkAddr },
184
+ ...(tronAddr ? [{ chain: "Tron", token: "USDT", address: tronAddr }] : []),
189
185
  ],
190
- instructions: `Send USDT (Polygon/ERC-20) to this address: ${wdkAddr}. Funds will appear in your agent wallet within 1-3 minutes.`,
191
- note: "Gasless payments via ERC-4337 Paymaster. Your agent pays ~$0.001 in gas per tx."
186
+ instructions: `Send USDT (Ethereum ERC-20) to: ${wdkAddr}${tronAddr ? `\nSend USDT (Tron TRC-20) to: ${tronAddr}` : ''}`,
187
+ note: "Gasless payments via ERC-4337 Paymaster (Ethereum) / GasFree (Tron)."
192
188
  }, null, 2),
193
189
  }],
194
190
  };
195
191
  }
196
- // ── Custodial Mode Fallback ───────────────────────────────────────────
197
- const addresses = data?.deposit_addresses;
198
- if (!addresses) {
199
- return {
200
- content: [{
201
- type: "text",
202
- text: "Failed to retrieve deposit addresses. Please ensure your Z_ZERO_API_KEY (Passport Key) is valid. You can find it at https://www.clawcard.store/dashboard/agents",
203
- }],
204
- isError: true,
205
- };
206
- }
192
+ // No WDK wallet connected
207
193
  return {
208
194
  content: [{
209
195
  type: "text",
210
- text: JSON.stringify({
211
- wallet_type: "custodial",
212
- evm: {
213
- address: addresses.evm,
214
- supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
215
- tokens: ["USDC", "USDT"]
216
- },
217
- tron: {
218
- address: addresses.tron,
219
- supported_chains: ["Tron (TRC-20)"],
220
- tokens: ["USDT"]
221
- },
222
- note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
223
- }, null, 2),
196
+ text: "No WDK wallet found. Please create one at https://www.clawcard.store/dashboard/agent-wallet",
224
197
  }],
198
+ isError: true,
225
199
  };
226
200
  });
227
201
  // ============================================================
228
202
  // TOOL 3: Request a temporary payment token (issues secure JIT card)
229
203
  // ============================================================
230
- server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A secure single-use virtual card is issued via the Z-ZERO network. The token is valid for 30 minutes. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
204
+ server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A secure single-use virtual card is issued via the Z-ZERO network. The token is valid for 1 hour. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
231
205
  card_alias: zod_1.z
232
206
  .string()
233
207
  .describe("Which card to charge, e.g. 'Card_01'"),
@@ -288,7 +262,8 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
288
262
  merchant: token.merchant,
289
263
  expires_at: expiresAt,
290
264
  card_issued: true,
291
- instructions: "Use this token with execute_payment within 30 minutes. IMPORTANT: If the actual checkout price is HIGHER than the token amount, do NOT proceed — call cancel_payment_token first and request a new token with the correct amount.",
265
+ instructions: "Use this token with execute_payment within 1 hour. IMPORTANT: If the actual checkout price is HIGHER than the token amount, do NOT proceed — call cancel_payment_token first and request a new token with the correct amount.",
266
+ ...(token.mcp_warning ? { _mcp_warning: token.mcp_warning } : {}),
292
267
  }, null, 2),
293
268
  },
294
269
  ],
@@ -495,49 +470,6 @@ server.tool("request_human_approval", "Request human approval before proceeding
495
470
  };
496
471
  });
497
472
  // ============================================================
498
- // TOOL 6.4: Check for Updates (polls clawcard.store, not npm)
499
- // ============================================================
500
- server.tool("check_for_updates", "Check if a newer version of this MCP server is available. Polls clawcard.store/api/version — independent of npm so works even if distribution changes. Call this when: (1) user asks if MCP is up to date, (2) a payment fails and you want to rule out a stale MCP version.", {}, async () => {
501
- try {
502
- const res = await fetch(ZZERO_VERSION_API, {
503
- headers: { "User-Agent": `z-zero-mcp/${CURRENT_MCP_VERSION}` },
504
- signal: AbortSignal.timeout(8_000),
505
- });
506
- if (!res.ok)
507
- throw new Error(`HTTP ${res.status}`);
508
- const data = await res.json();
509
- const latest = data.latest_version;
510
- const isUpToDate = CURRENT_MCP_VERSION === latest;
511
- return {
512
- content: [{
513
- type: "text",
514
- text: JSON.stringify({
515
- current_version: CURRENT_MCP_VERSION,
516
- latest_version: latest,
517
- up_to_date: isUpToDate,
518
- status: isUpToDate ? "✅ You are on the latest version." : `⚠️ Update available: ${CURRENT_MCP_VERSION} → ${latest}`,
519
- release_notes: isUpToDate ? null : data.release_notes,
520
- how_to_update: isUpToDate ? null : data.update_instructions,
521
- }, null, 2),
522
- }],
523
- };
524
- }
525
- catch (err) {
526
- return {
527
- content: [{
528
- type: "text",
529
- text: JSON.stringify({
530
- current_version: CURRENT_MCP_VERSION,
531
- status: "⚠️ Could not reach version server. Check your internet connection.",
532
- error: err.message,
533
- fallback_check: `Visit https://www.clawcard.store to see latest version.`,
534
- }, null, 2),
535
- }],
536
- isError: true,
537
- };
538
- }
539
- });
540
- // ============================================================
541
473
  // TOOL 6.5: Set API Key (Hot-Swap Passport Key — NO restart needed)
542
474
  // ============================================================
543
475
  server.tool("set_api_key", "Update the Z-ZERO Passport Key immediately WITHOUT restarting the AI tool. Call this when the human provides a new key (e.g. 'zk_live_xxxxx'). The key is validated and activated instantly for all subsequent API calls. IMPORTANT: Never ask for the key proactively — only call this when the human explicitly provides it.", {
@@ -584,9 +516,6 @@ server.tool("show_api_key_status", "Show whether a Passport Key is currently con
584
516
  }],
585
517
  };
586
518
  });
587
- // ============================================================
588
- // TOOL 7: Auto Pay Checkout (Phase 2 — Smart Routing)
589
- // ============================================================
590
519
  server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout tool. Provide a checkout URL and this tool will:\n" +
591
520
  "1. Scan the page to detect if it supports Web3 (Crypto) payments via window.ethereum or EIP-681 links.\n" +
592
521
  "2. SCENARIO A (Web3): If detected, automatically send USDT on-chain via WDK (gas ~$0.001). No Visa card needed.\n" +
@@ -597,22 +526,20 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
597
526
  .describe("Full URL of the checkout/payment page to analyze and pay."),
598
527
  card_alias: zod_1.z
599
528
  .string()
600
- .describe("Card alias to charge for JIT Fiat fallback, e.g. 'Card_01'. Required even if Web3 route is used (used for WDK wallet lookup)."),
529
+ .describe("Card alias to charge for JIT Fiat fallback, e.g. 'Card_01'."),
601
530
  }, async ({ checkout_url, card_alias }) => {
602
531
  const ZZERO_API = process.env.Z_ZERO_API_BASE || "https://www.clawcard.store";
603
- const API_KEY = process.env.Z_ZERO_API_KEY || "";
604
- // ── C2 FIX: API key guard ────────────────────────────────────────────
532
+ const API_KEY = (0, key_store_js_1.getPassportKey)(); // ✅ FIX: use hot-swap key store, not process.env
605
533
  if (!API_KEY) {
606
534
  return {
607
535
  content: [{ type: "text", text: JSON.stringify({
608
536
  status: "CONFIG_ERROR",
609
- message: "Z_ZERO_API_KEY is not configured on this MCP server. Cannot proceed.",
537
+ message: "Z_ZERO_API_KEY is not configured.",
610
538
  }, null, 2) }],
611
539
  isError: true,
612
540
  };
613
541
  }
614
- // ── C1 FIX: SSRF guard — validate checkout_url ───────────────────────
615
- // Block non-https and internal/private IP ranges
542
+ // ── SSRF guard ───────────────────────────────────────────────
616
543
  (() => {
617
544
  let url;
618
545
  try {
@@ -623,13 +550,9 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
623
550
  }
624
551
  const isDev = process.env.NODE_ENV !== "production";
625
552
  const hostname = url.hostname;
626
- // BUG 4 FIX: Allow http://localhost in dev for testing local checkout pages
627
- if (isDev && (hostname === "localhost" || hostname === "127.0.0.1")) {
628
- console.error(`[SMART ROUTE] Dev mode: allowing localhost checkout URL`);
629
- }
630
- else {
553
+ if (!(isDev && (hostname === "localhost" || hostname === "127.0.0.1"))) {
631
554
  if (url.protocol !== "https:") {
632
- throw new Error(`checkout_url must use HTTPS. Got: ${url.protocol}`);
555
+ throw new Error(`checkout_url must use HTTPS.`);
633
556
  }
634
557
  const privatePatterns = [
635
558
  /^localhost$/i, /^127\./, /^10\./,
@@ -637,187 +560,115 @@ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout to
637
560
  /^169\.254\./, /^\[::1\]$/, /^0\.0\.0\.0$/,
638
561
  ];
639
562
  if (privatePatterns.some(p => p.test(hostname))) {
640
- throw new Error(`SSRF blocked: checkout_url points to private/internal host: ${hostname}`);
563
+ throw new Error(`SSRF blocked host: ${hostname}`);
641
564
  }
642
565
  }
643
566
  })();
644
- // ── STEP A: Try to detect Web3 payment on the page ──────────────────
645
- console.error(`[SMART ROUTE] Scanning ${checkout_url} for Web3 payment...`);
646
- const web3Result = await (0, web3_detector_js_1.detectWeb3Payment)(checkout_url);
647
- if (web3Result.detected && web3Result.params) {
648
- // ── SCENARIO A: Web3 Payment Detected ────────────────────────────
649
- const { to, eip681_amount, data } = web3Result.params;
650
- // If amount is baked into EIP-681 link, use it. Otherwise need user to confirm.
651
- if (!eip681_amount && !data) {
567
+ // ── Single Browser Instance for efficiency ──────────────────
568
+ const browser = await playwright_1.chromium.launch({ headless: true });
569
+ const context = await browser.newContext();
570
+ const page = await context.newPage();
571
+ try {
572
+ // STEP 1: Web3 Detection
573
+ console.error(`[SMART ROUTE] Scanning ${checkout_url} for Web3...`);
574
+ const web3Result = await (0, web3_detector_js_1.detectWeb3Payment)(page, checkout_url);
575
+ if (web3Result.detected && web3Result.params) {
576
+ const { to, eip681_amount, data } = web3Result.params;
577
+ let amount = eip681_amount ?? 0;
578
+ if (!amount && data && data.length >= 138) {
579
+ const amountHex = data.slice(-64);
580
+ amount = Number(BigInt(`0x${amountHex}`)) / 1_000_000;
581
+ }
582
+ if (!amount || amount <= 0) {
583
+ return {
584
+ content: [{ type: "text", text: JSON.stringify({
585
+ route: "WEB3", status: "AMOUNT_REQUIRED", recipient: to,
586
+ message: "Web3 detected but amount is unknown.",
587
+ }, null, 2) }],
588
+ };
589
+ }
590
+ const resp = await fetch(`${ZZERO_API}/api/wdk/transfer`, {
591
+ method: "POST",
592
+ headers: { "Content-Type": "application/json", "x-passport-key": API_KEY },
593
+ body: JSON.stringify({ to, amount, card_alias }),
594
+ });
595
+ const txResult = await resp.json();
596
+ if (!resp.ok || !txResult.success) {
597
+ return {
598
+ content: [{ type: "text", text: JSON.stringify({
599
+ route: "WEB3", status: "FAILED", reason: txResult.message || "Transfer failed",
600
+ }, null, 2) }],
601
+ isError: true,
602
+ };
603
+ }
652
604
  return {
653
- content: [{
654
- type: "text",
655
- text: JSON.stringify({
656
- route: "WEB3",
657
- detected: true,
658
- status: "AMOUNT_REQUIRED",
659
- recipient: to,
660
- message: `Web3 payment gateway detected (${web3Result.method}), recipient: ${to}. Could not determine exact USDT amount automatically. Please confirm the amount with the user, then call this tool again with the amount in the checkout_url or use pay_crypto(to=${to}, amount=<confirmed_amount>).`,
661
- }, null, 2),
662
- }],
605
+ content: [{ type: "text", text: JSON.stringify({
606
+ route: "WEB3", method: web3Result.method, status: "SUCCESS",
607
+ recipient: to, amount_usdt: amount, tx_hash: txResult.txHash,
608
+ message: `✅ Web3 payment sent on-chain! ${amount} USDT → ${to}.`,
609
+ gas_savings: "~$0.001 (ERC-4337 Paymaster, gasless for user)",
610
+ }, null, 2) }],
663
611
  };
664
612
  }
665
- // Call backend WDK transfer API
666
- let amount = eip681_amount ?? 0;
667
- // Decode ERC-20 transfer amount from calldata if EIP-681 amount not available
668
- if (!amount && data && data.length >= 138) {
669
- const amountHex = data.slice(-64);
670
- const amountRaw = BigInt(`0x${amountHex}`);
671
- amount = Number(amountRaw) / 1_000_000;
672
- }
673
- // Guard: if we still can't determine amount, ask user
674
- if (!amount || amount <= 0) {
613
+ // STEP 2: Fiat Fallback (Price Extraction)
614
+ console.error(`[SMART ROUTE] No Web3. Extracting price...`);
615
+ const totalPrice = await (0, extract_total_price_js_1.extractTotalPrice)(page);
616
+ if (!totalPrice) {
675
617
  return {
676
- content: [{
677
- type: "text",
678
- text: JSON.stringify({
679
- route: "WEB3",
680
- detected: true,
681
- status: "AMOUNT_REQUIRED",
682
- recipient: to,
683
- message: `Web3 tx detected but could not decode USDT amount from calldata. Please confirm the exact amount with the user and use pay_crypto(to=${to}, amount=<amount>).`,
684
- }, null, 2),
685
- }],
618
+ content: [{ type: "text", text: JSON.stringify({
619
+ route: "FIAT", status: "PRICE_NOT_FOUND",
620
+ }, null, 2) }],
621
+ isError: true,
686
622
  };
687
623
  }
688
- const resp = await fetch(`${ZZERO_API}/api/wdk/transfer`, {
689
- method: "POST",
690
- headers: {
691
- "Content-Type": "application/json",
692
- "x-passport-key": API_KEY,
693
- },
694
- body: JSON.stringify({ to, amount, card_alias }),
695
- });
696
- const txResult = await resp.json();
697
- if (!resp.ok || !txResult.success) {
624
+ if (totalPrice < 1 || totalPrice > 100) {
698
625
  return {
699
- content: [{
700
- type: "text",
701
- text: JSON.stringify({
702
- route: "WEB3",
703
- status: "FAILED",
704
- reason: txResult.message || txResult.error || "Unknown error from WDK transfer API",
705
- }, null, 2),
706
- }],
626
+ content: [{ type: "text", text: JSON.stringify({
627
+ route: "FIAT", status: "AMOUNT_OUT_OF_RANGE", detected_price: totalPrice,
628
+ }, null, 2) }],
707
629
  isError: true,
708
630
  };
709
631
  }
632
+ // Issue Token
633
+ const token = await issueTokenRemote(card_alias, totalPrice, checkout_url);
634
+ if (!token || token.error)
635
+ throw new Error(token?.message || "Token issue failed");
636
+ // Resolve Card Data
637
+ const cardData = await resolveTokenRemote(token.token);
638
+ if (!cardData || cardData.error)
639
+ throw new Error("Card resolve failed");
640
+ // Fill Form (Reusing the same page!)
641
+ const fillResult = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData, page);
642
+ if (fillResult.success) {
643
+ const burnOk = await burnTokenRemote(token.token);
644
+ if (!burnOk)
645
+ console.error(`[WARN] Token burn failed for ${token.token} — manual check needed`);
646
+ }
710
647
  return {
711
- content: [{
712
- type: "text",
713
- text: JSON.stringify({
714
- route: "WEB3",
715
- method: web3Result.method,
716
- status: "SUCCESS",
717
- recipient: to,
718
- amount_usdt: amount,
719
- tx_hash: txResult.txHash,
720
- message: `✅ Web3 payment sent on-chain! ${amount} USDT → ${to}. Tx: ${txResult.txHash}`,
721
- gas_savings: "~$0.001 (ERC-4337 Paymaster, gasless for user)",
722
- }, null, 2),
723
- }],
724
- };
725
- }
726
- // ── SCENARIO B: No Web3 — Fiat JIT Card fallback ────────────────────
727
- console.error(`[SMART ROUTE] No Web3 detected. Scanning DOM for total price...`);
728
- // ✅ BUG 1+2 FIX: Use try/finally so browser ALWAYS closes (even on success).
729
- // fillCheckoutForm opens its own browser — this avoids triple browser + leak.
730
- let totalPrice = null;
731
- const priceBrowser = await playwright_1.chromium.launch({ headless: true });
732
- try {
733
- const pricePage = await priceBrowser.newPage();
734
- await pricePage.goto(checkout_url, { waitUntil: "domcontentloaded", timeout: 20_000 });
735
- totalPrice = await (0, extract_total_price_js_1.extractTotalPrice)(pricePage);
736
- }
737
- catch {
738
- // ignore — totalPrice stays null, handled below
739
- }
740
- finally {
741
- await priceBrowser.close().catch(() => { }); // ✅ Always close
742
- }
743
- if (!totalPrice) {
744
- return {
745
- content: [{
746
- type: "text",
747
- text: JSON.stringify({
748
- route: "FIAT",
749
- status: "PRICE_NOT_FOUND",
750
- message: "Could not automatically detect the total price on this page. Please manually confirm the exact checkout price and use request_payment_token + execute_payment instead.",
751
- }, null, 2),
752
- }],
753
- isError: true,
754
- };
755
- }
756
- if (totalPrice < 1 || totalPrice > 100) {
757
- return {
758
- content: [{
759
- type: "text",
760
- text: JSON.stringify({
761
- route: "FIAT",
762
- status: "AMOUNT_OUT_OF_RANGE",
648
+ content: [{ type: "text", text: JSON.stringify({
649
+ route: "FIAT", status: fillResult.success ? "SUCCESS" : "FILL_FAILED",
763
650
  detected_price: totalPrice,
764
- message: `Detected price $${totalPrice} is outside the JIT card limit ($1-$100). Please confirm with user and use request_payment_token manually.`,
765
- }, null, 2),
766
- }],
767
- isError: true,
768
- };
769
- }
770
- // Issue JIT card for exactly the detected total
771
- console.error(`[SMART ROUTE] Fiat route: issuing JIT card for $${totalPrice}`);
772
- const token = await issueTokenRemote(card_alias, totalPrice, checkout_url);
773
- if (!token || token.error) {
774
- return {
775
- content: [{
776
- type: "text",
777
- text: JSON.stringify({
778
- route: "FIAT",
779
- status: "TOKEN_ISSUE_FAILED",
780
- message: token?.message || "Could not issue JIT card. Check balance or card alias.",
781
- }, null, 2),
782
- }],
783
- isError: true,
651
+ message: fillResult.success
652
+ ? `✅ JIT card issued for $${totalPrice} and checkout filled.`
653
+ : `❌ Fill failed: ${fillResult.message}`,
654
+ receipt_id: fillResult.receipt_id || null,
655
+ }, null, 2) }],
656
+ isError: !fillResult.success,
784
657
  };
785
658
  }
786
- // Resolve token and auto-fill form
787
- const cardData = await resolveTokenRemote(token.token);
788
- if (!cardData || cardData.error) {
659
+ catch (err) {
660
+ // FIX 8: Sanitize error message before returning to agent — avoid leaking internal paths
661
+ const safeMsg = (err?.message || String(err)).replace(/\/.*(src|dist)\/.*\.ts/g, '[internal]').slice(0, 200);
789
662
  return {
790
- content: [{
791
- type: "text",
792
- text: JSON.stringify({
793
- route: "FIAT",
794
- status: "CARD_RESOLVE_FAILED",
795
- message: "Failed to resolve JIT card data. Token may have expired.",
796
- }, null, 2),
797
- }],
663
+ content: [{ type: "text", text: JSON.stringify({
664
+ status: "ERROR", message: safeMsg,
665
+ }, null, 2) }],
798
666
  isError: true,
799
667
  };
800
668
  }
801
- const fillResult = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
802
- if (fillResult.success) {
803
- await burnTokenRemote(token.token);
669
+ finally {
670
+ await browser.close().catch(() => { });
804
671
  }
805
- return {
806
- content: [{
807
- type: "text",
808
- text: JSON.stringify({
809
- route: "FIAT",
810
- status: fillResult.success ? "SUCCESS" : "FILL_FAILED",
811
- detected_price: totalPrice,
812
- jit_card_issued: true,
813
- message: fillResult.success
814
- ? `✅ JIT Visa card issued for $${totalPrice} and checkout filled successfully.`
815
- : `❌ JIT card issued but checkout form fill failed: ${fillResult.message}`,
816
- receipt_id: fillResult.receipt_id || null,
817
- }, null, 2),
818
- }],
819
- isError: !fillResult.success,
820
- };
821
672
  });
822
673
  // ============================================================
823
674
  // RESOURCE: Z-ZERO Autonomous Payment SOP
@@ -840,7 +691,7 @@ When asked to make a purchase, execute the following steps precisely in order:
840
691
 
841
692
  ## Step 2: Requesting the JIT Token
842
693
  1. Request Token: Call the \`request_payment_token\` tool with the exact \`amount\` required and the \`merchant\` name.
843
- 2. Receive Token: You will receive a temporary \`token\` (e.g., \`temp_auth_1a2b...\`). This token is locked to the requested amount and is valid for 30 minutes.
694
+ 2. Receive Token: You will receive a temporary \`token\` (e.g., \`temp_auth_1a2b...\`). This token is locked to the requested amount and is valid for 1 hour.
844
695
 
845
696
  ## Step 3: Locating the Checkout
846
697
  1. Identify Checkout URL: Find the merchant's checkout/payment page where credit card details are normally entered.
@@ -857,6 +708,13 @@ When asked to make a purchase, execute the following steps precisely in order:
857
708
  - NEVER print full tokens in the human chat logs.
858
709
  - NO MANUAL ENTRY: If a merchant asks you to type a credit card number into a text box, REFUSE.
859
710
  - FAIL GRACEFULLY: If \`execute_payment\` returns \`success: false\`, report the error message to the human. Do not try again.
711
+
712
+ ## ⚡ Smart Route Alternative (Recommended)
713
+ For most purchases, use \`auto_pay_checkout\` instead of the 4-step flow above.
714
+ It auto-detects Web3 checkout (pay on-chain with USDT) vs Fiat checkout (JIT Visa card), all in a single call.
715
+ - Web3 route: sends USDT directly on-chain to merchant wallet
716
+ - Fiat route: issues JIT card + fills checkout form automatically
717
+ Call \`auto_pay_checkout\` with just \`checkout_url\` and \`card_alias\`. The tool handles the rest.
860
718
  `;
861
719
  return {
862
720
  contents: [
@@ -874,7 +732,7 @@ When asked to make a purchase, execute the following steps precisely in order:
874
732
  async function main() {
875
733
  const transport = new stdio_js_1.StdioServerTransport();
876
734
  await server.connect(transport);
877
- console.error("🔐 OpenClaw MCP Server v2.0.0 running (Phase 2: Smart Routing enabled)...");
735
+ console.error(`🔐 OpenClaw MCP Server v${version_js_2.CURRENT_MCP_VERSION} running (Phase 2: Smart Routing enabled)...`);
878
736
  console.error("Status: Secure & Connected to Z-ZERO Gateway");
879
737
  console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval, auto_pay_checkout");
880
738
  }
@@ -101,7 +101,9 @@ async function extractTotalPrice(page) {
101
101
  }
102
102
  if (values.length === 0)
103
103
  return null;
104
- return Math.max(...values);
104
+ // ✅ FIX 10: Use median instead of max — prevents merchant injecting hidden large price
105
+ values.sort((a, b) => a - b);
106
+ return values[Math.floor(values.length / 2)];
105
107
  });
106
108
  if (amount !== null && amount > 0) {
107
109
  console.error(`[PRICE] ⚠️ Strategy 3 (largest $ on page): $${amount}. Low confidence.`);
@@ -27,9 +27,9 @@ function setPassportKey(newKey) {
27
27
  if (!trimmed.startsWith("zk_live_") && !trimmed.startsWith("zk_test_")) {
28
28
  return { ok: false, message: `Invalid key format — must start with "zk_live_" or "zk_test_". Got: "${trimmed.slice(0, 12)}..."` };
29
29
  }
30
- const previous = _passportKey;
31
30
  _passportKey = trimmed;
32
- console.error(`[KEY-STORE]Passport Key updated: ${previous.slice(0, 10)}... ${trimmed.slice(0, 10)}...`);
31
+ //FIX 11: Don't log partial key — even 10 chars helps brute-force
32
+ console.error(`[KEY-STORE] ✅ Passport Key updated successfully.`);
33
33
  return { ok: true, message: `Passport Key updated successfully. Active key prefix: ${trimmed.slice(0, 10)}...` };
34
34
  }
35
35
  /** Check if a key is currently configured. */
@@ -12,9 +12,8 @@ export interface Web3DetectionResult {
12
12
  message: string;
13
13
  }
14
14
  /**
15
- * Opens the checkout URL in a headless browser.
15
+ * Checks a page for Web3 payment indicators (EIP-681 or interrupted eth_sendTransaction).
16
16
  * Injects a mock window.ethereum (MetaMask emulation).
17
- * Scans for EIP-681 links.
18
17
  * Returns the tx params if Web3 payment is detected, else null.
19
18
  */
20
- export declare function detectWeb3Payment(checkoutUrl: string): Promise<Web3DetectionResult>;
19
+ export declare function detectWeb3Payment(page: import("playwright").Page, checkoutUrl: string): Promise<Web3DetectionResult>;
@@ -6,21 +6,15 @@
6
6
  // If page is not Web3-aware → return null (caller falls back to Fiat JIT card).
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.detectWeb3Payment = detectWeb3Payment;
9
- const playwright_1 = require("playwright");
10
9
  const WEB3_DETECT_TIMEOUT_MS = 12_000; // 12s max to detect Web3 button click
11
10
  /**
12
- * Opens the checkout URL in a headless browser.
11
+ * Checks a page for Web3 payment indicators (EIP-681 or interrupted eth_sendTransaction).
13
12
  * Injects a mock window.ethereum (MetaMask emulation).
14
- * Scans for EIP-681 links.
15
13
  * Returns the tx params if Web3 payment is detected, else null.
16
14
  */
17
- async function detectWeb3Payment(checkoutUrl) {
18
- const browser = await playwright_1.chromium.launch({ headless: true });
19
- const context = await browser.newContext();
20
- const page = await context.newPage();
15
+ async function detectWeb3Payment(page, checkoutUrl) {
21
16
  try {
22
17
  // ─── Inject mock window.ethereum BEFORE page loads ───────────────────
23
- // This ensures the page's `if (typeof window.ethereum !== 'undefined')` check passes.
24
18
  await page.addInitScript(() => {
25
19
  const mockEthereum = {
26
20
  isMetaMask: true,
@@ -44,7 +38,6 @@ async function detectWeb3Payment(checkoutUrl) {
44
38
  return null;
45
39
  if (method === "eth_getBalance")
46
40
  return "0x0";
47
- // C3 FIX: Log unrecognized methods for debugging DApp compatibility
48
41
  window["__wdkUnknownMethod_" + method] = true;
49
42
  return null;
50
43
  },
@@ -67,7 +60,6 @@ async function detectWeb3Payment(checkoutUrl) {
67
60
  // ─── Navigate to page ─────────────────────────────────────────────────
68
61
  await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
69
62
  // ─── Strategy A: Scan for EIP-681 links ──────────────────────────────
70
- // Look for links like: ethereum:0xabc...@137/transfer?address=0xabc&uint256=10000000
71
63
  const eip681Result = await page.evaluate(() => {
72
64
  const links = Array.from(document.querySelectorAll("a[href]"))
73
65
  .map(a => a.href)
@@ -78,16 +70,13 @@ async function detectWeb3Payment(checkoutUrl) {
78
70
  const href = links[0] ?? inlineMatch?.[0] ?? null;
79
71
  if (!href)
80
72
  return null;
81
- // Parse: ethereum:0xCONTRACT@137/transfer?address=0xRECIPIENT&uint256=10000000
82
73
  const contractMatch = href.match(/ethereum:(0x[0-9a-fA-F]{40})/);
83
74
  const chainMatch = href.match(/@(\d+)/);
84
75
  const uint256Match = href.match(/uint256=(\d+)/);
85
- // ✅ BUG 6 FIX: Parse recipient from 'address=' query param (not the contract address)
86
76
  const recipientMatch = href.match(/[?&]address=(0x[0-9a-fA-F]{40})/);
87
77
  return {
88
- // Prefer recipient address from query param; fall back to contract only as last resort
89
78
  to: recipientMatch?.[1] ?? contractMatch?.[1] ?? null,
90
- contract: contractMatch?.[1] ?? null, // USDT contract (kept for logging)
79
+ contract: contractMatch?.[1] ?? null,
91
80
  chainId: chainMatch ? parseInt(chainMatch[1]) : 137,
92
81
  uint256: uint256Match?.[1] ?? null,
93
82
  raw: href,
@@ -95,9 +84,8 @@ async function detectWeb3Payment(checkoutUrl) {
95
84
  });
96
85
  if (eip681Result?.to) {
97
86
  const usdtAmount = eip681Result.uint256
98
- ? parseInt(eip681Result.uint256) / 1_000_000 // USDT has 6 decimals
87
+ ? parseInt(eip681Result.uint256) / 1_000_000
99
88
  : null;
100
- console.error(`[WEB3] ✅ EIP-681 detected: recipient=${eip681Result.to}, contract=${eip681Result.contract}, amount=${usdtAmount} USDT`);
101
89
  return {
102
90
  detected: true,
103
91
  method: "EIP-681",
@@ -110,55 +98,46 @@ async function detectWeb3Payment(checkoutUrl) {
110
98
  };
111
99
  }
112
100
  // ─── Strategy B: Wait for eth_sendTransaction after clicking "Pay" ────
113
- // ✅ BUG 5 FIX: Auto-click common Web3 pay buttons so DApp triggers eth_sendTransaction
114
101
  const web3ButtonSelectors = [
115
102
  'button:has-text("MetaMask")', 'button:has-text("Pay with Crypto")',
116
103
  'button:has-text("Connect Wallet")', 'button:has-text("Pay with Web3")',
117
104
  'button:has-text("Pay with Wallet")', '#pay-metamask-btn',
118
105
  '[class*="web3"] button', '[class*="metamask"] button',
119
106
  ];
107
+ // Click buttons sequentially, small delay to let UI reaction
120
108
  for (const sel of web3ButtonSelectors) {
121
109
  try {
122
110
  const btn = await page.$(sel);
123
111
  if (btn && await btn.isVisible()) {
124
112
  console.error(`[WEB3] Clicking Web3 pay button: ${sel}`);
125
113
  await btn.click();
126
- break;
114
+ await page.waitForTimeout(500);
127
115
  }
128
116
  }
129
- catch { /* selector may not exist — continue */ }
117
+ catch { /* skip */ }
130
118
  }
131
- // Poll for captured tx (window.ethereum.request intercepted by mock)
132
- const capturedTx = await Promise.race([
133
- // Poll for captured tx
134
- (async () => {
135
- const start = Date.now();
136
- while (Date.now() - start < WEB3_DETECT_TIMEOUT_MS) {
137
- const tx = await page.evaluate(() => {
138
- return window["__wdkCapturedTx"] ?? null;
139
- });
140
- if (tx)
141
- return tx;
142
- await page.waitForTimeout(500);
143
- }
144
- return null;
145
- })(),
146
- ]);
147
- if (capturedTx) {
148
- console.error(`[WEB3] eth_sendTransaction captured: to=${capturedTx["to"]}`);
149
- return {
150
- detected: true,
151
- method: "eth_sendTransaction",
152
- params: {
153
- to: capturedTx["to"],
154
- value: capturedTx["value"],
155
- data: capturedTx["data"],
156
- chainId: 137,
157
- },
158
- message: `Web3 payment detected via eth_sendTransaction. Recipient: ${capturedTx["to"]}`,
159
- };
119
+ // Poll for captured tx
120
+ const start = Date.now();
121
+ while (Date.now() - start < WEB3_DETECT_TIMEOUT_MS) {
122
+ const tx = await page.evaluate(() => {
123
+ return window["__wdkCapturedTx"] ?? null;
124
+ });
125
+ if (tx) {
126
+ const capturedTx = tx;
127
+ return {
128
+ detected: true,
129
+ method: "eth_sendTransaction",
130
+ params: {
131
+ to: capturedTx["to"],
132
+ value: capturedTx["value"],
133
+ data: capturedTx["data"],
134
+ chainId: 137,
135
+ },
136
+ message: `Web3 payment detected via eth_sendTransaction. Recipient: ${capturedTx["to"]}`,
137
+ };
138
+ }
139
+ await page.waitForTimeout(500);
160
140
  }
161
- console.error("[WEB3] ❌ No Web3 payment detected on this page. Fallback to Fiat.");
162
141
  return {
163
142
  detected: false,
164
143
  method: null,
@@ -168,7 +147,6 @@ async function detectWeb3Payment(checkoutUrl) {
168
147
  }
169
148
  catch (err) {
170
149
  const msg = err instanceof Error ? err.message : String(err);
171
- console.error(`[WEB3] Error during detection: ${msg}`);
172
150
  return {
173
151
  detected: false,
174
152
  method: null,
@@ -176,7 +154,4 @@ async function detectWeb3Payment(checkoutUrl) {
176
154
  message: `Detection failed: ${msg}. Defaulting to Fiat route.`,
177
155
  };
178
156
  }
179
- finally {
180
- await browser.close().catch(() => { });
181
- }
182
157
  }
@@ -4,4 +4,4 @@ import type { CardData, PaymentResult } from "./types.js";
4
4
  * Card data exists ONLY in RAM and is wiped after injection.
5
5
  * Hard timeout of 60s prevents merchant page from hanging indefinitely.
6
6
  */
7
- export declare function fillCheckoutForm(checkoutUrl: string, cardData: CardData): Promise<PaymentResult>;
7
+ export declare function fillCheckoutForm(checkoutUrl: string, cardData: CardData, existingPage?: import("playwright").Page): Promise<PaymentResult>;
@@ -11,17 +11,22 @@ const CHECKOUT_HARD_TIMEOUT_MS = 60_000; // 60s absolute cap — prevents slow-l
11
11
  * Card data exists ONLY in RAM and is wiped after injection.
12
12
  * Hard timeout of 60s prevents merchant page from hanging indefinitely.
13
13
  */
14
- async function fillCheckoutForm(checkoutUrl, cardData) {
15
- const browser = await playwright_1.chromium.launch({ headless: true });
16
- const context = await browser.newContext();
17
- const page = await context.newPage();
18
- // 🔒 Wrap entire checkout flow in 60s hard timeout
19
- // onTimeout: force-close browser to free memory
14
+ async function fillCheckoutForm(checkoutUrl, cardData, existingPage) {
15
+ let browser = null;
16
+ let page;
17
+ if (existingPage) {
18
+ page = existingPage;
19
+ }
20
+ else {
21
+ browser = await playwright_1.chromium.launch({ headless: true });
22
+ const context = await browser.newContext();
23
+ page = await context.newPage();
24
+ }
20
25
  try {
21
- return await (0, with_timeout_js_1.withTimeout)(_fillCheckoutFormInner(page, checkoutUrl, cardData), // BUG 8 FIX: pass URL
22
- CHECKOUT_HARD_TIMEOUT_MS, 'fillCheckoutForm', async () => {
26
+ return await (0, with_timeout_js_1.withTimeout)(_fillCheckoutFormInner(page, checkoutUrl, cardData), CHECKOUT_HARD_TIMEOUT_MS, 'fillCheckoutForm', async () => {
23
27
  console.error('[PLAYWRIGHT] ⚠️ Hard timeout hit — force-closing browser');
24
- await browser.close().catch(() => { });
28
+ if (browser)
29
+ await browser.close().catch(() => { });
25
30
  });
26
31
  }
27
32
  catch (err) {
@@ -35,145 +40,223 @@ async function fillCheckoutForm(checkoutUrl, cardData) {
35
40
  return { success: false, message: `Payment failed: ${errMsg}` };
36
41
  }
37
42
  finally {
38
- // RAM wipe happens inside _fillCheckoutFormInner's finally block
39
- // Ensure browser is closed even if timeout cleanup failed
40
- await browser.close().catch(() => { });
43
+ if (browser)
44
+ await browser.close().catch(() => { });
41
45
  }
42
46
  }
47
+ // ============================================================
48
+ // HELPER: Smart fill — handles both <input> and <select>
49
+ // Prevents crash when dropdown uses <select> instead of <input>
50
+ // ============================================================
51
+ async function smartFill(el, value) {
52
+ try {
53
+ const tag = await el.evaluate(e => e.tagName.toLowerCase());
54
+ if (tag === 'select') {
55
+ // Try by value first (most common: "01", "12", "2030")
56
+ try {
57
+ await el.selectOption({ value });
58
+ return true;
59
+ }
60
+ catch { /* next */ }
61
+ // Try matching option label containing the value
62
+ try {
63
+ await el.selectOption({ label: value });
64
+ return true;
65
+ }
66
+ catch { /* next */ }
67
+ // Last resort: numeric index (month "01" → index 1 if 0="Select...")
68
+ const idx = parseInt(value, 10);
69
+ if (!isNaN(idx)) {
70
+ try {
71
+ await el.selectOption({ index: idx });
72
+ return true;
73
+ }
74
+ catch { /* give up */ }
75
+ }
76
+ return false;
77
+ }
78
+ else {
79
+ await el.fill(value);
80
+ return true;
81
+ }
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ /** Try each selector in order, smartFill the first match */
88
+ async function tryFillField(page, selectors, value) {
89
+ for (const selector of selectors) {
90
+ const el = await page.$(selector);
91
+ if (el) {
92
+ const ok = await smartFill(el, value);
93
+ if (ok)
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
43
99
  /** Internal implementation — called by fillCheckoutForm inside a timeout wrapper */
44
- async function _fillCheckoutFormInner(page, checkoutUrl, // ✅ BUG 8 FIX: need URL to navigate to
45
- cardData) {
100
+ async function _fillCheckoutFormInner(page, checkoutUrl, cardData) {
46
101
  try {
47
- // ✅ BUG 8 FIX: Navigate to the checkout page (was missing page was blank!)
48
- await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
102
+ // ✅ FIX: Skip navigation if page already loaded (Single Browser reuse from auto_pay_checkout)
103
+ // Prevents double-navigate which would reload page and lose cart/session state.
104
+ const currentUrl = page.url();
105
+ const baseCheckoutUrl = checkoutUrl.split("?")[0];
106
+ if (!currentUrl || currentUrl === "about:blank" || !currentUrl.startsWith(baseCheckoutUrl)) {
107
+ await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
108
+ }
49
109
  // ============================================================
50
110
  // STRATEGY 1: Standard HTML form fields
111
+ // Covers: plain forms, Shopify, WooCommerce, custom checkouts
112
+ // Priority: autocomplete (W3C) → name → platform-specific → placeholder → aria-label
51
113
  // ============================================================
52
- const standardSelectors = {
114
+ const S = {
53
115
  number: [
116
+ '[autocomplete="cc-number"]',
54
117
  'input[name="cardnumber"]',
55
118
  'input[name="card-number"]',
56
119
  'input[name="cc-number"]',
57
- 'input[autocomplete="cc-number"]',
120
+ 'input[name="card_number"]',
121
+ 'input[name="checkout[payment][card_number]"]', // Shopify
122
+ 'input[id="wc-stripe-cc-number"]', // WooCommerce
58
123
  'input[data-elements-stable-field-name="cardNumber"]',
59
124
  'input[placeholder*="card number" i]',
60
- 'input[placeholder*="Card number" i]',
61
125
  'input[aria-label*="card number" i]',
62
126
  ],
63
127
  expiry: [
128
+ '[autocomplete="cc-exp"]',
64
129
  'input[name="exp-date"]',
65
130
  'input[name="cc-exp"]',
66
- 'input[autocomplete="cc-exp"]',
131
+ 'input[name="expiry"]',
132
+ 'input[name="checkout[payment][card_expiry]"]', // Shopify
67
133
  'input[placeholder*="MM / YY" i]',
68
134
  'input[placeholder*="MM/YY" i]',
69
135
  'input[aria-label*="expir" i]',
70
136
  ],
71
137
  exp_month: [
72
- 'input[name="exp-month"]',
138
+ '[autocomplete="cc-exp-month"]',
73
139
  'select[name="exp-month"]',
74
- 'input[autocomplete="cc-exp-month"]',
140
+ 'select[name="exp_month"]',
141
+ 'select[name="card_exp_month"]',
142
+ 'select[id*="exp-month" i]',
143
+ 'select[id*="exp_month" i]',
144
+ 'input[name="exp-month"]',
145
+ 'input[name="exp_month"]',
75
146
  ],
76
147
  exp_year: [
77
- 'input[name="exp-year"]',
148
+ '[autocomplete="cc-exp-year"]',
78
149
  'select[name="exp-year"]',
79
- 'input[autocomplete="cc-exp-year"]',
150
+ 'select[name="exp_year"]',
151
+ 'select[name="card_exp_year"]',
152
+ 'select[id*="exp-year" i]',
153
+ 'select[id*="exp_year" i]',
154
+ 'input[name="exp-year"]',
155
+ 'input[name="exp_year"]',
80
156
  ],
81
157
  cvv: [
158
+ '[autocomplete="cc-csc"]',
82
159
  'input[name="cvc"]',
83
160
  'input[name="cvv"]',
84
161
  'input[name="cc-csc"]',
85
- 'input[autocomplete="cc-csc"]',
162
+ 'input[name="security_code"]',
163
+ 'input[name="checkout[payment][card_cvc]"]', // Shopify
164
+ 'input[id="wc-stripe-cc-cvc"]', // WooCommerce
86
165
  'input[placeholder*="CVC" i]',
87
166
  'input[placeholder*="CVV" i]',
167
+ 'input[placeholder*="security" i]',
88
168
  'input[aria-label*="security code" i]',
169
+ 'input[aria-label*="CVC" i]',
89
170
  ],
90
171
  name: [
172
+ '[autocomplete="cc-name"]',
91
173
  'input[name="ccname"]',
92
174
  'input[name="cc-name"]',
93
- 'input[autocomplete="cc-name"]',
175
+ 'input[name="card-name"]',
176
+ 'input[name="card_name"]',
94
177
  'input[placeholder*="name on card" i]',
178
+ 'input[placeholder*="cardholder" i]',
95
179
  'input[aria-label*="name on card" i]',
96
180
  ],
97
181
  };
98
- // Try to fill each field
99
182
  let filledFields = 0;
100
183
  // Card Number
101
- for (const selector of standardSelectors.number) {
102
- const el = await page.$(selector);
103
- if (el) {
104
- await el.fill(cardData.number);
184
+ if (await tryFillField(page, S.number, cardData.number))
185
+ filledFields++;
186
+ // Expiry: try combined MM/YY first
187
+ const expiryValue = `${cardData.exp_month}/${cardData.exp_year.slice(-2)}`;
188
+ let expiryFilled = await tryFillField(page, S.expiry, expiryValue);
189
+ if (expiryFilled)
190
+ filledFields++;
191
+ // Expiry: fallback to separate month + year (works with both <input> AND <select>)
192
+ if (!expiryFilled) {
193
+ if (await tryFillField(page, S.exp_month, cardData.exp_month))
105
194
  filledFields++;
106
- break;
107
- }
108
- }
109
- // Expiry (combined MM/YY format)
110
- let expiryFilled = false;
111
- for (const selector of standardSelectors.expiry) {
112
- const el = await page.$(selector);
113
- if (el) {
114
- await el.fill(`${cardData.exp_month}/${cardData.exp_year.slice(-2)}`);
195
+ if (await tryFillField(page, S.exp_year, cardData.exp_year))
115
196
  filledFields++;
116
- expiryFilled = true;
117
- break;
118
- }
119
- }
120
- // Expiry (separate month/year fields)
121
- if (!expiryFilled) {
122
- for (const selector of standardSelectors.exp_month) {
123
- const el = await page.$(selector);
124
- if (el) {
125
- await el.fill(cardData.exp_month);
126
- filledFields++;
127
- break;
128
- }
129
- }
130
- for (const selector of standardSelectors.exp_year) {
131
- const el = await page.$(selector);
132
- if (el) {
133
- await el.fill(cardData.exp_year);
134
- filledFields++;
135
- break;
136
- }
137
- }
138
197
  }
139
198
  // CVV
140
- for (const selector of standardSelectors.cvv) {
141
- const el = await page.$(selector);
142
- if (el) {
143
- await el.fill(cardData.cvv);
144
- filledFields++;
145
- break;
146
- }
147
- }
199
+ if (await tryFillField(page, S.cvv, cardData.cvv))
200
+ filledFields++;
148
201
  // Name on Card
149
- for (const selector of standardSelectors.name) {
150
- const el = await page.$(selector);
151
- if (el) {
152
- await el.fill(cardData.name);
153
- filledFields++;
154
- break;
155
- }
156
- }
202
+ if (await tryFillField(page, S.name, cardData.name))
203
+ filledFields++;
157
204
  // ============================================================
158
205
  // STRATEGY 2: Stripe Elements (iframe-based)
206
+ // Stripe renders payment inputs inside iframes from js.stripe.com.
207
+ // Some merchants use __privateStripeFrame* named frames.
208
+ // Stripe has 2 modes:
209
+ // - Unified: 1 iframe with all fields (card, exp, cvc in one)
210
+ // - Split: separate iframes per field (cardNumber, cardExpiry, cardCvc)
211
+ // We handle both by scanning ALL matching Stripe frames.
159
212
  // ============================================================
160
213
  if (filledFields === 0) {
161
- const stripeFrames = page.frames().filter((f) => f.url().includes("js.stripe.com"));
214
+ const stripeFrames = page.frames().filter((f) => {
215
+ const url = f.url();
216
+ const name = f.name();
217
+ return url.includes('js.stripe.com')
218
+ || url.includes('stripe.com/elements')
219
+ || name.startsWith('__privateStripeFrame');
220
+ });
162
221
  for (const frame of stripeFrames) {
163
- const cardInput = await frame.$('input[name="cardnumber"]');
164
- if (cardInput) {
165
- await cardInput.fill(cardData.number);
166
- filledFields++;
222
+ // Card Number
223
+ for (const sel of [
224
+ 'input[name="cardnumber"]',
225
+ 'input[autocomplete="cc-number"]',
226
+ 'input[data-elements-stable-field-name="cardNumber"]',
227
+ ]) {
228
+ const el = await frame.$(sel);
229
+ if (el) {
230
+ await el.fill(cardData.number);
231
+ filledFields++;
232
+ break;
233
+ }
167
234
  }
168
- const expInput = await frame.$('input[name="exp-date"]');
169
- if (expInput) {
170
- await expInput.fill(`${cardData.exp_month}${cardData.exp_year.slice(-2)}`);
171
- filledFields++;
235
+ // Expiry (Stripe uses MM/YY without slash)
236
+ for (const sel of [
237
+ 'input[name="exp-date"]',
238
+ 'input[autocomplete="cc-exp"]',
239
+ 'input[data-elements-stable-field-name="cardExpiry"]',
240
+ ]) {
241
+ const el = await frame.$(sel);
242
+ if (el) {
243
+ await el.fill(`${cardData.exp_month}${cardData.exp_year.slice(-2)}`);
244
+ filledFields++;
245
+ break;
246
+ }
172
247
  }
173
- const cvcInput = await frame.$('input[name="cvc"]');
174
- if (cvcInput) {
175
- await cvcInput.fill(cardData.cvv);
176
- filledFields++;
248
+ // CVC
249
+ for (const sel of [
250
+ 'input[name="cvc"]',
251
+ 'input[autocomplete="cc-csc"]',
252
+ 'input[data-elements-stable-field-name="cardCvc"]',
253
+ ]) {
254
+ const el = await frame.$(sel);
255
+ if (el) {
256
+ await el.fill(cardData.cvv);
257
+ filledFields++;
258
+ break;
259
+ }
177
260
  }
178
261
  }
179
262
  }
@@ -0,0 +1 @@
1
+ export declare const CURRENT_MCP_VERSION = "1.1.1";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CURRENT_MCP_VERSION = void 0;
4
+ // Single source of truth for MCP version
5
+ // Imported by both index.ts and wdk_backend.ts to avoid circular dependency
6
+ exports.CURRENT_MCP_VERSION = "1.1.1";
@@ -20,6 +20,8 @@ exports.refundUnderspendRemote = refundUnderspendRemote;
20
20
  const key_store_js_1 = require("./lib/key-store.js");
21
21
  const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://www.clawcard.store";
22
22
  const INTERNAL_SECRET = process.env.Z_ZERO_INTERNAL_SECRET || "";
23
+ // Injected at build time — always reflects the actual running version
24
+ const version_js_1 = require("./version.js");
23
25
  if (!(0, key_store_js_1.hasPassportKey)()) {
24
26
  console.error("❌ ERROR: Z_ZERO_API_KEY (Passport Key) is missing!");
25
27
  console.error("🔐 Get your key: https://www.clawcard.store/dashboard/agents");
@@ -40,6 +42,7 @@ async function apiRequest(endpoint, method = 'GET', body = null) {
40
42
  headers: {
41
43
  "Authorization": `Bearer ${PASSPORT_KEY}`,
42
44
  "Content-Type": "application/json",
45
+ "X-MCP-Version": version_js_1.CURRENT_MCP_VERSION,
43
46
  },
44
47
  body: body ? JSON.stringify(body) : null,
45
48
  });
@@ -54,8 +57,9 @@ async function apiRequest(endpoint, method = 'GET', body = null) {
54
57
  }
55
58
  }
56
59
  async function internalApiRequest(endpoint, method, body) {
57
- if (!INTERNAL_SECRET) {
58
- return { error: "CONFIG_ERROR", message: "INTERNAL_SECRET not configured" };
60
+ // ✅ FIX 9: Validate INTERNAL_SECRET presence and minimum length
61
+ if (!INTERNAL_SECRET || INTERNAL_SECRET.length < 16) {
62
+ return { error: "CONFIG_ERROR", message: "INTERNAL_SECRET is missing or too short (min 16 chars)" };
59
63
  }
60
64
  const url = `${API_BASE_URL.replace(/\/$/, '')}${endpoint}`;
61
65
  try {
@@ -64,6 +68,7 @@ async function internalApiRequest(endpoint, method, body) {
64
68
  headers: {
65
69
  "x-internal-secret": INTERNAL_SECRET,
66
70
  "Content-Type": "application/json",
71
+ "X-MCP-Version": version_js_1.CURRENT_MCP_VERSION,
67
72
  },
68
73
  body: body ? JSON.stringify(body) : null,
69
74
  });
@@ -94,25 +99,19 @@ async function getBalanceRemote(cardAlias) {
94
99
  // finds connected WDK wallet, queries on-chain balance)
95
100
  const data = await apiRequest('/api/wdk/balance', 'GET');
96
101
  if (data?.error) {
97
- // Fallback: try custodial balance if WDK not set up yet
98
- const custodialData = await apiRequest('/api/tokens/cards', 'GET');
99
- if (custodialData?.cards?.[0]) {
100
- return {
101
- wallet_balance: custodialData.cards[0].balance,
102
- currency: 'USD',
103
- mode: 'custodial_fallback',
104
- note: 'WDK wallet not connected yet. Showing custodial balance.'
105
- };
106
- }
107
- return data;
102
+ return {
103
+ error: true,
104
+ message: 'WDK wallet not connected. Create one at https://www.clawcard.store/dashboard/agent-wallet'
105
+ };
108
106
  }
109
107
  return {
110
108
  wallet_balance: data.balance_usdt,
111
109
  currency: 'USDT',
112
- chain: data.chain || 'polygon',
110
+ chain: data.chain || 'ethereum',
113
111
  address: data.address,
112
+ tron_address: data.tron_address,
114
113
  mode: 'wdk_onchain',
115
- note: `Non-custodial WDK wallet balance (live on-chain). Address: ${data.address}`
114
+ note: `Non-custodial WDK wallet. On-chain USDT balance. Address: ${data.address}`
116
115
  };
117
116
  }
118
117
  // ──────────────────────────────────────────────────────────────────────────────
@@ -121,22 +120,22 @@ async function getBalanceRemote(cardAlias) {
121
120
  async function getDepositAddressesRemote() {
122
121
  const data = await apiRequest('/api/wdk/balance', 'GET');
123
122
  if (data?.error) {
124
- // Fallback to custodial addresses
125
- const custodial = await apiRequest('/api/tokens/cards', 'GET');
126
123
  return {
127
- ...custodial,
128
- note: 'WDK wallet not connected. Showing custodial deposit addresses as fallback.'
124
+ error: true,
125
+ message: 'WDK wallet not connected. Create one at https://www.clawcard.store/dashboard/agent-wallet'
129
126
  };
130
127
  }
131
128
  return {
132
129
  cards: [{ alias: 'wdk-wallet', balance: data.balance_usdt, currency: 'USDT' }],
133
130
  deposit_addresses: {
134
- polygon: data.address, // Primary: USDT on Polygon (ERC-20)
135
- note: 'Send USDT (Polygon/ERC-20) to fund your Agent wallet. Gasless payments via ERC-4337.'
131
+ ethereum: data.address,
132
+ tron: data.tron_address || null,
133
+ note: 'Send USDT to your WDK wallet. Gasless via ERC-4337 Paymaster (Ethereum) or GasFree (Tron).'
136
134
  },
137
135
  wdk_wallet: {
138
136
  address: data.address,
139
- chain: data.chain || 'polygon',
137
+ tron_address: data.tron_address || null,
138
+ chain: data.chain || 'ethereum',
140
139
  balance_usdt: data.balance_usdt
141
140
  }
142
141
  };
@@ -169,10 +168,11 @@ async function issueTokenRemote(cardAlias, amount, merchant) {
169
168
  amount,
170
169
  merchant,
171
170
  created_at: Date.now(),
172
- ttl_seconds: 1800,
171
+ ttl_seconds: 3600,
173
172
  used: false,
174
- tx_hash: data.tx_hash, // On-chain tx hash (for transparency)
175
- mode: 'wdk_noncustodial'
173
+ tx_hash: data.tx_hash,
174
+ mode: 'wdk_noncustodial',
175
+ mcp_warning: data._mcp_warning || null, // Relay backend version warning to agent
176
176
  };
177
177
  }
178
178
  // ──────────────────────────────────────────────────────────────────────────────
@@ -182,11 +182,15 @@ async function resolveTokenRemote(token) {
182
182
  const data = await internalApiRequest('/api/tokens/resolve', 'POST', { token });
183
183
  if (!data || data.error)
184
184
  return data;
185
+ // ✅ FIX 7: Reject incomplete card data — don't silently fallback to fake values
186
+ if (!data.number || !data.cvv || !data.exp) {
187
+ return { error: "INCOMPLETE_CARD", message: "Card data missing required fields (number/cvv/exp). Token may be invalid or expired." };
188
+ }
185
189
  return {
186
190
  number: data.number,
187
- exp_month: data.exp?.split('/')[0] || "12",
188
- exp_year: "20" + (data.exp?.split('/')[1] || "30"),
189
- cvv: data.cvv || "123",
191
+ exp_month: data.exp.split('/')[0],
192
+ exp_year: "20" + data.exp.split('/')[1],
193
+ cvv: data.cvv,
190
194
  name: data.name || "Z-ZERO AI AGENT",
191
195
  authorized_amount: data.authorized_amount ? Number(data.authorized_amount) : undefined,
192
196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "z-zero-mcp-server",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, Cursor, AutoGPT) — Real Single-Use Visa Cards",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",