@quackai/q402-mcp 0.8.22 → 0.8.24
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 +131 -15
- package/package.json +73 -75
package/dist/index.js
CHANGED
|
@@ -211,7 +211,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
|
211
211
|
// package.json
|
|
212
212
|
var package_default = {
|
|
213
213
|
name: "@quackai/q402-mcp",
|
|
214
|
-
version: "0.8.
|
|
214
|
+
version: "0.8.24",
|
|
215
215
|
description: "MCP server for Q402 \u2014 gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
|
|
216
216
|
mcpName: "io.github.bitgett/q402-mcp",
|
|
217
217
|
keywords: [
|
|
@@ -262,7 +262,7 @@ var package_default = {
|
|
|
262
262
|
},
|
|
263
263
|
devDependencies: {
|
|
264
264
|
"@types/node": "^20.11.0",
|
|
265
|
-
tsup: "^8.
|
|
265
|
+
tsup: "^8.5.1",
|
|
266
266
|
typescript: "^5.5.0"
|
|
267
267
|
},
|
|
268
268
|
repository: {
|
|
@@ -279,9 +279,7 @@ var package_default = {
|
|
|
279
279
|
access: "public"
|
|
280
280
|
},
|
|
281
281
|
overrides: {
|
|
282
|
-
|
|
283
|
-
qs: "^6.15.2",
|
|
284
|
-
hono: "^4.12.21"
|
|
282
|
+
esbuild: "^0.28.1"
|
|
285
283
|
}
|
|
286
284
|
};
|
|
287
285
|
|
|
@@ -927,6 +925,29 @@ function sandboxPay(chain, input) {
|
|
|
927
925
|
};
|
|
928
926
|
}
|
|
929
927
|
|
|
928
|
+
// src/consent.ts
|
|
929
|
+
import { sha256, toUtf8Bytes } from "ethers";
|
|
930
|
+
function canonicalIntent(intent) {
|
|
931
|
+
const sortValue = (v) => {
|
|
932
|
+
if (Array.isArray(v)) return v.map(sortValue);
|
|
933
|
+
if (v && typeof v === "object") {
|
|
934
|
+
const src = v;
|
|
935
|
+
const out = {};
|
|
936
|
+
for (const k of Object.keys(src).sort()) out[k] = sortValue(src[k]);
|
|
937
|
+
return out;
|
|
938
|
+
}
|
|
939
|
+
return v;
|
|
940
|
+
};
|
|
941
|
+
return JSON.stringify(sortValue(intent));
|
|
942
|
+
}
|
|
943
|
+
function consentTokenFor(intent) {
|
|
944
|
+
return "ct_" + sha256(toUtf8Bytes(canonicalIntent(intent))).slice(2, 18);
|
|
945
|
+
}
|
|
946
|
+
function checkConsent(intent, provided) {
|
|
947
|
+
const expected = consentTokenFor(intent);
|
|
948
|
+
return { ok: provided === expected, expected };
|
|
949
|
+
}
|
|
950
|
+
|
|
930
951
|
// src/tools/pay.ts
|
|
931
952
|
var PayInputSchema = z2.object({
|
|
932
953
|
chain: z2.enum(["avax", "bnb", "eth", "xlayer", "stable", "mantle", "injective", "monad", "scroll", "arbitrum"]),
|
|
@@ -951,6 +972,9 @@ When MORE THAN ONE wallet is configured in the user's environment, you MUST ask
|
|
|
951
972
|
confirm: z2.literal(true).describe(
|
|
952
973
|
"MUST be true. Prove the user explicitly approved this exact payment in the conversation right before this tool was called. When hookParams is set you MUST confirm what it actually does to the money: the split RECIPIENTS and their shares (funds go to those addresses, not `to`), and any oracle condition gating the settlement \u2014 not just the top-level recipient and amount. Setting this to true on behalf of the user without that confirmation is a violation of the tool contract."
|
|
953
974
|
),
|
|
975
|
+
consentToken: z2.string().optional().describe(
|
|
976
|
+
'Two-phase consent. LEAVE THIS UNSET on the first call: the tool will NOT send \u2014 it returns status="needs_confirmation" with a human-readable `preview` of the exact payment and a `consentToken`. Relay that preview to the user verbatim, get their explicit yes, then call again with the SAME args plus this `consentToken`. The tool re-derives the token from the params it is about to execute and refuses on mismatch, so you cannot preview one payment and execute another. Never fabricate a token.'
|
|
977
|
+
),
|
|
954
978
|
hookParams: z2.object({
|
|
955
979
|
recipientAgentId: z2.string().optional().describe("ReputationGate: the recipient's ERC-8004 agent id."),
|
|
956
980
|
condition: z2.object({
|
|
@@ -1086,6 +1110,29 @@ async function runPay(input) {
|
|
|
1086
1110
|
if (CONFIG.allowedRecipients.length > 0) {
|
|
1087
1111
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
1088
1112
|
}
|
|
1113
|
+
const consentIntent = {
|
|
1114
|
+
t: "pay",
|
|
1115
|
+
chain: input.chain,
|
|
1116
|
+
to: input.to.toLowerCase(),
|
|
1117
|
+
amount: input.amount,
|
|
1118
|
+
token: input.token,
|
|
1119
|
+
...input.hookParams?.splits ? { splits: input.hookParams.splits.map((s) => ({ r: s.recipient.toLowerCase(), bps: s.bps })) } : {}
|
|
1120
|
+
};
|
|
1121
|
+
const consent = checkConsent(consentIntent, input.consentToken);
|
|
1122
|
+
if (!consent.ok) {
|
|
1123
|
+
const splitNote = input.hookParams?.splits ? ` \u2014 split ${input.hookParams.splits.length} ways; funds go to the split recipients, not ${input.to}` : "";
|
|
1124
|
+
const fromNote = senderWallet ? ` from ${senderWallet.addressShort}` : "";
|
|
1125
|
+
return {
|
|
1126
|
+
result: failureResult("consent"),
|
|
1127
|
+
guardsApplied: [...guardsApplied, "two_phase_consent"],
|
|
1128
|
+
senderWallet,
|
|
1129
|
+
needsConsent: {
|
|
1130
|
+
status: "needs_confirmation",
|
|
1131
|
+
preview: `Send ${input.amount} ${input.token} to ${input.to} on ${chain.key}${fromNote}${splitNote}. Confirm with the user, then re-call q402_pay with the same args plus consentToken="${consent.expected}".`,
|
|
1132
|
+
consentToken: consent.expected
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1089
1136
|
const scopeRequest = input.keyScope ?? "auto";
|
|
1090
1137
|
const resolved = resolveApiKey(input.chain, scopeRequest);
|
|
1091
1138
|
guardsApplied.push(`scope=${resolved.scope}${resolved.fromLegacyFallback ? "(legacy)" : ""}`);
|
|
@@ -1325,7 +1372,7 @@ function describeSandboxReason(resolvedKey, scope) {
|
|
|
1325
1372
|
}
|
|
1326
1373
|
var PAY_TOOL = {
|
|
1327
1374
|
name: "q402_pay",
|
|
1328
|
-
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 10-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, arbitrum \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.",
|
|
1375
|
+
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 10-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, arbitrum \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. \n\nTWO-PHASE CONSENT: confirm:true alone does NOT send. Call this tool first WITHOUT consentToken \u2014 it returns status=\"needs_confirmation\" with a `preview` of the exact payment and a `consentToken`, and moves no money. Relay that preview to the user, get their explicit yes, then re-call with the SAME args plus that `consentToken` to execute. The token is re-derived from the params about to run, so a previewed payment can't be swapped for a different one.",
|
|
1329
1376
|
inputSchema: {
|
|
1330
1377
|
type: "object",
|
|
1331
1378
|
properties: {
|
|
@@ -1427,9 +1474,13 @@ var BatchPayInputSchema = z3.object({
|
|
|
1427
1474
|
),
|
|
1428
1475
|
confirm: z3.literal(true).describe(
|
|
1429
1476
|
"MUST be true. The user must have explicitly approved this exact set of recipients, amounts, chain, and token in the conversation right before this tool was called. Setting confirm=true on behalf of the user without that approval is a violation of the tool contract."
|
|
1477
|
+
),
|
|
1478
|
+
consentToken: z3.string().optional().describe(
|
|
1479
|
+
'Two-phase consent. LEAVE UNSET on the first call: the tool will NOT send \u2014 it returns status="needs_confirmation" with a `setupHint` preview of every recipient + amount and a `consentToken`. Relay that preview to the user, get an explicit yes, then re-call with the SAME args plus this `consentToken`. The tool re-derives it from the batch it is about to send and refuses on mismatch, so you cannot preview one batch and execute another. Never fabricate a token.'
|
|
1430
1480
|
)
|
|
1431
1481
|
});
|
|
1432
1482
|
function maxAmountGuardBatch(recipients, cap) {
|
|
1483
|
+
let total = 0;
|
|
1433
1484
|
for (let i = 0; i < recipients.length; i++) {
|
|
1434
1485
|
const r = recipients[i];
|
|
1435
1486
|
const numeric = Number(r.amount);
|
|
@@ -1441,6 +1492,12 @@ function maxAmountGuardBatch(recipients, cap) {
|
|
|
1441
1492
|
`recipients[${i}]: amount $${r.amount} exceeds the per-call cap of $${cap}. Set Q402_MAX_AMOUNT_PER_CALL to a higher value if intentional.`
|
|
1442
1493
|
);
|
|
1443
1494
|
}
|
|
1495
|
+
total += numeric;
|
|
1496
|
+
}
|
|
1497
|
+
if (total > cap) {
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`batch total $${total.toFixed(2)} across ${recipients.length} recipients exceeds the per-call cap of $${cap}. Q402_MAX_AMOUNT_PER_CALL bounds the WHOLE batch, not each row. Raise the cap if this batch is intentional, or split it into smaller batches.`
|
|
1500
|
+
);
|
|
1444
1501
|
}
|
|
1445
1502
|
}
|
|
1446
1503
|
function recipientAllowlistGuardBatch(recipients, allow) {
|
|
@@ -1464,11 +1521,31 @@ async function runBatchPay(input) {
|
|
|
1464
1521
|
}
|
|
1465
1522
|
const guardsApplied = [];
|
|
1466
1523
|
maxAmountGuardBatch(input.recipients, CONFIG.maxAmountPerCallUsd);
|
|
1467
|
-
guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd} (per
|
|
1524
|
+
guardsApplied.push(`max_amount<=${CONFIG.maxAmountPerCallUsd} (per row AND batch total)`);
|
|
1468
1525
|
recipientAllowlistGuardBatch(input.recipients, CONFIG.allowedRecipients);
|
|
1469
1526
|
if (CONFIG.allowedRecipients.length > 0) {
|
|
1470
1527
|
guardsApplied.push(`recipient_allowlist[${CONFIG.allowedRecipients.length}]`);
|
|
1471
1528
|
}
|
|
1529
|
+
const consentIntent = {
|
|
1530
|
+
t: "batch",
|
|
1531
|
+
chain: input.chain,
|
|
1532
|
+
token: input.token,
|
|
1533
|
+
recipients: input.recipients.map((r) => ({ to: r.to.toLowerCase(), amount: r.amount }))
|
|
1534
|
+
};
|
|
1535
|
+
const consent = checkConsent(consentIntent, input.consentToken);
|
|
1536
|
+
if (!consent.ok) {
|
|
1537
|
+
const total = input.recipients.reduce((s, r) => s + Number(r.amount), 0);
|
|
1538
|
+
const lines = input.recipients.map((r, i) => ` ${i + 1}. ${r.amount} ${input.token} -> ${r.to}`).join("\n");
|
|
1539
|
+
return {
|
|
1540
|
+
mode: "none",
|
|
1541
|
+
status: "needs_confirmation",
|
|
1542
|
+
guardsApplied: [...guardsApplied, "two_phase_consent"],
|
|
1543
|
+
consentToken: consent.expected,
|
|
1544
|
+
setupHint: `Batch on ${input.chain}: ${input.recipients.length} recipients, total ${total} ${input.token}.
|
|
1545
|
+
${lines}
|
|
1546
|
+
Confirm the full list with the user, then re-call q402_batch_pay with the same args plus consentToken="${consent.expected}".`
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1472
1549
|
const modes = detectAgenticModes(CONFIG);
|
|
1473
1550
|
const available = [];
|
|
1474
1551
|
if (modes.modeA && CONFIG.privateKey && isValidPrivateKey(CONFIG.privateKey)) {
|
|
@@ -1811,7 +1888,9 @@ Send gasless payments to MULTIPLE recipients on a single chain \xD7 token in one
|
|
|
1811
1888
|
|
|
1812
1889
|
MULTI-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 firing with \`status='needs_wallet_choice'\` and an \`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. Server-mediated batches go through /api/wallet/agentic/batch and are paid-only (the trial key cannot batch).
|
|
1813
1890
|
|
|
1814
|
-
ALWAYS get explicit user confirmation of the complete recipient + amount list, chain, and token in conversation immediately before calling this tool \u2014 the user must approve the full batch, not the individual rows
|
|
1891
|
+
ALWAYS get explicit user confirmation of the complete recipient + amount list, chain, and token in conversation immediately before calling this tool \u2014 the user must approve the full batch, not the individual rows.
|
|
1892
|
+
|
|
1893
|
+
TWO-PHASE CONSENT: confirm:true alone does NOT send. Call this tool first WITHOUT consentToken \u2014 it returns status="needs_confirmation" with a \`setupHint\` preview of every recipient + amount and a \`consentToken\`, and moves no money. Relay that preview to the user, get an explicit yes, then re-call with the SAME args plus the \`consentToken\` to execute. The token is re-derived from the batch about to run, so the previewed batch can't be swapped.`,
|
|
1815
1894
|
inputSchema: {
|
|
1816
1895
|
type: "object",
|
|
1817
1896
|
properties: {
|
|
@@ -1953,7 +2032,7 @@ var BALANCE_TOOL = {
|
|
|
1953
2032
|
|
|
1954
2033
|
// src/tools/receipt.ts
|
|
1955
2034
|
import { z as z5 } from "zod";
|
|
1956
|
-
import { keccak256, toUtf8Bytes, getBytes, verifyMessage } from "ethers";
|
|
2035
|
+
import { keccak256, toUtf8Bytes as toUtf8Bytes2, getBytes, verifyMessage } from "ethers";
|
|
1957
2036
|
var ReceiptShape = z5.object({
|
|
1958
2037
|
receiptId: z5.string(),
|
|
1959
2038
|
createdAt: z5.string(),
|
|
@@ -1994,8 +2073,9 @@ function canonicalize(fields) {
|
|
|
1994
2073
|
return JSON.stringify(sorted);
|
|
1995
2074
|
}
|
|
1996
2075
|
function digest(canonical) {
|
|
1997
|
-
return keccak256(
|
|
2076
|
+
return keccak256(toUtf8Bytes2(canonical));
|
|
1998
2077
|
}
|
|
2078
|
+
var RELAYER_SIGNER = "0xfc77ff29178b7286a8ba703d7a70895ca74ff466";
|
|
1999
2079
|
function verifyReceiptSignature(r) {
|
|
2000
2080
|
try {
|
|
2001
2081
|
const fields = {
|
|
@@ -2012,7 +2092,7 @@ function verifyReceiptSignature(r) {
|
|
|
2012
2092
|
sandbox: r.sandbox
|
|
2013
2093
|
};
|
|
2014
2094
|
const recovered = verifyMessage(getBytes(digest(canonicalize(fields))), r.signature).toLowerCase();
|
|
2015
|
-
return recovered === r.signedBy.toLowerCase();
|
|
2095
|
+
return recovered === RELAYER_SIGNER && r.signedBy.toLowerCase() === RELAYER_SIGNER;
|
|
2016
2096
|
} catch {
|
|
2017
2097
|
return false;
|
|
2018
2098
|
}
|
|
@@ -3091,6 +3171,9 @@ var BridgeSendInputSchema = z11.object({
|
|
|
3091
3171
|
maxFeeRaw: z11.string().regex(/^\d+$/).optional().describe("Optional client-side fee cap in raw 18-dec wei. Server still clamps to its 10% slippage ceiling; clients may LOWER but not RAISE."),
|
|
3092
3172
|
confirm: z11.boolean().optional().describe(
|
|
3093
3173
|
"MUST be true to fire a LIVE bridge (ignored in sandbox). Set this only after the user has explicitly approved this exact bridge (src, dst, amount, feeToken) in the conversation. When omitted or false on a live call the tool previews the action and does NOT move any funds. Never set confirm:true on the user's behalf without approval."
|
|
3174
|
+
),
|
|
3175
|
+
consentToken: z11.string().optional().describe(
|
|
3176
|
+
"Two-phase consent. LEAVE UNSET on the first live call: the tool previews the bridge (without moving funds) and returns a `consentToken`. Relay the preview to the user, get an explicit yes, then re-call with sandbox:false, confirm:true, AND this consentToken. The tool re-derives it from the bridge it is about to execute (src, dst, amount, feeToken) and refuses on mismatch."
|
|
3094
3177
|
)
|
|
3095
3178
|
}).refine((d) => d.src !== d.dst, {
|
|
3096
3179
|
// Local Zod rejection saves a network round-trip + a Q402 backend log
|
|
@@ -3101,7 +3184,7 @@ var BridgeSendInputSchema = z11.object({
|
|
|
3101
3184
|
});
|
|
3102
3185
|
var BRIDGE_SEND_TOOL = {
|
|
3103
3186
|
name: "q402_bridge_send",
|
|
3104
|
-
description: "Execute a Chainlink CCIP USDC bridge across the 3-chain triangle (eth/avax/arbitrum) on behalf of the user's server-managed Agentic Wallet (Mode C). Sandbox-by-default \u2014 returns a synthetic messageId unless `sandbox: false` is passed AND Q402_ENABLE_REAL_PAYMENTS=1 AND a live Multichain API key is configured. The server signs ccipSend with the Agent Wallet's encrypted PK, auto-funds source-chain gas from the user's Gas Tank, and debits both the auto- fund cost and the CCIP fee per the bridge's settled receipt.
|
|
3187
|
+
description: "Execute a Chainlink CCIP USDC bridge across the 3-chain triangle (eth/avax/arbitrum) on behalf of the user's server-managed Agentic Wallet (Mode C). Sandbox-by-default \u2014 returns a synthetic messageId unless `sandbox: false` is passed AND Q402_ENABLE_REAL_PAYMENTS=1 AND a live Multichain API key is configured. The server signs ccipSend with the Agent Wallet's encrypted PK, auto-funds source-chain gas from the user's Gas Tank, and debits both the auto- fund cost and the CCIP fee per the bridge's settled receipt. TWO-PHASE CONSENT \u2014 a LIVE bridge (sandbox: false) refuses to execute unless BOTH confirm: true AND a matching consentToken are set. Call it first WITHOUT consentToken to get a preview (src, dst, amount, fee token) plus a consentToken; show that to the user, get explicit approval, THEN re-call with sandbox: false, confirm: true, AND that consentToken. The token is re-derived from the bridge about to run, so the previewed bridge can't be swapped. Never fabricate a token. Recommended flow: q402_bridge_quote first \u2192 preview + confirm cost with the user \u2192 q402_bridge_send with sandbox: false, confirm: true, consentToken. Live mode needs a Multichain subscription; trial keys are rejected. If the bridge returns AGENT_WALLET_DELEGATED, clear the delegation first: server-managed Agent Wallets (Mode C / API key) use the Clear delegation button on the dashboard; local-key modes (Q402_PRIVATE_KEY set) can run q402_clear_delegation.",
|
|
3105
3188
|
inputSchema: {
|
|
3106
3189
|
type: "object",
|
|
3107
3190
|
properties: {
|
|
@@ -3141,6 +3224,10 @@ var BRIDGE_SEND_TOOL = {
|
|
|
3141
3224
|
confirm: {
|
|
3142
3225
|
type: "boolean",
|
|
3143
3226
|
description: "MUST be true to fire a LIVE bridge (ignored in sandbox) \u2014 set only after the user explicitly approved this exact bridge in chat. Omit (or false) on a live call to preview without moving funds."
|
|
3227
|
+
},
|
|
3228
|
+
consentToken: {
|
|
3229
|
+
type: "string",
|
|
3230
|
+
description: "Two-phase consent token. Leave unset on the first live call to get a preview + token; re-call with confirm:true AND this token after the user approves. Bound to (src, dst, amount, feeToken) \u2014 re-derived server-side-of-the-tool and refused on mismatch."
|
|
3144
3231
|
}
|
|
3145
3232
|
},
|
|
3146
3233
|
required: ["src", "dst", "amount"]
|
|
@@ -3170,13 +3257,21 @@ async function runBridgeSend(input) {
|
|
|
3170
3257
|
}]
|
|
3171
3258
|
};
|
|
3172
3259
|
}
|
|
3173
|
-
|
|
3260
|
+
const consentIntent = {
|
|
3261
|
+
t: "bridge",
|
|
3262
|
+
src: input.src,
|
|
3263
|
+
dst: input.dst,
|
|
3264
|
+
amount: input.amount,
|
|
3265
|
+
feeToken: input.feeToken === "native" ? "native" : "LINK"
|
|
3266
|
+
};
|
|
3267
|
+
const consent = checkConsent(consentIntent, input.consentToken);
|
|
3268
|
+
if (input.confirm !== true || !consent.ok) {
|
|
3174
3269
|
const walletDesc = typeof input.walletId === "string" && input.walletId.length > 0 ? `wallet ${input.walletId.toLowerCase()}` : "your default Agent Wallet";
|
|
3175
3270
|
const fee = input.feeToken === "native" ? "native" : "LINK";
|
|
3176
3271
|
return {
|
|
3177
3272
|
content: [{
|
|
3178
3273
|
type: "text",
|
|
3179
|
-
text: `Will bridge ${input.amount} raw USDC units from ${input.src}
|
|
3274
|
+
text: `Will bridge ${input.amount} raw USDC units from ${input.src} -> ${input.dst} via Chainlink CCIP from ${walletDesc} (fee paid in ${fee}). This MOVES FUNDS on-chain. Confirm with the user, then re-call with sandbox:false, confirm:true, AND consentToken="${consent.expected}".`
|
|
3180
3275
|
}]
|
|
3181
3276
|
};
|
|
3182
3277
|
}
|
|
@@ -4007,7 +4102,7 @@ var RecurringCreateInputSchema = z19.object({
|
|
|
4007
4102
|
});
|
|
4008
4103
|
var RECURRING_CREATE_TOOL = {
|
|
4009
4104
|
name: "q402_recurring_create",
|
|
4010
|
-
description: "Author a new recurring-payment rule on the user's Agent Wallet. Single-recipient (use the dashboard for multi-recipient payroll). Pick a cadence \u2014 hourly:N, daily, weekly:{day}, monthly:N, or monthly:last \u2014 and a recipient + amount + chain + token. Authenticated by the configured Multichain API key; no private key required. Recurring requires the paid Multichain subscription on EVERY chain including bnb \u2014 trial keys are rejected at create time with MULTICHAIN_REQUIRED and should keep using q402_pay for one-shot Trial sends. Each fire is bounded by the wallet's perTxMax
|
|
4105
|
+
description: "Author a new recurring-payment rule on the user's Agent Wallet. Single-recipient (use the dashboard for multi-recipient payroll). Pick a cadence \u2014 hourly:N, daily, weekly:{day}, monthly:N, or monthly:last \u2014 and a recipient + amount + chain + token. Authenticated by the configured Multichain API key; no private key required. Recurring requires the paid Multichain subscription on EVERY chain including bnb \u2014 trial keys are rejected at create time with MULTICHAIN_REQUIRED and should keep using q402_pay for one-shot Trial sends. Each fire is bounded server-side by BOTH the wallet's perTxMax AND its dailyLimit \u2014 a rule's daily total reserves against the same daily bucket as manual sends (the scheduler skips the fire if the bucket can't cover it), so scheduled rules can't outrun the dashboard caps. This tool also enforces your local Q402_MAX_AMOUNT_PER_CALL + Q402_ALLOWED_RECIPIENTS rails at create time. The user can stop a rule any time via q402_recurring_cancel.",
|
|
4011
4106
|
inputSchema: {
|
|
4012
4107
|
type: "object",
|
|
4013
4108
|
properties: {
|
|
@@ -4082,6 +4177,27 @@ async function runRecurringCreate(input) {
|
|
|
4082
4177
|
dashboardUrl
|
|
4083
4178
|
};
|
|
4084
4179
|
}
|
|
4180
|
+
const amountNum = Number(input.amount);
|
|
4181
|
+
if (Number.isFinite(amountNum) && amountNum > CONFIG.maxAmountPerCallUsd) {
|
|
4182
|
+
return {
|
|
4183
|
+
ok: false,
|
|
4184
|
+
walletId: null,
|
|
4185
|
+
rule: null,
|
|
4186
|
+
error: "AMOUNT_EXCEEDS_CAP",
|
|
4187
|
+
message: `Per-fire amount $${input.amount} exceeds your Q402_MAX_AMOUNT_PER_CALL cap of $${CONFIG.maxAmountPerCallUsd}. Each recurring fire is bounded by the same per-call cap as a one-shot q402_pay \u2014 raise the cap if this schedule is intentional.`,
|
|
4188
|
+
dashboardUrl
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
if (CONFIG.allowedRecipients.length > 0 && !CONFIG.allowedRecipients.includes(input.recipient.toLowerCase())) {
|
|
4192
|
+
return {
|
|
4193
|
+
ok: false,
|
|
4194
|
+
walletId: null,
|
|
4195
|
+
rule: null,
|
|
4196
|
+
error: "RECIPIENT_NOT_ALLOWED",
|
|
4197
|
+
message: `Recipient ${input.recipient} is not in Q402_ALLOWED_RECIPIENTS. A recurring rule would send to it on every fire \u2014 add it to the allowlist or unset the env var to disable the guard.`,
|
|
4198
|
+
dashboardUrl
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4085
4201
|
const explicitWalletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId;
|
|
4086
4202
|
try {
|
|
4087
4203
|
const res = await fetch(`${base}/wallet/agentic/recurring-by-key`, {
|
package/package.json
CHANGED
|
@@ -1,75 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "MCP server for Q402 — gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, 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.
|
|
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
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@quackai/q402-mcp",
|
|
3
|
+
"version": "0.8.24",
|
|
4
|
+
"description": "MCP server for Q402 — gasless USDC/USDT/RLUSD payments on 10 EVM chains + Chainlink CCIP USDC bridge on the eth/avax/arbitrum triangle, 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.5.1",
|
|
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
|
+
"esbuild": "^0.28.1"
|
|
72
|
+
}
|
|
73
|
+
}
|