@quackai/q402-mcp 0.5.12 → 0.5.14

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.
Files changed (2) hide show
  1. package/dist/index.js +123 -41
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,7 +14,13 @@ import { homedir } from "os";
14
14
  import { join } from "path";
15
15
  import { isAddress } from "ethers";
16
16
  var Q402_ENV_FILE = join(homedir(), ".q402", "mcp.env");
17
+ var lastReadError = null;
18
+ function getQ402EnvFileReadError() {
19
+ return lastReadError;
20
+ }
21
+ var MAX_ENV_FILE_BYTES = 64 * 1024;
17
22
  function loadQ402EnvFileFromPath(path) {
23
+ lastReadError = null;
18
24
  if (!existsSync(path)) return {};
19
25
  if (process.platform !== "win32") {
20
26
  try {
@@ -28,15 +34,26 @@ function loadQ402EnvFileFromPath(path) {
28
34
  } catch {
29
35
  }
30
36
  }
37
+ try {
38
+ const size = statSync(path).size;
39
+ if (size > MAX_ENV_FILE_BYTES) {
40
+ const msg = `file is ${size} bytes (max ${MAX_ENV_FILE_BYTES}); refusing to load. Check ~/.q402/mcp.env \u2014 is it a misdirected log file or symlink?`;
41
+ lastReadError = msg;
42
+ process.stderr.write(`[q402-mcp] warning: ${msg}
43
+ `);
44
+ return {};
45
+ }
46
+ } catch {
47
+ }
31
48
  const out = {};
32
49
  let raw;
33
50
  try {
34
51
  raw = readFileSync(path, "utf-8");
35
52
  } catch (e) {
36
- process.stderr.write(
37
- `[q402-mcp] warning: could not read ${path}: ${e instanceof Error ? e.message : String(e)}
38
- `
39
- );
53
+ const msg = `could not read ${path}: ${e instanceof Error ? e.message : String(e)}`;
54
+ lastReadError = msg;
55
+ process.stderr.write(`[q402-mcp] warning: ${msg}
56
+ `);
40
57
  return {};
41
58
  }
42
59
  if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
@@ -169,7 +186,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
169
186
 
170
187
  // src/version.ts
171
188
  var PACKAGE_NAME = "@quackai/q402-mcp";
172
- var PACKAGE_VERSION = "0.5.12";
189
+ var PACKAGE_VERSION = "0.5.14";
173
190
 
174
191
  // src/tools/quote.ts
175
192
  import { z } from "zod";
@@ -505,7 +522,15 @@ var Q402NodeClient = class _Q402NodeClient {
505
522
  }
506
523
  async fetchFacilitator() {
507
524
  const url = `${this.opts.relayBaseUrl.replace(/\/$/, "")}/relay/info`;
508
- const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
525
+ let resp;
526
+ try {
527
+ resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
528
+ } catch (e) {
529
+ if (e instanceof Error && (e.name === "TimeoutError" || /aborted/i.test(e.message))) {
530
+ throw new Error("Q402 relay didn't respond within 10s while reading facilitator info \u2014 the relay may be temporarily degraded. Safe to retry.");
531
+ }
532
+ throw e;
533
+ }
509
534
  if (!resp.ok) {
510
535
  throw new Error(
511
536
  `failed to fetch relay facilitator info from ${url} (${resp.status})`
@@ -576,12 +601,20 @@ var Q402NodeClient = class _Q402NodeClient {
576
601
  facilitator
577
602
  };
578
603
  const body = chain.key === "xlayer" ? { ...baseBody, xlayerNonce: paymentNonce.toString() } : chain.key === "stable" ? { ...baseBody, stableNonce: paymentNonce.toString() } : { ...baseBody, nonce: paymentNonce.toString() };
579
- const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay`, {
580
- method: "POST",
581
- headers: { "Content-Type": "application/json" },
582
- body: JSON.stringify(body),
583
- signal: AbortSignal.timeout(3e4)
584
- });
604
+ let resp;
605
+ try {
606
+ resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay`, {
607
+ method: "POST",
608
+ headers: { "Content-Type": "application/json" },
609
+ body: JSON.stringify(body),
610
+ signal: AbortSignal.timeout(3e4)
611
+ });
612
+ } catch (e) {
613
+ if (e instanceof Error && (e.name === "TimeoutError" || /aborted/i.test(e.message))) {
614
+ throw new Error("Q402 relay didn't respond within 30s \u2014 the relay may be temporarily degraded. Safe to retry.");
615
+ }
616
+ throw e;
617
+ }
585
618
  const data = await resp.json();
586
619
  if (!resp.ok) {
587
620
  throw new Error(data.error ?? `relay failed (HTTP ${resp.status})`);
@@ -692,18 +725,26 @@ var Q402NodeClient = class _Q402NodeClient {
692
725
  authorization
693
726
  });
694
727
  }
695
- const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/batch`, {
696
- method: "POST",
697
- headers: { "Content-Type": "application/json" },
698
- body: JSON.stringify({
699
- apiKey,
700
- chain: chain.key,
701
- token,
702
- facilitator,
703
- recipients: signedRows
704
- }),
705
- signal: AbortSignal.timeout(6e4)
706
- });
728
+ let resp;
729
+ try {
730
+ resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay/batch`, {
731
+ method: "POST",
732
+ headers: { "Content-Type": "application/json" },
733
+ body: JSON.stringify({
734
+ apiKey,
735
+ chain: chain.key,
736
+ token,
737
+ facilitator,
738
+ recipients: signedRows
739
+ }),
740
+ signal: AbortSignal.timeout(6e4)
741
+ });
742
+ } catch (e) {
743
+ if (e instanceof Error && (e.name === "TimeoutError" || /aborted/i.test(e.message))) {
744
+ throw new Error("Q402 relay didn't respond within 60s on the batch path \u2014 the relay may be temporarily degraded. Safe to retry.");
745
+ }
746
+ throw e;
747
+ }
707
748
  const data = await resp.json();
