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