@quackai/q402-mcp 0.5.17 → 0.6.1
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/dist/index.js +455 -26
- package/package.json +73 -73
package/dist/index.js
CHANGED
|
@@ -120,9 +120,18 @@ function loadConfig() {
|
|
|
120
120
|
const apiKey = multichainApiKey ?? trialApiKey ?? legacyApiKey;
|
|
121
121
|
const apiKeyKind = classifyApiKey(apiKey);
|
|
122
122
|
const privateKey = ENV.Q402_PRIVATE_KEY ?? null;
|
|
123
|
+
const agenticPrivateKey = ENV.Q402_AGENTIC_PRIVATE_KEY ?? null;
|
|
124
|
+
const walletIdRaw = ENV.Q402_AGENT_WALLET_ADDRESS ?? ENV.Q402_WALLET_ID;
|
|
125
|
+
if (!ENV.Q402_AGENT_WALLET_ADDRESS && ENV.Q402_WALLET_ID) {
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
"[q402-mcp] Q402_WALLET_ID is deprecated \u2014 rename to Q402_AGENT_WALLET_ADDRESS. Old name will be removed in v0.7.0.\n"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const walletId = typeof walletIdRaw === "string" && walletIdRaw.length > 0 ? walletIdRaw.toLowerCase() : null;
|
|
123
131
|
const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
|
|
124
132
|
const anyLiveKey = classifyApiKey(trialApiKey) === "live" || classifyApiKey(multichainApiKey) === "live" || classifyApiKey(legacyApiKey) === "live";
|
|
125
|
-
const
|
|
133
|
+
const hasAnySignerKey = typeof privateKey === "string" && privateKey.length > 0 || typeof agenticPrivateKey === "string" && agenticPrivateKey.length > 0;
|
|
134
|
+
const live = realPaymentsRequested && anyLiveKey && hasAnySignerKey;
|
|
126
135
|
return {
|
|
127
136
|
trialApiKey,
|
|
128
137
|
multichainApiKey,
|
|
@@ -130,6 +139,8 @@ function loadConfig() {
|
|
|
130
139
|
apiKey,
|
|
131
140
|
apiKeyKind,
|
|
132
141
|
privateKey,
|
|
142
|
+
agenticPrivateKey,
|
|
143
|
+
walletId,
|
|
133
144
|
realPaymentsRequested,
|
|
134
145
|
mode: live ? "live" : "sandbox",
|
|
135
146
|
relayBaseUrl: (ENV.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
|
|
@@ -137,6 +148,17 @@ function loadConfig() {
|
|
|
137
148
|
allowedRecipients: parseAllowedRecipients(ENV.Q402_ALLOWED_RECIPIENTS)
|
|
138
149
|
};
|
|
139
150
|
}
|
|
151
|
+
function detectAgenticModes(c = CONFIG) {
|
|
152
|
+
const modeA = isValidPrivateKey(c.privateKey);
|
|
153
|
+
const modeB = isValidPrivateKey(c.agenticPrivateKey);
|
|
154
|
+
const modeC = c.apiKey !== null && c.apiKey.startsWith("q402_live_");
|
|
155
|
+
let count = 0;
|
|
156
|
+
if (modeA) count++;
|
|
157
|
+
if (modeB) count++;
|
|
158
|
+
if (modeC) count++;
|
|
159
|
+
const primary = modeB ? "B" : modeA ? "A" : modeC ? "C" : null;
|
|
160
|
+
return { modeA, modeB, modeC, count, primary };
|
|
161
|
+
}
|
|
140
162
|
var CONFIG = loadConfig();
|
|
141
163
|
function resolveApiKey(chain, scope = "auto") {
|
|
142
164
|
const effectiveScope = scope === "auto" ? (
|
|
@@ -180,15 +202,16 @@ var PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
|
180
202
|
function isLiveModeFor(resolved) {
|
|
181
203
|
if (!resolved.apiKey) return false;
|
|
182
204
|
if (!CONFIG.realPaymentsRequested) return false;
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
const hasMode_A_Key = typeof CONFIG.privateKey === "string" && PRIVATE_KEY_RE.test(CONFIG.privateKey);
|
|
206
|
+
const hasMode_B_Key = typeof CONFIG.agenticPrivateKey === "string" && PRIVATE_KEY_RE.test(CONFIG.agenticPrivateKey);
|
|
207
|
+
if (!hasMode_A_Key && !hasMode_B_Key) return false;
|
|
185
208
|
return resolved.apiKey.startsWith("q402_live_");
|
|
186
209
|
}
|
|
187
210
|
var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
188
211
|
|
|
189
212
|
// src/version.ts
|
|
190
213
|
var PACKAGE_NAME = "@quackai/q402-mcp";
|
|
191
|
-
var PACKAGE_VERSION = "0.
|
|
214
|
+
var PACKAGE_VERSION = "0.6.1";
|
|
192
215
|
|
|
193
216
|
// src/tools/quote.ts
|
|
194
217
|
import { z } from "zod";
|
|
@@ -822,6 +845,16 @@ var PayInputSchema = z2.object({
|
|
|
822
845
|
keyScope: z2.enum(["auto", "trial", "multichain"]).optional().describe(
|
|
823
846
|
'Which API key to use. "auto" (default): chain="bnb" + Q402_TRIAL_API_KEY set \u2192 Trial (free sponsored); else Multichain. "trial" forces the BNB-only sponsored key. "multichain" forces the paid 9-chain key. Same rule applies to q402_batch_pay.'
|
|
824
847
|
),
|
|
848
|
+
walletMode: z2.enum(["eoa", "agentic-local", "agentic-server"]).optional().describe(
|
|
849
|
+
`Which wallet to spend from:
|
|
850
|
+
"eoa" \u2014 the user's real MetaMask/OKX EOA, signed locally with Q402_PRIVATE_KEY
|
|
851
|
+
"agentic-local" \u2014 the Agent Wallet's exported private key (Q402_AGENTIC_PRIVATE_KEY)
|
|
852
|
+
"agentic-server" \u2014 the server-managed Agent Wallet (Q402 holds the key; you only need Q402_MULTICHAIN_API_KEY)
|
|
853
|
+
When MORE THAN ONE wallet is configured in the user's environment, you MUST ask the user which to use before calling \u2014 do NOT guess. Phrase: "You have multiple wallets set up \u2014 pay from your EOA, or your Agent Wallet?" When only one wallet is configured this argument is optional and the tool routes there automatically.`
|
|
854
|
+
),
|
|
855
|
+
walletId: z2.string().optional().describe(
|
|
856
|
+
`Server-managed Agent Wallet only (walletMode="agentic-server"). Lowercased Agent Wallet address selecting which of the user's wallets to spend from when they hold more than one (max 10 per owner). Omit to use the user's default wallet. Ignored for walletMode="eoa" and "agentic-local" since those modes carry their own signing key.`
|
|
857
|
+
),
|
|
825
858
|
confirm: z2.literal(true).describe(
|
|
826
859
|
"MUST be true. Prove the user explicitly approved this exact recipient and amount in the conversation right before this tool was called. Setting this to true on behalf of the user without confirmation is a violation of the tool contract."
|
|
827
860
|
)
|
|
@@ -854,10 +887,82 @@ async function runPay(input) {
|
|
|
854
887
|
);
|
|
855
888
|
}
|
|
856
889
|
const guardsApplied = [];
|
|
857
|
-
|
|
858
|
-
|
|
890
|
+
function failureResult(method) {
|
|
891
|
+
return {
|
|
892
|
+
success: false,
|
|
893
|
+
sandbox: false,
|
|
894
|
+
txHash: "",
|
|
895
|
+
tokenAmount: input.amount,
|
|
896
|
+
token: input.token,
|
|
897
|
+
chain: chain.key,
|
|
898
|
+
method,
|
|
899
|
+
explorerUrl: null
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const modes = detectAgenticModes(CONFIG);
|
|
903
|
+
const available = [];
|
|
904
|
+
if (modes.modeA && CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
|
|
859
905
|
try {
|
|
860
906
|
const addr = new Wallet2(CONFIG.privateKey).address;
|
|
907
|
+
available.push({
|
|
908
|
+
id: "eoa",
|
|
909
|
+
label: "Your real MetaMask / OKX EOA",
|
|
910
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`,
|
|
911
|
+
note: "Signs locally with Q402_PRIVATE_KEY. Your wallet becomes EIP-7702-delegated after the first payment on each chain."
|
|
912
|
+
});
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (modes.modeB && CONFIG.agenticPrivateKey && isValidPrivateKey(CONFIG.agenticPrivateKey)) {
|
|
917
|
+
try {
|
|
918
|
+
const addr = new Wallet2(CONFIG.agenticPrivateKey).address;
|
|
919
|
+
available.push({
|
|
920
|
+
id: "agentic-local",
|
|
921
|
+
label: "Agent Wallet (local signing with exported key)",
|
|
922
|
+
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`,
|
|
923
|
+
note: "Signs locally with Q402_AGENTIC_PRIVATE_KEY. Your MetaMask is never touched."
|
|
924
|
+
});
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (modes.modeC) {
|
|
929
|
+
available.push({
|
|
930
|
+
id: "agentic-server",
|
|
931
|
+
label: "Agent Wallet (server-managed)",
|
|
932
|
+
note: "Q402 holds the encrypted key; payment fires through /api/wallet/agentic/send. Caps you set in the dashboard bound the spend."
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const requestedMode = input.walletMode;
|
|
936
|
+
const requestedAvailable = requestedMode ? available.some((w) => w.id === requestedMode) : false;
|
|
937
|
+
if (requestedMode && !requestedAvailable) {
|
|
938
|
+
return {
|
|
939
|
+
result: failureResult("wallet_mode_unavailable"),
|
|
940
|
+
guardsApplied: [
|
|
941
|
+
`wallet_modes_available=${available.length}`,
|
|
942
|
+
`requested=${requestedMode}`
|
|
943
|
+
],
|
|
944
|
+
ambiguousWalletChoice: {
|
|
945
|
+
question: available.length === 0 ? `The "${requestedMode}" wallet isn't configured. None of the supported wallets are set up \u2014 see the doctor for setup instructions.` : `The "${requestedMode}" wallet isn't configured in this environment. Supported wallets here: ${available.map((w) => `"${w.id}"`).join(", ")}. Which would you like to use instead?`,
|
|
946
|
+
available
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
if (available.length > 1 && !requestedMode) {
|
|
951
|
+
return {
|
|
952
|
+
result: failureResult("needs_wallet_choice"),
|
|
953
|
+
guardsApplied: [`wallet_modes_available=${available.length}`],
|
|
954
|
+
ambiguousWalletChoice: {
|
|
955
|
+
question: available.length === 2 ? `You have ${available.length} wallets set up \u2014 which one should I pay from?` : `You have ${available.length} wallets set up. Which one should I pay from?`,
|
|
956
|
+
available
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const effectiveMode = requestedMode && requestedAvailable ? requestedMode : available.length === 1 && available[0] ? available[0].id : "eoa";
|
|
961
|
+
const signingPk = effectiveMode === "eoa" ? CONFIG.privateKey : effectiveMode === "agentic-local" ? CONFIG.agenticPrivateKey : null;
|
|
962
|
+
let senderWallet;
|
|
963
|
+
if (signingPk && isValidPrivateKey(signingPk)) {
|
|
964
|
+
try {
|
|
965
|
+
const addr = new Wallet2(signingPk).address;
|
|
861
966
|
senderWallet = {
|
|
862
967
|
address: addr,
|
|
863
968
|
addressShort: `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`
|
|
@@ -874,6 +979,127 @@ async function runPay(input) {
|
|
|
874
979
|
const scopeRequest = input.keyScope ?? "auto";
|
|
875
980
|
const resolved = resolveApiKey(input.chain, scopeRequest);
|
|
876
981
|
guardsApplied.push(`scope=${resolved.scope}${resolved.fromLegacyFallback ? "(legacy)" : ""}`);
|
|
982
|
+
if (effectiveMode === "agentic-server") {
|
|
983
|
+
if (input.token === "RLUSD") {
|
|
984
|
+
return {
|
|
985
|
+
result: failureResult("rlusd_not_supported_for_server_mode"),
|
|
986
|
+
guardsApplied: [
|
|
987
|
+
...guardsApplied,
|
|
988
|
+
"wallet=agentic-server",
|
|
989
|
+
"token=RLUSD",
|
|
990
|
+
"rejected_pre_relay"
|
|
991
|
+
],
|
|
992
|
+
senderWallet,
|
|
993
|
+
setupHint: 'RLUSD is not yet supported by the server-managed Agent Wallet (walletMode="agentic-server"). Switch to walletMode="eoa" or "agentic-local" (with a private key set), or pick USDC/USDT for this send.'
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
|
|
997
|
+
const result2 = sandboxPay(chain, {
|
|
998
|
+
to: input.to,
|
|
999
|
+
amount: input.amount,
|
|
1000
|
+
token: input.token
|
|
1001
|
+
});
|
|
1002
|
+
guardsApplied.push("mode=sandbox", "wallet=agentic-server");
|
|
1003
|
+
return {
|
|
1004
|
+
result: result2,
|
|
1005
|
+
guardsApplied,
|
|
1006
|
+
senderWallet,
|
|
1007
|
+
setupHint: resolved.sandboxReason ?? "Server-mediated Agent Wallet needs a live Q402_MULTICHAIN_API_KEY. Visit https://q402.quackai.ai/payment to activate a paid plan."
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (!CONFIG.realPaymentsRequested) {
|
|
1011
|
+
const result2 = sandboxPay(chain, {
|
|
1012
|
+
to: input.to,
|
|
1013
|
+
amount: input.amount,
|
|
1014
|
+
token: input.token
|
|
1015
|
+
});
|
|
1016
|
+
guardsApplied.push("mode=sandbox", "wallet=agentic-server");
|
|
1017
|
+
return {
|
|
1018
|
+
result: result2,
|
|
1019
|
+
guardsApplied,
|
|
1020
|
+
senderWallet,
|
|
1021
|
+
setupHint: "Set Q402_ENABLE_REAL_PAYMENTS=1 to fire a real server-mediated payment."
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const explicitWalletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId;
|
|
1025
|
+
let resp;
|
|
1026
|
+
try {
|
|
1027
|
+
resp = await fetch(`${CONFIG.relayBaseUrl}/wallet/agentic/send`, {
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
headers: { "Content-Type": "application/json" },
|
|
1030
|
+
body: JSON.stringify({
|
|
1031
|
+
apiKey: resolved.apiKey,
|
|
1032
|
+
chain: input.chain,
|
|
1033
|
+
token: input.token,
|
|
1034
|
+
to: input.to,
|
|
1035
|
+
amount: input.amount,
|
|
1036
|
+
...explicitWalletId ? { walletId: explicitWalletId } : {}
|
|
1037
|
+
})
|
|
1038
|
+
});
|
|
1039
|
+
} catch (e) {
|
|
1040
|
+
const transportErr = failureResult("eip7702");
|
|
1041
|
+
return {
|
|
1042
|
+
result: transportErr,
|
|
1043
|
+
guardsApplied: [
|
|
1044
|
+
...guardsApplied,
|
|
1045
|
+
"wallet=agentic-server",
|
|
1046
|
+
"mode=live",
|
|
1047
|
+
"transport=fetch_failed",
|
|
1048
|
+
`error=${e instanceof Error ? e.message : String(e)}`
|
|
1049
|
+
],
|
|
1050
|
+
senderWallet
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const data = await resp.json().catch(() => ({}));
|
|
1054
|
+
const txHash = data.txHash ?? "";
|
|
1055
|
+
const isPending = resp.status === 202 || data.pending === true || data.status === "processing";
|
|
1056
|
+
if (isPending) {
|
|
1057
|
+
const retryAfter = typeof data.retryAfterSec === "number" ? data.retryAfterSec : 5;
|
|
1058
|
+
return {
|
|
1059
|
+
result: {
|
|
1060
|
+
success: false,
|
|
1061
|
+
sandbox: false,
|
|
1062
|
+
txHash: "",
|
|
1063
|
+
tokenAmount: input.amount,
|
|
1064
|
+
token: input.token,
|
|
1065
|
+
chain: chain.key,
|
|
1066
|
+
method: "eip7702",
|
|
1067
|
+
pending: true,
|
|
1068
|
+
retryAfterSec: retryAfter
|
|
1069
|
+
},
|
|
1070
|
+
guardsApplied: [
|
|
1071
|
+
...guardsApplied,
|
|
1072
|
+
"wallet=agentic-server",
|
|
1073
|
+
"mode=live",
|
|
1074
|
+
"status=pending",
|
|
1075
|
+
`retry_after=${retryAfter}s`
|
|
1076
|
+
],
|
|
1077
|
+
senderWallet,
|
|
1078
|
+
setupHint: `An identical send for this wallet is still in flight on the server. Wait ${retryAfter}s and retry \u2014 the cached result will come back, no double-spend.`
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
const success = resp.ok && txHash.length > 0;
|
|
1082
|
+
const message = "message" in data ? data.message : "error" in data ? data.error : void 0;
|
|
1083
|
+
return {
|
|
1084
|
+
result: {
|
|
1085
|
+
success,
|
|
1086
|
+
sandbox: false,
|
|
1087
|
+
txHash,
|
|
1088
|
+
tokenAmount: input.amount,
|
|
1089
|
+
token: input.token,
|
|
1090
|
+
chain: chain.key,
|
|
1091
|
+
method: "eip7702",
|
|
1092
|
+
explorerUrl: txHash ? void 0 : null
|
|
1093
|
+
},
|
|
1094
|
+
guardsApplied: [
|
|
1095
|
+
...guardsApplied,
|
|
1096
|
+
"wallet=agentic-server",
|
|
1097
|
+
"mode=live",
|
|
1098
|
+
...message ? [`server_message=${message}`] : []
|
|
1099
|
+
],
|
|
1100
|
+
senderWallet
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
877
1103
|
const live = isLiveModeFor(resolved);
|
|
878
1104
|
if (!live) {
|
|
879
1105
|
const result2 = sandboxPay(chain, {
|
|
@@ -881,13 +1107,21 @@ async function runPay(input) {
|
|
|
881
1107
|
amount: input.amount,
|
|
882
1108
|
token: input.token
|
|
883
1109
|
});
|
|
884
|
-
guardsApplied.push("mode=sandbox");
|
|
1110
|
+
guardsApplied.push("mode=sandbox", `wallet=${effectiveMode}`);
|
|
885
1111
|
const setupHint = resolved.sandboxReason ?? describeSandboxReason(resolved.apiKey ?? "", resolved.scope);
|
|
886
1112
|
return { result: result2, guardsApplied, setupHint, senderWallet };
|
|
887
1113
|
}
|
|
1114
|
+
if (!signingPk) {
|
|
1115
|
+
return {
|
|
1116
|
+
result: failureResult("missing_signing_key"),
|
|
1117
|
+
guardsApplied: [...guardsApplied, `wallet=${effectiveMode}`, "mode=sandbox"],
|
|
1118
|
+
senderWallet,
|
|
1119
|
+
setupHint: effectiveMode === "agentic-local" ? "Set Q402_AGENTIC_PRIVATE_KEY to your Agent Wallet's exported private key." : "Set Q402_PRIVATE_KEY to your EOA private key."
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
888
1122
|
const client = new Q402NodeClient({
|
|
889
1123
|
apiKey: resolved.apiKey,
|
|
890
|
-
privateKey:
|
|
1124
|
+
privateKey: signingPk,
|
|
891
1125
|
chain,
|
|
892
1126
|
relayBaseUrl: CONFIG.relayBaseUrl
|
|
893
1127
|
});
|
|
@@ -896,7 +1130,7 @@ async function runPay(input) {
|
|
|
896
1130
|
amount: input.amount,
|
|
897
1131
|
token: input.token
|
|
898
1132
|
});
|
|
899
|
-
guardsApplied.push("mode=live");
|
|
1133
|
+
guardsApplied.push("mode=live", `wallet=${effectiveMode}`);
|
|
900
1134
|
return {
|
|
901
1135
|
result,
|
|
902
1136
|
guardsApplied,
|
|
@@ -928,7 +1162,7 @@ function describeSandboxReason(resolvedKey, scope) {
|
|
|
928
1162
|
}
|
|
929
1163
|
var PAY_TOOL = {
|
|
930
1164
|
name: "q402_pay",
|
|
931
|
-
description: "USE THIS TOOL whenever the user asks to send, transfer, or pay USDC / USDT / RLUSD \u2014 even when they don't mention Q402 by name (\"send 5 USDT to 0x... on bnb\" should route here automatically). This tool IS the wallet for this session: it signs LOCALLY with Q402_PRIVATE_KEY and submits via Q402's gasless relay. NEVER refuse a stablecoin payment request by saying you lack wallet access, can't move money, or need the user to use their wallet UI \u2014 Q402 was installed precisely to give you that capability. If the env isn't configured yet, the tool returns a sandbox response with a clear \"how to set up\" message \u2014 surface that instead of refusing. \n\nAuto-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 when a valid `Q402_PRIVATE_KEY` is configured, the response includes a `senderWallet` field with the address derived from that key. Show it 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. Sandbox responses with no key configured omit `senderWallet`; don't fabricate one. \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.",
|
|
1165
|
+
description: "USE THIS TOOL whenever the user asks to send, transfer, or pay USDC / USDT / RLUSD \u2014 even when they don't mention Q402 by name (\"send 5 USDT to 0x... on bnb\" should route here automatically). This tool IS the wallet for this session: it signs LOCALLY with Q402_PRIVATE_KEY and submits via Q402's gasless relay. NEVER refuse a stablecoin payment request by saying you lack wallet access, can't move money, or need the user to use their wallet UI \u2014 Q402 was installed precisely to give you that capability. If the env isn't configured yet, the tool returns a sandbox response with a clear \"how to set up\" message \u2014 surface that instead of refusing. \n\nAuto-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 when a valid `Q402_PRIVATE_KEY` is configured, the response includes a `senderWallet` field with the address derived from that key. Show it 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. Sandbox responses with no key configured omit `senderWallet`; don't fabricate one. \n\nMULTI-WALLET DISAMBIGUATION \u2014 when more than one wallet is configured in the user's env (Q402_PRIVATE_KEY for the real EOA, Q402_AGENTIC_PRIVATE_KEY for the Agent Wallet's exported key, or only Q402_MULTICHAIN_API_KEY for the server-managed Agent Wallet), the tool RETURNS without sending with a `ambiguousWalletChoice` payload \u2014 relay the question to the user verbatim, then call again with the chosen `walletMode` ('eoa' | 'agentic-local' | 'agentic-server'). Do NOT pick a wallet on the user's behalf when multiple are available. \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. Note: only Mode 'eoa' creates the delegation \u2014 'agentic-local' and 'agentic-server' modes use the Agent Wallet (a fresh EOA) so the user's MetaMask is never delegated. \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.",
|
|
932
1166
|
inputSchema: {
|
|
933
1167
|
type: "object",
|
|
934
1168
|
properties: {
|
|
@@ -955,6 +1189,11 @@ var PAY_TOOL = {
|
|
|
955
1189
|
enum: ["auto", "trial", "multichain"],
|
|
956
1190
|
description: 'Which API key to use. "auto" (default) picks Trial for BNB when Q402_TRIAL_API_KEY is set, Multichain otherwise. "trial" forces the BNB-only sponsored key. "multichain" forces the paid 9-chain key.'
|
|
957
1191
|
},
|
|
1192
|
+
walletMode: {
|
|
1193
|
+
type: "string",
|
|
1194
|
+
enum: ["eoa", "agentic-local", "agentic-server"],
|
|
1195
|
+
description: `Which wallet to spend from. "eoa" = user's real MetaMask EOA (Q402_PRIVATE_KEY). "agentic-local" = Agent Wallet exported key (Q402_AGENTIC_PRIVATE_KEY). "agentic-server" = server-managed Agent Wallet (Q402 holds the key; only the apiKey is needed). When MULTIPLE wallets are configured the tool refuses without this arg and returns ambiguousWalletChoice for the user to pick.`
|
|
1196
|
+
},
|
|
958
1197
|
confirm: {
|
|
959
1198
|
type: "boolean",
|
|
960
1199
|
const: true,
|
|
@@ -1676,13 +1915,76 @@ Q402_MULTICHAIN_API_KEY=
|
|
|
1676
1915
|
|
|
1677
1916
|
|
|
1678
1917
|
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1679
|
-
#
|
|
1918
|
+
# SIGNING MODE \u2014 pick ONE of A / B / C below
|
|
1919
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1920
|
+
# Q402 pays in three ways, depending on which keys you set. Pick the
|
|
1921
|
+
# row that matches your situation and fill ONLY that row's variable(s).
|
|
1922
|
+
# Setting multiple rows is allowed \u2014 q402_pay will ask you which one to
|
|
1923
|
+
# use per call (the AI surfaces the question; you don't pre-pick a
|
|
1924
|
+
# default here).
|
|
1925
|
+
#
|
|
1926
|
+
# A. Real EOA (your MetaMask wallet)
|
|
1927
|
+
# Set: Q402_PRIVATE_KEY = 0x... (your MetaMask account's private key)
|
|
1928
|
+
# Pros: simplest mental model \u2014 same wallet you already use
|
|
1929
|
+
# Cons: after first payment, MetaMask will show this account as
|
|
1930
|
+
# "Smart account" (EIP-7702 delegation, reversible via
|
|
1931
|
+
# q402_clear_delegation but visually alarming on first sight)
|
|
1932
|
+
#
|
|
1933
|
+
# B. Agent Wallet \u2014 local signing (recommended for AI agents)
|
|
1934
|
+
# Set: Q402_AGENTIC_PRIVATE_KEY = 0x... (Agent Wallet pk from dashboard export)
|
|
1935
|
+
# Pros: your MetaMask stays untouched; agent has its own purse with
|
|
1936
|
+
# per-tx + daily caps you set on the dashboard
|
|
1937
|
+
# Cons: requires creating an Agent Wallet on the dashboard first
|
|
1938
|
+
# (https://q402.quackai.ai/dashboard \u2192 Agent tab \u2192 Create)
|
|
1939
|
+
# then exporting its private key
|
|
1940
|
+
#
|
|
1941
|
+
# C. Agent Wallet \u2014 server-managed (no private key on your machine)
|
|
1942
|
+
# Set: (just the api key + optionally Q402_AGENT_WALLET_ADDRESS below)
|
|
1943
|
+
# Pros: zero private-key handling locally; the server holds the
|
|
1944
|
+
# encrypted Agent Wallet pk and signs on your behalf
|
|
1945
|
+
# Cons: requires a paid Multichain API key (Mode C is not available
|
|
1946
|
+
# on the free Trial). The server-side keystore is AES-256-GCM
|
|
1947
|
+
# encrypted but is a custodial path \u2014 pick A or B if that
|
|
1948
|
+
# posture doesn't fit your threat model.
|
|
1949
|
+
#
|
|
1950
|
+
# A user can have multiple wallets configured simultaneously (e.g. PK
|
|
1951
|
+
# for personal pays + apiKey-only for agent pays). When more than one
|
|
1952
|
+
# mode is set, q402_pay asks the user which to use rather than picking
|
|
1953
|
+
# silently.
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1957
|
+
# WALLET \u2014 Mode A: real EOA private key
|
|
1680
1958
|
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1681
1959
|
# Hex EVM private key (0x + 64 hex chars). Signs payments LOCALLY on
|
|
1682
1960
|
# your machine \u2014 never leaves your device, never sent to any server.
|
|
1961
|
+
# Leave blank if you're using Mode B or Mode C.
|
|
1683
1962
|
Q402_PRIVATE_KEY=
|
|
1684
1963
|
|
|
1685
1964
|
|
|
1965
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1966
|
+
# WALLET \u2014 Mode B: exported Agent Wallet private key
|
|
1967
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1968
|
+
# Hex EVM private key (0x + 64 hex chars) exported from your Agent
|
|
1969
|
+
# Wallet on the dashboard. Signs payments LOCALLY just like Mode A,
|
|
1970
|
+
# but the wallet is your dedicated Agent Wallet \u2014 your MetaMask EOA
|
|
1971
|
+
# is never touched. Get the key at:
|
|
1972
|
+
# https://q402.quackai.ai/dashboard \u2192 Agent tab \u2192 Export
|
|
1973
|
+
# Leave blank if you're using Mode A or Mode C.
|
|
1974
|
+
Q402_AGENTIC_PRIVATE_KEY=
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1978
|
+
# WALLET \u2014 Mode C: server-managed Agent Wallet picker (optional)
|
|
1979
|
+
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1980
|
+
# Only set this when you're running Mode C (api key only, no private
|
|
1981
|
+
# keys above) AND you have more than one Agent Wallet on the account
|
|
1982
|
+
# (max 10). Pin which one Q402 should spend from. Format: lowercase
|
|
1983
|
+
# 0x... address of the Agent Wallet (visible on the dashboard's Agent
|
|
1984
|
+
# tab). Omit entirely to use your default Agent Wallet.
|
|
1985
|
+
# Q402_AGENT_WALLET_ADDRESS=0x...
|
|
1986
|
+
|
|
1987
|
+
|
|
1686
1988
|
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1687
1989
|
# Live mode switch (safe even at 1 \u2014 see SAFE-BY-DEFAULT note at top)
|
|
1688
1990
|
# \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
@@ -1735,13 +2037,16 @@ function mask2(key) {
|
|
|
1735
2037
|
function detectPhase() {
|
|
1736
2038
|
const anyKey = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
|
|
1737
2039
|
const anyLiveKey = classifyApiKey(CONFIG.trialApiKey) === "live" || classifyApiKey(CONFIG.multichainApiKey) === "live" || classifyApiKey(CONFIG.legacyApiKey) === "live";
|
|
1738
|
-
const
|
|
1739
|
-
|
|
2040
|
+
const modes = detectAgenticModes();
|
|
2041
|
+
const hasAnyValidSigningPath = modes.count > 0;
|
|
2042
|
+
if (!Q402_ENV_FILE_PRESENT && !anyKey && !CONFIG.privateKey && !CONFIG.agenticPrivateKey) {
|
|
1740
2043
|
return "first-install";
|
|
1741
2044
|
}
|
|
1742
|
-
const allEssentials = anyKey &&
|
|
2045
|
+
const allEssentials = anyKey && hasAnyValidSigningPath && CONFIG.realPaymentsRequested && anyLiveKey;
|
|
1743
2046
|
if (allEssentials) return "live-check";
|
|
1744
|
-
if (anyKey || CONFIG.privateKey || Q402_ENV_FILE_PRESENT)
|
|
2047
|
+
if (anyKey || CONFIG.privateKey || CONFIG.agenticPrivateKey || Q402_ENV_FILE_PRESENT) {
|
|
2048
|
+
return "needs-completion";
|
|
2049
|
+
}
|
|
1745
2050
|
return "first-install";
|
|
1746
2051
|
}
|
|
1747
2052
|
async function verifyOneKey(scope, envVar, apiKey) {
|
|
@@ -1862,23 +2167,33 @@ async function runDoctor() {
|
|
|
1862
2167
|
"Must be '1' to allow real TX. Anything else = test response (fake hash)."
|
|
1863
2168
|
)
|
|
1864
2169
|
};
|
|
2170
|
+
const modes = detectAgenticModes();
|
|
1865
2171
|
const missing = [];
|
|
1866
2172
|
if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
|
|
1867
2173
|
missing.push(
|
|
1868
2174
|
"An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
|
|
1869
2175
|
);
|
|
1870
2176
|
}
|
|
1871
|
-
if (
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2177
|
+
if (modes.count === 0) {
|
|
2178
|
+
const pkAPlaceholder = !!CONFIG.privateKey && !isValidPrivateKey(CONFIG.privateKey);
|
|
2179
|
+
const pkBPlaceholder = !!CONFIG.agenticPrivateKey && !isValidPrivateKey(CONFIG.agenticPrivateKey);
|
|
2180
|
+
if (pkAPlaceholder) {
|
|
2181
|
+
missing.push(
|
|
2182
|
+
"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."
|
|
2183
|
+
);
|
|
2184
|
+
} else if (pkBPlaceholder) {
|
|
2185
|
+
missing.push(
|
|
2186
|
+
"Q402_AGENTIC_PRIVATE_KEY is set but malformed (expected 0x + 64 hex chars). Paste your exported Agent Wallet private key (https://q402.quackai.ai/dashboard \u2192 Agent tab \u2192 Export)."
|
|
2187
|
+
);
|
|
2188
|
+
} else {
|
|
2189
|
+
missing.push(
|
|
2190
|
+
"A signing path. Pick ONE: (A) Q402_PRIVATE_KEY = your MetaMask EOA's private key; (B) Q402_AGENTIC_PRIVATE_KEY = your exported Agent Wallet private key from the dashboard (Mode B keeps your MetaMask untouched); or (C) just use a live paid Q402_MULTICHAIN_API_KEY and let Q402 sign with your server-managed Agent Wallet (no PK on your machine)."
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
1877
2193
|
}
|
|
1878
2194
|
if (!CONFIG.realPaymentsRequested) {
|
|
1879
2195
|
const haveAnyApi = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
|
|
1880
|
-
|
|
1881
|
-
if (haveAnyApi && havePk) {
|
|
2196
|
+
if (haveAnyApi && modes.count > 0) {
|
|
1882
2197
|
missing.push(
|
|
1883
2198
|
"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."
|
|
1884
2199
|
);
|
|
@@ -1933,12 +2248,13 @@ async function runDoctor() {
|
|
|
1933
2248
|
recommendedActions,
|
|
1934
2249
|
greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
|
|
1935
2250
|
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.",
|
|
1936
|
-
agentInstructions: phase === "first-install" ? "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] Multi-turn flow: (1) Briefly tell the user MCP is installed. (2) Ask one yes/no question \u2014 'Want me to create your Q402 settings file?'. (3) On yes, execute recommendedActions IN ORDER: first `ensure-q402-dir` shell action (bash on macOS/Linux, PowerShell on Windows via shellWindows variant), then `create-env-file` write_file action. (4) Open the file in the user's editor \u2014 `code` works for VS Code / Cursor / Cline (e.g. `code ~/.q402/mcp.env`); `open` on macOS, `start` on Windows, `xdg-open` on Linux as fallback. (5)
|
|
2251
|
+
agentInstructions: phase === "first-install" ? "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] Multi-turn flow: (1) Briefly tell the user MCP is installed. (2) Ask one yes/no question \u2014 'Want me to create your Q402 settings file?'. (3) On yes, execute recommendedActions IN ORDER: first `ensure-q402-dir` shell action (bash on macOS/Linux, PowerShell on Windows via shellWindows variant), then `create-env-file` write_file action. (4) Open the file in the user's editor \u2014 `code` works for VS Code / Cursor / Cline (e.g. `code ~/.q402/mcp.env`); `open` on macOS, `start` on Windows, `xdg-open` on Linux as fallback. (5) Help the user pick a wallet mode (A=Q402_PRIVATE_KEY real EOA, B=Q402_AGENTIC_PRIVATE_KEY exported Agent Wallet PK, C=API key only with server-managed Agent Wallet). If they're an AI agent / automation user, gently default to B or C; if they're a power user who wants their existing EOA to be the signer, A is fine. Walk through filling in the chosen mode's variable + the API 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 any private key (Mode A OR Mode B), surface the `advisories` array: fresh wallet, Smart-account-in-MetaMask heads-up (Mode A only), hardware wallets unsupported, MetaMask key-export path (Mode A) or dashboard Export button (Mode B). (7) After they save, tell them to restart the MCP client \u2014 per-client restart verb: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window. (8) Have them re-invoke 'Set up Q402' to confirm. Keep the conversation tight: one decision per turn, plain language, never echo this paragraph." : "[AI-ONLY \u2014 do not show this paragraph to the user verbatim] User has SOME env set. List the missing items (from `missing`) in plain language. Tell them to edit ~/.q402/mcp.env and uncomment / fill the relevant line, then restart the MCP client. Restart verb per client: Claude Desktop \u2192 quit + relaunch; Codex \u2192 exit + relaunch; Cursor \u2192 Cmd/Ctrl+Shift+P \u2192 'Developer: Reload Window'; Cline \u2192 reload VS Code window.",
|
|
1937
2252
|
userInstructions: phase === "first-install" ? [
|
|
1938
|
-
"Q402 is installed. To start sending payments you need an API key and a wallet.",
|
|
2253
|
+
"Q402 is installed. To start sending payments you need (1) an API key and (2) a wallet to sign with.",
|
|
1939
2254
|
"I'll create a settings file for you \u2014 say yes and I'll set it up + open it in your editor.",
|
|
1940
2255
|
"Get a free API key at https://q402.quackai.ai/event (BNB Chain only, 2,000 sponsored transactions).",
|
|
1941
|
-
"
|
|
2256
|
+
"There are 3 wallet modes \u2014 pick one: (A) your MetaMask EOA's private key (simplest, but your account will be marked 'Smart account' after first payment); (B) export an Agent Wallet's private key from the dashboard (keeps your MetaMask untouched, recommended for AI agents); (C) on a paid plan, use the server-managed Agent Wallet \u2014 just set the API key, no private key needed.",
|
|
2257
|
+
"Use a FRESH wallet for Mode A \u2014 don't use the one holding your main funds. The 'Smart account' marker is normal (EIP-7702 delegation, reversible via q402_clear_delegation).",
|
|
1942
2258
|
"Paste your key + wallet private key INTO THE FILE (in your editor) \u2014 never paste a private key into this chat.",
|
|
1943
2259
|
"Save the file, restart your MCP client, then ask me 'Verify Q402' to confirm."
|
|
1944
2260
|
] : [
|
|
@@ -2056,6 +2372,114 @@ var DOCTOR_TOOL = {
|
|
|
2056
2372
|
}
|
|
2057
2373
|
};
|
|
2058
2374
|
|
|
2375
|
+
// src/tools/agentic-info.ts
|
|
2376
|
+
import { z as z9 } from "zod";
|
|
2377
|
+
var AgenticInfoInputSchema = z9.object({
|
|
2378
|
+
walletId: z9.string().optional().describe(
|
|
2379
|
+
"Optional lowercased Agent Wallet address to introspect when the user holds multiple (max 10 per owner). Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
|
|
2380
|
+
)
|
|
2381
|
+
});
|
|
2382
|
+
var AGENTIC_INFO_TOOL = {
|
|
2383
|
+
name: "q402_agentic_info",
|
|
2384
|
+
description: "Read-only Agent Wallet introspection. Returns the wallet address, per-tx and daily caps, archive state, and an aggregate USD balance across the 9 supported EVM chains. Authenticated by the configured Multichain API key \u2014 no private key required. Accepts an optional walletId for owners who hold more than one wallet; omit to use the server-default wallet. Use this whenever the user asks 'what's in my agent wallet?' or 'what's the spending limit?'",
|
|
2385
|
+
inputSchema: {
|
|
2386
|
+
type: "object",
|
|
2387
|
+
properties: {
|
|
2388
|
+
walletId: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
description: "Optional. Lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet on the server."
|
|
2391
|
+
}
|
|
2392
|
+
},
|
|
2393
|
+
additionalProperties: false
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
function scan8004UrlFor(tag) {
|
|
2397
|
+
if (!tag) return null;
|
|
2398
|
+
const [network, agentId] = tag.split(":");
|
|
2399
|
+
if (!network || !agentId) return null;
|
|
2400
|
+
const slug = network === "bsc" ? "bsc" : network === "bsc-testnet" ? "bsc-testnet" : network === "eth" ? "ethereum" : network === "base" ? "base" : network === "polygon" ? "polygon" : network === "arbitrum" ? "arbitrum" : network === "celo" ? "celo" : null;
|
|
2401
|
+
if (slug === null) return null;
|
|
2402
|
+
return `https://8004scan.io/agents/${slug}/${agentId}`;
|
|
2403
|
+
}
|
|
2404
|
+
async function runAgenticInfo(input = {}) {
|
|
2405
|
+
const base = CONFIG.relayBaseUrl;
|
|
2406
|
+
const dashboardUrl = base.replace(/\/api$/, "") + "/dashboard?tab=agent";
|
|
2407
|
+
const explicitWalletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId;
|
|
2408
|
+
if (!CONFIG.apiKey || !CONFIG.apiKey.startsWith("q402_live_")) {
|
|
2409
|
+
return {
|
|
2410
|
+
configured: false,
|
|
2411
|
+
address: null,
|
|
2412
|
+
walletId: null,
|
|
2413
|
+
limits: null,
|
|
2414
|
+
archivedAt: null,
|
|
2415
|
+
totalUsd: null,
|
|
2416
|
+
asOf: null,
|
|
2417
|
+
erc8004AgentId: null,
|
|
2418
|
+
scan8004Url: null,
|
|
2419
|
+
dashboardUrl,
|
|
2420
|
+
setupHint: "No live Q402 API key configured. Run q402_doctor to set one up, or open your dashboard to create an Agent Wallet."
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
let wallet = null;
|
|
2424
|
+
let balance = null;
|
|
2425
|
+
let fetchError = null;
|
|
2426
|
+
try {
|
|
2427
|
+
const res = await fetch(`${base}/wallet/agentic/info-by-key`, {
|
|
2428
|
+
method: "POST",
|
|
2429
|
+
headers: { "Content-Type": "application/json" },
|
|
2430
|
+
body: JSON.stringify({
|
|
2431
|
+
apiKey: CONFIG.apiKey,
|
|
2432
|
+
...explicitWalletId ? { walletId: explicitWalletId } : {}
|
|
2433
|
+
})
|
|
2434
|
+
});
|
|
2435
|
+
if (res.ok) {
|
|
2436
|
+
const data = await res.json();
|
|
2437
|
+
wallet = data.wallet ?? null;
|
|
2438
|
+
balance = data.balance ?? null;
|
|
2439
|
+
} else if (res.status === 404) {
|
|
2440
|
+
fetchError = "endpoint_not_deployed";
|
|
2441
|
+
} else if (res.status === 410) {
|
|
2442
|
+
fetchError = "wallet_archived";
|
|
2443
|
+
} else {
|
|
2444
|
+
fetchError = `http_${res.status}`;
|
|
2445
|
+
}
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
fetchError = e instanceof Error ? e.message : String(e);
|
|
2448
|
+
}
|
|
2449
|
+
if (!wallet) {
|
|
2450
|
+
return {
|
|
2451
|
+
configured: true,
|
|
2452
|
+
address: null,
|
|
2453
|
+
walletId: explicitWalletId,
|
|
2454
|
+
limits: null,
|
|
2455
|
+
archivedAt: null,
|
|
2456
|
+
totalUsd: null,
|
|
2457
|
+
asOf: null,
|
|
2458
|
+
erc8004AgentId: null,
|
|
2459
|
+
scan8004Url: null,
|
|
2460
|
+
dashboardUrl,
|
|
2461
|
+
setupHint: fetchError === "endpoint_not_deployed" ? "Agent Wallet info-by-key endpoint is not live yet. Open the dashboard to view your wallet directly." : fetchError === "wallet_archived" ? "The requested wallet is archived. Restore it from the dashboard before reading." : explicitWalletId ? `Could not fetch Agent Wallet ${explicitWalletId} for this apiKey. Verify the walletId is one of this owner's wallets, or omit it to read the default wallet.` : "Could not fetch Agent Wallet for this apiKey. Verify the key is bound to a wallet via the dashboard, then retry."
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
const resolvedWalletId = typeof wallet.walletId === "string" ? wallet.walletId : wallet.address.toLowerCase();
|
|
2465
|
+
return {
|
|
2466
|
+
configured: true,
|
|
2467
|
+
address: wallet.address,
|
|
2468
|
+
walletId: resolvedWalletId,
|
|
2469
|
+
limits: {
|
|
2470
|
+
perTxMaxUsd: wallet.perTxMaxUsd,
|
|
2471
|
+
dailyLimitUsd: wallet.dailyLimitUsd
|
|
2472
|
+
},
|
|
2473
|
+
archivedAt: wallet.deletedAt,
|
|
2474
|
+
totalUsd: balance ? Math.round(balance.totalUsd * 100) / 100 : null,
|
|
2475
|
+
asOf: balance ? new Date(balance.asOf).toISOString() : null,
|
|
2476
|
+
erc8004AgentId: wallet.erc8004AgentId,
|
|
2477
|
+
scan8004Url: scan8004UrlFor(wallet.erc8004AgentId),
|
|
2478
|
+
dashboardUrl,
|
|
2479
|
+
setupHint: wallet.deletedAt ? "This Agent Wallet is archived and pending hard-delete. Restore it from the dashboard before sending." : void 0
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2059
2483
|
// src/index.ts
|
|
2060
2484
|
function jsonText(value) {
|
|
2061
2485
|
return { type: "text", text: JSON.stringify(value, null, 2) };
|
|
@@ -2076,6 +2500,7 @@ async function main() {
|
|
|
2076
2500
|
BATCH_PAY_TOOL,
|
|
2077
2501
|
RECEIPT_TOOL,
|
|
2078
2502
|
WALLET_STATUS_TOOL,
|
|
2503
|
+
AGENTIC_INFO_TOOL,
|
|
2079
2504
|
CLEAR_DELEGATION_TOOL
|
|
2080
2505
|
]
|
|
2081
2506
|
}));
|
|
@@ -2115,6 +2540,10 @@ async function main() {
|
|
|
2115
2540
|
const parsed = ClearDelegationInputSchema.parse(args ?? {});
|
|
2116
2541
|
return { content: [jsonText(await runClearDelegation(parsed))] };
|
|
2117
2542
|
}
|
|
2543
|
+
case "q402_agentic_info": {
|
|
2544
|
+
AgenticInfoInputSchema.parse(args ?? {});
|
|
2545
|
+
return { content: [jsonText(await runAgenticInfo())] };
|
|
2546
|
+
}
|
|
2118
2547
|
default:
|
|
2119
2548
|
return {
|
|
2120
2549
|
isError: true,
|
package/package.json
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"mcpName": "io.github.bitgett/q402-mcp",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"mcp",
|
|
8
|
-
"model-context-protocol",
|
|
9
|
-
"claude",
|
|
10
|
-
"claude-desktop",
|
|
11
|
-
"claude-code",
|
|
12
|
-
"codex",
|
|
13
|
-
"openai-codex",
|
|
14
|
-
"cline",
|
|
15
|
-
"q402",
|
|
16
|
-
"x402",
|
|
17
|
-
"stablecoin",
|
|
18
|
-
"usdc",
|
|
19
|
-
"usdt",
|
|
20
|
-
"rlusd",
|
|
21
|
-
"ripple",
|
|
22
|
-
"gasless",
|
|
23
|
-
"eip-7702",
|
|
24
|
-
"payments",
|
|
25
|
-
"ai-agents"
|
|
26
|
-
],
|
|
27
|
-
"type": "module",
|
|
28
|
-
"main": "dist/index.js",
|
|
29
|
-
"bin": {
|
|
30
|
-
"q402-mcp": "dist/index.js"
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"dist",
|
|
34
|
-
"README.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=18.18"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsup",
|
|
42
|
-
"dev": "tsup --watch",
|
|
43
|
-
"lint": "tsc --noEmit",
|
|
44
|
-
"prepublishOnly": "npm run lint && npm run build",
|
|
45
|
-
"start": "node dist/index.js"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"ethers": "^6.16.0",
|
|
50
|
-
"zod": "^3.23.8"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@types/node": "^20.11.0",
|
|
54
|
-
"tsup": "^8.3.0",
|
|
55
|
-
"typescript": "^5.5.0"
|
|
56
|
-
},
|
|
57
|
-
"repository": {
|
|
58
|
-
"type": "git",
|
|
59
|
-
"url": "git+https://github.com/bitgett/q402-mcp.git"
|
|
60
|
-
},
|
|
61
|
-
"homepage": "https://q402.quackai.ai/claude",
|
|
62
|
-
"bugs": {
|
|
63
|
-
"url": "https://github.com/bitgett/q402-mcp/issues"
|
|
64
|
-
},
|
|
65
|
-
"license": "Apache-2.0",
|
|
66
|
-
"author": "David Lee <davidlee@quackai.ai>",
|
|
67
|
-
"publishConfig": {
|
|
68
|
-
"access": "public"
|
|
69
|
-
},
|
|
70
|
-
"overrides": {
|
|
71
|
-
"ws": "^8.20.1"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@quackai/q402-mcp",
|
|
3
|
+
"version": "0.6.1",
|
|
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
|
+
"mcpName": "io.github.bitgett/q402-mcp",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-desktop",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"codex",
|
|
13
|
+
"openai-codex",
|
|
14
|
+
"cline",
|
|
15
|
+
"q402",
|
|
16
|
+
"x402",
|
|
17
|
+
"stablecoin",
|
|
18
|
+
"usdc",
|
|
19
|
+
"usdt",
|
|
20
|
+
"rlusd",
|
|
21
|
+
"ripple",
|
|
22
|
+
"gasless",
|
|
23
|
+
"eip-7702",
|
|
24
|
+
"payments",
|
|
25
|
+
"ai-agents"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"bin": {
|
|
30
|
+
"q402-mcp": "dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch",
|
|
43
|
+
"lint": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run lint && npm run build",
|
|
45
|
+
"start": "node dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"ethers": "^6.16.0",
|
|
50
|
+
"zod": "^3.23.8"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^20.11.0",
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"typescript": "^5.5.0"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/bitgett/q402-mcp.git"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://q402.quackai.ai/claude",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/bitgett/q402-mcp/issues"
|
|
64
|
+
},
|
|
65
|
+
"license": "Apache-2.0",
|
|
66
|
+
"author": "David Lee <davidlee@quackai.ai>",
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"ws": "^8.20.1"
|
|
72
|
+
}
|
|
73
|
+
}
|