@quackai/q402-mcp 0.4.3 → 0.4.5
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 +14 -7
- package/dist/index.js +37 -21
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -76,10 +76,13 @@ command = "npx"
|
|
|
76
76
|
args = ["-y", "@quackai/q402-mcp"]
|
|
77
77
|
startup_timeout_sec = 20.0
|
|
78
78
|
env = {
|
|
79
|
-
# Two-key model (v0.4.
|
|
79
|
+
# Two-key model (v0.4.5+): set whichever applies — both is best.
|
|
80
80
|
# The server auto-routes by chain: BNB → trial key, else multichain key.
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
# Both keys use the same q402_live_ prefix — the env var name is what
|
|
82
|
+
# carries the scope, not the key string. Get the values from the
|
|
83
|
+
# dashboard (each key has its own copy button per view).
|
|
84
|
+
Q402_TRIAL_API_KEY = "q402_live_...", # BNB-only sponsored (from /event)
|
|
85
|
+
Q402_MULTICHAIN_API_KEY = "q402_live_...", # paid 8-chain (from /payment)
|
|
83
86
|
# Legacy fallback — used if neither scoped key above is set.
|
|
84
87
|
Q402_API_KEY = "q402_live_...",
|
|
85
88
|
Q402_PRIVATE_KEY = "0xabc...",
|
|
@@ -130,14 +133,14 @@ By default the MCP server operates in **sandbox mode**: `q402_pay` returns a det
|
|
|
130
133
|
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`:
|
|
131
134
|
|
|
132
135
|
```bash
|
|
133
|
-
# Two-key model (v0.4.
|
|
136
|
+
# Two-key model (v0.4.5+) — set whichever applies. Both is best.
|
|
134
137
|
# Auto-routing: chain="bnb" → trial key (if set), otherwise multichain key.
|
|
135
138
|
# Override per call with keyScope: "auto" | "trial" | "multichain".
|
|
136
|
-
Q402_TRIAL_API_KEY=
|
|
139
|
+
Q402_TRIAL_API_KEY=q402_live_... # BNB-only sponsored Trial key (from /event)
|
|
137
140
|
Q402_MULTICHAIN_API_KEY=q402_live_... # paid 8-chain key (per-chain Gas Tank)
|
|
138
141
|
|
|
139
142
|
# Legacy fallback. Used for both scopes when the two above are unset —
|
|
140
|
-
# pre-v0.4.
|
|
143
|
+
# pre-v0.4.5 users keep working without any config change.
|
|
141
144
|
Q402_API_KEY=q402_live_...
|
|
142
145
|
|
|
143
146
|
Q402_PRIVATE_KEY=0xabc... # signer for the payer EOA
|
|
@@ -146,6 +149,10 @@ Q402_ENABLE_REAL_PAYMENTS=1 # explicit opt-in
|
|
|
146
149
|
|
|
147
150
|
Anything missing for the resolved scope → automatic sandbox fallback with a hint pointing at what to set.
|
|
148
151
|
|
|
152
|
+
> ⚠️ **Sandbox returns a deterministic-looking fake `txHash` and a synthetic success result.** A user who *expected* a live transfer (e.g. forgot to set `Q402_ENABLE_REAL_PAYMENTS=1`, mis-typed a scoped env var, or hit an impossible chain×scope combination like `keyScope: "trial"` + `chain: "monad"`) gets a "success" back and may believe funds actually moved.
|
|
153
|
+
>
|
|
154
|
+
> Two-layer mitigation: every sandbox response carries a `setupHint` field on the tool result describing **exactly why** sandbox was selected, and the `q402_balance` tool's `apiKeyKind: "missing"` makes the same diagnosis explicit. Always check `setupHint` on the first call from a new install. The deterministic `txHash` pattern (`0x` + 64 hex derived from `keccak256(chain, to, amount, token, "sandbox")`) is intentional so the agent can recognise it post-hoc, but the safer habit is to inspect `setupHint` before showing the user a success message.
|
|
155
|
+
|
|
149
156
|
### Hard caps
|
|
150
157
|
|
|
151
158
|
Two additional guards run before every payment regardless of mode:
|
|
@@ -165,7 +172,7 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
|
|
|
165
172
|
|---|---|---|
|
|
166
173
|
| `Q402_TRIAL_API_KEY` | live-pay (BNB) | BNB-only sponsored Trial key. Free at https://q402.quackai.ai/event. Used automatically for `chain="bnb"` when set. |
|
|
167
174
|
| `Q402_MULTICHAIN_API_KEY` | live-pay (8-chain) | Paid 8-chain key. Get one at https://q402.quackai.ai/payment. Used for all non-BNB chains and for BNB when no Trial key is set. |
|
|
168
|
-
| `Q402_API_KEY` | legacy fallback | Pre-v0.4.
|
|
175
|
+
| `Q402_API_KEY` | legacy fallback | Pre-v0.4.5 single-env path. Used for both scopes when the two above are unset. Keep set if you only have one key. |
|
|
169
176
|
| `Q402_PRIVATE_KEY` | live-pay | Signer for the payer EOA. **Never share. Never paste in chat.** |
|
|
170
177
|
| `Q402_ENABLE_REAL_PAYMENTS` | live-pay | Set to `1` to opt in. Any other value (or unset) → sandbox. |
|
|
171
178
|
| `Q402_MAX_AMOUNT_PER_CALL` | optional | USD-equivalent cap. Defaults to `5`. |
|
package/dist/index.js
CHANGED
|
@@ -52,31 +52,47 @@ function loadConfig() {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
var CONFIG = loadConfig();
|
|
55
|
-
function resolveApiKey(chain, scope = "auto") {
|
|
56
|
-
const effectiveScope = scope === "auto" ?
|
|
55
|
+
function resolveApiKey(chain, scope = "auto", intent = "single") {
|
|
56
|
+
const effectiveScope = scope === "auto" ? (
|
|
57
|
+
// Smart routing: batches default to multichain (trial cap=5 would
|
|
58
|
+
// silently fail any 6+ recipient batch). Single payments default to
|
|
59
|
+
// trial on BNB when a trial key is set, so the free sponsored
|
|
60
|
+
// allotment gets used naturally.
|
|
61
|
+
intent === "batch" ? "multichain" : chain === "bnb" && CONFIG.trialApiKey ? "trial" : "multichain"
|
|
62
|
+
) : scope;
|
|
57
63
|
if (effectiveScope === "trial") {
|
|
58
64
|
if (chain !== "bnb") {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
return {
|
|
66
|
+
apiKey: null,
|
|
67
|
+
scope: "trial",
|
|
68
|
+
fromLegacyFallback: false,
|
|
69
|
+
sandboxReason: `keyScope="trial" requested but chain="${chain}" \u2014 Trial keys support BNB Chain only. Drop keyScope (or set keyScope="multichain") to use the paid Multichain key on ${chain}.`
|
|
70
|
+
};
|
|
62
71
|
}
|
|
63
72
|
const key2 = CONFIG.trialApiKey ?? CONFIG.legacyApiKey;
|
|
64
73
|
if (!key2) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
return {
|
|
75
|
+
apiKey: null,
|
|
76
|
+
scope: "trial",
|
|
77
|
+
fromLegacyFallback: false,
|
|
78
|
+
sandboxReason: "keyScope='trial' requested but neither Q402_TRIAL_API_KEY nor Q402_API_KEY is set. Get a free Trial key at https://q402.quackai.ai/event."
|
|
79
|
+
};
|
|
68
80
|
}
|
|
69
81
|
return { apiKey: key2, scope: "trial", fromLegacyFallback: !CONFIG.trialApiKey };
|
|
70
82
|
}
|
|
71
83
|
const key = CONFIG.multichainApiKey ?? CONFIG.legacyApiKey;
|
|
72
84
|
if (!key) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
return {
|
|
86
|
+
apiKey: null,
|
|
87
|
+
scope: "multichain",
|
|
88
|
+
fromLegacyFallback: false,
|
|
89
|
+
sandboxReason: (scope === "multichain" ? "keyScope='multichain' requested but neither Q402_MULTICHAIN_API_KEY" : `chain="${chain}" routes to the Multichain scope but neither Q402_MULTICHAIN_API_KEY`) + " nor Q402_API_KEY is set. Activate a paid plan at https://q402.quackai.ai/payment to get one."
|
|
90
|
+
};
|
|
76
91
|
}
|
|
77
92
|
return { apiKey: key, scope: "multichain", fromLegacyFallback: !CONFIG.multichainApiKey };
|
|
78
93
|
}
|
|
79
94
|
function isLiveModeFor(resolved) {
|
|
95
|
+
if (!resolved.apiKey) return false;
|
|
80
96
|
if (!CONFIG.realPaymentsRequested) return false;
|
|
81
97
|
if (!CONFIG.privateKey) return false;
|
|
82
98
|
return resolved.apiKey.startsWith("q402_live_");
|
|
@@ -214,7 +230,7 @@ var CHAIN_CONFIG = {
|
|
|
214
230
|
}
|
|
215
231
|
};
|
|
216
232
|
var BNB_FOCUS_MODE = false;
|
|
217
|
-
var BNB_FOCUS_REJECTION_MESSAGE = 'BNB-
|
|
233
|
+
var BNB_FOCUS_REJECTION_MESSAGE = 'BNB-only mode active: this chain/token is temporarily hidden. Full multi-chain support is the normal state. Pass chain: "bnb" with token "USDC" or "USDT".';
|
|
218
234
|
if (BNB_FOCUS_MODE) {
|
|
219
235
|
for (const key of CHAIN_KEYS) {
|
|
220
236
|
if (key !== "bnb") {
|
|
@@ -290,7 +306,7 @@ function runQuote(input) {
|
|
|
290
306
|
}
|
|
291
307
|
var QUOTE_TOOL = {
|
|
292
308
|
name: "q402_quote",
|
|
293
|
-
description: "Compare gas costs and supported tokens across the 8 chains Q402 relays for (avax, bnb, eth, xlayer, stable, mantle, injective, monad).
|
|
309
|
+
description: "Compare gas costs and supported tokens across the 8 chains Q402 relays for (avax, bnb, eth, xlayer, stable, mantle, injective, monad). Returns the full chain \xD7 token matrix unconditionally \u2014 this tool does not read any API key, so it can't filter by trial vs multichain scope. When the caller intends to settle with a Trial API Key, treat any non-BNB row as informational only (q402_pay will return 403 TRIAL_BNB_ONLY for those). Includes RLUSD on Ethereum and Injective USDT-only. Read-only \u2014 no API key needed, no funds move. Use this before q402_pay so the user can see what's available and pick a chain.",
|
|
294
310
|
// Plain JSON schema mirroring the Zod schema above; MCP servers receive parameters as JSON.
|
|
295
311
|
inputSchema: {
|
|
296
312
|
type: "object",
|
|
@@ -705,7 +721,7 @@ async function runPay(input) {
|
|
|
705
721
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
706
722
|
}
|
|
707
723
|
const scopeRequest = input.keyScope ?? "auto";
|
|
708
|
-
const resolved = resolveApiKey(input.chain, scopeRequest);
|
|
724
|
+
const resolved = resolveApiKey(input.chain, scopeRequest, "single");
|
|
709
725
|
guardsApplied.push(`scope=${resolved.scope}${resolved.fromLegacyFallback ? "(legacy)" : ""}`);
|
|
710
726
|
const live = isLiveModeFor(resolved);
|
|
711
727
|
if (!live) {
|
|
@@ -715,7 +731,7 @@ async function runPay(input) {
|
|
|
715
731
|
token: input.token
|
|
716
732
|
});
|
|
717
733
|
guardsApplied.push("mode=sandbox");
|
|
718
|
-
const setupHint = describeSandboxReason(resolved.apiKey);
|
|
734
|
+
const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "");
|
|
719
735
|
return { result: result2, guardsApplied, setupHint };
|
|
720
736
|
}
|
|
721
737
|
const client = new Q402NodeClient({
|
|
@@ -823,10 +839,10 @@ function maxAmountGuardBatch(recipients, cap) {
|
|
|
823
839
|
function recipientAllowlistGuardBatch(recipients, allow) {
|
|
824
840
|
if (allow.length === 0) return;
|
|
825
841
|
for (let i = 0; i < recipients.length; i++) {
|
|
826
|
-
const
|
|
827
|
-
if (!allow.includes(to)) {
|
|
842
|
+
const r = recipients[i];
|
|
843
|
+
if (!allow.includes(r.to.toLowerCase())) {
|
|
828
844
|
throw new Error(
|
|
829
|
-
`recipients[${i}]: ${
|
|
845
|
+
`recipients[${i}]: ${r.to} is not in Q402_ALLOWED_RECIPIENTS. Either add this address to the allowlist or unset the env var to disable the guard.`
|
|
830
846
|
);
|
|
831
847
|
}
|
|
832
848
|
}
|
|
@@ -847,7 +863,7 @@ async function runBatchPay(input) {
|
|
|
847
863
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
848
864
|
}
|
|
849
865
|
const scopeRequest = input.keyScope ?? "auto";
|
|
850
|
-
const resolved = resolveApiKey(input.chain, scopeRequest);
|
|
866
|
+
const resolved = resolveApiKey(input.chain, scopeRequest, "batch");
|
|
851
867
|
guardsApplied.push(`scope=${resolved.scope}${resolved.fromLegacyFallback ? "(legacy)" : ""}`);
|
|
852
868
|
const live = isLiveModeFor(resolved);
|
|
853
869
|
if (!live) {
|
|
@@ -855,7 +871,7 @@ async function runBatchPay(input) {
|
|
|
855
871
|
(r) => sandboxPay(chain, { to: r.to, amount: r.amount, token: input.token })
|
|
856
872
|
);
|
|
857
873
|
guardsApplied.push("mode=sandbox");
|
|
858
|
-
const reason = describeSandboxReason2(resolved.apiKey);
|
|
874
|
+
const reason = resolved.sandboxReason ?? describeSandboxReason2(resolved.apiKey ?? "");
|
|
859
875
|
return {
|
|
860
876
|
mode: "sandbox",
|
|
861
877
|
status: "sandbox",
|
|
@@ -1205,7 +1221,7 @@ var RECEIPT_TOOL = {
|
|
|
1205
1221
|
|
|
1206
1222
|
// src/index.ts
|
|
1207
1223
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
1208
|
-
var PACKAGE_VERSION = "0.4.
|
|
1224
|
+
var PACKAGE_VERSION = "0.4.5";
|
|
1209
1225
|
function jsonText(value) {
|
|
1210
1226
|
return { type: "text", text: JSON.stringify(value, null, 2) };
|
|
1211
1227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 8 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": [
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup",
|
|
42
42
|
"dev": "tsup --watch",
|
|
43
|
-
"
|
|
43
|
+
"lint": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run lint && npm run build",
|
|
44
45
|
"start": "node dist/index.js"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
@@ -65,5 +66,8 @@
|
|
|
65
66
|
"author": "David Lee <davidlee@quackai.ai>",
|
|
66
67
|
"publishConfig": {
|
|
67
68
|
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"ws": "^8.20.1"
|
|
68
72
|
}
|
|
69
73
|
}
|