scan_yields
Identify top DeFi yields from Aave, Compound, Morpho, Lido, Pendle. Filter by chain, asset, minimum TVL. Returns APY, TVL, risk score.
Instructions
Scan top DeFi yield opportunities across protocols: Aave, Compound, Morpho, Lido, Pendle, and more. Filter by chain, asset, and minimum TVL. Returns APY, TVL, risk score, and protocol details. Costs 0.005 USDC per call (x402 micropayment on Base).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| chain | No | Blockchain to filter by (e.g. "ethereum", "base", "arbitrum", "polygon"). Omit for all chains. | |
| asset | No | Filter by asset symbol (e.g. "ETH", "USDC", "stETH"). Omit for all assets. | |
| min_tvl | No | Minimum TVL in USD (e.g. 1000000 for $1M). Omit for no minimum. | |
| limit | No | Maximum number of results to return (1–50). Defaults to 20. |
Implementation Reference
- src/index.ts:342-372 (registration)Tool definition registration for 'scan_yields' — defines the name, description, and input schema (chain, asset, min_tvl, limit) in the TOOLS array.
{ name: 'scan_yields', description: 'Scan top DeFi yield opportunities across protocols: Aave, Compound, Morpho, Lido, Pendle, and more. ' + 'Filter by chain, asset, and minimum TVL. Returns APY, TVL, risk score, and protocol details. ' + 'Costs 0.005 USDC per call (x402 micropayment on Base).', inputSchema: { type: 'object', properties: { chain: { type: 'string', description: 'Blockchain to filter by (e.g. "ethereum", "base", "arbitrum", "polygon"). ' + 'Omit for all chains.', }, asset: { type: 'string', description: 'Filter by asset symbol (e.g. "ETH", "USDC", "stETH"). Omit for all assets.', }, min_tvl: { type: 'number', description: 'Minimum TVL in USD (e.g. 1000000 for $1M). Omit for no minimum.', }, limit: { type: 'number', description: 'Maximum number of results to return (1–50). Defaults to 20.', }, }, required: [], }, }, - src/index.ts:489-496 (handler)Handler for the 'scan_yields' tool — calls the '/api/yield-scanner' API endpoint with chain, asset, min_tvl, and limit parameters.
case 'scan_yields': result = await callApi('/api/yield-scanner', { chain: params.chain, asset: params.asset, min_tvl: params.min_tvl, limit: params.limit, }); break; - src/index.ts:114-180 (helper)The callApi helper function that makes HTTP requests to the backend API, used by the scan_yields handler to call /api/yield-scanner.
async function callApi( endpoint: string, params: Record<string, string | number | undefined> = {} ): Promise<ApiResponse> { const url = new URL(`${API_BASE_URL}${endpoint}`); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== '') { url.searchParams.set(key, String(value)); } } const fetchFn = await getX402Fetch(); let response: Response; const controller = new AbortController(); const fetchTimeout = setTimeout(() => controller.abort(), 30_000); try { response = await fetchFn(url.toString(), { headers: { 'Accept': 'application/json', 'User-Agent': `x402-api-mcp/${SERVER_VERSION}`, }, signal: controller.signal, }); } catch (err) { const isTimeout = err instanceof Error && err.name === 'AbortError'; throw new McpError( ErrorCode.InternalError, isTimeout ? `Request to ${endpoint} timed out after 30 seconds` : `Network error calling ${endpoint}: ${err instanceof Error ? err.message : String(err)}` ); } finally { clearTimeout(fetchTimeout); } if (response.status === 402) { // Clone before reading so we can fall back to text() if JSON parsing fails. // Without the clone, calling response.json() consumes the body; a subsequent // response.text() call then throws "body already used". const cloned = response.clone(); let paymentDetails: unknown; try { paymentDetails = await response.json(); } catch { paymentDetails = await cloned.text(); } return { status: 402, data: null, paymentRequired: true, paymentDetails }; } if (!response.ok) { const errorText = await response.text(); if (response.status === 400 || response.status === 422) { throw new McpError( ErrorCode.InvalidParams, `Invalid request to ${endpoint}: ${errorText}` ); } throw new McpError( ErrorCode.InternalError, `API error ${response.status} from ${endpoint}: ${errorText}` ); } const data = await response.json(); return { status: response.status, data }; } - src/index.ts:185-242 (helper)The formatResult helper function that formats the API response, including handling 402 payment-required responses for scan_yields.
function formatResult(result: ApiResponse, toolName: string): string { if (result.paymentRequired) { const details = result.paymentDetails as Record<string, unknown> | null; const accepts = details?.accepts as Array<Record<string, unknown>> | undefined; const first = accepts?.[0]; let message = `## Payment Required — ${toolName}\n\n`; message += `This endpoint requires a USDC micropayment on Base network.\n\n`; if (first) { const amountRaw = Number(first.maxAmountRequired ?? 0); const amountUsdc = (amountRaw / 1_000_000).toFixed(6); message += `**Cost:** ${amountUsdc} USDC\n`; message += `**Pay to:** \`${first.payTo}\`\n`; message += `**Network:** Base mainnet (chain ID 8453)\n`; message += `**Asset:** USDC (\`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\`)\n\n`; } message += `### To enable automatic payments:\n\n`; message += `1. Install dependencies:\n`; message += ` \`\`\`bash\n npm install x402-fetch viem\n \`\`\`\n\n`; message += `2. Set your wallet private key:\n`; message += ` \`\`\`bash\n export X402_WALLET_PRIVATE_KEY=0x...\n \`\`\`\n\n`; message += `3. Restart the MCP server.\n\n`; message += `### To pay manually:\n\n`; message += `1. Send USDC to the address above on Base (chain ID 8453).\n`; message += `2. Encode the payment as Base64 JSON and send it as the \`X-Payment\` header:\n\n`; message += ` \`\`\`js\n`; message += ` // After sending the transaction on-chain:\n`; message += ` const payment = Buffer.from(JSON.stringify({ txHash: "0x<your_tx_hash>", payer: "0x<your_wallet_address>" })).toString("base64");\n`; message += ` // Then set the header: X-Payment: <payment>\n`; message += ` \`\`\`\n\n`; message += ` Or for EIP-3009 transferWithAuthorization (advanced):\n\n`; message += ` \`\`\`js\n`; message += ` const payment = Buffer.from(JSON.stringify({\n`; message += ` signature: "0x...",\n`; message += ` payload: {\n`; message += ` authorization: {\n`; message += ` from: "0x<your_wallet>",\n`; message += ` to: "0x<payTo_address>",\n`; message += ` value: "<amount_in_micro_usdc>",\n`; message += ` validAfter: "0",\n`; message += ` validBefore: "<unix_timestamp>",\n`; message += ` nonce: "0x<random_32_bytes>"\n`; message += ` }\n`; message += ` }\n`; message += ` })).toString("base64");\n`; message += ` \`\`\`\n\n`; message += ` **Note:** The \`X-Payment\` header must be Base64-encoded JSON — raw transaction hashes are not accepted.\n\n`; message += `---\n**Raw 402 response:**\n\`\`\`json\n`; message += JSON.stringify(result.paymentDetails, null, 2); message += `\n\`\`\``; return message; } return JSON.stringify(result.data, null, 2); }