z-zero-mcp-server 1.0.7 → 1.1.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/index.js CHANGED
@@ -1,26 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- // OpenClaw MCP Server (z-zero-mcp-server) v1.0.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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
6
40
  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";
7
43
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
44
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
45
  const zod_1 = require("zod");
10
- const api_backend_js_1 = require("./api_backend.js");
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;
56
+ const issueTokenRemote = activeBackend.issueTokenRemote;
57
+ const resolveTokenRemote = activeBackend.resolveTokenRemote;
58
+ const burnTokenRemote = activeBackend.burnTokenRemote;
59
+ const cancelTokenRemote = activeBackend.cancelTokenRemote;
60
+ const refundUnderspendRemote = activeBackend.refundUnderspendRemote;
61
+ const getBalanceRemote = activeBackend.getBalanceRemote;
62
+ const listCardsRemote = activeBackend.listCardsRemote;
63
+ const getDepositAddressesRemote = activeBackend.getDepositAddressesRemote;
64
+ console.error(`[Z-ZERO MCP] 🚀 Wallet mode: ${WALLET_MODE.toUpperCase()}`);
65
+ // ────────────────────────────────────────────────────────────────────────────
11
66
  const playwright_bridge_js_1 = require("./playwright_bridge.js");
67
+ const web3_detector_js_1 = require("./lib/web3-detector.js");
68
+ const extract_total_price_js_1 = require("./lib/extract-total-price.js");
69
+ const playwright_1 = require("playwright");
70
+ const key_store_js_1 = require("./lib/key-store.js"); // ✅ Hot-Swap support
12
71
  // ============================================================
13
72
  // CREATE MCP SERVER
14
73
  // ============================================================
15
74
  const server = new mcp_js_1.McpServer({
16
75
  name: "z-zero-mcp-server",
17
- version: "1.0.3",
76
+ version: "2.0.0",
18
77
  });
19
78
  // ============================================================
20
79
  // TOOL 1: List available cards (safe - no sensitive data)
21
80
  // ============================================================
