@quackai/q402-mcp 0.8.16 → 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 +608 -38
  2. package/package.json +75 -75
package/dist/index.js CHANGED
@@ -209,9 +209,86 @@ function isLiveModeFor(resolved) {
209
209
  }
210
210
  var isValidPrivateKey = (s) => typeof s === "string" && PRIVATE_KEY_RE.test(s);
211
211
 
212
+ // package.json
213
+ var package_default = {
214
+ name: "@quackai/q402-mcp",
215
+ version: "0.8.18",
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
+ mcpName: "io.github.bitgett/q402-mcp",
218
+ keywords: [
219
+ "mcp",
220
+ "model-context-protocol",
221
+ "claude",
222
+ "claude-desktop",
223
+ "claude-code",
224
+ "codex",
225
+ "openai-codex",
226
+ "cline",
227
+ "q402",
228
+ "x402",
229
+ "stablecoin",
230
+ "usdc",
231
+ "usdt",
232
+ "rlusd",
233
+ "ripple",
234
+ "gasless",
235
+ "eip-7702",
236
+ "payments",
237
+ "ai-agents"
238
+ ],
239
+ type: "module",
240
+ main: "dist/index.js",
241
+ bin: {
242
+ "q402-mcp": "dist/index.js"
243
+ },
244
+ files: [
245
+ "dist",
246
+ "README.md",
247
+ "LICENSE"
248
+ ],
249
+ engines: {
250
+ node: ">=18.18"
251
+ },
252
+ scripts: {
253
+ build: "tsup",
254
+ dev: "tsup --watch",
255
+ lint: "tsc --noEmit",
256
+ prepublishOnly: "npm run lint && npm run build",
257
+ start: "node dist/index.js"
258
+ },
259
+ dependencies: {
260
+ "@modelcontextprotocol/sdk": "^1.29.0",
261
+ ethers: "^6.16.0",
262
+ zod: "^3.23.8"
263
+ },
264
+ devDependencies: {
265
+ "@types/node": "^20.11.0",
266
+ tsup: "^8.3.0",
267
+ typescript: "^5.5.0"
268
+ },
269
+ repository: {
270
+ type: "git",
271
+ url: "git+https://github.com/bitgett/q402-mcp.git"
272
+ },
273
+ homepage: "https://q402.quackai.ai/claude",
274
+ bugs: {
275
+ url: "https://github.com/bitgett/q402-mcp/issues"
276
+ },
277
+ license: "Apache-2.0",
278
+ author: "David Lee <davidlee@quackai.ai>",
279
+ publishConfig: {
280
+ access: "public"
281
+ },
282
+ overrides: {
283
+ ws: "^8.20.1",
284
+ qs: "^6.15.2",
285
+ hono: "^4.12.21"
286
+ }
287
+ };
288
+
212
289
  // src/version.ts
213
- var PACKAGE_NAME = "@quackai/q402-mcp";
214
- var PACKAGE_VERSION = "0.8.15";
290
+ var PACKAGE_NAME = package_default.name;
291
+ var PACKAGE_VERSION = package_default.version;
215
292
 
216
293
  // src/tools/quote.ts
217
294
  import { z } from "zod";
@@ -3258,10 +3335,476 @@ async function runBridgeGasTank(_input) {
3258
3335
  };
3259
3336
  }
3260
3337
 
3261
- // src/tools/recurring-list.ts
3338
+ // src/tools/yield-reserves.ts
3262
3339
  import { z as z14 } from "zod";
3263
- var RecurringListInputSchema = z14.object({
3264
- 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(
3265
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."
3266
3809
  )
3267
3810
  });
@@ -3345,29 +3888,29 @@ async function runRecurringList(input = {}) {
3345
3888
  }
3346
3889
 
3347
3890
  // src/tools/recurring-create.ts
3348
- import { z as z15 } from "zod";
3891
+ import { z as z19 } from "zod";
3349
3892
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
3350
3893
  var AMOUNT_RE = /^\d+(\.\d{1,18})?$/;
