@quackai/q402-mcp 0.5.10 → 0.5.12

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 +27 -10
  2. package/dist/index.js +129 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,7 @@ 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. 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.)
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 `1`. Uncomment the lines you need and paste real values; the server only flips into live mode once both a `q402_live_*` API key AND a valid 32-byte private key are configured, so saving the template as-is is safe (placeholders stay in sandbox). Change the flag to `0` if you want to force sandbox even with real keys (e.g. for chained testing).
53
53
 
54
54
  ```bash
55
55
  # ~/.q402/mcp.env
@@ -65,8 +65,12 @@ Create `~/.q402/mcp.env` yourself. The template below matches what `q402_doctor`
65
65
  # Hardware wallets (Ledger / Trezor) are not supported yet.
66
66
  # Q402_PRIVATE_KEY=0x...
67
67
 
68
- # Start at 0 (sandbox). Flip to 1 only after real values are pasted above.
69
- Q402_ENABLE_REAL_PAYMENTS=0
68
+ # Live mode switch:
69
+ # 0 = sandbox (test mode, no funds move)
70
+ # 1 = real on-chain payments
71
+ # Default 1 — safe because mode only flips to live when BOTH a live
72
+ # API key AND a valid 32-byte private key are uncommented above.
73
+ Q402_ENABLE_REAL_PAYMENTS=1
70
74
 
71
75
  # Default Q402 deployment. Only change for self-hosted.
72
76
  Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
@@ -132,9 +136,11 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
132
136
 
133
137
  `q402_receipt` is the natural follow-up: after `q402_pay` returns a `receiptUrl`, hand the agent the `rct_…` id and ask *"verify this receipt"* — the tool re-runs the same canonical-JSON + EIP-191 recovery the receipt page does in the browser, so the verification doesn't depend on trusting any UI. Example prompts that work today:
134
138
 
135
- > *"Pay 0.10 USDT on BNB to vitalik.eth, then verify the receipt."*
139
+ > *"Pay 0.10 USDT on BNB to 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, then verify the receipt."*
136
140
  > *"Is `rct_afa5f50bc49a65ebba3b28ab` a real Q402 receipt? Verify the signature."*
137
141
 
142
+ > ℹ️ `q402_pay` takes a 0x-prefixed EVM address — ENS names are not resolved by the tool. If your prompt mentions a name like `vitalik.eth`, your AI client needs to resolve it client-side before invoking the tool.
143
+
138
144
  > Per-chain gas tank balances and full transaction history live in the [dashboard](https://q402.quackai.ai/dashboard) — those endpoints require a wallet signature, not a bare API key, so the MCP server points the agent there instead of exposing them.
139
145
 
140
146
  ---
@@ -143,7 +149,7 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
143
149
 
144
150
  By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash with `success: false` and `sandbox: true`, no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an accidental payment — if the agent misreads the conversation and fires `q402_pay` before you intended, nothing moves AND the response cannot be mistaken for a confirmed settlement.
145
151
 
146
- To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set to a valid 32-byte hex key, and `Q402_ENABLE_REAL_PAYMENTS=1`. The block below is the template `q402_doctor` writes to `~/.q402/mcp.env` — every secret line is commented out and the live flag defaults to `0`. Uncomment the lines you need, paste real values in your editor, then flip the flag to `1`:
152
+ To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set to a valid 32-byte hex key, and `Q402_ENABLE_REAL_PAYMENTS=1`. The block below is the template `q402_doctor` writes to `~/.q402/mcp.env` — every secret line is commented out and the live flag defaults to `1`. Uncomment the lines you need and paste real values in your editor; the live-mode gate only flips once a real key + valid PK are present, so saving the template as-is stays in sandbox. Change the flag to `0` if you want to force sandbox even with real keys (e.g. for chained testing on a paid plan):
147
153
 
148
154
  ```bash
149
155
  # Two-key model — uncomment ONE (or both for auto-routing).
@@ -159,10 +165,16 @@ To enable real on-chain transactions, the resolved API key must be live (`q402_l
159
165
 
160
166
  # Q402_PRIVATE_KEY=0x... # signer for the payer EOA (32-byte hex)
161
167
 
162
- # Start at 0 (sandbox). Flip to 1 only after real values are pasted above —
163
- # a malformed Q402_PRIVATE_KEY (e.g. the "0x..." placeholder) is rejected at
164
- # the live-mode gate, so partial setups stay in sandbox with a clear hint.
165
- Q402_ENABLE_REAL_PAYMENTS=0
168
+ # Live mode switch:
169
+ # 0 = sandbox (test mode, no funds move every q402_pay returns a fake hash)
170
+ # 1 = real on-chain payments (live mode)
171
+ # Default is 1: real payments enabled. Safe because mode only flips
172
+ # to live when BOTH a live API key (q402_live_*) AND a valid 32-byte
173
+ # private key are set above. Placeholders ("0x...") are rejected by
174
+ # the live-mode gate, so partial setups stay in sandbox with a hint.
175
+ # Change to 0 to force sandbox even with real keys (e.g. for chained
176
+ # testing on a paid plan).
177
+ Q402_ENABLE_REAL_PAYMENTS=1
166
178
  ```
167
179
 
168
180
  Anything missing for the resolved scope → automatic sandbox fallback with a hint pointing at what to set.
@@ -194,7 +206,12 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
194
206
  | `Q402_ALLOWED_RECIPIENTS` | optional | Comma-separated lowercase addresses. Defaults to no allowlist. |
195
207
  | `Q402_RELAY_BASE_URL` | optional | Defaults to `https://q402.quackai.ai/api`. Override for self-hosted Q402. |
196
208
 
197
- > 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.
209
+ <details>
210
+ <summary>Migrating from legacy single-key setups</summary>
211
+
212
+ If you set up Q402 before v0.5.0 you may have a single `Q402_API_KEY` env var. The server still resolves that silently — your existing integration won't break. New installs should use the two-key model above (`Q402_TRIAL_API_KEY` and/or `Q402_MULTICHAIN_API_KEY`); `q402_doctor` and the rest of the docs only guide users to those two. To migrate, rename your existing var to `Q402_MULTICHAIN_API_KEY` in `~/.q402/mcp.env` and restart your MCP client.
213
+
214
+ </details>
198
215
 
199
216
  ---
200
217
 
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ function loadQ402EnvFileFromPath(path) {
39
39
  );
40
40
  return {};
41
41
  }
42
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
42
43
  for (const line of raw.split(/\r?\n/)) {
43
44
  const t = line.trim();
44
45
  if (!t || t.startsWith("#")) continue;
@@ -168,7 +169,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
168
169
 
169
170
  // src/version.ts
170
171
  var PACKAGE_NAME = "@quackai/q402-mcp";
171
- var PACKAGE_VERSION = "0.5.10";
172
+ var PACKAGE_VERSION = "0.5.12";
172
173
 
173
174
  // src/tools/quote.ts
174
175
  import { z } from "zod";
@@ -421,7 +422,7 @@ var QUOTE_TOOL = {
421
422
  };
422
423
 
423
424
  // src/tools/pay.ts
424
- import { isAddress as isAddress2 } from "ethers";
425
+ import { isAddress as isAddress2, Wallet as Wallet2 } from "ethers";
425
426
  import { z as z2 } from "zod";
426
427
 
427
428
  // src/client.ts
@@ -504,7 +505,7 @@ var Q402NodeClient = class _Q402NodeClient {
504
505
  }
505
506
  async fetchFacilitator() {
506
507
  const url = `${this.opts.relayBaseUrl.replace(/\/$/, "")}/relay/info`;
507
- const resp = await fetch(url);
508
+ const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
508
509
  if (!resp.ok) {
509
510
  throw new Error(
510
511
  `failed to fetch relay facilitator info from ${url} (${resp.status})`
@@ -578,7 +579,8 @@ var Q402NodeClient = class _Q402NodeClient {
578
579
  const resp = await fetch(`${relayBaseUrl.replace(/\/$/, "")}/relay`, {
579
580
  method: "POST",
580
581
  headers: { "Content-Type": "application/json" },
581
- body: JSON.stringify(body)
582
+ body: JSON.stringify(body),
583
+ signal: AbortSignal.timeout(3e4)
582
584
  });
583
585
  const data = await resp.json();
584
586
  if (!resp.ok) {
@@ -699,7 +701,8 @@ var Q402NodeClient = class _Q402NodeClient {
699
701
  token,
700
702
  facilitator,
701
703
  recipients: signedRows
702
- })
704
+ }),
705
+ signal: AbortSignal.timeout(6e4)
703
706
  });
704
707
  const data = await resp.json();
705
708
  if (!resp.ok || data.ok === false) {
@@ -808,6 +811,17 @@ async function runPay(input) {
808
811
  );
809
812
  }
810
813
  const guardsApplied = [];
814
+ let senderWallet;
815
+ if (CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
816
+ try {
817
+ const addr = new Wallet2(CONFIG.privateKey).address;
818
+ senderWallet = {
819
+ address: addr,
820
+ addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`
821
+ };
822
+ } catch {
823
+ }
824
+ }
811
825
  maxAmountGuard(input.amount, CONFIG.maxAmountPerCallUsd);
812
826
  guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd}`);
813
827
  recipientGuard(input.to, CONFIG.allowedRecipients);
@@ -826,7 +840,7 @@ async function runPay(input) {
826
840
  });
827
841
  guardsApplied.push("mode=sandbox");
828
842
  const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "", resolved.scope);
829
- return { result: result2, guardsApplied, setupHint };
843
+ return { result: result2, guardsApplied, setupHint, senderWallet };
830
844
  }