22
81
  server.tool("list_cards", "List all available virtual card aliases and their balances. No sensitive data is returned.", {}, async () => {
23
- const data = await (0, api_backend_js_1.listCardsRemote)();
82
+ const data = await listCardsRemote();
24
83
  if (data?.error === "AUTH_REQUIRED") {
25
84
  return {
26
85
  content: [{
@@ -43,7 +102,7 @@ server.tool("list_cards", "List all available virtual card aliases and their bal
43
102
  }
44
103
  const cards = data?.cards || [];
45
104
  const activeTokens = data?.active_tokens || [];
46
- const recentTokens = data?.recent_tokens || [];
105
+ const historySummary = data?.history_summary || {};
47
106
  return {
48
107
  content: [
49
108
  {
@@ -51,8 +110,8 @@ server.tool("list_cards", "List all available virtual card aliases and their bal
51
110
  text: JSON.stringify({
52
111
  cards,
53
112
  active_tokens: activeTokens,
54
- recent_tokens: recentTokens,
55
- note: "Use card aliases to request payment tokens. Never ask for real card numbers.",
113
+ history_summary: historySummary,
114
+ note: "Only active tokens are shown. Use card aliases to request payment tokens.",
56
115
  }, null, 2),
57
116
  },
58
117
  ],
@@ -66,7 +125,7 @@ server.tool("check_balance", "Check the wallet balance (human-level total). This
66
125
  .string()
67
126
  .describe("The alias of the card to check, e.g. 'Card_01'"),
68
127
  }, async ({ card_alias }) => {
69
- const data = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
128
+ const data = await getBalanceRemote(card_alias);
70
129
  if (data?.error === "AUTH_REQUIRED") {
71
130
  return {
72
131
  content: [{
@@ -102,7 +161,7 @@ server.tool("check_balance", "Check the wallet balance (human-level total). This
102
161
  // TOOL 2.5: Get deposit addresses (Phase 14 feature)
103
162
  // ============================================================
104
163
  server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM networks (Base, BSC, Ethereum) and Tron. Provide these to the human user when they need to add funds to their Z-ZERO balance.", {}, async () => {
105
- const data = await (0, api_backend_js_1.getDepositAddressesRemote)();
164
+ const data = await getDepositAddressesRemote();
106
165
  if (data?.error === "AUTH_REQUIRED") {
107
166
  return {
108
167
  content: [{
@@ -114,39 +173,55 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
114
173
  isError: true
115
174
  };
116
175
  }
176
+ // ── WDK Non-Custodial Mode ────────────────────────────────────────────
177
+ if (data?.wdk_wallet?.address) {
178
+ const wdkAddr = data.wdk_wallet.address;
179
+ const balance = data.wdk_wallet.balance_usdt ?? 0;
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: JSON.stringify({
184
+ wallet_type: "non-custodial (WDK)",
185
+ address: wdkAddr,
186
+ balance_usdt: balance,
187
+ supported_chains: [
188
+ { chain: "Polygon", token: "USDT", address: wdkAddr },
189
+ ],
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."
192
+ }, null, 2),
193
+ }],
194
+ };
195
+ }
196
+ // ── Custodial Mode Fallback ───────────────────────────────────────────
117
197
  const addresses = data?.deposit_addresses;
118
198
  if (!addresses) {
119
199
  return {
120
- content: [
121
- {
200
+ content: [{
122
201
  type: "text",
123
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",
124
- },
125
- ],
203
+ }],
126
204
  isError: true,
127
205
  };
128
206
  }
129
207
  return {
130
- content: [
131
- {
208
+ content: [{
132
209
  type: "text",
133
210
  text: JSON.stringify({
134
- networks: {
135
- evm: {
136
- address: addresses.evm,
137
- supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
138
- tokens: ["USDC", "USDT"]
139
- },
140
- tron: {
141
- address: addresses.tron,
142
- supported_chains: ["Tron (TRC-20)"],
143
- tokens: ["USDT"]
144
- }
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"]
145
221
  },
146
222
  note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
147
223
  }, null, 2),
148
- },
149
- ],
224
+ }],
150
225
  };
151
226
  });
152
227
  // ============================================================
@@ -165,7 +240,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
165
240
  .string()
166
241
  .describe("Name or URL of the merchant/service being purchased"),
167
242
  }, async ({ card_alias, amount, merchant }) => {
168
- const token = await (0, api_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
243
+ const token = await issueTokenRemote(card_alias, amount, merchant);
169
244
  if (token?.error === "AUTH_REQUIRED") {
170
245
  return {
171
246
  content: [{
@@ -187,7 +262,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
187
262
  isError: true,
188
263
  };
189
264
  }
190
- const balanceData = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
265
+ const balanceData = await getBalanceRemote(card_alias);
191
266
  const balance = balanceData?.balance;
192
267
  return {
193
268
  content: [
@@ -208,6 +283,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
208
283
  type: "text",
209
284
  text: JSON.stringify({
210
285
  token: token.token,
286
+ token_ref: `...${token.token.slice(-6)}`,
211
287
  amount: token.amount,
212
288
  merchant: token.merchant,
213
289
  expires_at: expiresAt,
@@ -235,7 +311,7 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
235
311
  .describe("The actual final amount on the checkout page. If different from token amount, system will auto-refund the difference."),
236
312
  }, async ({ token, checkout_url, actual_amount }) => {
237
313
  // Step 1: Resolve token → card data (RAM only)
238
- const cardData = await (0, api_backend_js_1.resolveTokenRemote)(token);
314
+ const cardData = await resolveTokenRemote(token);
239
315
  if (cardData?.error === "AUTH_REQUIRED") {
240
316
  return {
241
317
  content: [{
@@ -257,13 +333,60 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
257
333
  isError: true,
258
334
  };
259
335
  }
336
+ // 🔒 PRE-FLIGHT AMOUNT GUARD (Prompt Injection Defense)
337
+ // If the agent passes actual_amount, verify it doesn't exceed the token's authorized amount.
338
+ // Tolerance: 5% to allow for minor price rounding (e.g., taxes calculated at checkout).
339
+ // Attack scenario: Merchant shows $99 on page, but agent was authorized $10 → block + alert human.
340
+ if (actual_amount !== undefined && cardData.authorized_amount !== undefined) {
341
+ const tokenAmount = Number(cardData.authorized_amount);
342
+ const TOLERANCE = 1.05; // 5% buffer
343
+ if (actual_amount > tokenAmount * TOLERANCE) {
344
+ // Auto-cancel the token to free up funds
345
+ await cancelTokenRemote(token);
346
+ return {
347
+ content: [{
348
+ type: "text",
349
+ text: JSON.stringify({
350
+ success: false,
351
+ blocked: true,
352
+ reason: "PRICE_MISMATCH",
353
+ message: `🚨 PAYMENT BLOCKED: Checkout shows $${actual_amount} but token only authorizes $${tokenAmount}. Token has been cancelled and funds returned to wallet.`,
354
+ token_status: "CANCELLED",
355
+ action_required: "Request a new token with the correct amount if you wish to proceed.",
356
+ }, null, 2),
357
+ }],
358
+ isError: true,
359
+ };
360
+ }
361
+ }
260
362
  // Step 2: Use Playwright to inject card into checkout form
261
363
  const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
262
- // Step 3: Burn the token (wallet was pre-debited at issue time)
263
- await (0, api_backend_js_1.burnTokenRemote)(token);
364
+ // Step 3: Burn the token ONLY if payment succeeded
365
+ // If merchant declines → keep token ACTIVE so webhook decline flow can refund correctly
366
+ if (result.success) {
367
+ await burnTokenRemote(token);
368
+ }
369
+ else {
370
+ // Payment failed — do NOT burn token
371
+ // Airwallex will fire a 'card.authorization.declined' webhook → refund handled there
372
+ return {
373
+ content: [
374
+ {
375
+ type: "text",
376
+ text: JSON.stringify({
377
+ success: false,
378
+ message: result.message || "Payment was declined by merchant.",
379
+ token_status: "ACTIVE",
380
+ note: "Token NOT burned. Funds will be refunded automatically via webhook within minutes.",
381
+ }, null, 2),
382
+ },
383
+ ],
384
+ isError: true,
385
+ };
386
+ }
264
387
  // Step 4: Refund underspend if actual amount was less than token amount
265
388
  if (actual_amount !== undefined && result.success) {
266
- await (0, api_backend_js_1.refundUnderspendRemote)(token, actual_amount);
389
+ await refundUnderspendRemote(token, actual_amount);
267
390
  }
268
391
  // Step 5: Return result (NEVER includes card numbers)
269
392
  return {
@@ -292,7 +415,7 @@ server.tool("cancel_payment_token", "Cancel a payment token that has not been us
292
415
  .string()
293
416
  .describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
294
417
  }, async ({ token, reason }) => {
295
- const result = await (0, api_backend_js_1.cancelTokenRemote)(token);
418
+ const result = await cancelTokenRemote(token);
296
419
  if (result?.error === "AUTH_REQUIRED") {
297
420
  return {
298
421
  content: [{
@@ -372,6 +495,331 @@ server.tool("request_human_approval", "Request human approval before proceeding
372
495
  };
373
496
  });
374
497
  // ============================================================
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
+ // TOOL 6.5: Set API Key (Hot-Swap Passport Key — NO restart needed)
542
+ // ============================================================
543
+ 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.", {
544
+ api_key: zod_1.z
545
+ .string()
546
+ .describe("The new Passport Key to activate. Must start with 'zk_live_' or 'zk_test_'. Get from: https://www.clawcard.store/dashboard/agents"),
547
+ }, async ({ api_key }) => {
548
+ const result = (0, key_store_js_1.setPassportKey)(api_key);
549
+ if (!result.ok) {
550
+ return {
551
+ content: [{ type: "text", text: `❌ ${result.message}` }],
552
+ isError: true,
553
+ };
554
+ }
555
+ return {
556
+ content: [{
557
+ type: "text",
558
+ text: JSON.stringify({
559
+ status: "SUCCESS",
560
+ message: `✅ ${result.message}`,
561
+ active_key_prefix: api_key.slice(0, 12) + "...",
562
+ note: "All subsequent API calls will use this key. No restart needed.",
563
+ }, null, 2),
564
+ }],
565
+ };
566
+ });
567
+ // ============================================================
568
+ // TOOL 6.6: Show current API Key status (for debugging)
569
+ // ============================================================
570
+ server.tool("show_api_key_status", "Show whether a Passport Key is currently configured, and its prefix (first 12 chars). Does NOT reveal the full key. Use this to debug authentication issues.", {}, async () => {
571
+ const key = (0, key_store_js_1.getPassportKey)();
572
+ const hasKey = key.length > 0;
573
+ return {
574
+ content: [{
575
+ type: "text",
576
+ text: JSON.stringify({
577
+ configured: hasKey,
578
+ key_prefix: hasKey ? key.slice(0, 12) + "..." : null,
579
+ wallet_mode: process.env.Z_ZERO_WALLET_MODE || "custodial",
580
+ note: hasKey
581
+ ? "Key is active. Call set_api_key to update it."
582
+ : "No key configured. Call set_api_key with your Passport Key from https://www.clawcard.store/dashboard/agents",
583
+ }, null, 2),
584
+ }],
585
+ };
586
+ });
587
+ // ============================================================
588
+ // TOOL 7: Auto Pay Checkout (Phase 2 — Smart Routing)
589
+ // ============================================================
590
+ server.tool("auto_pay_checkout", "[Phase 2] Autonomous Smart Routing checkout tool. Provide a checkout URL and this tool will:\n" +
591
+ "1. Scan the page to detect if it supports Web3 (Crypto) payments via window.ethereum or EIP-681 links.\n" +
592
+ "2. SCENARIO A (Web3): If detected, automatically send USDT on-chain via WDK (gas ~$0.001). No Visa card needed.\n" +
593
+ "3. SCENARIO B (Fiat): If no Web3 detected, scan DOM for total price, issue a JIT Visa card for exact amount, auto-fill form.", {
594
+ checkout_url: zod_1.z
595
+ .string()
596
+ .url()
597
+ .describe("Full URL of the checkout/payment page to analyze and pay."),
598
+ card_alias: zod_1.z
599
+ .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)."),
601
+ }, async ({ checkout_url, card_alias }) => {
602
+ 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 ────────────────────────────────────────────
605
+ if (!API_KEY) {
606
+ return {
607
+ content: [{ type: "text", text: JSON.stringify({
608
+ status: "CONFIG_ERROR",
609
+ message: "Z_ZERO_API_KEY is not configured on this MCP server. Cannot proceed.",
610
+ }, null, 2) }],
611
+ isError: true,
612
+ };
613
+ }
614
+ // ── C1 FIX: SSRF guard — validate checkout_url ───────────────────────
615
+ // Block non-https and internal/private IP ranges
616
+ (() => {
617
+ let url;
618
+ try {
619
+ url = new URL(checkout_url);
620
+ }
621
+ catch {
622
+ throw new Error(`Invalid checkout_url: ${checkout_url}`);
623
+ }
624
+ const isDev = process.env.NODE_ENV !== "production";
625
+ 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 {
631
+ if (url.protocol !== "https:") {
632
+ throw new Error(`checkout_url must use HTTPS. Got: ${url.protocol}`);
633
+ }
634
+ const privatePatterns = [
635
+ /^localhost$/i, /^127\./, /^10\./,
636
+ /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./,
637
+ /^169\.254\./, /^\[::1\]$/, /^0\.0\.0\.0$/,
638
+ ];
639
+ if (privatePatterns.some(p => p.test(hostname))) {
640
+ throw new Error(`SSRF blocked: checkout_url points to private/internal host: ${hostname}`);
641
+ }
642
+ }
643
+ })();
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) {
652
+ 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
+ }],
663
+ };
664
+ }
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) {
675
+ 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
+ }],
686
+ };
687
+ }
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) {
698
+ 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
+ }],
707
+ isError: true,
708
+ };
709
+ }
710
+ 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",
763
+ 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,
784
+ };
785
+ }
786
+ // Resolve token and auto-fill form
787
+ const cardData = await resolveTokenRemote(token.token);
788
+ if (!cardData || cardData.error) {
789
+ 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
+ }],
798
+ isError: true,
799
+ };
800
+ }
801
+ const fillResult = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
802
+ if (fillResult.success) {
803
+ await burnTokenRemote(token.token);
804
+ }
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
+ });
822
+ // ============================================================
375
823
  // RESOURCE: Z-ZERO Autonomous Payment SOP
376
824
  // ============================================================
377
825
  server.resource("Standard Operating Procedure (SOP) for Autonomous Payments", "mcp://resources/sop", {
@@ -426,8 +874,8 @@ When asked to make a purchase, execute the following steps precisely in order:
426
874
  async function main() {
427
875
  const transport = new stdio_js_1.StdioServerTransport();
428
876
  await server.connect(transport);
429
- console.error("🔐 OpenClaw MCP Server v1.0.3 running...");
877
+ console.error("🔐 OpenClaw MCP Server v2.0.0 running (Phase 2: Smart Routing enabled)...");
430
878
  console.error("Status: Secure & Connected to Z-ZERO Gateway");
431
- console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
879
+ console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval, auto_pay_checkout");
432
880
  }
433
881
  main().catch(console.error);
@@ -0,0 +1,9 @@
1
+ import type { Page } from "playwright";
2
+ /**
3
+ * Multi-strategy total price extractor.
4
+ * Strategy order (most reliable → least reliable):
5
+ * 1. Known CSS selectors for major platforms (Shopify, Stripe, Woo)
6
+ * 2. Aria / data-testid patterns
7
+ * 3. Text heuristic: find largest $ amount near keywords "total", "amount due"
8
+ */
9
+ export declare function extractTotalPrice(page: Page): Promise<number | null>;