708
749
  if (!resp.ok || data.ok === false) {
709
750
  const err = new BatchPayError(
@@ -862,8 +903,14 @@ async function runPay(input) {
862
903
  };
863
904
  }
864
905
  function describeSandboxReason(resolvedKey, scope) {
906
+ const noApiKey = !resolvedKey.startsWith("q402_live_");
907
+ const noPk = !CONFIG.privateKey;
908
+ const noEnable = !CONFIG.realPaymentsRequested;
909
+ if (noApiKey && noPk && noEnable) {
910
+ return `You haven't configured Q402 yet. Say "Set up Q402" and I'll walk you through it (creates a settings file in your editor, you paste an API key from https://q402.quackai.ai/event, done).`;
911
+ }
865
912
  const missing = [];
866
- if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
913
+ if (noApiKey) missing.push("a live API key (must start with q402_live_)");
867
914
  if (!CONFIG.privateKey) {
868
915
  missing.push("Q402_PRIVATE_KEY");
869
916
  } else if (!isValidPrivateKey(CONFIG.privateKey)) {
@@ -871,7 +918,7 @@ function describeSandboxReason(resolvedKey, scope) {
871
918
  "Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
872
919
  );
873
920
  }
874
- if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
921
+ if (noEnable) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
875
922
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
876
923
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
877
924
  const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
@@ -879,7 +926,7 @@ function describeSandboxReason(resolvedKey, scope) {
879
926
  }
880
927
  var PAY_TOOL = {
881
928
  name: "q402_pay",
882
- description: "Send a gasless USDC, USDT, or RLUSD payment via Q402. Auto-routing: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial (free sponsored); anything else \u2192 Multichain (paid 9-chain). Same rule for q402_batch_pay. Set keyScope='trial' or 'multichain' to force one explicitly. Trial keys reject any non-BNB chain server-side with TRIAL_BNB_ONLY. Multichain keys cover avax, bnb, eth, xlayer, stable, mantle, injective, monad, scroll \u2014 USDC/USDT on most chains, RLUSD on Ethereum only, Injective USDT-only. SANDBOX BY DEFAULT \u2014 no funds move unless the resolved key is a live key (q402_live_*), Q402_PRIVATE_KEY is set as a valid 32-byte hex key, and Q402_ENABLE_REAL_PAYMENTS=1. Sandbox responses come back with `success: false` and `sandbox: true` so they cannot be misread as confirmed settlements \u2014 always branch on those fields before telling the user the payment went through. The recipient receives the full amount; the sender pays $0 in gas. \n\nSENDER ECHO \u2014 every response includes a `senderWallet` field with the address derived from the configured `Q402_PRIVATE_KEY`. Show this alongside the recipient/amount when you confirm the payment with the user (e.g. 'Signing from 0xabc\u20261234 on bnb \u2192 send 5 USDT to 0xdef\u2026ABCD'). Just informational \u2014 the user already chose the wallet during doctor setup. \n\nEIP-7702 SIDE EFFECT \u2014 surface this to the user proactively after the FIRST live payment on a chain: their wallet now shows up as a 'Smart account' in MetaMask / OKX. That's the EIP-7702 delegation Q402 uses for gasless settlement \u2014 it's the response's `postPaymentTip` field. Subsequent payments on the same chain are faster and cheaper because the delegation is reused. \n\nIf the user EVER reports that native gas tokens (BNB / ETH / AVAX / etc.) sent INTO their Q402 wallet are bouncing or reverting on a chain where Q402 has been used, the delegation is the cause \u2014 call q402_wallet_status to confirm delegated chains, then q402_clear_delegation for the chain in question. Q402 sponsors the gas for the clear, so the user pays $0. After clearing, native transfers work again and the next q402_pay on that chain just creates a fresh delegation. \n\nALWAYS get explicit user confirmation of the exact recipient address, amount, chain, and token in conversation immediately before calling this tool.",
929
+ description: "Send a gasless USDC, USDT, or RLUSD payment via Q402. Auto-routing: chain='bnb' + Q402_TRIAL_API_KEY set \u2192 Trial (free sponsored); anything else \u2192 Multichain (paid 9-chain). Same rule for q402_batch_pay. Set keyScope='trial' or 'multichain' to force one explicitly. Trial keys reject any non-BNB chain server-side with TRIAL_BNB_ONLY. Multichain keys cover avax, bnb, eth, xlayer, stable, mantle, injective, monad, scroll \u2014 USDC/USDT on most chains, RLUSD on Ethereum only, Injective USDT-only. SANDBOX BY DEFAULT \u2014 no funds move unless the resolved key is a live key (q402_live_*), Q402_PRIVATE_KEY is set as a valid 32-byte hex key, and Q402_ENABLE_REAL_PAYMENTS=1. Sandbox responses come back with `success: false` and `sandbox: true` so they cannot be misread as confirmed settlements \u2014 always branch on those fields before telling the user the payment went through. The recipient receives the full amount; the sender pays $0 in gas. \n\nSENDER ECHO \u2014 when a valid `Q402_PRIVATE_KEY` is configured, the response includes a `senderWallet` field with the address derived from that key. Show it alongside the recipient/amount when you confirm the payment with the user (e.g. 'Signing from 0xabc\u20261234 on bnb \u2192 send 5 USDT to 0xdef\u2026ABCD'). Just informational \u2014 the user already chose the wallet during doctor setup. Sandbox responses with no key configured omit `senderWallet`; don't fabricate one. \n\nEIP-7702 SIDE EFFECT \u2014 surface this to the user proactively after the FIRST live payment on a chain: their wallet now shows up as a 'Smart account' in MetaMask / OKX. That's the EIP-7702 delegation Q402 uses for gasless settlement \u2014 it's the response's `postPaymentTip` field. Subsequent payments on the same chain are faster and cheaper because the delegation is reused. \n\nIf the user EVER reports that native gas tokens (BNB / ETH / AVAX / etc.) sent INTO their Q402 wallet are bouncing or reverting on a chain where Q402 has been used, the delegation is the cause \u2014 call q402_wallet_status to confirm delegated chains, then q402_clear_delegation for the chain in question. Q402 sponsors the gas for the clear, so the user pays $0. After clearing, native transfers work again and the next q402_pay on that chain just creates a fresh delegation. \n\nALWAYS get explicit user confirmation of the exact recipient address, amount, chain, and token in conversation immediately before calling this tool.",
883
930
  inputSchema: {
884
931
  type: "object",
885
932
  properties: {
@@ -1069,8 +1116,14 @@ async function runBatchPay(input) {
1069
1116
  }
1070
1117
  }
1071
1118
  function describeSandboxReason2(resolvedKey, scope) {
1119
+ const noApiKey = !resolvedKey.startsWith("q402_live_");
1120
+ const noPk = !CONFIG.privateKey;
1121
+ const noEnable = !CONFIG.realPaymentsRequested;
1122
+ if (noApiKey && noPk && noEnable) {
1123
+ return `You haven't configured Q402 yet. Say "Set up Q402" and I'll walk you through it (creates a settings file in your editor, you paste an API key from https://q402.quackai.ai/event, done).`;
1124
+ }
1072
1125
  const missing = [];
1073
- if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
1126
+ if (noApiKey) missing.push("a live API key (must start with q402_live_)");
1074
1127
  if (!CONFIG.privateKey) {
1075
1128
  missing.push("Q402_PRIVATE_KEY");
1076
1129
  } else if (!isValidPrivateKey(CONFIG.privateKey)) {
@@ -1078,7 +1131,7 @@ function describeSandboxReason2(resolvedKey, scope) {
1078
1131
  "Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
1079
1132
  );
1080
1133
  }
1081
- if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1134
+ if (noEnable) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1082
1135
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
1083
1136
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
1084
1137
  const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
@@ -1587,6 +1640,12 @@ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
1587
1640
  # Read automatically by @quackai/q402-mcp on startup.
1588
1641
  # Edit this file in your editor. NEVER paste your private key into chat.
1589
1642
  # After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
1643
+ #
1644
+ # SAFE-BY-DEFAULT: this template ships with both api-key + private-key
1645
+ # lines COMMENTED OUT. Even though Q402_ENABLE_REAL_PAYMENTS defaults
1646
+ # to 1, the live-mode gate refuses to settle until BOTH a real api key
1647
+ # AND a valid 32-byte private key are configured. Saving this template
1648
+ # as-is and restarting your client just leaves you in sandbox.
1590
1649
  # \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1591
1650
 
1592
1651
  # \u2500\u2500\u2500 API key \u2014 uncomment ONE (or both for auto-routing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -1647,9 +1706,13 @@ function mask2(key) {
1647
1706
  }
1648
1707
  function detectPhase() {
1649
1708
  const anyKey = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
1650
- const allEssentials = anyKey && !!CONFIG.privateKey && CONFIG.realPaymentsRequested && CONFIG.apiKeyKind === "live";
1709
+ const hasValidPrivateKey = isValidPrivateKey(CONFIG.privateKey);
1710
+ if (!Q402_ENV_FILE_PRESENT && !anyKey && !CONFIG.privateKey) {
1711
+ return "first-install";
1712
+ }
1713
+ const allEssentials = anyKey && hasValidPrivateKey && CONFIG.realPaymentsRequested && CONFIG.apiKeyKind === "live";
1651
1714
  if (allEssentials) return "live-check";
1652
- if (anyKey || CONFIG.privateKey || CONFIG.realPaymentsRequested) return "needs-completion";
1715
+ if (anyKey || CONFIG.privateKey || Q402_ENV_FILE_PRESENT) return "needs-completion";
1653
1716
  return "first-install";
1654
1717
  }
1655
1718
  async function verifyOneKey(scope, envVar, apiKey) {
@@ -1746,9 +1809,11 @@ async function fetchDelegation(address) {
1746
1809
  }
1747
1810
  async function runDoctor() {
1748
1811
  const phase = detectPhase();
1812
+ const envFileReadError = getQ402EnvFileReadError();
1749
1813
  const envFile = {
1750
1814
  path: Q402_ENV_FILE_PATH,
1751
- exists: Q402_ENV_FILE_PRESENT
1815
+ exists: Q402_ENV_FILE_PRESENT,
1816
+ warning: envFileReadError ?? void 0
1752
1817
  };
1753
1818
  const envState = {
1754
1819
  Q402_TRIAL_API_KEY: envSlot(
@@ -1814,12 +1879,18 @@ async function runDoctor() {
1814
1879
  }
1815
1880
  const warnings = [];
1816
1881
  for (const name of Q402_ENV_FILE_KEYS_ALL) {
1817
- if (process.env[name] !== void 0 && Q402_ENV_FILE_KEYS_ALL.has(name) && !Q402_ENV_FILE_KEYS.has(name)) {
1882
+ if (process.env[name] !== void 0 && !Q402_ENV_FILE_KEYS.has(name)) {
1818
1883
  warnings.push(
1819
1884
  `${name} is set in both your shell (process.env) AND ~/.q402/mcp.env \u2014 the shell value wins. Editing the file will have NO effect until you \`unset ${name}\` in your shell (or update the shell value to match).`
1820
1885
  );
1821
1886
  }
1822
1887
  }
1888
+ const enableExplicit = process.env.Q402_ENABLE_REAL_PAYMENTS !== void 0 || Q402_ENV_FILE_KEYS_ALL.has("Q402_ENABLE_REAL_PAYMENTS");
1889
+ if (CONFIG.legacyApiKey && CONFIG.realPaymentsRequested && !enableExplicit) {
1890
+ warnings.push(
1891
+ "You have a legacy Q402_API_KEY set, and Q402_ENABLE_REAL_PAYMENTS wasn't explicitly set by you \u2014 so it's defaulting to 1 (real payments) since v0.5.11. To stay in sandbox while you check this, add `Q402_ENABLE_REAL_PAYMENTS=0` to ~/.q402/mcp.env (or your shell) and restart the MCP client."
1892
+ );
1893
+ }
1823
1894
  if (phase !== "live-check") {
1824
1895
  return {
1825
1896
  package: PACKAGE_NAME,
@@ -1833,7 +1904,7 @@ async function runDoctor() {
1833
1904
  recommendedActions,
1834
1905
  greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
1835
1906
  nextStep: phase === "first-install" ? "Show userInstructions verbatim to the user; do NOT show agentInstructions verbatim (it's prescription for you, the AI)." : "Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Show userInstructions for the human-readable steps.",
1836
- agentInstructions: phase === "first-install" ? "Multi-turn flow: (1) Briefly tell the user MCP is installed. (2) Ask one yes/no question \u2014 'Want me to create your Q402 settings file?'. (3) On yes, execute recommendedActions IN ORDER: first `ensure-q402-dir` shell action (bash on macOS/Linux, PowerShell on Windows via shellWindows variant), then `create-env-file` write_file action. (4) Open the file in the user's editor \u2014 `code` works for VS Code / Cursor / Cline (e.g. `code ~/.q402/mcp.env`); `open` on macOS, `start` on Windows, `xdg-open` on Linux as fallback. (5) Walk through filling in the API key (from /event for free Trial or /payment for paid Multichain) and private key one at a time. (6) Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor. BEFORE they paste a private key, surface the `advisories` array: fresh wallet, Smart-account-in-MetaMask heads-up, hardware wallets unsupported, MetaMask key-export path. (7) After they save, tell them to restart the MCP client \u2014 per-client restart verb: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window. (8) Have them re-invoke 'Set up Q402' to confirm. Keep the conversation tight: one decision per turn, plain language, never echo this paragraph." : "User has SOME env set. List the missing items (from `missing`) in plain language. Tell them to edit ~/.q402/mcp.env and uncomment / fill the relevant line, then restart the MCP client. Restart verb per client: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window.",
1907
+ agentInstructions: phase === "first-install" ? "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] Multi-turn flow: (1) Briefly tell the user MCP is installed. (2) Ask one yes/no question \u2014 'Want me to create your Q402 settings file?'. (3) On yes, execute recommendedActions IN ORDER: first `ensure-q402-dir` shell action (bash on macOS/Linux, PowerShell on Windows via shellWindows variant), then `create-env-file` write_file action. (4) Open the file in the user's editor \u2014 `code` works for VS Code / Cursor / Cline (e.g. `code ~/.q402/mcp.env`); `open` on macOS, `start` on Windows, `xdg-open` on Linux as fallback. (5) Walk through filling in the API key (from /event for free Trial or /payment for paid Multichain) and private key one at a time. (6) Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor. BEFORE they paste a private key, surface the `advisories` array: fresh wallet, Smart-account-in-MetaMask heads-up, hardware wallets unsupported, MetaMask key-export path. (7) After they save, tell them to restart the MCP client \u2014 per-client restart verb: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window. (8) Have them re-invoke 'Set up Q402' to confirm. Keep the conversation tight: one decision per turn, plain language, never echo this paragraph." : "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] User has SOME env set. List the missing items (from `missing`) in plain language. Tell them to edit ~/.q402/mcp.env and uncomment / fill the relevant line, then restart the MCP client. Restart verb per client: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window.",
1837
1908
  userInstructions: phase === "first-install" ? [
1838
1909
  "Q402 is installed. To start sending payments you need an API key and a wallet.",
1839
1910
  "I'll create a settings file for you \u2014 say yes and I'll set it up + open it in your editor.",
@@ -1873,11 +1944,12 @@ async function runDoctor() {
1873
1944
  if (verifyTargets.length === 0 && CONFIG.legacyApiKey) {
1874
1945
  verifyTargets.push({ scope: "legacy", envVar: "Q402_API_KEY", key: CONFIG.legacyApiKey });
1875
1946
  }
1876
- const [keys, delegation, relay] = await Promise.all([
1947
+ const [keys, delegation] = await Promise.all([
1877
1948
  Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
1878
- walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0),
1879
- pingRelay()
1949
+ walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0)
1880
1950
  ]);
1951
+ const anyResponse = keys.some((k) => !k.error || !/timeout|unreachable|ENOTFOUND|ECONN/i.test(k.error));
1952
+ const relay = anyResponse && keys.length > 0 ? { url: `${CONFIG.relayBaseUrl}/keys/verify`, reachable: true } : await pingRelay();
1881
1953
  for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
1882
1954
  for (const k of keys) {
1883
1955
  if (typeof k.remainingCredits === "number" && k.remainingCredits === 0) {
@@ -1890,9 +1962,19 @@ async function runDoctor() {
1890
1962
  );
1891
1963
  }
1892
1964
  if (!k.valid) {
1893
- warnings.push(
1894
- k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
1895
- );
1965
+ const isTrialExpired = (k.error ?? "").toLowerCase().includes("trial expired");
1966
+ if (isTrialExpired && k.trialExpiresAt) {
1967
+ const exp = new Date(k.trialExpiresAt);
1968
+ const days = Math.floor((Date.now() - exp.getTime()) / 864e5);
1969
+ const ago = days > 0 ? ` (${days} day${days === 1 ? "" : "s"} ago)` : "";
1970
+ warnings.push(
1971
+ `${k.envVar}: Trial expired on ${exp.toISOString().slice(0, 10)}${ago}. Upgrade to a Multichain plan at https://q402.quackai.ai/payment.`
1972
+ );
1973
+ } else {
1974
+ warnings.push(
1975
+ k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
1976
+ );
1977
+ }
1896
1978
  }
1897
1979
  }
1898
1980
  if (relay && !relay.reachable) {
@@ -1917,7 +1999,7 @@ async function runDoctor() {
1917
1999
  recommendedActions,
1918
2000
  greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
1919
2001
  nextStep: ready ? "Show userInstructions verbatim. Then offer to make a small test quote (q402_quote) to confirm everything works end-to-end." : "Walk the user through each warning in order. Show userInstructions verbatim for the cleanup steps.",
1920
- agentInstructions: ready ? "Live mode is fully configured. Summarize the wallet address (mask middle), plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Offer a tiny test (q402_quote, not q402_pay) to confirm. Don't echo the full keys array verbatim \u2014 pick the most useful 2-3 fields per scope." : "Walk the user through each warning IN ORDER, plain language. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client (Cursor / Cline: reload window; Claude / Codex: quit + relaunch). Surface body.error strings from any verify failure as the user-visible reason (e.g. 'your Trial expired 3 days ago', 'API key has been rotated') \u2014 don't generic-out to 'check the key value'.",
2002
+ agentInstructions: ready ? "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] Live mode is fully configured. Summarize the wallet address (mask middle), plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Offer a tiny test (q402_quote, not q402_pay) to confirm. Don't echo the full keys array verbatim \u2014 pick the most useful 2-3 fields per scope." : "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] Walk the user through each warning IN ORDER, plain language. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client (Cursor / Cline: reload window; Claude / Codex: quit + relaunch). Surface body.error strings from any verify failure as the user-visible reason (e.g. 'your Trial expired 3 days ago', 'API key has been rotated') \u2014 don't generic-out to 'check the key value'.",
1921
2003
  userInstructions: ready ? [
1922
2004
  `Your wallet: ${walletAddress ? walletAddress.slice(0, 6) + "\u2026" + walletAddress.slice(-4) : "(derive failed \u2014 check Q402_PRIVATE_KEY)"}`,
1923
2005
  "Q402 is live. You can now ask me to quote, pay, batch-pay, or check Trust Receipts.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 9 EVM chains, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
5
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [