@quackai/q402-mcp 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +202 -64
- 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 `
|
|
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
|
|
@@ -149,7 +149,7 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
|
|
|
149
149
|
|
|
150
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.
|
|
151
151
|
|
|
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 `
|
|
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):
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
155
|
# Two-key model — uncomment ONE (or both for auto-routing).
|
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,17 +34,29 @@ 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
|
-
|
|
37
|
-
|
|
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
|
}
|
|
59
|
+
if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
|
|
42
60
|
for (const line of raw.split(/\r?\n/)) {
|
|
43
61
|
const t = line.trim();
|
|
44
62
|
if (!t || t.startsWith("#")) continue;
|
|
@@ -168,7 +186,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
|
168
186
|
|
|
169
187
|
// src/version.ts
|
|
170
188
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
171
|
-
var PACKAGE_VERSION = "0.5.
|
|
189
|
+
var PACKAGE_VERSION = "0.5.13";
|
|
172
190
|
|
|
173
191
|
// src/tools/quote.ts
|
|
174
192
|
import { z } from "zod";
|
|
@@ -421,7 +439,7 @@ var QUOTE_TOOL = {
|
|
|
421
439
|
};
|
|
422
440
|
|
|
423
441
|
// src/tools/pay.ts
|
|
424
|
-
import { isAddress as isAddress2 } from "ethers";
|
|
442
|
+
import { isAddress as isAddress2, Wallet as Wallet2 } from "ethers";
|
|
425
443
|
import { z as z2 } from "zod";
|
|
426
444
|
|
|
427
445
|
// src/client.ts
|
|
@@ -504,7 +522,15 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
504
522
|
}
|
|
505
523
|
async fetchFacilitator() {
|
|
506
524
|
const url = `${this.opts.relayBaseUrl.replace(/\/$/, "")}/relay/info`;
|
|
507
|
-
|
|
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
|
+
}
|
|
508
534
|
if (!resp.ok) {
|
|
509
535
|
throw new Error(
|
|
510
536
|
`failed to fetch relay facilitator info from ${url} (${resp.status})`
|
|
@@ -575,12 +601,20 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
575
601
|
facilitator
|
|
576
602
|
};
|
|
577
603
|
const body = chain.key === "xlayer" ? { ...baseBody, xlayerNonce: paymentNonce.toString() } : chain.key === "stable" ? { ...baseBody, stableNonce: paymentNonce.toString() } : { ...baseBody, nonce: paymentNonce.toString() };
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
618
|
const data = await resp.json();
|
|
585
619
|
if (!resp.ok) {
|
|
586
620
|
throw new Error(data.error ?? `relay failed (HTTP ${resp.status})`);
|
|
@@ -691,18 +725,26 @@ var Q402NodeClient = class _Q402NodeClient {
|
|
|
691
725
|
authorization
|
|
692
726
|
});
|
|
693
727
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
+
}
|
|
706
748
|
const data = await resp.json();
|
|
707
749
|
if (!resp.ok || data.ok === false) {
|
|
708
750
|
const err = new BatchPayError(
|
|
@@ -810,6 +852,17 @@ async function runPay(input) {
|
|
|
810
852
|
);
|
|
811
853
|
}
|
|
812
854
|
const guardsApplied = [];
|
|
855
|
+
let senderWallet;
|
|
856
|
+
if (CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
|
|
857
|
+
try {
|
|
858
|
+
const addr = new Wallet2(CONFIG.privateKey).address;
|
|
859
|
+
senderWallet = {
|
|
860
|
+
address: addr,
|
|
861
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`
|
|
862
|
+
};
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
}
|
|
813
866
|
maxAmountGuard(input.amount, CONFIG.maxAmountPerCallUsd);
|
|
814
867
|
guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd}`);
|
|
815
868
|
recipientGuard(input.to, CONFIG.allowedRecipients);
|
|
@@ -828,7 +881,7 @@ async function runPay(input) {
|
|
|
828
881
|
});
|
|
829
882
|
guardsApplied.push("mode=sandbox");
|
|
830
883
|
const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "", resolved.scope);
|
|
831
|
-
return { result: result2, guardsApplied, setupHint };
|
|
884
|
+
return { result: result2, guardsApplied, setupHint, senderWallet };
|
|
832
885
|
}
|
|
833
886
|
const client = new Q402NodeClient({
|
|
834
887
|
apiKey: resolved.apiKey,
|
|
@@ -845,14 +898,27 @@ async function runPay(input) {
|
|
|
845
898
|
return {
|
|
846
899
|
result,
|
|
847
900
|
guardsApplied,
|
|
901
|
+
senderWallet,
|
|
848
902
|
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
|
|
849
903
|
};
|
|
850
904
|
}
|
|
851
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
|
+
}
|
|
852
912
|
const missing = [];
|
|
853
|
-
if (
|
|
854
|
-
if (!CONFIG.privateKey)
|
|
855
|
-
|
|
913
|
+
if (noApiKey) missing.push("a live API key (must start with q402_live_)");
|
|
914
|
+
if (!CONFIG.privateKey) {
|
|
915
|
+
missing.push("Q402_PRIVATE_KEY");
|
|
916
|
+
} else if (!isValidPrivateKey(CONFIG.privateKey)) {
|
|
917
|
+
missing.push(
|
|
918
|
+
"Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (noEnable) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
856
922
|
if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
|
|
857
923
|
const tier = scope === "trial" ? "Free Trial" : "Multichain";
|
|
858
924
|
const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
|
|
@@ -860,7 +926,7 @@ function describeSandboxReason(resolvedKey, scope) {
|
|
|
860
926
|
}
|
|
861
927
|
var PAY_TOOL = {
|
|
862
928
|
name: "q402_pay",
|
|
863
|
-
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.",
|
|
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 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.",
|
|
864
930
|
inputSchema: {
|
|
865
931
|
type: "object",
|
|
866
932
|
properties: {
|
|
@@ -899,7 +965,7 @@ var PAY_TOOL = {
|
|
|
899
965
|
};
|
|
900
966
|
|
|
901
967
|
// src/tools/batch-pay.ts
|
|
902
|
-
import { isAddress as isAddress3 } from "ethers";
|
|
968
|
+
import { isAddress as isAddress3, Wallet as Wallet3 } from "ethers";
|
|
903
969
|
import { z as z3 } from "zod";
|
|
904
970
|
var RECIPIENT_LIMIT_TRIAL = 5;
|
|
905
971
|
var RECIPIENT_LIMIT_PAID = 20;
|
|
@@ -964,6 +1030,17 @@ async function runBatchPay(input) {
|
|
|
964
1030
|
if (CONFIG.allowedRecipients.length > 0) {
|
|
965
1031
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
966
1032
|
}
|
|
1033
|
+
let senderWallet;
|
|
1034
|
+
if (CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
|
|
1035
|
+
try {
|
|
1036
|
+
const addr = new Wallet3(CONFIG.privateKey).address;
|
|
1037
|
+
senderWallet = {
|
|
1038
|
+
address: addr,
|
|
1039
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`
|
|
1040
|
+
};
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
967
1044
|
const scopeRequest = input.keyScope ?? "auto";
|
|
968
1045
|
if (scopeRequest === "auto" && input.chain === "bnb" && CONFIG.trialApiKey && input.recipients.length > RECIPIENT_LIMIT_TRIAL) {
|
|
969
1046
|
const overflow = input.recipients.length - RECIPIENT_LIMIT_TRIAL;
|
|
@@ -972,6 +1049,7 @@ async function runBatchPay(input) {
|
|
|
972
1049
|
mode: "none",
|
|
973
1050
|
status: "ambiguous",
|
|
974
1051
|
guardsApplied,
|
|
1052
|
+
senderWallet,
|
|
975
1053
|
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:
|
|
976
1054
|
\u2022 keyScope="trial" \u2014 keep only the first ${RECIPIENT_LIMIT_TRIAL} recipients (free, sponsored). Drop the remaining ${overflow}.
|
|
977
1055
|
\u2022 keyScope="multichain" \u2014 send all ${input.recipients.length} on the paid Multichain key (charges the paid pool + Gas Tank).
|
|
@@ -991,6 +1069,7 @@ async function runBatchPay(input) {
|
|
|
991
1069
|
mode: "sandbox",
|
|
992
1070
|
status: "sandbox",
|
|
993
1071
|
result: { sandbox: sandboxResults, reason },
|
|
1072
|
+
senderWallet,
|
|
994
1073
|
guardsApplied,
|
|
995
1074
|
setupHint: reason
|
|
996
1075
|
};
|
|
@@ -1009,7 +1088,7 @@ async function runBatchPay(input) {
|
|
|
1009
1088
|
guardsApplied.push("mode=live");
|
|
1010
1089
|
guardsApplied.push(`scope=${result.scope} (server enforced)`);
|
|
1011
1090
|
guardsApplied.push(`batch_size=${input.recipients.length}/${result.limit}`);
|
|
1012
|
-
return { mode: "live", status: "success", result, guardsApplied };
|
|
1091
|
+
return { mode: "live", status: "success", result, guardsApplied, senderWallet };
|
|
1013
1092
|
} catch (err) {
|
|
1014
1093
|
if (err instanceof BatchPayError) {
|
|
1015
1094
|
guardsApplied.push("mode=live");
|
|
@@ -1029,6 +1108,7 @@ async function runBatchPay(input) {
|
|
|
1029
1108
|
results: err.results
|
|
1030
1109
|
},
|
|
1031
1110
|
guardsApplied,
|
|
1111
|
+
senderWallet,
|
|
1032
1112
|
error: err.message
|
|
1033
1113
|
};
|
|
1034
1114
|
}
|
|
@@ -1036,10 +1116,22 @@ async function runBatchPay(input) {
|
|
|
1036
1116
|
}
|
|
1037
1117
|
}
|
|
1038
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
|
+
}
|
|
1039
1125
|
const missing = [];
|
|
1040
|
-
if (
|
|
1041
|
-
if (!CONFIG.privateKey)
|
|
1042
|
-
|
|
1126
|
+
if (noApiKey) missing.push("a live API key (must start with q402_live_)");
|
|
1127
|
+
if (!CONFIG.privateKey) {
|
|
1128
|
+
missing.push("Q402_PRIVATE_KEY");
|
|
1129
|
+
} else if (!isValidPrivateKey(CONFIG.privateKey)) {
|
|
1130
|
+
missing.push(
|
|
1131
|
+
"Q402_PRIVATE_KEY (currently the placeholder '0x...' \u2014 paste a real 0x + 64-hex key into ~/.q402/mcp.env)"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
if (noEnable) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
1043
1135
|
if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
|
|
1044
1136
|
const tier = scope === "trial" ? "Free Trial" : "Multichain";
|
|
1045
1137
|
const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
|
|
@@ -1285,7 +1377,9 @@ async function runReceipt(input) {
|
|
|
1285
1377
|
notFound: true
|
|
1286
1378
|
};
|
|
1287
1379
|
}
|
|
1288
|
-
const resp = await fetch(`${apiBase}/receipt/${receiptId}
|
|
1380
|
+
const resp = await fetch(`${apiBase}/receipt/${receiptId}`, {
|
|
1381
|
+
signal: AbortSignal.timeout(1e4)
|
|
1382
|
+
});
|
|
1289
1383
|
if (resp.status === 404) {
|
|
1290
1384
|
return {
|
|
1291
1385
|
receiptId,
|
|
@@ -1338,7 +1432,7 @@ var RECEIPT_TOOL = {
|
|
|
1338
1432
|
|
|
1339
1433
|
// src/tools/wallet-status.ts
|
|
1340
1434
|
import { z as z6 } from "zod";
|
|
1341
|
-
import { Wallet as
|
|
1435
|
+
import { Wallet as Wallet4 } from "ethers";
|
|
1342
1436
|
var WalletStatusInputSchema = z6.object({});
|
|
1343
1437
|
async function runWalletStatus() {
|
|
1344
1438
|
if (!CONFIG.privateKey) {
|
|
@@ -1349,7 +1443,7 @@ async function runWalletStatus() {
|
|
|
1349
1443
|
}
|
|
1350
1444
|
let address;
|
|
1351
1445
|
try {
|
|
1352
|
-
address = new
|
|
1446
|
+
address = new Wallet4(CONFIG.privateKey).address;
|
|
1353
1447
|
} catch {
|
|
1354
1448
|
return {
|
|
1355
1449
|
error: "INVALID_PRIVATE_KEY",
|
|
@@ -1360,7 +1454,7 @@ async function runWalletStatus() {
|
|
|
1360
1454
|
let body;
|
|
1361
1455
|
let res;
|
|
1362
1456
|
try {
|
|
1363
|
-
res = await fetch(url);
|
|
1457
|
+
res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
1364
1458
|
body = await res.json();
|
|
1365
1459
|
} catch (e) {
|
|
1366
1460
|
return {
|
|
@@ -1397,7 +1491,7 @@ var WALLET_STATUS_TOOL = {
|
|
|
1397
1491
|
|
|
1398
1492
|
// src/tools/clear-delegation.ts
|
|
1399
1493
|
import { z as z7 } from "zod";
|
|
1400
|
-
import { Wallet as
|
|
1494
|
+
import { Wallet as Wallet5, JsonRpcProvider as JsonRpcProvider2 } from "ethers";
|
|
1401
1495
|
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
1402
1496
|
var DEFAULT_RPC2 = {
|
|
1403
1497
|
1: "https://ethereum.publicnode.com",
|
|
@@ -1428,7 +1522,7 @@ async function runClearDelegation(input) {
|
|
|
1428
1522
|
let wallet;
|
|
1429
1523
|
try {
|
|
1430
1524
|
const provider = new JsonRpcProvider2(DEFAULT_RPC2[cfg.chainId]);
|
|
1431
|
-
wallet = new
|
|
1525
|
+
wallet = new Wallet5(CONFIG.privateKey, provider);
|
|
1432
1526
|
} catch {
|
|
1433
1527
|
return {
|
|
1434
1528
|
ok: false,
|
|
@@ -1482,7 +1576,8 @@ async function runClearDelegation(input) {
|
|
|
1482
1576
|
res = await fetch(url, {
|
|
1483
1577
|
method: "POST",
|
|
1484
1578
|
headers: { "Content-Type": "application/json" },
|
|
1485
|
-
body: JSON.stringify(body)
|
|
1579
|
+
body: JSON.stringify(body),
|
|
1580
|
+
signal: AbortSignal.timeout(3e4)
|
|
1486
1581
|
});
|
|
1487
1582
|
json = await res.json();
|
|
1488
1583
|
} catch (e) {
|
|
@@ -1538,13 +1633,19 @@ var CLEAR_DELEGATION_TOOL = {
|
|
|
1538
1633
|
|
|
1539
1634
|
// src/tools/doctor.ts
|
|
1540
1635
|
import { z as z8 } from "zod";
|
|
1541
|
-
import { Wallet as
|
|
1636
|
+
import { Wallet as Wallet6 } from "ethers";
|
|
1542
1637
|
var DoctorInputSchema = z8.object({});
|
|
1543
1638
|
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
|
|
1544
1639
|
# Q402 MCP \u2014 secrets
|
|
1545
1640
|
# Read automatically by @quackai/q402-mcp on startup.
|
|
1546
1641
|
# Edit this file in your editor. NEVER paste your private key into chat.
|
|
1547
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.
|
|
1548
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
|
|
1549
1650
|
|
|
1550
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
|
|
@@ -1584,10 +1685,10 @@ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
|
|
|
1584
1685
|
`;
|
|
1585
1686
|
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.";
|
|
1586
1687
|
var FIRST_INSTALL_ADVISORY = [
|
|
1587
|
-
|
|
1588
|
-
"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
|
|
1589
|
-
"Hardware wallets (Ledger / Trezor)
|
|
1590
|
-
"To
|
|
1688
|
+
`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.`,
|
|
1689
|
+
"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.",
|
|
1690
|
+
"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.",
|
|
1691
|
+
"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)."
|
|
1591
1692
|
];
|
|
1592
1693
|
function envSource(name) {
|
|
1593
1694
|
if (process.env[name] !== void 0) return "process";
|
|
@@ -1704,9 +1805,11 @@ async function fetchDelegation(address) {
|
|
|
1704
1805
|
}
|
|
1705
1806
|
async function runDoctor() {
|
|
1706
1807
|
const phase = detectPhase();
|
|
1808
|
+
const envFileReadError = getQ402EnvFileReadError();
|
|
1707
1809
|
const envFile = {
|
|
1708
1810
|
path: Q402_ENV_FILE_PATH,
|
|
1709
|
-
exists: Q402_ENV_FILE_PRESENT
|
|
1811
|
+
exists: Q402_ENV_FILE_PRESENT,
|
|
1812
|
+
warning: envFileReadError ?? void 0
|
|
1710
1813
|
};
|
|
1711
1814
|
const envState = {
|
|
1712
1815
|
Q402_TRIAL_API_KEY: envSlot(
|
|
@@ -1739,7 +1842,17 @@ async function runDoctor() {
|
|
|
1739
1842
|
"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."
|
|
1740
1843
|
);
|
|
1741
1844
|
}
|
|
1742
|
-
if (!CONFIG.realPaymentsRequested)
|
|
1845
|
+
if (!CONFIG.realPaymentsRequested) {
|
|
1846
|
+
const haveAnyApi = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
|
|
1847
|
+
const havePk = isValidPrivateKey(CONFIG.privateKey);
|
|
1848
|
+
if (haveAnyApi && havePk) {
|
|
1849
|
+
missing.push(
|
|
1850
|
+
"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."
|
|
1851
|
+
);
|
|
1852
|
+
} else {
|
|
1853
|
+
missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1743
1856
|
const recommendedActions = [];
|
|
1744
1857
|
if (!envFile.exists) {
|
|
1745
1858
|
recommendedActions.push({
|
|
@@ -1762,12 +1875,18 @@ async function runDoctor() {
|
|
|
1762
1875
|
}
|
|
1763
1876
|
const warnings = [];
|
|
1764
1877
|
for (const name of Q402_ENV_FILE_KEYS_ALL) {
|
|
1765
|
-
if (process.env[name] !== void 0 &&
|
|
1878
|
+
if (process.env[name] !== void 0 && !Q402_ENV_FILE_KEYS.has(name)) {
|
|
1766
1879
|
warnings.push(
|
|
1767
1880
|
`${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).`
|
|
1768
1881
|
);
|
|
1769
1882
|
}
|
|
1770
1883
|
}
|
|
1884
|
+
const enableExplicit = process.env.Q402_ENABLE_REAL_PAYMENTS !== void 0 || Q402_ENV_FILE_KEYS_ALL.has("Q402_ENABLE_REAL_PAYMENTS");
|
|
1885
|
+
if (CONFIG.legacyApiKey && CONFIG.realPaymentsRequested && !enableExplicit) {
|
|
1886
|
+
warnings.push(
|
|
1887
|
+
"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."
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1771
1890
|
if (phase !== "live-check") {
|
|
1772
1891
|
return {
|
|
1773
1892
|
package: PACKAGE_NAME,
|
|
@@ -1781,7 +1900,7 @@ async function runDoctor() {
|
|
|
1781
1900
|
recommendedActions,
|
|
1782
1901
|
greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
|
|
1783
1902
|
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.",
|
|
1784
|
-
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.",
|
|
1903
|
+
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.",
|
|
1785
1904
|
userInstructions: phase === "first-install" ? [
|
|
1786
1905
|
"Q402 is installed. To start sending payments you need an API key and a wallet.",
|
|
1787
1906
|
"I'll create a settings file for you \u2014 say yes and I'll set it up + open it in your editor.",
|
|
@@ -1796,13 +1915,19 @@ async function runDoctor() {
|
|
|
1796
1915
|
"Then ask me 'Verify Q402' to re-check."
|
|
1797
1916
|
],
|
|
1798
1917
|
securityNotice: SECURITY_NOTICE,
|
|
1799
|
-
|
|
1918
|
+
// Advisories are useful in BOTH first-install and needs-completion:
|
|
1919
|
+
// a user who already pasted an API key but hasn't yet added their
|
|
1920
|
+
// private key is exactly the audience that needs the "MetaMask
|
|
1921
|
+
// export path" + "Smart-account-is-normal" heads-up. Suppressing
|
|
1922
|
+
// them once any env was set (the pre-0.5.12 behaviour) left a
|
|
1923
|
+
// gap right at the moment they were most useful.
|
|
1924
|
+
advisories: FIRST_INSTALL_ADVISORY
|
|
1800
1925
|
};
|
|
1801
1926
|
}
|
|
1802
1927
|
let walletAddress;
|
|
1803
1928
|
let walletError;
|
|
1804
1929
|
try {
|
|
1805
|
-
walletAddress = new
|
|
1930
|
+
walletAddress = new Wallet6(CONFIG.privateKey).address;
|
|
1806
1931
|
} catch (e) {
|
|
1807
1932
|
walletError = e instanceof Error ? e.message : String(e);
|
|
1808
1933
|
warnings.push(
|
|
@@ -1815,11 +1940,12 @@ async function runDoctor() {
|
|
|
1815
1940
|
if (verifyTargets.length === 0 && CONFIG.legacyApiKey) {
|
|
1816
1941
|
verifyTargets.push({ scope: "legacy", envVar: "Q402_API_KEY", key: CONFIG.legacyApiKey });
|
|
1817
1942
|
}
|
|
1818
|
-
const [keys, delegation
|
|
1943
|
+
const [keys, delegation] = await Promise.all([
|
|
1819
1944
|
Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
|
|
1820
|
-
walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0)
|
|
1821
|
-
pingRelay()
|
|
1945
|
+
walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0)
|
|
1822
1946
|
]);
|
|
1947
|
+
const anyResponse = keys.some((k) => !k.error || !/timeout|unreachable|ENOTFOUND|ECONN/i.test(k.error));
|
|
1948
|
+
const relay = anyResponse && keys.length > 0 ? { url: `${CONFIG.relayBaseUrl}/keys/verify`, reachable: true } : await pingRelay();
|
|
1823
1949
|
for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
|
|
1824
1950
|
for (const k of keys) {
|
|
1825
1951
|
if (typeof k.remainingCredits === "number" && k.remainingCredits === 0) {
|
|
@@ -1832,9 +1958,19 @@ async function runDoctor() {
|
|
|
1832
1958
|
);
|
|
1833
1959
|
}
|
|
1834
1960
|
if (!k.valid) {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1961
|
+
const isTrialExpired = (k.error ?? "").toLowerCase().includes("trial expired");
|
|
1962
|
+
if (isTrialExpired && k.trialExpiresAt) {
|
|
1963
|
+
const exp = new Date(k.trialExpiresAt);
|
|
1964
|
+
const days = Math.floor((Date.now() - exp.getTime()) / 864e5);
|
|
1965
|
+
const ago = days > 0 ? ` (${days} day${days === 1 ? "" : "s"} ago)` : "";
|
|
1966
|
+
warnings.push(
|
|
1967
|
+
`${k.envVar}: Trial expired on ${exp.toISOString().slice(0, 10)}${ago}. Upgrade to a Multichain plan at https://q402.quackai.ai/payment.`
|
|
1968
|
+
);
|
|
1969
|
+
} else {
|
|
1970
|
+
warnings.push(
|
|
1971
|
+
k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1838
1974
|
}
|
|
1839
1975
|
}
|
|
1840
1976
|
if (relay && !relay.reachable) {
|
|
@@ -1859,20 +1995,22 @@ async function runDoctor() {
|
|
|
1859
1995
|
recommendedActions,
|
|
1860
1996
|
greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
|
|
1861
1997
|
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.",
|
|
1862
|
-
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'.",
|
|
1998
|
+
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'.",
|
|
1863
1999
|
userInstructions: ready ? [
|
|
1864
2000
|
`Your wallet: ${walletAddress ? walletAddress.slice(0, 6) + "\u2026" + walletAddress.slice(-4) : "(derive failed \u2014 check Q402_PRIVATE_KEY)"}`,
|
|
1865
2001
|
"Q402 is live. You can now ask me to quote, pay, batch-pay, or check Trust Receipts.",
|
|
1866
|
-
"Want me to run a quick gas comparison across all 9 chains as a smoke test?"
|
|
2002
|
+
"Want me to run a quick gas comparison across all 9 chains as a smoke test?",
|
|
2003
|
+
"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."
|
|
1867
2004
|
] : [
|
|
1868
2005
|
`Q402 has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to fix:`,
|
|
1869
2006
|
...warnings.map((w) => `\u2022 ${w}`),
|
|
1870
2007
|
"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."
|
|
1871
2008
|
],
|
|
1872
2009
|
securityNotice: SECURITY_NOTICE,
|
|
1873
|
-
// advisories
|
|
1874
|
-
//
|
|
1875
|
-
|
|
2010
|
+
// Carry advisories through live-check too — even a fully-configured
|
|
2011
|
+
// user benefits from the "Smart-account in MetaMask is normal" line
|
|
2012
|
+
// appearing alongside their first ready state.
|
|
2013
|
+
advisories: FIRST_INSTALL_ADVISORY
|
|
1876
2014
|
};
|
|
1877
2015
|
}
|
|
1878
2016
|
var DOCTOR_TOOL = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
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": [
|