831
845
  const client = new Q402NodeClient({
832
846
  apiKey: resolved.apiKey,
@@ -843,13 +857,20 @@ async function runPay(input) {
843
857
  return {
844
858
  result,
845
859
  guardsApplied,
860
+ senderWallet,
846
861
  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
862
  };
848
863
  }
849
864
  function describeSandboxReason(resolvedKey, scope) {
850
865
  const missing = [];
851
866
  if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
852
- if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
867
+ if (!CONFIG.privateKey) {
868
+ missing.push("Q402_PRIVATE_KEY");
869
+ } else if (!isValidPrivateKey(CONFIG.privateKey)) {
870
+ missing.push(
871
+ "Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
872
+ );
873
+ }
853
874
  if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
854
875
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
855
876
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
@@ -858,7 +879,7 @@ function describeSandboxReason(resolvedKey, scope) {
858
879
  }
859
880
  var PAY_TOOL = {
860
881
  name: "q402_pay",
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.",
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.",
862
883
  inputSchema: {
863
884
  type: "object",
864
885
  properties: {
@@ -897,7 +918,7 @@ var PAY_TOOL = {
897
918
  };
898
919
 
899
920
  // src/tools/batch-pay.ts
900
- import { isAddress as isAddress3 } from "ethers";
921
+ import { isAddress as isAddress3, Wallet as Wallet3 } from "ethers";
901
922
  import { z as z3 } from "zod";
902
923
  var RECIPIENT_LIMIT_TRIAL = 5;
903
924
  var RECIPIENT_LIMIT_PAID = 20;
@@ -962,6 +983,17 @@ async function runBatchPay(input) {
962
983
  if (CONFIG.allowedRecipients.length > 0) {
963
984
  guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
964
985
  }
986
+ let senderWallet;
987
+ if (CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
988
+ try {
989
+ const addr = new Wallet3(CONFIG.privateKey).address;
990
+ senderWallet = {
991
+ address: addr,
992
+ addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`
993
+ };
994
+ } catch {
995
+ }
996
+ }
965
997
  const scopeRequest = input.keyScope ?? "auto";
966
998
  if (scopeRequest === "auto" && input.chain === "bnb" && CONFIG.trialApiKey && input.recipients.length > RECIPIENT_LIMIT_TRIAL) {
967
999
  const overflow = input.recipients.length - RECIPIENT_LIMIT_TRIAL;
@@ -970,6 +1002,7 @@ async function runBatchPay(input) {
970
1002
  mode: "none",
971
1003
  status: "ambiguous",
972
1004
  guardsApplied,
1005
+ senderWallet,
973
1006
  setupHint: `Batch of ${input.recipients.length} on BNB exceeds the Trial cap of ${RECIPIENT_LIMIT_TRIAL}. Ask the user to pick one and re-invoke q402_batch_pay with explicit keyScope:
974
1007
  \u2022 keyScope="trial" \u2014 keep only the first ${RECIPIENT_LIMIT_TRIAL} recipients (free, sponsored). Drop the remaining ${overflow}.
975
1008
  \u2022 keyScope="multichain" \u2014 send all ${input.recipients.length} on the paid Multichain key (charges the paid pool + Gas Tank).
@@ -989,6 +1022,7 @@ async function runBatchPay(input) {
989
1022
  mode: "sandbox",
990
1023
  status: "sandbox",
991
1024
  result: { sandbox: sandboxResults, reason },
1025
+ senderWallet,
992
1026
  guardsApplied,
993
1027
  setupHint: reason
994
1028
  };
@@ -1007,7 +1041,7 @@ async function runBatchPay(input) {
1007
1041
  guardsApplied.push("mode=live");
1008
1042
  guardsApplied.push(`scope=${result.scope} (server enforced)`);
1009
1043
  guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
1010
- return { mode: "live", status: "success", result, guardsApplied };
1044
+ return { mode: "live", status: "success", result, guardsApplied, senderWallet };
1011
1045
  } catch (err) {
1012
1046
  if (err instanceof BatchPayError) {
1013
1047
  guardsApplied.push("mode=live");
@@ -1027,6 +1061,7 @@ async function runBatchPay(input) {
1027
1061
  results: err.results
1028
1062
  },
1029
1063
  guardsApplied,
1064
+ senderWallet,
1030
1065
  error: err.message
1031
1066
  };
1032
1067
  }
@@ -1036,7 +1071,13 @@ async function runBatchPay(input) {
1036
1071
  function describeSandboxReason2(resolvedKey, scope) {
1037
1072
  const missing = [];
1038
1073
  if (!resolvedKey.startsWith("q402_live_")) missing.push("a live API key (must start with q402_live_)");
1039
- if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
1074
+ if (!CONFIG.privateKey) {
1075
+ missing.push("Q402_PRIVATE_KEY");
1076
+ } else if (!isValidPrivateKey(CONFIG.privateKey)) {
1077
+ missing.push(
1078
+ "Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
1079
+ );
1080
+ }
1040
1081
  if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1041
1082
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
1042
1083
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
@@ -1129,7 +1170,7 @@ async function runBalance() {
1129
1170
  if (CONFIG.trialApiKey) targets.push({ scope: "trial", key: CONFIG.trialApiKey });
1130
1171
  if (CONFIG.multichainApiKey) targets.push({ scope: "multichain", key: CONFIG.multichainApiKey });
1131
1172
  if (targets.length === 0 && CONFIG.legacyApiKey) {
1132
- targets.push({ scope: "legacy", key: CONFIG.legacyApiKey });
1173
+ targets.push({ scope: "multichain", key: CONFIG.legacyApiKey });
1133
1174
  }
1134
1175
  if (targets.length === 0) {
1135
1176
  return {
@@ -1283,7 +1324,9 @@ async function runReceipt(input) {
1283
1324
  notFound: true
1284
1325
  };
1285
1326
  }
1286
- const resp = await fetch(`${apiBase}/receipt/${receiptId}`);
1327
+ const resp = await fetch(`${apiBase}/receipt/${receiptId}`, {
1328
+ signal: AbortSignal.timeout(1e4)
1329
+ });
1287
1330
  if (resp.status === 404) {
1288
1331
  return {
1289
1332
  receiptId,
@@ -1336,7 +1379,7 @@ var RECEIPT_TOOL = {
1336
1379
 
1337
1380
  // src/tools/wallet-status.ts
1338
1381
  import { z as z6 } from "zod";
1339
- import { Wallet as Wallet2 } from "ethers";
1382
+ import { Wallet as Wallet4 } from "ethers";
1340
1383
  var WalletStatusInputSchema = z6.object({});
1341
1384
  async function runWalletStatus() {
1342
1385
  if (!CONFIG.privateKey) {
@@ -1347,7 +1390,7 @@ async function runWalletStatus() {
1347
1390
  }
1348
1391
  let address;
1349
1392
  try {
1350
- address = new Wallet2(CONFIG.privateKey).address;
1393
+ address = new Wallet4(CONFIG.privateKey).address;
1351
1394
  } catch {
1352
1395
  return {
1353
1396
  error: "INVALID_PRIVATE_KEY",
@@ -1358,7 +1401,7 @@ async function runWalletStatus() {
1358
1401
  let body;
1359
1402
  let res;
1360
1403
  try {
1361
- res = await fetch(url);
1404
+ res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
1362
1405
  body = await res.json();
1363
1406
  } catch (e) {
1364
1407
  return {
@@ -1395,7 +1438,7 @@ var WALLET_STATUS_TOOL = {
1395
1438
 
1396
1439
  // src/tools/clear-delegation.ts
1397
1440
  import { z as z7 } from "zod";
1398
- import { Wallet as Wallet3, JsonRpcProvider as JsonRpcProvider2 } from "ethers";
1441
+ import { Wallet as Wallet5, JsonRpcProvider as JsonRpcProvider2 } from "ethers";
1399
1442
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1400
1443
  var DEFAULT_RPC2 = {
1401
1444
  1: "https://ethereum.publicnode.com",
@@ -1426,7 +1469,7 @@ async function runClearDelegation(input) {
1426
1469
  let wallet;
1427
1470
  try {
1428
1471
  const provider = new JsonRpcProvider2(DEFAULT_RPC2[cfg.chainId]);
1429
- wallet = new Wallet3(CONFIG.privateKey, provider);
1472
+ wallet = new Wallet5(CONFIG.privateKey, provider);
1430
1473
  } catch {
1431
1474
  return {
1432
1475
  ok: false,
@@ -1480,7 +1523,8 @@ async function runClearDelegation(input) {
1480
1523
  res = await fetch(url, {
1481
1524
  method: "POST",
1482
1525
  headers: { "Content-Type": "application/json" },
1483
- body: JSON.stringify(body)
1526
+ body: JSON.stringify(body),
1527
+ signal: AbortSignal.timeout(3e4)
1484
1528
  });
1485
1529
  json = await res.json();
1486
1530
  } catch (e) {
@@ -1536,7 +1580,7 @@ var CLEAR_DELEGATION_TOOL = {
1536
1580
 
1537
1581
  // src/tools/doctor.ts
1538
1582
  import { z as z8 } from "zod";
1539
- import { Wallet as Wallet4 } from "ethers";
1583
+ import { Wallet as Wallet6 } from "ethers";
1540
1584
  var DoctorInputSchema = z8.object({});
1541
1585
  var ENV_FILE_TEMPLATE = `# \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
1542
1586
  # Q402 MCP \u2014 secrets
@@ -1559,11 +1603,15 @@ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
1559
1603
  # your machine \u2014 never leaves your device, never sent to any server.
1560
1604
  # Q402_PRIVATE_KEY=0x...
1561
1605
 
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
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
1606
+ # \u2500\u2500\u2500 Live mode switch \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
1607
+ # 0 = sandbox (test mode, no funds move \u2014 every q402_pay returns a fake hash)
1608
+ # 1 = real on-chain payments (live mode)
1609
+ # Default is 1: real payments enabled. This is safe because mode only
1610
+ # flips to live when BOTH a live API key (q402_live_*) AND a valid
1611
+ # 32-byte private key are set above. Until you uncomment + paste both,
1612
+ # you stay in sandbox. Change to 0 to force sandbox even with real
1613
+ # keys (e.g. for chained testing on a paid plan).
1614
+ Q402_ENABLE_REAL_PAYMENTS=1
1567
1615
 
1568
1616
  # \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
1569
1617
  # Default canonical Q402 deployment. Only change for self-hosted.
@@ -1578,10 +1626,10 @@ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
1578
1626
  `;
1579
1627
  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
1628
  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."
1629
+ `Tip: a separate MetaMask account dedicated to Q402 keeps your existing balances and history tidy \u2014 it's a quick "+ Add account" in MetaMask. Q402 works with any EOA you control, though.`,
1630
+ "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 via q402_clear_delegation.",
1631
+ "Hardware wallets (Ledger / Trezor) can't sign EIP-7702 type-4 authorizations yet, so they're not supported in Q402 today \u2014 a hot wallet works.",
1632
+ "To export the key in MetaMask: open the account menu \u2192 Account details \u2192 Show private key. Paste the 0x... string into ~/.q402/mcp.env in your editor (never into chat)."
1585
1633
  ];
1586
1634
  function envSource(name) {
1587
1635
  if (process.env[name] !== void 0) return "process";
@@ -1733,7 +1781,17 @@ async function runDoctor() {
1733
1781
  "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
1782
  );
1735
1783
  }
1736
- if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1784
+ if (!CONFIG.realPaymentsRequested) {
1785
+ const haveAnyApi = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
1786
+ const havePk = isValidPrivateKey(CONFIG.privateKey);
1787
+ if (haveAnyApi && havePk) {
1788
+ missing.push(
1789
+ "Q402_ENABLE_REAL_PAYMENTS=1 \u2014 your other config looks fine, but your MCP client isn't passing the registry default through. Add the line Q402_ENABLE_REAL_PAYMENTS=1 to ~/.q402/mcp.env explicitly and restart."
1790
+ );
1791
+ } else {
1792
+ missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1793
+ }
1794
+ }
1737
1795
  const recommendedActions = [];
1738
1796
  if (!envFile.exists) {
1739
1797
  recommendedActions.push({
@@ -1774,15 +1832,35 @@ async function runDoctor() {
1774
1832
  warnings,
1775
1833
  recommendedActions,
1776
1834
  greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
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.`,
1835
+ 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.",
1837
+ userInstructions: phase === "first-install" ? [
1838
+ "Q402 is installed. To start sending payments you need an API key and a wallet.",
1839
+ "I'll create a settings file for you \u2014 say yes and I'll set it up + open it in your editor.",
1840
+ "Get a free API key at https://q402.quackai.ai/event (BNB Chain only, 2,000 sponsored transactions).",
1841
+ "Use a FRESH wallet for Q402 \u2014 don't use your main wallet. The wallet will be marked 'Smart account' in MetaMask after your first payment (that's normal \u2014 Q402 reverses it on demand).",
1842
+ "Paste your key + wallet private key INTO THE FILE (in your editor) \u2014 never paste a private key into this chat.",
1843
+ "Save the file, restart your MCP client, then ask me 'Verify Q402' to confirm."
1844
+ ] : [
1845
+ "Q402 is installed but a few env vars are still missing.",
1846
+ "Open ~/.q402/mcp.env in your editor and fill in the lines I list below.",
1847
+ "Save the file, then restart your MCP client (close + reopen Claude/Codex, or Cmd/Ctrl+Shift+P \u2192 Reload Window for Cursor/Cline).",
1848
+ "Then ask me 'Verify Q402' to re-check."
1849
+ ],
1778
1850
  securityNotice: SECURITY_NOTICE,
1779
- advisories: phase === "first-install" ? FIRST_INSTALL_ADVISORY : void 0
1851
+ // Advisories are useful in BOTH first-install and needs-completion:
1852
+ // a user who already pasted an API key but hasn't yet added their
1853
+ // private key is exactly the audience that needs the "MetaMask
1854
+ // export path" + "Smart-account-is-normal" heads-up. Suppressing
1855
+ // them once any env was set (the pre-0.5.12 behaviour) left a
1856
+ // gap right at the moment they were most useful.
1857
+ advisories: FIRST_INSTALL_ADVISORY
1780
1858
  };
1781
1859
  }
1782
1860
  let walletAddress;
1783
1861
  let walletError;
1784
1862
  try {
1785
- walletAddress = new Wallet4(CONFIG.privateKey).address;
1863
+ walletAddress = new Wallet6(CONFIG.privateKey).address;
1786
1864
  } catch (e) {
1787
1865
  walletError = e instanceof Error ? e.message : String(e);
1788
1866
  warnings.push(
@@ -1838,13 +1916,28 @@ async function runDoctor() {
1838
1916
  warnings,
1839
1917
  recommendedActions,
1840
1918
  greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
1841
- nextStep: ready ? "Summarize the wallet address, plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Then offer to make a test payment via q402_quote." : "Walk the user through each warning in order. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client.",
1842
- securityNotice: SECURITY_NOTICE
1919
+ 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'.",
1921
+ userInstructions: ready ? [
1922
+ `Your wallet: ${walletAddress ? walletAddress.slice(0, 6) + "\u2026" + walletAddress.slice(-4) : "(derive failed \u2014 check Q402_PRIVATE_KEY)"}`,
1923
+ "Q402 is live. You can now ask me to quote, pay, batch-pay, or check Trust Receipts.",
1924
+ "Want me to run a quick gas comparison across all 9 chains as a smoke test?",
1925
+ "Need to chain-test against sandbox without changing keys? Set Q402_ENABLE_REAL_PAYMENTS=0 in ~/.q402/mcp.env and restart \u2014 every q402_pay returns a fake hash until you flip it back to 1."
1926
+ ] : [
1927
+ `Q402 has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to fix:`,
1928
+ ...warnings.map((w) => `\u2022 ${w}`),
1929
+ "Open ~/.q402/mcp.env, fix the lines above, save, then restart your MCP client (Cursor/Cline: Cmd/Ctrl+Shift+P \u2192 Reload Window; Claude/Codex: quit + relaunch). Then ask me 'Verify Q402' to re-check."
1930
+ ],
1931
+ securityNotice: SECURITY_NOTICE,
1932
+ // Carry advisories through live-check too — even a fully-configured
1933
+ // user benefits from the "Smart-account in MetaMask is normal" line
1934
+ // appearing alongside their first ready state.
1935
+ advisories: FIRST_INSTALL_ADVISORY
1843
1936
  };
1844
1937
  }
1845
1938
  var DOCTOR_TOOL = {
1846
1939
  name: "q402_doctor",
1847
- description: 'Run a Q402 health check \u2014 covers first-install onboarding AND ongoing diagnostics in one tool. Read-only, no API key required. Detects the current phase (first-install / needs-completion / live-check) and tailors output to it. \n\nUse when the user says any of: "set up Q402", "verify Q402", "why isn\'t Q402 working", "Q402 status", "check Q402". This is the FIRST tool to call after install, BEFORE q402_pay or q402_balance \u2014 it tells the agent what state the user is in. \n\nMulti-turn pattern the AI should follow when phase = first-install: (1) Tell user MCP is installed. (2) Ask one yes/no question: \'Want me to create your secrets file at ~/.q402/mcp.env?\' (3) On yes, execute the recommendedActions[].write_file action using the client\'s own filesystem tool, then open the file in the user\'s editor (e.g. `code ~/.q402/mcp.env`, `open` on macOS, `start` on Windows, `xdg-open` on Linux). (4) Guide the user through getting an API key (free Trial at https://q402.quackai.ai/event OR paid Multichain at /payment) and pasting it into the file (in their editor \u2014 NEVER in chat). (5) Same for the private key. (6) Tell them to save + restart the MCP client. (7) Call q402_doctor again to verify. \n\nSecurity policy carried in the response: AI MUST surface the securityNotice when first walking through setup. If the user pastes a private key directly in chat, DO NOT refuse \u2014 the exposure already happened. Help them by directing them to put it in the file themselves (via their editor), and inform them the chat history now contains the key (most clients store this locally, some sync to cloud) so they should treat the wallet as exposed if it holds valuables. \n\nLive-check phase additionally returns per-scope quota, EIP-7702 delegation state per chain, relay reachability, and slot-mismatch warnings (e.g. Trial key in Multichain slot silently burns paid quota \u2014 surface this to the user).',
1940
+ description: 'Run a Q402 health check \u2014 covers first-install onboarding AND ongoing diagnostics in one tool. Read-only, no API key required. Detects the current phase (first-install / needs-completion / live-check) and tailors output to it. \n\nUse when the user says any of: "set up Q402", "verify Q402", "why isn\'t Q402 working", "Q402 status", "check Q402". This is the FIRST tool to call after install, BEFORE q402_pay or q402_balance \u2014 it tells the agent what state the user is in. \n\nOutput uses TWO instruction surfaces \u2014 `agentInstructions` (prescription for you, the AI \u2014 do NOT echo verbatim) and `userInstructions` (plain language array you CAN show the user as a numbered list). Always show userInstructions; consult agentInstructions privately to decide what to ask next + which `recommendedActions` to execute. \n\nMulti-turn pattern the AI should follow when phase = first-install: (1) Tell user MCP is installed. (2) Ask one yes/no question: \'Want me to create your secrets file?\' (3) On yes, execute recommendedActions IN ORDER \u2014 first the `ensure-q402-dir` shell action (use shellWindows on Windows), then the `create-env-file` write_file action. Then open the file in the user\'s editor (e.g. `code` for VS Code / Cursor / Cline, `open` on macOS, `start` on Windows, `xdg-open` on Linux). (4) Guide the user through getting an API key (free Trial at https://q402.quackai.ai/event OR paid Multichain at /payment) and pasting it into the file (in their editor \u2014 NEVER in chat). (5) Same for the private key. (6) Tell them to save + restart the MCP client (per-client restart verb is in agentInstructions). (7) Call q402_doctor again to verify. \n\nSecurity policy carried in the response: AI MUST surface the securityNotice when first walking through setup. If the user pastes a private key directly in chat, DO NOT refuse \u2014 the exposure already happened. Help them by directing them to put it in the file themselves (via their editor), and inform them the chat history now contains the key (most clients store this locally, some sync to cloud) so they should treat the wallet as exposed if it holds valuables. \n\nLive-check phase additionally returns per-scope quota, EIP-7702 delegation state per chain, relay reachability, and slot-mismatch warnings (e.g. Trial key in Multichain slot silently burns paid quota \u2014 surface this to the user).',
1848
1941
  inputSchema: {
1849
1942
  type: "object",
1850
1943
  properties: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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": [