@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.
- package/README.md +19 -12
- package/dist/index.js +103 -35
- 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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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.
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1538
|
-
#
|
|
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
|
-
#
|
|
1543
|
-
#
|
|
1544
|
-
|
|
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}/
|
|
1661
|
+
const url = `${CONFIG.relayBaseUrl}/keys/verify`;
|
|
1620
1662
|
const t0 = Date.now();
|
|
1621
1663
|
try {
|
|
1622
|
-
const resp = await fetch(url, {
|
|
1623
|
-
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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
|
|
1748
|
-
warnings.push(
|
|
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.
|
|
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": [
|