@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.
Files changed (2) hide show
  1. package/dist/index.js +131 -15
  2. 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.22",
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.3.0",
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
- ws: "^8.20.1",
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 recipient)`);
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(toUtf8Bytes(canonical));
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. REQUIRES CONFIRMATION \u2014 like q402_pay and q402_yield_deposit, a LIVE bridge (sandbox: false) refuses to execute unless confirm: true is set. Call it first WITHOUT confirm to get a one-line preview (src, dst, amount, fee token); show that to the user, get explicit approval, THEN re-call with sandbox: false AND confirm: true. Never set confirm: true on the user's behalf. Recommended flow: q402_bridge_quote first \u2192 preview + confirm cost with the user \u2192 q402_bridge_send with sandbox: false, confirm: true. 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.",
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
- if (input.confirm !== true) {
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} \u2192 ${input.dst} via Chainlink CCIP from ${walletDesc} (fee paid in ${fee}). This MOVES FUNDS on-chain. Re-call with confirm:true to execute.`
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 (configured on the dashboard) \u2014 the dashboard's dailyLimit cap currently applies to manual sends only, NOT recurring fires, so an attacker with the apiKey could schedule N rules at perTxMax and drain the wallet's USDC balance over time. The user can stop a rule any time via q402_recurring_cancel.",
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.22",
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.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
- "qs": "^6.15.2",
73
- "hono": "^4.12.21"
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
+ }