@quackai/q402-mcp 0.8.19 → 0.8.21

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 +102 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -130,8 +130,7 @@ function loadConfig() {
130
130
  const walletId = typeof walletIdRaw === "string" && walletIdRaw.length > 0 ? walletIdRaw.toLowerCase() : null;
131
131
  const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
132
132
  const anyLiveKey = classifyApiKey(trialApiKey) === "live" || classifyApiKey(multichainApiKey) === "live" || classifyApiKey(legacyApiKey) === "live";
133
- const hasAnySignerKey = typeof privateKey === "string" && privateKey.length > 0 || typeof agenticPrivateKey === "string" && agenticPrivateKey.length > 0;
134
- const live = realPaymentsRequested && anyLiveKey && hasAnySignerKey;
133
+ const live = realPaymentsRequested && anyLiveKey;
135
134
  return {
136
135
  trialApiKey,
137
136
  multichainApiKey,
@@ -212,7 +211,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
212
211
  // package.json
213
212
  var package_default = {
214
213
  name: "@quackai/q402-mcp",
215
- version: "0.8.19",
214
+ version: "0.8.21",
216
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.",
217
216
  mcpName: "io.github.bitgett/q402-mcp",
218
217
  keywords: [
@@ -1648,6 +1647,21 @@ async function runBatchPay(input) {
1648
1647
  const data = await resp.json().catch(() => ({}));
1649
1648
  if (!resp.ok) {
1650
1649
  const errMsg = data && typeof data === "object" && "error" in data ? String(data.error) : `relay_http_${resp.status}`;
1650
+ if (data.status === "uncertain") {
1651
+ return {
1652
+ mode: "live",
1653
+ status: "settlement_uncertain",
1654
+ guardsApplied: [
1655
+ ...guardsApplied,
1656
+ "wallet=agentic-server",
1657
+ "mode=live",
1658
+ `http=${resp.status}`
1659
+ ],
1660
+ senderWallet,
1661
+ error: errMsg,
1662
+ setupHint: "The relay did not confirm whether these payments settled \u2014 they MAY have been sent. DO NOT retry this batch; verify the recipients' on-chain balances first. Re-sending could double-pay."
1663
+ };
1664
+ }
1651
1665
  return {
1652
1666
  mode: "live",
1653
1667
  status: "aborted",
@@ -3414,7 +3428,7 @@ var YieldPositionsInputSchema = z15.object({
3414
3428
  });
3415
3429
  var YIELD_POSITIONS_TOOL = {
3416
3430
  name: "q402_yield_positions",
3417
- description: "READ-ONLY \u2014 show the Agent Wallet's current Q402 Yield (Aave) lending positions. Returns each position's protocol, chain, asset, market address, balance, principal, accrued interest, and supply APY, plus the aggregate total supplied in USD. Authenticated by the configured live Multichain API key \u2014 no private key required and no funds move. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today. walletId is OPTIONAL: omit it and the server reads the owner's default Agent Wallet (resolved from the API key); pass one only when the owner holds more than one wallet. An optional chain filter is also accepted. Use this whenever the user asks 'how much am I earning?' or 'what are my open lending positions?'",
3431
+ description: "READ-ONLY \u2014 show the Agent Wallet's current Q402 Yield (Aave) lending positions. Returns each position's protocol, chain, asset, market address, CURRENT supplied position value (the live aToken balance, in token units), and live supply APY, plus the aggregate current value in USD. Authenticated by the configured live Multichain API key \u2014 no private key required and no funds move. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today. DOES NOT report principal or accrued earnings as separate numbers \u2014 the aToken balance already includes accrued interest but is not broken out, so do NOT claim a specific 'earnings/profit/interest earned' figure from this tool; report only the current position value and the APY. walletId is OPTIONAL: omit it and the server reads the owner's default Agent Wallet (resolved from the API key); pass one only when the owner holds more than one wallet. An optional chain filter is also accepted. Use this whenever the user asks 'what is my position worth?', 'what's my current yield balance / APY?', or 'what are my open lending positions?'",
3418
3432
  inputSchema: {
3419
3433
  type: "object",
3420
3434
  properties: {
@@ -3483,7 +3497,7 @@ async function runYieldPositions(input) {
3483
3497
  const unavailableChains = Array.isArray(data.unavailableChains) ? data.unavailableChains : [];
3484
3498
  const hasUnavailable = data.unavailable === true || unavailableChains.length > 0;
3485
3499
  const unavailableNote = hasUnavailable ? ` Some chains could not be read this cycle${unavailableChains.length ? ` (${unavailableChains.join(", ")})` : ""} \u2014 this snapshot may be incomplete; retry before treating a balance as zero.` : "";
3486
- const summary = positions.length ? `Total supplied: $${(data.totalSuppliedUsd ?? 0).toFixed(2)} across ${positions.length} position(s).${unavailableNote}` : hasUnavailable ? `No positions returned, but a read failure occurred${unavailableChains.length ? ` on ${unavailableChains.join(", ")}` : ""} \u2014 this is NOT a confirmed empty position; retry.` : "No open Q402 Yield positions for this wallet.";
3500
+ const summary = positions.length ? `Total current position value: $${(data.totalSuppliedUsd ?? 0).toFixed(2)} across ${positions.length} position(s) (live aToken balance incl. accrued interest; earnings are not broken out).${unavailableNote}` : hasUnavailable ? `No positions returned, but a read failure occurred${unavailableChains.length ? ` on ${unavailableChains.join(", ")}` : ""} \u2014 this is NOT a confirmed empty position; retry.` : "No open Q402 Yield positions for this wallet.";
3487
3501
  return {
3488
3502
  content: [
3489
3503
  { type: "text", text: summary },
@@ -3491,11 +3505,23 @@ async function runYieldPositions(input) {
3491
3505
  type: "text",
3492
3506
  text: JSON.stringify({
3493
3507
  walletId: data.walletId ?? walletId ?? null,
3508
+ // Surface only what's real: the current position value (live aToken
3509
+ // balance) + APY. `principal`/`accrued` come back null from this
3510
+ // Phase-0 read, so they're included ONLY when the server actually
3511
+ // populated them — never as a null placeholder that reads like a
3512
+ // real (zero) earnings figure.
3494
3513
  positions: positions.map((p) => ({
3495
- ...p,
3496
- supplyApyPct: Math.round(p.supplyApy * 100 * 100) / 100
3514
+ protocol: p.protocol,
3515
+ chain: p.chain,
3516
+ asset: p.asset,
3517
+ marketAddress: p.marketAddress,
3518
+ currentValue: p.balance,
3519
+ supplyApy: p.supplyApy,
3520
+ supplyApyPct: Math.round(p.supplyApy * 100 * 100) / 100,
3521
+ ...p.principal != null ? { principal: p.principal } : {},
3522
+ ...p.accrued != null ? { accrued: p.accrued } : {}
3497
3523
  })),
3498
- totalSuppliedUsd: data.totalSuppliedUsd ?? null,
3524
+ totalCurrentValueUsd: data.totalSuppliedUsd ?? null,
3499
3525
  asOf: data.asOf ?? null,
3500
3526
  unavailable: hasUnavailable,
3501
3527
  unavailableChains
@@ -3516,7 +3542,7 @@ var YieldDepositInputSchema = z16.object({
3516
3542
  "Optional Agent Wallet address to supply from (max 10 per owner). Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet (resolved server-side from the API key)."
3517
3543
  ),
3518
3544
  idempotencyKey: z16.string().optional().describe(
3519
- "Optional durable idempotency key for this logical deposit. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct deposit (two intentional same-amount deposits both run, and a re-deposit after a withdrawal is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-supplying."
3545
+ 'Optional durable idempotency key for this logical deposit. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct deposit (two intentional same-amount deposits both run, and a re-deposit after a withdrawal is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-supplying. IMPORTANT: if a call returns status="uncertain" (timeout / unconfirmed broadcast), it echoes back the idempotencyKey it used \u2014 pass THAT exact value here to safely resume the same deposit instead of starting a new one.'
3520
3546
  ),
3521
3547
  confirm: z16.boolean().optional().describe(
3522
3548
  "MUST be true to actually supply funds. Set this only after the user has explicitly approved this exact deposit (amount, token, chain, wallet) in the conversation. When omitted or false the tool previews the action and does NOT move any funds."
@@ -3524,7 +3550,7 @@ var YieldDepositInputSchema = z16.object({
3524
3550
  });
3525
3551
  var YIELD_DEPOSIT_TOOL = {
3526
3552
  name: "q402_yield_deposit",
3527
- description: "WRITE \u2014 MOVES FUNDS. Supplies the Agent Wallet's stablecoin (USDC / USDT) into Aave V3 (Q402 Yield) so it starts earning supply APY. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave supply, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user's behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain supply) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nUse q402_yield_reserves first to show available markets + APY, and q402_yield_positions afterward to confirm the supplied balance.",
3553
+ description: "WRITE \u2014 MOVES FUNDS. Supplies the Agent Wallet's stablecoin (USDC / USDT) into Aave V3 (Q402 Yield) so it starts earning supply APY. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave supply, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user's behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain supply) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nRETRY SAFETY \u2014 on a timeout or an unconfirmed broadcast the tool returns status=\"uncertain\" and echoes back the idempotencyKey it used. The deposit MAY have settled, so do NOT blindly call again \u2014 that starts a NEW deposit and can double-supply. To resume the SAME operation, re-call with idempotencyKey set to the echoed value; the server dedupes on it and replays the original result. \n\nUse q402_yield_reserves first to show available markets + APY, and q402_yield_positions afterward to confirm the supplied balance.",
3528
3554
  inputSchema: {
3529
3555
  type: "object",
3530
3556
  properties: {
@@ -3548,7 +3574,7 @@ var YIELD_DEPOSIT_TOOL = {
3548
3574
  },
3549
3575
  idempotencyKey: {
3550
3576
  type: "string",
3551
- description: "Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct deposit. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-supplying."
3577
+ description: 'Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct deposit. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-supplying. If a call returns status="uncertain", it echoes the idempotencyKey it used \u2014 pass that exact value back here to resume the same deposit rather than start a new one.'
3552
3578
  },
3553
3579
  confirm: {
3554
3580
  type: "boolean",
@@ -3631,13 +3657,43 @@ async function runYieldDeposit(input) {
3631
3657
  return {
3632
3658
  content: [{
3633
3659
  type: "text",
3634
- text: `Yield deposit failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3660
+ text: JSON.stringify({
3661
+ status: "uncertain",
3662
+ success: false,
3663
+ action: "yield_deposit",
3664
+ chain: input.chain,
3665
+ token: input.token,
3666
+ amount: input.amount,
3667
+ idempotencyKey,
3668
+ error: e instanceof Error ? e.message : String(e),
3669
+ message: `Network error before a confirmed response \u2014 the deposit may or may not have been submitted. Do NOT start a new deposit. To safely resume THIS operation, retry with idempotencyKey="${idempotencyKey}" (the server dedupes on it, so a retry that already landed replays the original result instead of double-supplying).`
3670
+ }, null, 2)
3635
3671
  }],
3636
3672
  isError: true
3637
3673
  };
3638
3674
  }
3639
3675
  const data = await res.json().catch(() => ({}));
3640
3676
  if (!res.ok) {
3677
+ if (res.status === 502) {
3678
+ return {
3679
+ content: [{
3680
+ type: "text",
3681
+ text: JSON.stringify({
3682
+ status: "uncertain",
3683
+ success: false,
3684
+ action: "yield_deposit",
3685
+ chain: input.chain,
3686
+ token: input.token,
3687
+ amount: input.amount,
3688
+ idempotencyKey,
3689
+ txHash: data.txHash ?? null,
3690
+ error: data.error ?? "settlement_uncertain",
3691
+ message: `Broadcast but unconfirmed \u2014 the deposit may have settled on-chain. Do NOT blindly start a new deposit. Verify on-chain first; if you must retry, reuse idempotencyKey="${idempotencyKey}" so the server resumes THIS operation (replays the original result) instead of supplying again.`
3692
+ }, null, 2)
3693
+ }],
3694
+ isError: true
3695
+ };
3696
+ }
3641
3697
  return {
3642
3698
  content: [{
3643
3699
  type: "text",
@@ -3666,7 +3722,7 @@ var YieldWithdrawInputSchema = z17.object({
3666
3722
  "Optional Agent Wallet address to withdraw to (max 10 per owner). Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet (resolved server-side from the API key)."
3667
3723
  ),
3668
3724
  idempotencyKey: z17.string().optional().describe(
3669
- "Optional durable idempotency key for this logical withdrawal. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct withdrawal (a re-deposit followed by another `withdraw max` is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-withdrawing."
3725
+ 'Optional durable idempotency key for this logical withdrawal. When omitted the tool generates a FRESH random key per invocation, so each call executes a distinct withdrawal (a re-deposit followed by another `withdraw max` is NOT replayed). Pass your own STABLE key only when you want retry-safety: re-calling with the same key replays the first result instead of double-withdrawing. IMPORTANT: if a call returns status="uncertain" (timeout / unconfirmed broadcast), it echoes back the idempotencyKey it used \u2014 pass THAT exact value here to safely resume the same withdrawal instead of starting a new one.'
3670
3726
  ),
3671
3727
  confirm: z17.boolean().optional().describe(
3672
3728
  "MUST be true to actually withdraw funds. Set this only after the user has explicitly approved this exact withdrawal (amount, token, chain, wallet) in the conversation. When omitted or false the tool previews the action and does NOT move any funds."
@@ -3674,7 +3730,7 @@ var YieldWithdrawInputSchema = z17.object({
3674
3730
  });
3675
3731
  var YIELD_WITHDRAW_TOOL = {
3676
3732
  name: "q402_yield_withdraw",
3677
- description: 'WRITE \u2014 MOVES FUNDS. Withdraws the Agent Wallet\'s supplied stablecoin (USDC / USDT) out of Aave V3 (Q402 Yield) back to the Agent Wallet. Pass amount="max" to withdraw the FULL position. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave withdraw, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user\'s behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain withdraw) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nUse q402_yield_positions first to see the current position size (especially before an amount="max" withdrawal).',
3733
+ description: 'WRITE \u2014 MOVES FUNDS. Withdraws the Agent Wallet\'s supplied stablecoin (USDC / USDT) out of Aave V3 (Q402 Yield) back to the Agent Wallet. Pass amount="max" to withdraw the FULL position. Server-managed Agent Wallet path (Mode C): authenticated by the configured live Multichain API key \u2014 the server holds the encrypted key, signs the Aave withdraw, and sponsors gas. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today; ETH / AVAX and other chains are not yet available. \n\nREQUIRES CONFIRMATION \u2014 like q402_pay, this tool refuses to execute unless `confirm: true` is set. Call it FIRST without confirm to get a one-line preview of exactly what will happen (amount, token, chain, wallet); show that to the user, get explicit approval, THEN re-call with confirm:true. Never set confirm:true on the user\'s behalf without that approval. \n\nSANDBOX BY DEFAULT \u2014 like q402_pay, no funds move unless a live Multichain key (q402_live_*) is configured AND Q402_ENABLE_REAL_PAYMENTS=1. Without both, confirm:true returns a sandbox preview (no on-chain withdraw) with a setup hint \u2014 confirm:true alone does NOT move real funds. \n\nRETRY SAFETY \u2014 on a timeout or an unconfirmed broadcast the tool returns status="uncertain" and echoes back the idempotencyKey it used. The withdrawal MAY have settled, so do NOT blindly call again \u2014 that starts a NEW withdrawal and can double-withdraw. To resume the SAME operation, re-call with idempotencyKey set to the echoed value; the server dedupes on it and replays the original result. \n\nUse q402_yield_positions first to see the current position size (especially before an amount="max" withdrawal).',
3678
3734
  inputSchema: {
3679
3735
  type: "object",
3680
3736
  properties: {
@@ -3698,7 +3754,7 @@ var YIELD_WITHDRAW_TOOL = {
3698
3754
  },
3699
3755
  idempotencyKey: {
3700
3756
  type: "string",
3701
- description: "Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct withdrawal. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-withdrawing."
3757
+ description: 'Optional durable idempotency key. Omit and the tool generates a FRESH random key per invocation, so every call executes a distinct withdrawal. Pass your own STABLE key only for opt-in retry-safety \u2014 re-calling with the same key replays the first result instead of double-withdrawing. If a call returns status="uncertain", it echoes the idempotencyKey it used \u2014 pass that exact value back here to resume the same withdrawal rather than start a new one.'
3702
3758
  },
3703
3759
  confirm: {
3704
3760
  type: "boolean",
@@ -3782,13 +3838,43 @@ async function runYieldWithdraw(input) {
3782
3838
  return {
3783
3839
  content: [{
3784
3840
  type: "text",
3785
- text: `Yield withdraw failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3841
+ text: JSON.stringify({
3842
+ status: "uncertain",
3843
+ success: false,
3844
+ action: "yield_withdraw",
3845
+ chain: input.chain,
3846
+ token: input.token,
3847
+ amount: input.amount,
3848
+ idempotencyKey,
3849
+ error: e instanceof Error ? e.message : String(e),
3850
+ message: `Network error before a confirmed response \u2014 the withdrawal may or may not have been submitted. Do NOT start a new withdrawal. To safely resume THIS operation, retry with idempotencyKey="${idempotencyKey}" (the server dedupes on it, so a retry that already landed replays the original result instead of double-withdrawing).`
3851
+ }, null, 2)
3786
3852
  }],
3787
3853
  isError: true
3788
3854
  };
3789
3855
  }
3790
3856
  const data = await res.json().catch(() => ({}));
3791
3857
  if (!res.ok) {
3858
+ if (res.status === 502) {
3859
+ return {
3860
+ content: [{
3861
+ type: "text",
3862
+ text: JSON.stringify({
3863
+ status: "uncertain",
3864
+ success: false,
3865
+ action: "yield_withdraw",
3866
+ chain: input.chain,
3867
+ token: input.token,
3868
+ amount: input.amount,
3869
+ idempotencyKey,
3870
+ txHash: data.txHash ?? null,
3871
+ error: data.error ?? "settlement_uncertain",
3872
+ message: `Broadcast but unconfirmed \u2014 the withdrawal may have settled on-chain. Do NOT blindly start a new withdrawal. Verify on-chain first; if you must retry, reuse idempotencyKey="${idempotencyKey}" so the server resumes THIS operation (replays the original result) instead of withdrawing again.`
3873
+ }, null, 2)
3874
+ }],
3875
+ isError: true
3876
+ };
3877
+ }
3792
3878
  return {
3793
3879
  content: [{
3794
3880
  type: "text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
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
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [