@quackai/q402-mcp 0.5.6 → 0.5.7
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 +3 -6
- package/dist/index.js +75 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ By default the MCP server operates in **sandbox mode**: `q402_pay` returns a ran
|
|
|
136
136
|
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
137
|
|
|
138
138
|
```bash
|
|
139
|
-
# Two-key model — set whichever applies
|
|
139
|
+
# Two-key model — set whichever applies (or both for auto-routing).
|
|
140
140
|
# Auto-routing (same for q402_pay AND q402_batch_pay):
|
|
141
141
|
# chain="bnb" + Q402_TRIAL_API_KEY set → Trial (free sponsored)
|
|
142
142
|
# anything else → Multichain (paid 9-chain)
|
|
@@ -146,10 +146,6 @@ To enable real on-chain transactions, the resolved API key must be live (`q402_l
|
|
|
146
146
|
Q402_TRIAL_API_KEY=q402_live_... # BNB-only sponsored Trial key (from /event)
|
|
147
147
|
Q402_MULTICHAIN_API_KEY=q402_live_... # paid 9-chain key (per-chain Gas Tank)
|
|
148
148
|
|
|
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
149
|
Q402_PRIVATE_KEY=0xabc... # signer for the payer EOA
|
|
154
150
|
Q402_ENABLE_REAL_PAYMENTS=1 # explicit opt-in
|
|
155
151
|
```
|
|
@@ -179,13 +175,14 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
|
|
|
179
175
|
|---|---|---|
|
|
180
176
|
| `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
177
|
| `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
178
|
| `Q402_PRIVATE_KEY` | live-pay | Signer for the payer EOA. **Never share. Never paste in chat.** |
|
|
184
179
|
| `Q402_ENABLE_REAL_PAYMENTS` | live-pay | Set to `1` to opt in. Any other value (or unset) → sandbox. |
|
|
185
180
|
| `Q402_MAX_AMOUNT_PER_CALL` | optional | USD-equivalent cap. Defaults to `5`. |
|
|
186
181
|
| `Q402_ALLOWED_RECIPIENTS` | optional | Comma-separated lowercase addresses. Defaults to no allowlist. |
|
|
187
182
|
| `Q402_RELAY_BASE_URL` | optional | Defaults to `https://q402.quackai.ai/api`. Override for self-hosted Q402. |
|
|
188
183
|
|
|
184
|
+
> 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.
|
|
185
|
+
|
|
189
186
|
---
|
|
190
187
|
|
|
191
188
|
## 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.7";
|
|
159
172
|
|
|
160
173
|
// src/tools/quote.ts
|
|
161
174
|
import { z } from "zod";
|
|
@@ -1116,7 +1129,7 @@ async function runBalance() {
|
|
|
1116
1129
|
apiKeyMasked: null,
|
|
1117
1130
|
scopes: [],
|
|
1118
1131
|
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
|
|
1132
|
+
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
1133
|
};
|
|
1121
1134
|
}
|
|
1122
1135
|
const scopes = await Promise.all(
|
|
@@ -1524,24 +1537,25 @@ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u250
|
|
|
1524
1537
|
# After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
|
|
1525
1538
|
# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
1539
|
|
|
1527
|
-
# \u2500\u2500\u2500 API key \u2014
|
|
1540
|
+
# \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
1541
|
# Free Trial: BNB Chain only, 2,000 sponsored TX
|
|
1529
1542
|
# Get one at: https://q402.quackai.ai/event
|
|
1530
|
-
Q402_TRIAL_API_KEY=q402_live_...
|
|
1543
|
+
# Q402_TRIAL_API_KEY=q402_live_...
|
|
1531
1544
|
|
|
1532
1545
|
# Paid Multichain: all 9 chains, per-chain Gas Tank
|
|
1533
1546
|
# Get one at: https://q402.quackai.ai/payment
|
|
1534
1547
|
# Q402_MULTICHAIN_API_KEY=q402_live_...
|
|
1535
1548
|
|
|
1536
1549
|
# \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...
|
|
1550
|
+
# Hex EVM private key (0x + 64 hex chars). Signs payments LOCALLY on
|
|
1551
|
+
# your machine \u2014 never leaves your device, never sent to any server.
|
|
1552
|
+
# Q402_PRIVATE_KEY=0x...
|
|
1540
1553
|
|
|
1541
1554
|
# \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
|
-
|
|
1555
|
+
# Default 0 = sandbox (test responses, no funds move). Flip to 1 only
|
|
1556
|
+
# AFTER you've pasted real values into the lines above \u2014 otherwise the
|
|
1557
|
+
# server will refuse the placeholders.
|
|
1558
|
+
Q402_ENABLE_REAL_PAYMENTS=0
|
|
1545
1559
|
|
|
1546
1560
|
# \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
1561
|
# Default canonical Q402 deployment. Only change for self-hosted.
|
|
@@ -1582,8 +1596,18 @@ async function verifyOneKey(scope, envVar, apiKey) {
|
|
|
1582
1596
|
const resp = await fetch(url, {
|
|
1583
1597
|
method: "POST",
|
|
1584
1598
|
headers: { "Content-Type": "application/json" },
|
|
1585
|
-
body: JSON.stringify({ apiKey })
|
|
1599
|
+
body: JSON.stringify({ apiKey }),
|
|
1600
|
+
signal: AbortSignal.timeout(1e4)
|
|
1586
1601
|
});
|
|
1602
|
+
if (resp.status === 429) {
|
|
1603
|
+
return {
|
|
1604
|
+
scope,
|
|
1605
|
+
envVar,
|
|
1606
|
+
apiKeyMasked: mask2(apiKey),
|
|
1607
|
+
valid: false,
|
|
1608
|
+
error: "rate limited by relay \u2014 wait 60s and re-run q402_doctor"
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1587
1611
|
if (!resp.ok) {
|
|
1588
1612
|
return { scope, envVar, apiKeyMasked: mask2(apiKey), valid: false, error: `HTTP ${resp.status}` };
|
|
1589
1613
|
}
|
|
@@ -1593,6 +1617,10 @@ async function verifyOneKey(scope, envVar, apiKey) {
|
|
|
1593
1617
|
envVar,
|
|
1594
1618
|
apiKeyMasked: mask2(apiKey),
|
|
1595
1619
|
valid: body.valid ?? false,
|
|
1620
|
+
// Propagate the relay's specific reason ("API key has been rotated",
|
|
1621
|
+
// "Subscription expired", "Trial expired") so the user gets the
|
|
1622
|
+
// exact failure mode instead of a generic "verified as invalid".
|
|
1623
|
+
error: body.valid === false ? body.error : void 0,
|
|
1596
1624
|
plan: body.plan,
|
|
1597
1625
|
remainingCredits: body.remainingCredits,
|
|
1598
1626
|
isTrial: body.isTrial,
|
|
@@ -1616,11 +1644,16 @@ async function verifyOneKey(scope, envVar, apiKey) {
|
|
|
1616
1644
|
}
|
|
1617
1645
|
}
|
|
1618
1646
|
async function pingRelay() {
|
|
1619
|
-
const url = `${CONFIG.relayBaseUrl}/
|
|
1647
|
+
const url = `${CONFIG.relayBaseUrl}/keys/verify`;
|
|
1620
1648
|
const t0 = Date.now();
|
|
1621
1649
|
try {
|
|
1622
|
-
const resp = await fetch(url, {
|
|
1623
|
-
|
|
1650
|
+
const resp = await fetch(url, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
headers: { "Content-Type": "application/json" },
|
|
1653
|
+
body: "{}",
|
|
1654
|
+
signal: AbortSignal.timeout(1e4)
|
|
1655
|
+
});
|
|
1656
|
+
const reachable = resp.status >= 200 && resp.status < 500;
|
|
1624
1657
|
return { url, reachable, latencyMs: Date.now() - t0 };
|
|
1625
1658
|
} catch (e) {
|
|
1626
1659
|
return {
|
|
@@ -1634,7 +1667,7 @@ async function pingRelay() {
|
|
|
1634
1667
|
async function fetchDelegation(address) {
|
|
1635
1668
|
const url = `${CONFIG.relayBaseUrl}/wallet/delegation-status?address=${address}`;
|
|
1636
1669
|
try {
|
|
1637
|
-
const resp = await fetch(url);
|
|
1670
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
1638
1671
|
if (!resp.ok) {
|
|
1639
1672
|
return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error: `HTTP ${resp.status}` }));
|
|
1640
1673
|
}
|
|
@@ -1673,17 +1706,19 @@ async function runDoctor() {
|
|
|
1673
1706
|
"Must be '1' to allow real TX. Anything else = test response (fake hash)."
|
|
1674
1707
|
)
|
|
1675
1708
|
};
|
|
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
1709
|
const missing = [];
|
|
1681
1710
|
if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
|
|
1682
1711
|
missing.push(
|
|
1683
1712
|
"An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
|
|
1684
1713
|
);
|
|
1685
1714
|
}
|
|
1686
|
-
if (!CONFIG.privateKey)
|
|
1715
|
+
if (!CONFIG.privateKey) {
|
|
1716
|
+
missing.push("Q402_PRIVATE_KEY");
|
|
1717
|
+
} else if (!isValidPrivateKey(CONFIG.privateKey)) {
|
|
1718
|
+
missing.push(
|
|
1719
|
+
"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."
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1687
1722
|
if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
|
|
1688
1723
|
const recommendedActions = [];
|
|
1689
1724
|
if (!envFile.exists) {
|
|
@@ -1699,6 +1734,13 @@ async function runDoctor() {
|
|
|
1699
1734
|
});
|
|
1700
1735
|
}
|
|
1701
1736
|
const warnings = [];
|
|
1737
|
+
for (const name of Q402_ENV_FILE_KEYS_ALL) {
|
|
1738
|
+
if (process.env[name] !== void 0 && Q402_ENV_FILE_KEYS_ALL.has(name) && !Q402_ENV_FILE_KEYS.has(name)) {
|
|
1739
|
+
warnings.push(
|
|
1740
|
+
`${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).`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1702
1744
|
if (phase !== "live-check") {
|
|
1703
1745
|
return {
|
|
1704
1746
|
package: PACKAGE_NAME,
|
|
@@ -1708,7 +1750,6 @@ async function runDoctor() {
|
|
|
1708
1750
|
envFile,
|
|
1709
1751
|
envState,
|
|
1710
1752
|
missing,
|
|
1711
|
-
legacyDetected,
|
|
1712
1753
|
warnings,
|
|
1713
1754
|
recommendedActions,
|
|
1714
1755
|
greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
|
|
@@ -1717,10 +1758,14 @@ async function runDoctor() {
|
|
|
1717
1758
|
};
|
|
1718
1759
|
}
|
|
1719
1760
|
let walletAddress;
|
|
1761
|
+
let walletError;
|
|
1720
1762
|
try {
|
|
1721
1763
|
walletAddress = new Wallet4(CONFIG.privateKey).address;
|
|
1722
|
-
} catch {
|
|
1723
|
-
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
walletError = e instanceof Error ? e.message : String(e);
|
|
1766
|
+
warnings.push(
|
|
1767
|
+
`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.`
|
|
1768
|
+
);
|
|
1724
1769
|
}
|
|
1725
1770
|
const verifyTargets = [];
|
|
1726
1771
|
if (CONFIG.trialApiKey) verifyTargets.push({ scope: "trial", envVar: "Q402_TRIAL_API_KEY", key: CONFIG.trialApiKey });
|
|
@@ -1730,7 +1775,7 @@ async function runDoctor() {
|
|
|
1730
1775
|
}
|
|
1731
1776
|
const [keys, delegation, relay] = await Promise.all([
|
|
1732
1777
|
Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
|
|
1733
|
-
walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(
|
|
1778
|
+
walletAddress ? fetchDelegation(walletAddress) : Promise.resolve(void 0),
|
|
1734
1779
|
pingRelay()
|
|
1735
1780
|
]);
|
|
1736
1781
|
for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
|
|
@@ -1744,8 +1789,10 @@ async function runDoctor() {
|
|
|
1744
1789
|
`${k.envVar} has only ${k.remainingCredits} credits left \u2014 top up before you run out.`
|
|
1745
1790
|
);
|
|
1746
1791
|
}
|
|
1747
|
-
if (!k.valid
|
|
1748
|
-
warnings.push(
|
|
1792
|
+
if (!k.valid) {
|
|
1793
|
+
warnings.push(
|
|
1794
|
+
k.error ? `${k.envVar}: ${k.error}.` : `${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`
|
|
1795
|
+
);
|
|
1749
1796
|
}
|
|
1750
1797
|
}
|
|
1751
1798
|
if (relay && !relay.reachable) {
|
|
@@ -1762,7 +1809,6 @@ async function runDoctor() {
|
|
|
1762
1809
|
envFile,
|
|
1763
1810
|
envState,
|
|
1764
1811
|
missing,
|
|
1765
|
-
legacyDetected,
|
|
1766
1812
|
wallet: walletAddress ? { address: walletAddress } : void 0,
|
|
1767
1813
|
keys,
|
|
1768
1814
|
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.7",
|
|
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": [
|