@quackai/q402-mcp 0.8.17 → 0.8.18

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 +530 -37
  2. package/package.json +75 -75
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.17",
215
+ version: "0.8.18",
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: [
@@ -3335,10 +3335,476 @@ async function runBridgeGasTank(_input) {
3335
3335
  };
3336
3336
  }
3337
3337
 
3338
- // src/tools/recurring-list.ts
3338
+ // src/tools/yield-reserves.ts
3339
3339
  import { z as z14 } from "zod";
3340
- var RecurringListInputSchema = z14.object({
3341
- walletId: z14.string().optional().describe(
3340
+ var YieldReservesInputSchema = z14.object({
3341
+ chain: z14.enum(["bnb"]).optional().describe("Optional chain filter. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted. Omit to list all supported chains.")
3342
+ });
3343
+ var YIELD_RESERVES_TOOL = {
3344
+ name: "q402_yield_reserves",
3345
+ description: "READ-ONLY \u2014 list the Q402 Yield (Aave) lending markets the Agent Wallet can supply into. Returns each market's protocol, chain, asset, asset address, position token, market address, and current supply APY (shown as a %). No auth required and no funds move \u2014 this is purely a preview of available yield. BNB CHAIN ONLY \u2014 Q402 Yield supports BNB Chain today. Pass an optional `chain` to filter; omit it to see every supported chain. Use this whenever the user asks 'where can I earn yield?' or 'what's the lending APY on <asset>?' before supplying.",
3346
+ inputSchema: {
3347
+ type: "object",
3348
+ properties: {
3349
+ chain: {
3350
+ type: "string",
3351
+ enum: ["bnb"],
3352
+ description: "Optional chain filter. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted. Omit for all supported chains."
3353
+ }
3354
+ },
3355
+ additionalProperties: false
3356
+ }
3357
+ };
3358
+ async function runYieldReserves(input) {
3359
+ const url = new URL(`${CONFIG.relayBaseUrl}/wallet/agentic/yield/reserves`);
3360
+ if (input.chain) url.searchParams.set("chain", input.chain);
3361
+ let res;
3362
+ try {
3363
+ res = await fetch(url, {
3364
+ method: "GET",
3365
+ headers: { Accept: "application/json" },
3366
+ signal: AbortSignal.timeout(15e3)
3367
+ });
3368
+ } catch (e) {
3369
+ return {
3370
+ content: [{
3371
+ type: "text",
3372
+ text: `Yield reserves fetch failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3373
+ }],
3374
+ isError: true
3375
+ };
3376
+ }
3377
+ const data = await res.json().catch(() => ({}));
3378
+ if (!res.ok) {
3379
+ return {
3380
+ content: [{
3381
+ type: "text",
3382
+ text: `Yield reserves failed (HTTP ${res.status}): ${JSON.stringify(data)}`
3383
+ }],
3384
+ isError: true
3385
+ };
3386
+ }
3387
+ const markets = data.markets ?? [];
3388
+ const summary = markets.length ? markets.map((m) => `${m.label} (${m.chain}): ${(m.supplyApy * 100).toFixed(2)}% APY`).join("\n") : "No yield markets returned for the requested filter.";
3389
+ return {
3390
+ content: [
3391
+ { type: "text", text: summary },
3392
+ {
3393
+ type: "text",
3394
+ text: JSON.stringify({
3395
+ supportedChains: data.supportedChains ?? [],
3396
+ markets: markets.map((m) => ({
3397
+ ...m,
3398
+ supplyApyPct: Math.round(m.supplyApy * 100 * 100) / 100
3399
+ })),
3400
+ asOf: data.asOf ?? null
3401
+ }, null, 2)
3402
+ }
3403
+ ]
3404
+ };
3405
+ }
3406
+
3407
+ // src/tools/yield-positions.ts
3408
+ import { z as z15 } from "zod";
3409
+ var YieldPositionsInputSchema = z15.object({
3410
+ walletId: z15.string().optional().describe(
3411
+ "Optional Agent Wallet address whose positions to read (max 10 per owner). Omit and the server defaults to the owner's default wallet (resolved from the API key); Q402_AGENT_WALLET_ADDRESS env fills it in when set."
3412
+ ),
3413
+ chain: z15.enum(["bnb"]).optional().describe("Optional chain filter. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted. Omit for all supported chains.")
3414
+ });
3415
+ var YIELD_POSITIONS_TOOL = {
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, 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?'",
3418
+ inputSchema: {
3419
+ type: "object",
3420
+ properties: {
3421
+ walletId: {
3422
+ type: "string",
3423
+ description: "Optional Agent Wallet address. Omit to read the owner's default wallet (the server resolves it from the API key); pass one only when the owner holds multiple wallets. Q402_AGENT_WALLET_ADDRESS env fills it in when set."
3424
+ },
3425
+ chain: {
3426
+ type: "string",
3427
+ enum: ["bnb"],
3428
+ description: "Optional chain filter. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted. Omit for all supported chains."
3429
+ }
3430
+ },
3431
+ additionalProperties: false
3432
+ }
3433
+ };
3434
+ async function runYieldPositions(input) {
3435
+ const resolved = resolveApiKey("eth", "multichain");
3436
+ if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
3437
+ return {
3438
+ content: [{
3439
+ type: "text",
3440
+ text: JSON.stringify({
3441
+ configured: false,
3442
+ positions: null,
3443
+ setupHint: resolved.sandboxReason ?? "No live Q402 Multichain API key configured. Set Q402_MULTICHAIN_API_KEY to a q402_live_\u2026 key from https://q402.quackai.ai/payment, or run q402_doctor."
3444
+ }, null, 2)
3445
+ }],
3446
+ isError: true
3447
+ };
3448
+ }
3449
+ const walletId = typeof input.walletId === "string" && input.walletId.length > 0 ? input.walletId.toLowerCase() : CONFIG.walletId ?? void 0;
3450
+ const url = new URL(`${CONFIG.relayBaseUrl}/wallet/agentic/yield/positions`);
3451
+ if (walletId) url.searchParams.set("walletId", walletId);
3452
+ if (input.chain) url.searchParams.set("chain", input.chain);
3453
+ let res;
3454
+ try {
3455
+ res = await fetch(url, {
3456
+ method: "GET",
3457
+ headers: {
3458
+ Accept: "application/json",
3459
+ "x-api-key": resolved.apiKey
3460
+ },
3461
+ signal: AbortSignal.timeout(15e3)
3462
+ });
3463
+ } catch (e) {
3464
+ return {
3465
+ content: [{
3466
+ type: "text",
3467
+ text: `Yield positions fetch failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3468
+ }],
3469
+ isError: true
3470
+ };
3471
+ }
3472
+ const data = await res.json().catch(() => ({}));
3473
+ if (!res.ok) {
3474
+ return {
3475
+ content: [{
3476
+ type: "text",
3477
+ text: `Yield positions failed (HTTP ${res.status}): ${JSON.stringify(data)}`
3478
+ }],
3479
+ isError: true
3480
+ };
3481
+ }
3482
+ const positions = data.positions ?? [];
3483
+ const summary = positions.length ? `Total supplied: $${(data.totalSuppliedUsd ?? 0).toFixed(2)} across ${positions.length} position(s).` : "No open Q402 Yield positions for this wallet.";
3484
+ return {
3485
+ content: [
3486
+ { type: "text", text: summary },
3487
+ {
3488
+ type: "text",
3489
+ text: JSON.stringify({
3490
+ walletId: data.walletId ?? walletId ?? null,
3491
+ positions: positions.map((p) => ({
3492
+ ...p,
3493
+ supplyApyPct: Math.round(p.supplyApy * 100 * 100) / 100
3494
+ })),
3495
+ totalSuppliedUsd: data.totalSuppliedUsd ?? null,
3496
+ asOf: data.asOf ?? null
3497
+ }, null, 2)
3498
+ }
3499
+ ]
3500
+ };
3501
+ }
3502
+
3503
+ // src/tools/yield-deposit.ts
3504
+ import { id } from "ethers";
3505
+ import { z as z16 } from "zod";
3506
+ var YieldDepositInputSchema = z16.object({
3507
+ chain: z16.enum(["bnb"]).default("bnb").describe("Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."),
3508
+ token: z16.enum(["USDC", "USDT"]).describe("Stablecoin to supply into Aave. USDC or USDT."),
3509
+ amount: z16.string().regex(/^\d+(\.\d+)?$/, "amount must be a positive decimal string").describe('Human-readable decimal amount to supply, e.g. "100.00".'),
3510
+ walletId: z16.string().optional().describe(
3511
+ "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
+ ),
3513
+ idempotencyKey: z16.string().optional().describe(
3514
+ "Optional durable idempotency key for this logical deposit. When omitted the tool derives a STABLE key from (walletId, chain, token, amount) so a lost response can be safely retried without double-supplying. Pass your own only if you need two genuinely distinct same-amount deposits to be treated separately."
3515
+ ),
3516
+ confirm: z16.boolean().optional().describe(
3517
+ "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."
3518
+ )
3519
+ });
3520
+ var YIELD_DEPOSIT_TOOL = {
3521
+ 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.",
3523
+ inputSchema: {
3524
+ type: "object",
3525
+ properties: {
3526
+ chain: {
3527
+ type: "string",
3528
+ enum: ["bnb"],
3529
+ description: "Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."
3530
+ },
3531
+ token: {
3532
+ type: "string",
3533
+ enum: ["USDC", "USDT"],
3534
+ description: "Stablecoin to supply into Aave. USDC or USDT."
3535
+ },
3536
+ amount: {
3537
+ type: "string",
3538
+ description: 'Human-readable decimal amount to supply, e.g. "100.00".'
3539
+ },
3540
+ walletId: {
3541
+ type: "string",
3542
+ description: "Optional Agent Wallet address to supply from when the owner holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet on the server."
3543
+ },
3544
+ idempotencyKey: {
3545
+ type: "string",
3546
+ description: "Optional durable idempotency key. Omit and the tool derives a stable key from (walletId, chain, token, amount) so a lost response is safe to retry without double-supplying. Pass your own only to force two same-amount deposits apart."
3547
+ },
3548
+ confirm: {
3549
+ type: "boolean",
3550
+ description: "MUST be true to actually supply funds \u2014 set only after the user explicitly approved this exact deposit in chat. Omit (or false) to preview without moving funds."
3551
+ }
3552
+ },
3553
+ required: ["token", "amount"],
3554
+ additionalProperties: false
3555
+ }
3556
+ };
3557
+ async function runYieldDeposit(input) {
3558
+ if (!(Number(input.amount) > 0)) {
3559
+ return {
3560
+ content: [{
3561
+ type: "text",
3562
+ text: `amount must be greater than zero (got "${input.amount}").`
3563
+ }],
3564
+ isError: true
3565
+ };
3566
+ }
3567
+ 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 : id(`yield:supply:${walletId ?? "default"}:${input.chain}:${input.token}:${input.amount}`);
3569
+ if (input.confirm !== true) {
3570
+ const walletDesc = walletId ? `wallet ${walletId}` : "your default Agent Wallet";
3571
+ return {
3572
+ content: [{
3573
+ type: "text",
3574
+ text: `Will supply ${input.amount} ${input.token} into Aave on ${input.chain} from ${walletDesc}. This MOVES FUNDS. Re-call with confirm:true to execute.`
3575
+ }]
3576
+ };
3577
+ }
3578
+ const resolved = resolveApiKey(input.chain, "multichain");
3579
+ if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
3580
+ return {
3581
+ content: [{
3582
+ type: "text",
3583
+ text: JSON.stringify({
3584
+ configured: false,
3585
+ status: "error",
3586
+ setupHint: resolved.sandboxReason ?? "No live Q402 Multichain API key configured. Set Q402_MULTICHAIN_API_KEY to a q402_live_\u2026 key from https://q402.quackai.ai/payment, or run q402_doctor."
3587
+ }, null, 2)
3588
+ }],
3589
+ isError: true
3590
+ };
3591
+ }
3592
+ if (!CONFIG.realPaymentsRequested) {
3593
+ return {
3594
+ content: [{
3595
+ type: "text",
3596
+ text: JSON.stringify({
3597
+ sandbox: true,
3598
+ success: false,
3599
+ status: "sandbox",
3600
+ action: "yield_deposit",
3601
+ chain: input.chain,
3602
+ token: input.token,
3603
+ amount: input.amount,
3604
+ walletId: walletId ?? null,
3605
+ setupHint: "Sandbox mode \u2014 set Q402_ENABLE_REAL_PAYMENTS=1 to fire a real Q402 Yield deposit. No funds moved."
3606
+ }, null, 2)
3607
+ }]
3608
+ };
3609
+ }
3610
+ let res;
3611
+ try {
3612
+ res = await fetch(`${CONFIG.relayBaseUrl}/wallet/agentic/yield/deposit`, {
3613
+ method: "POST",
3614
+ headers: { "Content-Type": "application/json" },
3615
+ body: JSON.stringify({
3616
+ apiKey: resolved.apiKey,
3617
+ chain: input.chain,
3618
+ token: input.token,
3619
+ amount: input.amount,
3620
+ idempotencyKey,
3621
+ ...walletId ? { walletId } : {}
3622
+ }),
3623
+ signal: AbortSignal.timeout(6e4)
3624
+ });
3625
+ } catch (e) {
3626
+ return {
3627
+ content: [{
3628
+ type: "text",
3629
+ text: `Yield deposit failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3630
+ }],
3631
+ isError: true
3632
+ };
3633
+ }
3634
+ const data = await res.json().catch(() => ({}));
3635
+ if (!res.ok) {
3636
+ return {
3637
+ content: [{
3638
+ type: "text",
3639
+ text: `Yield deposit failed (HTTP ${res.status}): ${JSON.stringify(data)}`
3640
+ }],
3641
+ isError: true
3642
+ };
3643
+ }
3644
+ const summary = data.txHash ? `Supplied ${data.amount ?? input.amount} ${data.asset ?? input.token} into ${data.protocol ?? "Aave"} on ${data.chain ?? input.chain}. txHash ${data.txHash}.` : `Yield deposit submitted on ${data.chain ?? input.chain}.`;
3645
+ return {
3646
+ content: [
3647
+ { type: "text", text: summary },
3648
+ { type: "text", text: JSON.stringify(data, null, 2) }
3649
+ ]
3650
+ };
3651
+ }
3652
+
3653
+ // src/tools/yield-withdraw.ts
3654
+ import { id as id2 } from "ethers";
3655
+ import { z as z17 } from "zod";
3656
+ var YieldWithdrawInputSchema = z17.object({
3657
+ chain: z17.enum(["bnb"]).default("bnb").describe("Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."),
3658
+ token: z17.enum(["USDC", "USDT"]).describe("Stablecoin to withdraw from Aave. USDC or USDT."),
3659
+ amount: z17.string().regex(/^(\d+(\.\d+)?|max)$/, 'amount must be a positive decimal string or "max"').describe('Human-readable decimal amount to withdraw, e.g. "100.00", or the literal "max" to withdraw the full position.'),
3660
+ walletId: z17.string().optional().describe(
3661
+ "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
+ ),
3663
+ idempotencyKey: z17.string().optional().describe(
3664
+ "Optional durable idempotency key for this logical withdrawal. When omitted the tool derives a STABLE key from (walletId, chain, token, amount) so a lost response can be safely retried without double-withdrawing. Pass your own only if you need two genuinely distinct same-amount withdrawals to be treated separately."
3665
+ ),
3666
+ confirm: z17.boolean().optional().describe(
3667
+ "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."
3668
+ )
3669
+ });
3670
+ var YIELD_WITHDRAW_TOOL = {
3671
+ 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).',
3673
+ inputSchema: {
3674
+ type: "object",
3675
+ properties: {
3676
+ chain: {
3677
+ type: "string",
3678
+ enum: ["bnb"],
3679
+ description: "Chain the Aave market lives on. Q402 Yield is BNB-only today \u2014 only 'bnb' is accepted."
3680
+ },
3681
+ token: {
3682
+ type: "string",
3683
+ enum: ["USDC", "USDT"],
3684
+ description: "Stablecoin to withdraw from Aave. USDC or USDT."
3685
+ },
3686
+ amount: {
3687
+ type: "string",
3688
+ description: 'Human-readable decimal amount to withdraw, e.g. "100.00", or the literal "max" to withdraw the full position.'
3689
+ },
3690
+ walletId: {
3691
+ type: "string",
3692
+ description: "Optional Agent Wallet address to withdraw to when the owner holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet on the server."
3693
+ },
3694
+ idempotencyKey: {
3695
+ type: "string",
3696
+ description: "Optional durable idempotency key. Omit and the tool derives a stable key from (walletId, chain, token, amount) so a lost response is safe to retry without double-withdrawing. Pass your own only to force two same-amount withdrawals apart."
3697
+ },
3698
+ confirm: {
3699
+ type: "boolean",
3700
+ description: "MUST be true to actually withdraw funds \u2014 set only after the user explicitly approved this exact withdrawal in chat. Omit (or false) to preview without moving funds."
3701
+ }
3702
+ },
3703
+ required: ["token", "amount"],
3704
+ additionalProperties: false
3705
+ }
3706
+ };
3707
+ async function runYieldWithdraw(input) {
3708
+ if (input.amount !== "max" && !(Number(input.amount) > 0)) {
3709
+ return {
3710
+ content: [{
3711
+ type: "text",
3712
+ text: `amount must be greater than zero (got "${input.amount}"), or the literal "max".`
3713
+ }],
3714
+ isError: true
3715
+ };
3716
+ }
3717
+ 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 : id2(`yield:withdraw:${walletId ?? "default"}:${input.chain}:${input.token}:${input.amount}`);
3719
+ const amountDesc = input.amount === "max" ? "the FULL position" : `${input.amount} ${input.token}`;
3720
+ if (input.confirm !== true) {
3721
+ const walletDesc = walletId ? `wallet ${walletId}` : "your default Agent Wallet";
3722
+ return {
3723
+ content: [{
3724
+ type: "text",
3725
+ text: `Will withdraw ${amountDesc} from Aave on ${input.chain} back to ${walletDesc}. This MOVES FUNDS. Re-call with confirm:true to execute.`
3726
+ }]
3727
+ };
3728
+ }
3729
+ const resolved = resolveApiKey(input.chain, "multichain");
3730
+ if (!resolved.apiKey || !resolved.apiKey.startsWith("q402_live_")) {
3731
+ return {
3732
+ content: [{
3733
+ type: "text",
3734
+ text: JSON.stringify({
3735
+ configured: false,
3736
+ status: "error",
3737
+ setupHint: resolved.sandboxReason ?? "No live Q402 Multichain API key configured. Set Q402_MULTICHAIN_API_KEY to a q402_live_\u2026 key from https://q402.quackai.ai/payment, or run q402_doctor."
3738
+ }, null, 2)
3739
+ }],
3740
+ isError: true
3741
+ };
3742
+ }
3743
+ if (!CONFIG.realPaymentsRequested) {
3744
+ return {
3745
+ content: [{
3746
+ type: "text",
3747
+ text: JSON.stringify({
3748
+ sandbox: true,
3749
+ success: false,
3750
+ status: "sandbox",
3751
+ action: "yield_withdraw",
3752
+ chain: input.chain,
3753
+ token: input.token,
3754
+ amount: input.amount,
3755
+ walletId: walletId ?? null,
3756
+ setupHint: "Sandbox mode \u2014 set Q402_ENABLE_REAL_PAYMENTS=1 to fire a real Q402 Yield withdrawal. No funds moved."
3757
+ }, null, 2)
3758
+ }]
3759
+ };
3760
+ }
3761
+ let res;
3762
+ try {
3763
+ res = await fetch(`${CONFIG.relayBaseUrl}/wallet/agentic/yield/withdraw`, {
3764
+ method: "POST",
3765
+ headers: { "Content-Type": "application/json" },
3766
+ body: JSON.stringify({
3767
+ apiKey: resolved.apiKey,
3768
+ chain: input.chain,
3769
+ token: input.token,
3770
+ amount: input.amount,
3771
+ idempotencyKey,
3772
+ ...walletId ? { walletId } : {}
3773
+ }),
3774
+ signal: AbortSignal.timeout(6e4)
3775
+ });
3776
+ } catch (e) {
3777
+ return {
3778
+ content: [{
3779
+ type: "text",
3780
+ text: `Yield withdraw failed: ${e instanceof Error ? e.message : String(e)}. Retry in a moment.`
3781
+ }],
3782
+ isError: true
3783
+ };
3784
+ }
3785
+ const data = await res.json().catch(() => ({}));
3786
+ if (!res.ok) {
3787
+ return {
3788
+ content: [{
3789
+ type: "text",
3790
+ text: `Yield withdraw failed (HTTP ${res.status}): ${JSON.stringify(data)}`
3791
+ }],
3792
+ isError: true
3793
+ };
3794
+ }
3795
+ const summary = data.txHash ? `Withdrew ${data.amount ?? amountDesc} ${data.asset ?? ""}`.trimEnd() + ` from ${data.protocol ?? "Aave"} on ${data.chain ?? input.chain}. txHash ${data.txHash}.` : `Yield withdraw submitted on ${data.chain ?? input.chain}.`;
3796
+ return {
3797
+ content: [
3798
+ { type: "text", text: summary },
3799
+ { type: "text", text: JSON.stringify(data, null, 2) }
3800
+ ]
3801
+ };
3802
+ }
3803
+
3804
+ // src/tools/recurring-list.ts
3805
+ import { z as z18 } from "zod";
3806
+ var RecurringListInputSchema = z18.object({
3807
+ walletId: z18.string().optional().describe(
3342
3808
  "Optional lowercased Agent Wallet address to list rules for when the user holds multiple wallets. Omit to use Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3343
3809
  )
3344
3810
  });
@@ -3422,29 +3888,29 @@ async function runRecurringList(input = {}) {
3422
3888
  }
3423
3889
 
3424
3890
  // src/tools/recurring-create.ts
3425
- import { z as z15 } from "zod";
3891
+ import { z as z19 } from "zod";
3426
3892
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
3427
3893
  var AMOUNT_RE = /^\d+(\.\d{1,18})?$/;
3428
- var RecurringCreateInputSchema = z15.object({
3429
- confirm: z15.literal(true).describe(
3894
+ var RecurringCreateInputSchema = z19.object({
3895
+ confirm: z19.literal(true).describe(
3430
3896
  'REQUIRED. Must be literally `true`. Authoring a recurring rule schedules future on-chain payments that the user does not click through one-by-one \u2014 the user has to explicitly say yes BEFORE this is called. Echo back the frequency + recipient + amount + chain + token + cancelWindow you intend to create, get a plain-language confirmation from the user (e.g. "yes, create the schedule"), and ONLY then call this with confirm: true. Mirrors the same guard q402_pay / q402_batch_pay use on one-shot sends.'
3431
3897
  ),
3432
- frequency: z15.string().min(1).describe(
3898
+ frequency: z19.string().min(1).describe(
3433
3899
  'Cadence string. One of: "hourly:N" (N=1..23), "daily", "weekly:{mon|tue|wed|thu|fri|sat|sun}", "monthly:N" (N=1..31), "monthly:last". Examples: "hourly:1" fires every hour, "weekly:fri" fires every Friday, "monthly:1" fires on the 1st of each month.'
3434
3900
  ),
3435
- recipient: z15.string().regex(ADDRESS_RE).describe("0x-prefixed 20-byte recipient address. Required."),
3436
- amount: z15.string().regex(AMOUNT_RE).describe(
3901
+ recipient: z19.string().regex(ADDRESS_RE).describe("0x-prefixed 20-byte recipient address. Required."),
3902
+ amount: z19.string().regex(AMOUNT_RE).describe(
3437
3903
  'Amount per fire, as a decimal string (e.g. "1.5", "0.0001"). Counted in the same unit as `token` (USDC or USDT, both 1:1 USD).'
3438
3904
  ),
3439
- chain: z15.enum(["bnb", "eth", "avax", "xlayer", "mantle", "injective", "monad", "scroll", "stable", "arbitrum"]).default("bnb").describe(
3905
+ chain: z19.enum(["bnb", "eth", "avax", "xlayer", "mantle", "injective", "monad", "scroll", "stable", "arbitrum"]).default("bnb").describe(
3440
3906
  "Chain to fire the recurring TX on. Defaults to bnb. Recurring requires the paid Multichain subscription on EVERY chain, including bnb \u2014 Trial keys are rejected at create time with MULTICHAIN_REQUIRED. Trial keys can still pay one-shot via q402_pay on BNB."
3441
3907
  ),
3442
- token: z15.enum(["USDC", "USDT"]).default("USDT").describe("Stablecoin to send. USDC or USDT. Both peg to USD-1."),
3443
- label: z15.string().max(64).optional().describe("Optional human-readable label (\u226464 chars). Shows up in q402_recurring_list and the dashboard."),
3444
- cancelWindowHours: z15.number().min(0).optional().describe(
3908
+ token: z19.enum(["USDC", "USDT"]).default("USDT").describe("Stablecoin to send. USDC or USDT. Both peg to USD-1."),
3909
+ label: z19.string().max(64).optional().describe("Optional human-readable label (\u226464 chars). Shows up in q402_recurring_list and the dashboard."),
3910
+ cancelWindowHours: z19.number().min(0).optional().describe(
3445
3911
  "Hours of advance notice before each fire during which the rule can be cancelled. 0 = fire immediately at the next slot, no alert. Capped at the cadence interval (e.g. \u2264 N-0.5h for hourly:N, \u226424 for daily). Defaults to 0."
3446
3912
  ),
3447
- walletId: z15.string().optional().describe(
3913
+ walletId: z19.string().optional().describe(
3448
3914
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet on the server."
3449
3915
  )
3450
3916
  });
@@ -3573,12 +4039,12 @@ async function runRecurringCreate(input) {
3573
4039
  }
3574
4040
 
3575
4041
  // src/tools/recurring-cancel.ts
3576
- import { z as z16 } from "zod";
3577
- var RecurringCancelInputSchema = z16.object({
3578
- ruleId: z16.string().min(1).describe(
4042
+ import { z as z20 } from "zod";
4043
+ var RecurringCancelInputSchema = z20.object({
4044
+ ruleId: z20.string().min(1).describe(
3579
4045
  "Rule id to cancel. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field. Cancelling is immediate."
3580
4046
  ),
3581
- walletId: z16.string().optional().describe(
4047
+ walletId: z20.string().optional().describe(
3582
4048
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3583
4049
  )
3584
4050
  });
@@ -3666,15 +4132,15 @@ async function runRecurringCancel(input) {
3666
4132
  }
3667
4133
 
3668
4134
  // src/tools/recurring-fires.ts
3669
- import { z as z17 } from "zod";
3670
- var RecurringFiresInputSchema = z17.object({
3671
- ruleId: z17.string().min(1).describe(
4135
+ import { z as z21 } from "zod";
4136
+ var RecurringFiresInputSchema = z21.object({
4137
+ ruleId: z21.string().min(1).describe(
3672
4138
  "Rule id whose fire history to fetch. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field."
3673
4139
  ),
3674
- limit: z17.number().int().min(1).max(50).optional().describe(
4140
+ limit: z21.number().int().min(1).max(50).optional().describe(
3675
4141
  "Max number of fires to return (newest first). Defaults to 50 (the server cap). Pass a smaller number when you only need the most recent few."
3676
4142
  ),
3677
- walletId: z17.string().optional().describe(
4143
+ walletId: z21.string().optional().describe(
3678
4144
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3679
4145
  )
3680
4146
  });
@@ -3780,12 +4246,12 @@ async function runRecurringFires(input) {
3780
4246
  }
3781
4247
 
3782
4248
  // src/tools/recurring-pause.ts
3783
- import { z as z18 } from "zod";
3784
- var RecurringPauseInputSchema = z18.object({
3785
- ruleId: z18.string().min(1).describe(
4249
+ import { z as z22 } from "zod";
4250
+ var RecurringPauseInputSchema = z22.object({
4251
+ ruleId: z22.string().min(1).describe(
3786
4252
  "Rule id to pause. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field. Pausing is immediate and reversible."
3787
4253
  ),
3788
- walletId: z18.string().optional().describe(
4254
+ walletId: z22.string().optional().describe(
3789
4255
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3790
4256
  )
3791
4257
  });
@@ -3873,12 +4339,12 @@ async function runRecurringPause(input) {
3873
4339
  }
3874
4340
 
3875
4341
  // src/tools/recurring-resume.ts
3876
- import { z as z19 } from "zod";
3877
- var RecurringResumeInputSchema = z19.object({
3878
- ruleId: z19.string().min(1).describe(
4342
+ import { z as z23 } from "zod";
4343
+ var RecurringResumeInputSchema = z23.object({
4344
+ ruleId: z23.string().min(1).describe(
3879
4345
  "Rule id to resume. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field. Resume is immediate; nextRunAt advances to the next valid slot."
3880
4346
  ),
3881
- walletId: z19.string().optional().describe(
4347
+ walletId: z23.string().optional().describe(
3882
4348
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3883
4349
  )
3884
4350
  });
@@ -3966,12 +4432,12 @@ async function runRecurringResume(input) {
3966
4432
  }
3967
4433
 
3968
4434
  // src/tools/recurring-skip-next.ts
3969
- import { z as z20 } from "zod";
3970
- var RecurringSkipNextInputSchema = z20.object({
3971
- ruleId: z20.string().min(1).describe(
4435
+ import { z as z24 } from "zod";
4436
+ var RecurringSkipNextInputSchema = z24.object({
4437
+ ruleId: z24.string().min(1).describe(
3972
4438
  "Rule id whose next scheduled fire to skip. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field."
3973
4439
  ),
3974
- walletId: z20.string().optional().describe(
4440
+ walletId: z24.string().optional().describe(
3975
4441
  "Optional lowercased Agent Wallet address when the user holds multiple wallets. Defaults to Q402_AGENT_WALLET_ADDRESS env, then the owner's default wallet."
3976
4442
  )
3977
4443
  });
@@ -4096,7 +4562,18 @@ async function main() {
4096
4562
  BRIDGE_QUOTE_TOOL,
4097
4563
  BRIDGE_SEND_TOOL,
4098
4564
  BRIDGE_HISTORY_TOOL,
4099
- BRIDGE_GAS_TANK_TOOL
4565
+ BRIDGE_GAS_TANK_TOOL,
4566
+ // Q402 Yield surface — read-only Aave lending market list + the
4567
+ // Agent Wallet's own positions. No funds move; positions auths via
4568
+ // the live Multichain apiKey (x-api-key header), reserves is public.
4569
+ YIELD_RESERVES_TOOL,
4570
+ YIELD_POSITIONS_TOOL,
4571
+ // Q402 Yield WRITE surface — supply / withdraw the Agent Wallet's
4572
+ // stablecoin to/from Aave V3. MOVES FUNDS, so both gate on confirm:true
4573
+ // (like q402_pay). Mode C: apiKey in the body, server signs the supply
4574
+ // / withdraw with the encrypted key.
4575
+ YIELD_DEPOSIT_TOOL,
4576
+ YIELD_WITHDRAW_TOOL
4100
4577
  ]
4101
4578
  }));
4102
4579
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -4183,6 +4660,22 @@ async function main() {
4183
4660
  const parsed = BridgeGasTankInputSchema.parse(args ?? {});
4184
4661
  return await runBridgeGasTank(parsed);
4185
4662
  }
4663
+ case "q402_yield_reserves": {
4664
+ const parsed = YieldReservesInputSchema.parse(args ?? {});
4665
+ return await runYieldReserves(parsed);
4666
+ }
4667
+ case "q402_yield_positions": {
4668
+ const parsed = YieldPositionsInputSchema.parse(args ?? {});
4669
+ return await runYieldPositions(parsed);
4670
+ }
4671
+ case "q402_yield_deposit": {
4672
+ const parsed = YieldDepositInputSchema.parse(args ?? {});
4673
+ return await runYieldDeposit(parsed);
4674
+ }
4675
+ case "q402_yield_withdraw": {
4676
+ const parsed = YieldWithdrawInputSchema.parse(args ?? {});
4677
+ return await runYieldWithdraw(parsed);
4678
+ }
4186
4679
  default:
4187
4680
  return {
4188
4681
  isError: true,
package/package.json CHANGED
@@ -1,75 +1,75 @@
1
- {
2
- "name": "@quackai/q402-mcp",
3
- "version": "0.8.17",
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.18",
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
+ }