@quackai/q402-mcp 0.8.18 → 0.8.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.js +96 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,7 +156,7 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
|
|
|
156
156
|
|
|
157
157
|
## Tools exposed
|
|
158
158
|
|
|
159
|
-
**
|
|
159
|
+
**24 tools** — read-only by default; live mode needs an API key + signing path + `Q402_ENABLE_REAL_PAYMENTS=1`.
|
|
160
160
|
|
|
161
161
|
| Tool | Auth | Purpose |
|
|
162
162
|
|---|---|---|
|
|
@@ -180,8 +180,12 @@ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config refere
|
|
|
180
180
|
| `q402_bridge_send` | live mode | Execute a CCIP bridge from the user's Agent Wallet. Mode C only (server-managed). Sandbox-by-default; `sandbox: false` + live Multichain key + `Q402_ENABLE_REAL_PAYMENTS=1` fires a real on-chain bridge. |
|
|
181
181
|
| `q402_bridge_history` | not yet wired | Pointer to the dashboard. Returns `{ implemented: false, dashboardUrl, dashboardPath }` — read-only guidance until owner-sig auth lands in MCP. |
|
|
182
182
|
| `q402_bridge_gas_tank` | not yet wired | Static guidance + dashboard pointer for the Bridge Gas Tank top-up flow. Live balance lookup needs owner-sig auth (dashboard for now). |
|
|
183
|
+
| `q402_yield_reserves` | none | List Q402 Yield (Aave V3) lending markets — protocol, chain, asset, market address, supply APY. BNB Chain only today. |
|
|
184
|
+
| `q402_yield_positions` | api key | Show the Agent Wallet's open Q402 Yield positions (balance, principal, accrued interest, APY) + total supplied in USD. Mode C. |
|
|
185
|
+
| `q402_yield_deposit` | live mode | Supply the Agent Wallet's USDC/USDT into Aave V3 (Q402 Yield) to earn supply APY. Mode C, BNB-only. Requires `confirm: true`; sandbox-by-default. |
|
|
186
|
+
| `q402_yield_withdraw` | live mode | Withdraw supplied USDC/USDT out of Aave V3 back to the Agent Wallet (`amount: "max"` = full position). Mode C, BNB-only. Requires `confirm: true`; sandbox-by-default. |
|
|
183
187
|
|
|
184
|
-
`q402_pay` + `q402_batch_pay` + `q402_bridge_send` require explicit in-chat confirmation. Batch confirmation = full batch, not per-row.
|
|
188
|
+
`q402_pay` + `q402_batch_pay` + `q402_bridge_send` + `q402_yield_deposit` + `q402_yield_withdraw` require explicit in-chat confirmation. Batch confirmation = full batch, not per-row.
|
|
185
189
|
|
|
186
190
|
> ℹ️ `q402_pay` expects a 0x address — ENS isn't resolved server-side. Resolve client-side first.
|
|
187
191
|
> Per-chain Gas Tank balances + full TX history live in the [dashboard](https://q402.quackai.ai/dashboard) (wallet-signature only).
|
package/dist/index.js
CHANGED
|
@@ -212,7 +212,7 @@ var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
|
|
|
212
212
|
// package.json
|
|
213
213
|
var package_default = {
|
|
214
214
|
name: "@quackai/q402-mcp",
|
|
215
|
-
version: "0.8.
|
|
215
|
+
version: "0.8.20",
|
|
216
216
|
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
217
|
mcpName: "io.github.bitgett/q402-mcp",
|
|
218
218
|
keywords: [
|
|
@@ -3414,7 +3414,7 @@ var YieldPositionsInputSchema = z15.object({
|
|
|
3414
3414
|
});
|
|
3415
3415
|
var YIELD_POSITIONS_TOOL = {
|
|
3416
3416
|
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,
|
|
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, 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
3418
|
inputSchema: {
|
|
3419
3419
|
type: "object",
|
|
3420
3420
|
properties: {
|
|
@@ -3480,7 +3480,10 @@ async function runYieldPositions(input) {
|
|
|
3480
3480
|
};
|
|
3481
3481
|
}
|
|
3482
3482
|
const positions = data.positions ?? [];
|
|
3483
|
-
const
|
|
3483
|
+
const unavailableChains = Array.isArray(data.unavailableChains) ? data.unavailableChains : [];
|
|
3484
|
+
const hasUnavailable = data.unavailable === true || unavailableChains.length > 0;
|
|
3485
|
+
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 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.";
|
|
3484
3487
|
return {
|
|
3485
3488
|
content: [
|
|
3486
3489
|
{ type: "text", text: summary },
|
|
@@ -3488,12 +3491,26 @@ async function runYieldPositions(input) {
|
|
|
3488
3491
|
type: "text",
|
|
3489
3492
|
text: JSON.stringify({
|
|
3490
3493
|
walletId: data.walletId ?? walletId ?? null,
|
|
3494
|
+
// Surface only what's real: the current position value (live aToken
|
|
3495
|
+
// balance) + APY. `principal`/`accrued` come back null from this
|
|
3496
|
+
// Phase-0 read, so they're included ONLY when the server actually
|
|
3497
|
+
// populated them — never as a null placeholder that reads like a
|
|
3498
|
+
// real (zero) earnings figure.
|
|
3491
3499
|
positions: positions.map((p) => ({
|
|
3492
|
-
|
|
3493
|
-
|
|
3500
|
+
protocol: p.protocol,
|
|
3501
|
+
chain: p.chain,
|
|
3502
|
+
asset: p.asset,
|
|
3503
|
+
marketAddress: p.marketAddress,
|
|
3504
|
+
currentValue: p.balance,
|
|
3505
|
+
supplyApy: p.supplyApy,
|
|
3506
|
+
supplyApyPct: Math.round(p.supplyApy * 100 * 100) / 100,
|
|
3507
|
+
...p.principal != null ? { principal: p.principal } : {},
|
|
3508
|
+
...p.accrued != null ? { accrued: p.accrued } : {}
|
|
3494
3509
|
})),
|
|
3495
|
-
|
|
3496
|
-
asOf: data.asOf ?? null
|
|
3510
|
+
totalCurrentValueUsd: data.totalSuppliedUsd ?? null,
|
|
3511
|
+
asOf: data.asOf ?? null,
|
|
3512
|
+
unavailable: hasUnavailable,
|
|
3513
|
+
unavailableChains
|
|
3497
3514
|
}, null, 2)
|
|
3498
3515
|
}
|
|
3499
3516
|
]
|
|
@@ -3501,7 +3518,7 @@ async function runYieldPositions(input) {
|
|
|
3501
3518
|
}
|
|
3502
3519
|
|
|
3503
3520
|
// src/tools/yield-deposit.ts
|
|
3504
|
-
import {
|
|
3521
|
+
import { hexlify as hexlify2, randomBytes as randomBytes2 } from "ethers";
|
|
3505
3522
|
import { z as z16 } from "zod";
|
|
3506
3523
|
var YieldDepositInputSchema = z16.object({
|
|
3507
3524
|
chain: z16.enum(["bnb"]).default("bnb").describe("Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."),
|
|
@@ -3511,7 +3528,7 @@ var YieldDepositInputSchema = z16.object({
|
|
|
3511
3528
|
"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)."
|
|
3512
3529
|
),
|
|
3513
3530
|
idempotencyKey: z16.string().optional().describe(
|
|
3514
|
-
|
|
3531
|
+
'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.'
|
|
3515
3532
|
),
|
|
3516
3533
|
confirm: z16.boolean().optional().describe(
|
|
3517
3534
|
"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."
|
|
@@ -3519,7 +3536,7 @@ var YieldDepositInputSchema = z16.object({
|
|
|
3519
3536
|
});
|
|
3520
3537
|
var YIELD_DEPOSIT_TOOL = {
|
|
3521
3538
|
name: "q402_yield_deposit",
|
|
3522
|
-
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.",
|
|
3539
|
+
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.",
|
|
3523
3540
|
inputSchema: {
|
|
3524
3541
|
type: "object",
|
|
3525
3542
|
properties: {
|
|
@@ -3543,7 +3560,7 @@ var YIELD_DEPOSIT_TOOL = {
|
|
|
3543
3560
|
},
|
|
3544
3561
|
idempotencyKey: {
|
|
3545
3562
|
type: "string",
|
|
3546
|
-
description:
|
|
3563
|
+
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.'
|
|
3547
3564
|
},
|
|
3548
3565
|
confirm: {
|
|
3549
3566
|
type: "boolean",
|
|
@@ -3565,7 +3582,7 @@ async function runYieldDeposit(input) {
|
|
|
3565
3582
|
};
|
|
3566
3583
|
}
|
|
3567
3584
|
const walletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId ?? void 0;
|
|
3568
|
-
const idempotencyKey = typeof input.idempotencyKey === "string" && input.idempotencyKey.length > 0 ? input.idempotencyKey :
|
|
3585
|
+
const idempotencyKey = typeof input.idempotencyKey === "string" && input.idempotencyKey.length > 0 ? input.idempotencyKey : hexlify2(randomBytes2(32));
|
|
3569
3586
|
if (input.confirm !== true) {
|
|
3570
3587
|
const walletDesc = walletId ? `wallet ${walletId}` : "your default Agent Wallet";
|
|
3571
3588
|
return {
|
|
@@ -3626,13 +3643,43 @@ async function runYieldDeposit(input) {
|
|
|
3626
3643
|
return {
|
|
3627
3644
|
content: [{
|
|
3628
3645
|
type: "text",
|
|
3629
|
-
text:
|
|
3646
|
+
text: JSON.stringify({
|
|
3647
|
+
status: "uncertain",
|
|
3648
|
+
success: false,
|
|
3649
|
+
action: "yield_deposit",
|
|
3650
|
+
chain: input.chain,
|
|
3651
|
+
token: input.token,
|
|
3652
|
+
amount: input.amount,
|
|
3653
|
+
idempotencyKey,
|
|
3654
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3655
|
+
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).`
|
|
3656
|
+
}, null, 2)
|
|
3630
3657
|
}],
|
|
3631
3658
|
isError: true
|
|
3632
3659
|
};
|
|
3633
3660
|
}
|
|
3634
3661
|
const data = await res.json().catch(() => ({}));
|
|
3635
3662
|
if (!res.ok) {
|
|
3663
|
+
if (res.status === 502) {
|
|
3664
|
+
return {
|
|
3665
|
+
content: [{
|
|
3666
|
+
type: "text",
|
|
3667
|
+
text: JSON.stringify({
|
|
3668
|
+
status: "uncertain",
|
|
3669
|
+
success: false,
|
|
3670
|
+
action: "yield_deposit",
|
|
3671
|
+
chain: input.chain,
|
|
3672
|
+
token: input.token,
|
|
3673
|
+
amount: input.amount,
|
|
3674
|
+
idempotencyKey,
|
|
3675
|
+
txHash: data.txHash ?? null,
|
|
3676
|
+
error: data.error ?? "settlement_uncertain",
|
|
3677
|
+
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.`
|
|
3678
|
+
}, null, 2)
|
|
3679
|
+
}],
|
|
3680
|
+
isError: true
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3636
3683
|
return {
|
|
3637
3684
|
content: [{
|
|
3638
3685
|
type: "text",
|
|
@@ -3651,7 +3698,7 @@ async function runYieldDeposit(input) {
|
|
|
3651
3698
|
}
|
|
3652
3699
|
|
|
3653
3700
|
// src/tools/yield-withdraw.ts
|
|
3654
|
-
import {
|
|
3701
|
+
import { hexlify as hexlify3, randomBytes as randomBytes3 } from "ethers";
|
|
3655
3702
|
import { z as z17 } from "zod";
|
|
3656
3703
|
var YieldWithdrawInputSchema = z17.object({
|
|
3657
3704
|
chain: z17.enum(["bnb"]).default("bnb").describe("Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."),
|
|
@@ -3661,7 +3708,7 @@ var YieldWithdrawInputSchema = z17.object({
|
|
|
3661
3708
|
"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)."
|
|
3662
3709
|
),
|
|
3663
3710
|
idempotencyKey: z17.string().optional().describe(
|
|
3664
|
-
|
|
3711
|
+
'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.'
|
|
3665
3712
|
),
|
|
3666
3713
|
confirm: z17.boolean().optional().describe(
|
|
3667
3714
|
"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."
|
|
@@ -3669,7 +3716,7 @@ var YieldWithdrawInputSchema = z17.object({
|
|
|
3669
3716
|
});
|
|
3670
3717
|
var YIELD_WITHDRAW_TOOL = {
|
|
3671
3718
|
name: "q402_yield_withdraw",
|
|
3672
|
-
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).',
|
|
3719
|
+
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).',
|
|
3673
3720
|
inputSchema: {
|
|
3674
3721
|
type: "object",
|
|
3675
3722
|
properties: {
|
|
@@ -3693,7 +3740,7 @@ var YIELD_WITHDRAW_TOOL = {
|
|
|
3693
3740
|
},
|
|
3694
3741
|
idempotencyKey: {
|
|
3695
3742
|
type: "string",
|
|
3696
|
-
description:
|
|
3743
|
+
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.'
|
|
3697
3744
|
},
|
|
3698
3745
|
confirm: {
|
|
3699
3746
|
type: "boolean",
|
|
@@ -3715,7 +3762,7 @@ async function runYieldWithdraw(input) {
|
|
|
3715
3762
|
};
|
|
3716
3763
|
}
|
|
3717
3764
|
const walletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId ?? void 0;
|
|
3718
|
-
const idempotencyKey = typeof input.idempotencyKey === "string" && input.idempotencyKey.length > 0 ? input.idempotencyKey :
|
|
3765
|
+
const idempotencyKey = typeof input.idempotencyKey === "string" && input.idempotencyKey.length > 0 ? input.idempotencyKey : hexlify3(randomBytes3(32));
|
|
3719
3766
|
const amountDesc = input.amount === "max" ? "the FULL position" : `${input.amount} ${input.token}`;
|
|
3720
3767
|
if (input.confirm !== true) {
|
|
3721
3768
|
const walletDesc = walletId ? `wallet ${walletId}` : "your default Agent Wallet";
|
|
@@ -3777,13 +3824,43 @@ async function runYieldWithdraw(input) {
|
|
|
3777
3824
|
return {
|
|
3778
3825
|
content: [{
|
|
3779
3826
|
type: "text",
|
|
3780
|
-
text:
|
|
3827
|
+
text: JSON.stringify({
|
|
3828
|
+
status: "uncertain",
|
|
3829
|
+
success: false,
|
|
3830
|
+
action: "yield_withdraw",
|
|
3831
|
+
chain: input.chain,
|
|
3832
|
+
token: input.token,
|
|
3833
|
+
amount: input.amount,
|
|
3834
|
+
idempotencyKey,
|
|
3835
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3836
|
+
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).`
|
|
3837
|
+
}, null, 2)
|
|
3781
3838
|
}],
|
|
3782
3839
|
isError: true
|
|
3783
3840
|
};
|
|
3784
3841
|
}
|
|
3785
3842
|
const data = await res.json().catch(() => ({}));
|
|
3786
3843
|
if (!res.ok) {
|
|
3844
|
+
if (res.status === 502) {
|
|
3845
|
+
return {
|
|
3846
|
+
content: [{
|
|
3847
|
+
type: "text",
|
|
3848
|
+
text: JSON.stringify({
|
|
3849
|
+
status: "uncertain",
|
|
3850
|
+
success: false,
|
|
3851
|
+
action: "yield_withdraw",
|
|
3852
|
+
chain: input.chain,
|
|
3853
|
+
token: input.token,
|
|
3854
|
+
amount: input.amount,
|
|
3855
|
+
idempotencyKey,
|
|
3856
|
+
txHash: data.txHash ?? null,
|
|
3857
|
+
error: data.error ?? "settlement_uncertain",
|
|
3858
|
+
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.`
|
|
3859
|
+
}, null, 2)
|
|
3860
|
+
}],
|
|
3861
|
+
isError: true
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3787
3864
|
return {
|
|
3788
3865
|
content: [{
|
|
3789
3866
|
type: "text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quackai/q402-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
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": [
|