@quackai/q402-mcp 0.5.6 → 0.5.8

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 (3) hide show
  1. package/README.md +19 -12
  2. package/dist/index.js +103 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,16 +49,26 @@ The agent calls `q402_doctor`. On first install, the tool tells the agent to:
49
49
 
50
50
  ### Manual setup (no AI)
51
51
 
52
- Create `~/.q402/mcp.env` yourself:
52
+ Create `~/.q402/mcp.env` yourself. The template below matches what `q402_doctor` writes — every secret line is commented out and `Q402_ENABLE_REAL_PAYMENTS` defaults to `0`. Uncomment the lines you need, paste real values, then flip the live flag to `1`. (Saving the template as-is is safe: invalid placeholders won't trip live mode.)
53
53
 
54
54
  ```bash
55
55
  # ~/.q402/mcp.env
56
- # Pick ONE of these:
57
- Q402_TRIAL_API_KEY=q402_live_...
56
+
57
+ # Free Trial — BNB only, 2,000 sponsored TX (from /event)
58
+ # Q402_TRIAL_API_KEY=q402_live_...
59
+
60
+ # Paid Multichain — all 9 chains (from /payment)
58
61
  # Q402_MULTICHAIN_API_KEY=q402_live_...
59
62
 
60
- Q402_PRIVATE_KEY=0x...
61
- Q402_ENABLE_REAL_PAYMENTS=1
63
+ # Hex EVM private key (0x + 64 hex). Use a FRESH wallet, NOT your main
64
+ # one — Q402 delegates this EOA via EIP-7702 on first payment.
65
+ # Hardware wallets (Ledger / Trezor) are not supported yet.
66
+ # Q402_PRIVATE_KEY=0x...
67
+
68
+ # Start at 0 (sandbox). Flip to 1 only after real values are pasted above.
69
+ Q402_ENABLE_REAL_PAYMENTS=0
70
+
71
+ # Default Q402 deployment. Only change for self-hosted.
62
72
  Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
63
73
 
64
74
  # Optional safety guards:
@@ -66,7 +76,7 @@ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
66
76
  # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
67
77
  ```
68
78
 
69
- Then `chmod 600 ~/.q402/mcp.env` (Unix) and restart your client. That's the full configuration.
79
+ Then `chmod 600 ~/.q402/mcp.env` (Unix) and restart your client. That's the full configuration. **Heads up on the EIP-7702 side effect:** after your first live payment on a chain, your wallet will show 'Smart account' in MetaMask / OKX — that's the delegation Q402 uses for gasless settlement, reversible anytime via `q402_clear_delegation`.
70
80
 
71
81
  ### Advanced — explicit env injection
72
82
 
@@ -136,7 +146,7 @@ By default the MCP server operates in **sandbox mode**: `q402_pay` returns a ran
136
146
  To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set, and `Q402_ENABLE_REAL_PAYMENTS=1`:
137
147
 
138
148
  ```bash
139
- # Two-key model — set whichever applies. Both is best.
149
+ # Two-key model — set whichever applies (or both for auto-routing).
140
150
  # Auto-routing (same for q402_pay AND q402_batch_pay):
141
151
  # chain="bnb" + Q402_TRIAL_API_KEY set → Trial (free sponsored)
142
152
  # anything else → Multichain (paid 9-chain)
@@ -146,10 +156,6 @@ To enable real on-chain transactions, the resolved API key must be live (`q402_l
146
156
  Q402_TRIAL_API_KEY=q402_live_... # BNB-only sponsored Trial key (from /event)
147
157
  Q402_MULTICHAIN_API_KEY=q402_live_... # paid 9-chain key (per-chain Gas Tank)
148
158
 
149
- # Legacy fallback. Used for both scopes when the two above are unset —
150
- # single-env setups (only Q402_API_KEY set) keep working unchanged.
151
- Q402_API_KEY=q402_live_...
152
-
153
159
  Q402_PRIVATE_KEY=0xabc... # signer for the payer EOA
154
160
  Q402_ENABLE_REAL_PAYMENTS=1 # explicit opt-in
155
161
  ```
@@ -179,13 +185,14 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
179
185
  |---|---|---|
180
186
  | `Q402_TRIAL_API_KEY` | live-pay (BNB) | BNB-only sponsored Trial key. Free at https://q402.quackai.ai/event. Auto-routed for `chain="bnb"` in both `q402_pay` and `q402_batch_pay` (≤5 recipients) when set. 6+ recipient BNB batches return `status="ambiguous"` so the agent can ask the user how to split. |
181
187
  | `Q402_MULTICHAIN_API_KEY` | live-pay (9-chain) | Paid 9-chain key. Get one at https://q402.quackai.ai/payment. Auto-routed for non-BNB chains AND for BNB when no Trial key is set. Cap: 20 recipients per batch. |
182
- | `Q402_API_KEY` | legacy fallback | Single-env legacy path. Used for both scopes when the two above are unset. Keep set if you only have one key. |
183
188
  | `Q402_PRIVATE_KEY` | live-pay | Signer for the payer EOA. **Never share. Never paste in chat.** |
184
189
  | `Q402_ENABLE_REAL_PAYMENTS` | live-pay | Set to `1` to opt in. Any other value (or unset) → sandbox. |
185
190
  | `Q402_MAX_AMOUNT_PER_CALL` | optional | USD-equivalent cap. Defaults to `5`. |
186
191
  | `Q402_ALLOWED_RECIPIENTS` | optional | Comma-separated lowercase addresses. Defaults to no allowlist. |
187
192
  | `Q402_RELAY_BASE_URL` | optional | Defaults to `https://q402.quackai.ai/api`. Override for self-hosted Q402. |
188
193
 
194
+ > Older integrations may still set `Q402_API_KEY` as a single-env fallback — that still works silently for back-compat. New setups should use the two-key model above; `q402_doctor` only guides users to those two.
195
+
189
196
  ---
190
197
 
191
198
  ## Supported chains
package/dist/index.js CHANGED
@@ -46,8 +46,15 @@ function loadQ402EnvFileFromPath(path) {
46
46
  if (eq < 0) continue;
47
47
  const k = t.slice(0, eq).trim();
48
48
  if (!k.startsWith("Q402_")) continue;
49
- const v = t.slice(eq + 1).trim().replace(/^['"](.*)['"]$/, "$1");
50
- out[k] = v;
49
+ let rawVal = t.slice(eq + 1).trim();
50
+ const quoted = /^(['"])(.*)\1\s*(?:#.*)?$/.exec(rawVal);
51
+ if (quoted) {
52
+ rawVal = quoted[2];
53
+ } else {
54
+ const hashIdx = rawVal.search(/\s#/);
55
+ if (hashIdx >= 0) rawVal = rawVal.slice(0, hashIdx).trimEnd();
56
+ }
57
+ out[k] = rawVal;
51
58
  }
52
59
  return out;
53
60
  }
@@ -66,6 +73,9 @@ var Q402_ENV_FILE_KEYS = Object.freeze(
66
73
  Object.keys(FILE_ENV).filter((k) => process.env[k] === void 0)
67
74
  )
68
75
  );
76
+ var Q402_ENV_FILE_KEYS_ALL = Object.freeze(
77
+ new Set(Object.keys(FILE_ENV))
78
+ );
69
79
  var DEFAULT_RELAY_BASE = "https://q402.quackai.ai/api";
70
80
  var DEFAULT_MAX_AMOUNT = 5;
71
81
  function classifyApiKey(k) {
@@ -146,16 +156,19 @@ function resolveApiKey(chain, scope = "auto") {
146
156
  }
147
157
  return { apiKey: key, scope: "multichain", fromLegacyFallback: !CONFIG.multichainApiKey };
148
158
  }
159
+ var PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
149
160
  function isLiveModeFor(resolved) {
150
161
  if (!resolved.apiKey) return false;
151
162
  if (!CONFIG.realPaymentsRequested) return false;
152
163
  if (!CONFIG.privateKey) return false;
164
+ if (!PRIVATE_KEY_RE.test(CONFIG.privateKey)) return false;
153
165
  return resolved.apiKey.startsWith("q402_live_");
154
166
  }
167
+ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
155
168
 
156
169
  // src/version.ts
157
170
  var PACKAGE_NAME = "@quackai/q402-mcp";
158
- var PACKAGE_VERSION = "0.5.6";
171
+ var PACKAGE_VERSION = "0.5.8";
159
172
 
160
173
  // src/tools/quote.ts
161
174
  import { z } from "zod";
@@ -737,7 +750,11 @@ function sandboxPay(chain, input) {
737
750
  const tokenAmount = toRawAmount(input.amount, tokenCfg.decimals);
738
751
  const fakeHash = "0x" + hexlify(randomBytes(32)).slice(2);
739
752
  return {
740
- success: true,
753
+ // `success: false` because no funds moved. The `sandbox: true` flag is
754
+ // the canonical "this was a simulation" marker — downstream callers
755
+ // should branch on EITHER field to avoid misreporting a settlement.
756
+ success: false,
757
+ sandbox: true,
741
758
  txHash: fakeHash,
742
759
  tokenAmount,
743
760
  token: input.token,
@@ -823,7 +840,11 @@ async function runPay(input) {
823
840
  token: input.token
824
841
  });
825
842
  guardsApplied.push("mode=live");
826
- return { result, guardsApplied };
843
+ return {
844
+ result,
845
+ guardsApplied,
846
+ postPaymentTip: result.success ? `After this payment your EOA is EIP-7702-delegated to Q402's impl on ${chain.name} \u2014 MetaMask / OKX will show it as a 'Smart account'. That's normal and reversible: q402_clear_delegation removes the delegation on a specific chain (Q402 sponsors the gas, so you pay $0). If you ever try to receive native gas tokens directly to this EOA and the transfer reverts, the delegation is the cause \u2014 clear it for that chain first.` : void 0
847
+ };
827
848
  }
828
849
  function describeSandboxReason(resolvedKey, scope) {
829
850
  const missing = [];
@@ -837,7 +858,7 @@ function describeSandboxReason(resolvedKey, scope) {
837
858
  }
838
859
  var PAY_TOOL = {
839
860
  name: "q402_pay",
840
- 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, and Q402_ENABLE_REAL_PAYMENTS=1. The recipient receives the full amount; the sender pays $0 in gas. After the first payment on a chain, follow-up payments on the same chain are faster and cheaper (Q402 reuses the wallet's setup); q402_clear_delegation resets it if the user ever asks. ALWAYS get explicit user confirmation of the exact recipient address, amount, chain, and token in conversation immediately before calling this tool.",
861
+ 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\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.",
841
862
  inputSchema: {
842
863
  type: "object",
843
864
  properties: {
@@ -1116,7 +1137,7 @@ async function runBalance() {
1116
1137
  apiKeyMasked: null,
1117
1138
  scopes: [],
1118
1139
  dashboardUrl: "https://q402.quackai.ai/dashboard",
1119
- setupHint: "No API key configured. Call q402_doctor for guided setup \u2014 it will offer to create ~/.q402/mcp.env with placeholders that the user can fill in. (Manual path: set Q402_TRIAL_API_KEY for BNB-only sponsored (free at https://q402.quackai.ai/event) and/or Q402_MULTICHAIN_API_KEY for paid 9-chain (https://q402.quackai.ai/payment). Q402_API_KEY is the legacy single-env fallback.)"
1140
+ setupHint: "No API key configured. Call q402_doctor for guided setup \u2014 it will offer to create ~/.q402/mcp.env with placeholders that the user can fill in. (Manual path: set Q402_TRIAL_API_KEY for BNB-only sponsored free trial (https://q402.quackai.ai/event) or Q402_MULTICHAIN_API_KEY for the paid 9-chain plan (https://q402.quackai.ai/payment).)"
1120
1141
  };
1121
1142
  }
1122
1143
  const scopes = await Promise.all(
@@ -1524,24 +1545,25 @@ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
1524
1545
  # After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
1525
1546
  # \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
1526
1547
 
1527
- # \u2500\u2500\u2500 API key \u2014 pick ONE (uncomment the one you have) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1548
+ # \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
1528
1549
  # Free Trial: BNB Chain only, 2,000 sponsored TX
1529
1550
  # Get one at: https://q402.quackai.ai/event
1530
- Q402_TRIAL_API_KEY=q402_live_...
1551
+ # Q402_TRIAL_API_KEY=q402_live_...
1531
1552
 
1532
1553
  # Paid Multichain: all 9 chains, per-chain Gas Tank
1533
1554
  # Get one at: https://q402.quackai.ai/payment
1534
1555
  # Q402_MULTICHAIN_API_KEY=q402_live_...
1535
1556
 
1536
1557
  # \u2500\u2500\u2500 Your wallet \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
1537
- # Hex EVM private key. Signs payments LOCALLY on your machine.
1538
- # Never leaves your device, never sent to any server.
1539
- Q402_PRIVATE_KEY=0x...
1558
+ # Hex EVM private key (0x + 64 hex chars). Signs payments LOCALLY on
1559
+ # your machine \u2014 never leaves your device, never sent to any server.
1560
+ # Q402_PRIVATE_KEY=0x...
1540
1561
 
1541
1562
  # \u2500\u2500\u2500 Live mode flag \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
1542
- # Must be exactly "1" to allow real on-chain transactions.
1543
- # Anything else = test response (fake hash, no funds move).
1544
- Q402_ENABLE_REAL_PAYMENTS=1
1563
+ # Default 0 = sandbox (test responses, no funds move). Flip to 1 only
1564
+ # AFTER you've pasted real values into the lines above \u2014 otherwise the
1565
+ # server will refuse the placeholders.
1566
+ Q402_ENABLE_REAL_PAYMENTS=0
1545
1567
 
1546
1568
  # \u2500\u2500\u2500 Q402 relay endpoint \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
1547
1569
  # Default canonical Q402 deployment. Only change for self-hosted.
@@ -1555,6 +1577,12 @@ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
1555
1577
  # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
1556
1578
  `;
1557
1579
  var SECURITY_NOTICE = "Q402 never asks you to paste your private key into chat. The MCP server signs payments LOCALLY on your machine \u2014 your key never leaves your device, never goes to a remote server. If a key was already pasted in chat by mistake, treat the wallet as exposed: move funds to a fresh wallet and use that new key in ~/.q402/mcp.env going forward.";
1580
+ var FIRST_INSTALL_ADVISORY = [
1581
+ "Use a FRESH wallet for Q402 \u2014 don't reuse the one with your main funds.",
1582
+ "After your first payment, that wallet will show 'Smart account' in MetaMask / OKX. That's EIP-7702 delegation (Q402's gasless settlement mechanism), reversible anytime with q402_clear_delegation.",
1583
+ "Hardware wallets (Ledger / Trezor) are NOT supported yet \u2014 they don't sign EIP-7702 type-4 authorizations.",
1584
+ "To get a hex private key from MetaMask: \xB7\xB7\xB7 (3-dot menu) \u2192 Account details \u2192 Show private key \u2192 enter password."
1585
+ ];
1558
1586
  function envSource(name) {
1559
1587
  if (process.env[name] !== void 0) return "process";
1560
1588
  if (Q402_ENV_FILE_KEYS.has(name)) return "file";
@@ -1582,8 +1610,18 @@ async function verifyOneKey(scope, envVar, apiKey) {
1582
1610
  const resp = await fetch(url, {
1583
1611
  method: "POST",
1584
1612
  headers: { "Content-Type": "application/json" },
1585
- body: JSON.stringify({ apiKey })
1613
+ body: JSON.stringify({ apiKey }),
1614
+ signal: AbortSignal.timeout(1e4)
1586
1615
  });
1616
+ if (resp.status === 429) {
1617
+ return {
1618
+ scope,
1619
+ envVar,
1620
+ apiKeyMasked: mask2(apiKey),
1621
+ valid: false,
1622
+ error: "rate limited by relay \u2014 wait 60s and re-run q402_doctor"
1623
+ };
1624
+ }
1587
1625
  if (!resp.ok) {
1588
1626
  return { scope, envVar, apiKeyMasked: mask2(apiKey), valid: false, error: `HTTP ${resp.status}` };
1589
1627
  }
@@ -1593,6 +1631,10 @@ async function verifyOneKey(scope, envVar, apiKey) {
1593
1631
  envVar,
1594
1632
  apiKeyMasked: mask2(apiKey),
1595
1633
  valid: body.valid ?? false,
1634
+ // Propagate the relay's specific reason ("API key has been rotated",
1635
+ // "Subscription expired", "Trial expired") so the user gets the
1636
+ // exact failure mode instead of a generic "verified as invalid".
1637
+ error: body.valid === false ? body.error : void 0,
1596
1638
  plan: body.plan,
1597
1639
  remainingCredits: body.remainingCredits,
1598
1640
  isTrial: body.isTrial,
@@ -1616,11 +1658,16 @@ async function verifyOneKey(scope, envVar, apiKey) {
1616
1658
  }
1617
1659
  }
1618
1660
  async function pingRelay() {
1619
- const url = `${CONFIG.relayBaseUrl}/health`;
1661
+ const url = `${CONFIG.relayBaseUrl}/keys/verify`;
1620
1662
  const t0 = Date.now();
1621
1663
  try {
1622
- const resp = await fetch(url, { method: "GET" });
1623
- const reachable = resp.status < 500;
1664
+ const resp = await fetch(url, {
1665
+ method: "POST",
1666
+ headers: { "Content-Type": "application/json" },
1667
+ body: "{}",
1668
+ signal: AbortSignal.timeout(1e4)
1669
+ });
1670
+ const reachable = resp.status >= 200 && resp.status < 500;
1624
1671
  return { url, reachable, latencyMs: Date.now() - t0 };
1625
1672
  } catch (e) {
1626
1673
  return {
@@ -1634,7 +1681,7 @@ async function pingRelay() {
1634
1681
  async function fetchDelegation(address) {
1635
1682
  const url = `${CONFIG.relayBaseUrl}/wallet/delegation-status?address=${address}`;
1636
1683
  try {
1637
- const resp = await fetch(url);
1684
+ const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
1638
1685
  if (!resp.ok) {
1639
1686
  return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error: `HTTP ${resp.status}` }));
1640
1687
  }
@@ -1673,25 +1720,34 @@ async function runDoctor() {
1673
1720
  "Must be '1' to allow real TX. Anything else = test response (fake hash)."
1674
1721
  )
1675
1722
  };
1676
- let legacyDetected;
1677
- if (CONFIG.legacyApiKey) {
1678
- legacyDetected = "Q402_API_KEY is set \u2014 works as a fallback for both scopes, but the newer two-key model (Q402_TRIAL_API_KEY + Q402_MULTICHAIN_API_KEY) gives you auto-routing between free Trial (BNB) and paid Multichain. Rename in ~/.q402/mcp.env when convenient.";
1679
- }
1680
1723
  const missing = [];
1681
1724
  if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
1682
1725
  missing.push(
1683
1726
  "An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
1684
1727
  );
1685
1728
  }
1686
- if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
1729
+ if (!CONFIG.privateKey) {
1730
+ missing.push("Q402_PRIVATE_KEY");
1731
+ } else if (!isValidPrivateKey(CONFIG.privateKey)) {
1732
+ missing.push(
1733
+ "Q402_PRIVATE_KEY is set but malformed (expected 0x + 64 hex chars). Looks like the placeholder '0x...' is still in ~/.q402/mcp.env \u2014 paste a real key in your editor."
1734
+ );
1735
+ }
1687
1736
  if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1688
1737
  const recommendedActions = [];
1689
1738
  if (!envFile.exists) {
1739
+ recommendedActions.push({
1740
+ id: "ensure-q402-dir",
1741
+ type: "shell",
1742
+ shell: 'mkdir -p "$HOME/.q402"',
1743
+ shellWindows: 'powershell -Command "New-Item -ItemType Directory -Force -Path $env:USERPROFILE\\.q402 | Out-Null"',
1744
+ requiresUserConfirm: false,
1745
+ description: "Ensure the ~/.q402 directory exists before writing the secrets file."
1746
+ });
1690
1747
  recommendedActions.push({
1691
1748
  id: "create-env-file",
1692
1749
  type: "write_file",
1693
1750
  path: Q402_ENV_FILE_PATH,
1694
- createParentDirs: true,
1695
1751
  content: ENV_FILE_TEMPLATE,
1696
1752
  requiresUserConfirm: true,
1697
1753
  description: "Create ~/.q402/mcp.env with placeholder values, then open it in the user's editor.",
@@ -1699,6 +1755,13 @@ async function runDoctor() {
1699
1755
  });
1700
1756
  }
1701
1757
  const warnings = [];
1758
+ for (const name of Q402_ENV_FILE_KEYS_ALL) {
1759
+ if (process.env[name] !== void 0 && Q402_ENV_FILE_KEYS_ALL.has(name) && !Q402_ENV_FILE_KEYS.has(name)) {
1760
+ warnings.push(
1761
+ `${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).`
1762
+ );
1763
+ }
1764
+ }
1702
1765
  if (phase !== "live-check") {
1703
1766
  return {
1704
1767
  package: PACKAGE_NAME,
@@ -1708,19 +1771,23 @@ async function runDoctor() {
1708
1771
  envFile,
1709
1772
  envState,
1710
1773
  missing,
1711
- legacyDetected,
1712
1774
  warnings,
1713
1775
  recommendedActions,
1714
1776
  greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
1715
- nextStep: phase === "first-install" ? "Offer to create ~/.q402/mcp.env. After yes, run the recommendedActions[].write_file action, then open the file in the user's editor (e.g. via `code` / `open` / `start` / `xdg-open`). Then walk through filling in the API key and private key, one at a time. Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor." : `Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Restart needed after editing.`,
1716
- securityNotice: SECURITY_NOTICE
1777
+ nextStep: phase === "first-install" ? "Offer to create ~/.q402/mcp.env. After yes, execute recommendedActions in order: first the `ensure-q402-dir` shell action (use bash on macOS/Linux, PowerShell on Windows via the shellWindows variant), then the `create-env-file` write_file action. Then 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. Walk through filling in the API key (from /event for free Trial or /payment for paid Multichain) and private key one at a time. 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: use a fresh wallet (not their main one), heads-up that the wallet will show 'Smart account' in MetaMask after the first payment (that's normal \u2014 EIP-7702 delegation), hardware wallets aren't supported, MetaMask key export path." : `Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Restart needed after editing. 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.`,
1778
+ securityNotice: SECURITY_NOTICE,
1779
+ advisories: phase === "first-install" ? FIRST_INSTALL_ADVISORY : void 0
1717
1780
  };
1718
1781
  }
1719
1782
  let walletAddress;
1783
+ let walletError;
1720
1784
  try {
1721
1785
  walletAddress = new Wallet4(CONFIG.privateKey).address;
1722
- } catch {
1723
- warnings.push("Q402_PRIVATE_KEY is set but does not parse as a 32-byte hex key. Live calls will fail.");
1786
+ } catch (e) {
1787
+ walletError = e instanceof Error ? e.message : String(e);
1788
+ warnings.push(
1789
+ `Q402_PRIVATE_KEY is set but does not parse as a 32-byte hex key: ${walletError}. Open ~/.q402/mcp.env in your editor and paste a real key (0x + 64 hex chars). Live calls will fail until this is fixed.`
1790
+ );
1724
1791
  }
1725
1792
  const verifyTargets = [];
1726
1793
  if (CONFIG.trialApiKey) verifyTargets.push({ scope: "trial", envVar: "Q402_TRIAL_API_KEY", key: CONFIG.trialApiKey });
@@ -1730,7 +1797,7 @@ async function runDoctor() {
1730
1797
  }
1731
1798
  const [keys, delegation, relay] = await Promise.all([
1732
1799
  Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
1733
- walletAddress ? fetchDelegation(walletAddress) : Promise.resolve([]),
1800
+ walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0),
1734
1801
  pingRelay()
1735
1802
  ]);
1736
1803
  for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
@@ -1744,8 +1811,10 @@ async function runDoctor() {
1744
1811
  `${k.envVar} has only ${k.remainingCredits} credits left \u2014 top up before you run out.`
1745
1812
  );
1746
1813
  }
1747
- if (!k.valid && !k.error) {
1748
- warnings.push(`${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`);
1814
+ if (!k.valid) {
1815
+ warnings.push(
1816
+ k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
1817
+ );
1749
1818
  }
1750
1819
  }
1751
1820
  if (relay && !relay.reachable) {
@@ -1762,7 +1831,6 @@ async function runDoctor() {
1762
1831
  envFile,
1763
1832
  envState,
1764
1833
  missing,
1765
- legacyDetected,
1766
1834
  wallet: walletAddress ? { address: walletAddress } : void 0,
1767
1835
  keys,
1768
1836
  delegation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
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": [