3351
- var RecurringCreateInputSchema = z15.object({
3352
- confirm: z15.literal(true).describe(
3894
+ var RecurringCreateInputSchema = z19.object({
3895
+ confirm: z19.literal(true).describe(
3353
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.'
3354
3897
  ),
3355
- frequency: z15.string().min(1).describe(
3898
+ frequency: z19.string().min(1).describe(
3356
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.'
3357
3900
  ),
3358
- recipient: z15.string().regex(ADDRESS_RE).describe("0x-prefixed 20-byte recipient address. Required."),
3359
- 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(
3360
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).'
3361
3904
  ),
3362
- 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(
3363
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."
3364
3907
  ),
3365
- token: z15.enum(["USDC", "USDT"]).default("USDT").describe("Stablecoin to send. USDC or USDT. Both peg to USD-1."),
3366
- label: z15.string().max(64).optional().describe("Optional human-readable label (\u226464 chars). Shows up in q402_recurring_list and the dashboard."),
3367
- 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(
3368
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."
3369
3912
  ),
3370
- walletId: z15.string().optional().describe(
3913
+ walletId: z19.string().optional().describe(
3371
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."
3372
3915
  )
3373
3916
  });
@@ -3496,12 +4039,12 @@ async function runRecurringCreate(input) {
3496
4039
  }
3497
4040
 
3498
4041
  // src/tools/recurring-cancel.ts
3499
- import { z as z16 } from "zod";
3500
- var RecurringCancelInputSchema = z16.object({
3501
- 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(
3502
4045
  "Rule id to cancel. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field. Cancelling is immediate."
3503
4046
  ),
3504
- walletId: z16.string().optional().describe(
4047
+ walletId: z20.string().optional().describe(
3505
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."
3506
4049
  )
3507
4050
  });
@@ -3589,15 +4132,15 @@ async function runRecurringCancel(input) {
3589
4132
  }
3590
4133
 
3591
4134
  // src/tools/recurring-fires.ts
3592
- import { z as z17 } from "zod";
3593
- var RecurringFiresInputSchema = z17.object({
3594
- 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(
3595
4138
  "Rule id whose fire history to fetch. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field."
3596
4139
  ),
3597
- limit: z17.number().int().min(1).max(50).optional().describe(
4140
+ limit: z21.number().int().min(1).max(50).optional().describe(
3598
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."
3599
4142
  ),
3600
- walletId: z17.string().optional().describe(
4143
+ walletId: z21.string().optional().describe(
3601
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."
3602
4145
  )
3603
4146
  });
@@ -3703,12 +4246,12 @@ async function runRecurringFires(input) {
3703
4246
  }
3704
4247
 
3705
4248
  // src/tools/recurring-pause.ts
3706
- import { z as z18 } from "zod";
3707
- var RecurringPauseInputSchema = z18.object({
3708
- 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(
3709
4252
  "Rule id to pause. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field. Pausing is immediate and reversible."
3710
4253
  ),
3711
- walletId: z18.string().optional().describe(
4254
+ walletId: z22.string().optional().describe(
3712
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."
3713
4256
  )
3714
4257
  });
@@ -3796,12 +4339,12 @@ async function runRecurringPause(input) {
3796
4339
  }
3797
4340
 
3798
4341
  // src/tools/recurring-resume.ts
3799
- import { z as z19 } from "zod";
3800
- var RecurringResumeInputSchema = z19.object({
3801
- 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(
3802
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."
3803
4346
  ),
3804
- walletId: z19.string().optional().describe(
4347
+ walletId: z23.string().optional().describe(
3805
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."
3806
4349
  )
3807
4350
  });
@@ -3889,12 +4432,12 @@ async function runRecurringResume(input) {
3889
4432
  }
3890
4433
 
3891
4434
  // src/tools/recurring-skip-next.ts
3892
- import { z as z20 } from "zod";
3893
- var RecurringSkipNextInputSchema = z20.object({
3894
- 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(
3895
4438
  "Rule id whose next scheduled fire to skip. Obtain from q402_recurring_list \u2014 each entry's `ruleId` field."
3896
4439
  ),
3897
- walletId: z20.string().optional().describe(
4440
+ walletId: z24.string().optional().describe(
3898
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."
3899
4442
  )
3900
4443
  });
@@ -4019,7 +4562,18 @@ async function main() {
4019
4562
  BRIDGE_QUOTE_TOOL,
4020
4563
  BRIDGE_SEND_TOOL,
4021
4564
  BRIDGE_HISTORY_TOOL,
4022
- 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
4023
4577
  ]
4024
4578
  }));
4025
4579
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -4106,6 +4660,22 @@ async function main() {
4106
4660
  const parsed = BridgeGasTankInputSchema.parse(args ?? {});
4107
4661
  return await runBridgeGasTank(parsed);
4108
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
+ }
4109
4679
  default:
4110
4680
  return {
4111
4681
  isError: true,
package/package.json CHANGED
@@ -1,75 +1,75 @@
1
- {
2
- "name": "@quackai/q402-mcp",
3
- "version": "0.8.16",
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
+ }