UNPKG

@elizaos/plugin-defillama

Version:

DeFiLlama plugin for ElizaOS - provides DeFi market data, analytics, and yield information

1,489 lines (1,467 loc) 182 kB
// src/index.ts import { logger as logger15 } from "@elizaos/core"; import { z } from "zod"; // src/services/defiLlamaService.ts import { logger, Service } from "@elizaos/core"; var _DefiLlamaService = class _DefiLlamaService extends Service { constructor(runtime) { super(runtime); this.capabilityDescription = "DeFiLlama API integration for DeFi market data and analytics"; this.requestQueue = []; this.requestCount = 0; this.isProcessingQueue = false; this.defiConfig = { apiBaseUrl: "https://api.llama.fi", rateLimitPerMinute: Number(process.env.RATE_LIMIT_PER_MINUTE) || 300, maxConcurrentRequests: Number(process.env.MAX_CONCURRENT_REQUESTS) || 10, retryAttempts: 3, retryDelay: 1e3 }; this.resetTime = Date.now() + 6e4; this.rateLimitStatus = { remaining: this.defiConfig.rateLimitPerMinute, resetTime: this.resetTime, limit: this.defiConfig.rateLimitPerMinute }; } static async start(runtime) { logger.info("Starting DeFiLlama service"); const service = new _DefiLlamaService(runtime); service.rateLimitResetInterval = setInterval(() => { service.requestCount = 0; service.resetTime = Date.now() + 6e4; service.rateLimitStatus.remaining = service.defiConfig.rateLimitPerMinute; service.rateLimitStatus.resetTime = service.resetTime; }, 6e4); service.queueProcessInterval = setInterval( () => service.processQueue(), 100 ); return service; } static async stop(runtime) { logger.info("Stopping DeFiLlama service"); } async stop() { if (this.queueProcessInterval) { clearInterval(this.queueProcessInterval); } if (this.rateLimitResetInterval) { clearInterval(this.rateLimitResetInterval); } this.requestQueue = []; } // API Methods async getProtocols() { return this.makeRequest({ endpoint: "/protocols", priority: "medium" }); } async getProtocol(slug) { return this.makeRequest({ endpoint: `/protocol/${slug}`, priority: "high" }); } async getTVL(protocol) { if (protocol) { const tvlValue = await this.makeRequest({ endpoint: `/tvl/${protocol}`, priority: "medium" }); return { date: (/* @__PURE__ */ new Date()).toISOString(), totalLiquidityUSD: tvlValue }; } else { const chains = await this.makeRequest({ endpoint: "/v2/chains", priority: "medium" }); const totalTVL = chains.reduce((sum, chain) => sum + (chain.tvl || 0), 0); return { date: (/* @__PURE__ */ new Date()).toISOString(), totalLiquidityUSD: totalTVL }; } } async getYields() { const response = await this.makeRequest({ endpoint: "/pools", priority: "medium" }); return response.data || []; } async getChains() { return this.makeRequest({ endpoint: "/v2/chains", priority: "low" }); } async getHistoricalTVL(protocol, period) { const endpoint = `/protocol/${protocol}`; const data = await this.makeRequest({ endpoint, priority: "medium" }); const now = Date.now() / 1e3; const periodSeconds = this.parsePeriod(period); const startTime = now - periodSeconds; const allTvlData = []; const dateMap = /* @__PURE__ */ new Map(); for (const chain of Object.values(data.chainTvls)) { if (chain.tvl) { for (const tvlPoint of chain.tvl) { if (tvlPoint.date >= startTime) { const existingValue = dateMap.get(tvlPoint.date) || 0; dateMap.set( tvlPoint.date, existingValue + tvlPoint.totalLiquidityUSD ); } } } } for (const [date, totalLiquidityUSD] of dateMap) { allTvlData.push({ date, totalLiquidityUSD }); } return allTvlData.sort((a, b) => a.date - b.date); } async getBridges(params) { const queryParams = params ? { includeChains: String(params.includeChains) } : void 0; const response = await this.makeRequest({ endpoint: "/bridges", priority: "low", params: queryParams }); return response.bridges || []; } async getStablecoins(params) { const queryParams = params ? { includePrices: String(params.includePrices) } : void 0; const response = await this.makeRequest({ endpoint: "/stablecoins", priority: "medium", params: queryParams }); return response.peggedAssets; } async getStablecoinChains() { return this.makeRequest({ endpoint: "/stablecoinchains", priority: "low" }); } // Additional endpoints based on DeFiLlama API specification async getYieldChart(poolId) { return this.makeRequest({ endpoint: `/chart/${poolId}`, priority: "medium" }); } async getDexVolumes(params) { return this.makeRequest({ endpoint: "/overview/dexs", priority: "medium", params }); } async getDexVolumesByChain(chain, params) { return this.makeRequest({ endpoint: `/overview/dexs/${chain}`, priority: "medium", params }); } async getProtocolFees(params) { return this.makeRequest({ endpoint: "/overview/fees", priority: "medium", params }); } async getProtocolFeesByChain(chain, params) { return this.makeRequest({ endpoint: `/overview/fees/${chain}`, priority: "medium", params }); } async getProtocolFeesById(protocolId) { return this.makeRequest({ endpoint: `/summary/fees/${protocolId}`, priority: "high" }); } async getCoinPrices(coins, searchWidth) { const params = {}; if (searchWidth) { params.searchWidth = searchWidth; } return this.makeRequest({ endpoint: `/prices/current/${coins.join(",")}`, params, priority: "high" }); } async getHistoricalPrices(coins, timestamp) { return this.makeRequest({ endpoint: `/prices/historical/${timestamp}/${coins.join(",")}`, priority: "medium" }); } async getBatchHistoricalPrices(coins, searchWidth) { const params = { coins: coins.join(",") }; if (searchWidth) { params.searchWidth = searchWidth; } return this.makeRequest({ endpoint: "/batchHistorical", params, priority: "medium" }); } async getFirstPrices(coins) { return this.makeRequest({ endpoint: `/prices/first/${coins.join(",")}`, priority: "low" }); } // Rate Limiting Methods async getRateLimitStatus() { return this.rateLimitStatus; } async batchRequests(requests) { const sortedRequests = requests.sort((a, b) => { const priorityOrder = { high: 0, medium: 1, low: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); const results = await Promise.all( sortedRequests.map((req) => this.makeRequest(req)) ); return results; } // Private Methods async makeRequest(request) { return new Promise((resolve, reject) => { this.requestQueue.push({ request, resolve, reject, timestamp: Date.now() }); this.processQueue(); }); } async processQueue() { if (this.isProcessingQueue || this.requestQueue.length === 0) { return; } this.isProcessingQueue = true; try { const now = Date.now(); if (now >= this.resetTime) { this.requestCount = 0; this.resetTime = now + 6e4; this.rateLimitStatus.remaining = this.defiConfig.rateLimitPerMinute; this.rateLimitStatus.resetTime = this.resetTime; } const availableSlots = this.defiConfig.rateLimitPerMinute - this.requestCount; const requestsToProcess = Math.min( availableSlots, this.defiConfig.maxConcurrentRequests, this.requestQueue.length ); if (requestsToProcess <= 0) { return; } this.requestQueue.sort((a, b) => { const priorityOrder = { high: 0, medium: 1, low: 2 }; const priorityDiff = priorityOrder[a.request.priority] - priorityOrder[b.request.priority]; if (priorityDiff !== 0) return priorityDiff; return a.timestamp - b.timestamp; }); const batch = this.requestQueue.splice(0, requestsToProcess); this.requestCount += batch.length; this.rateLimitStatus.remaining = this.defiConfig.rateLimitPerMinute - this.requestCount; await Promise.all( batch.map(async (item) => { try { const result = await this.executeRequest(item.request); item.resolve(result); } catch (error) { item.reject(error); } }) ); } finally { this.isProcessingQueue = false; } } async executeRequest(request) { let url; if (request.endpoint.startsWith("/stablecoins") || request.endpoint.startsWith("/stablecoin")) { url = `https://stablecoins.llama.fi${request.endpoint}`; } else if (request.endpoint.startsWith("/pools") || request.endpoint.startsWith("/chart/")) { url = `https://yields.llama.fi${request.endpoint}`; } else if (request.endpoint.startsWith("/bridges")) { url = `https://bridges.llama.fi${request.endpoint}`; } else if (request.endpoint.startsWith("/prices/") || request.endpoint === "/batchHistorical") { url = `https://coins.llama.fi${request.endpoint}`; } else if (request.endpoint.startsWith("https://")) { url = request.endpoint; } else { url = `${this.defiConfig.apiBaseUrl}${request.endpoint}`; } const params = new URLSearchParams(request.params || {}); const fullUrl = params.toString() ? `${url}?${params}` : url; for (let attempt = 0; attempt < this.defiConfig.retryAttempts; attempt++) { try { logger.debug(`DeFiLlama API request: ${fullUrl}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1e4); const response = await fetch(fullUrl, { method: "GET", headers: { Accept: "application/json", "User-Agent": "ElizaOS-DeFiLlama-Plugin/1.0" }, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const shouldRetry = this.shouldRetryError(response.status); if (!shouldRetry && attempt === 0) { } throw new Error( `API request failed: ${response.status} ${response.statusText}` ); } const data = await response.json(); return data; } catch (error) { logger.error(`DeFiLlama API error (attempt ${attempt + 1}):`, error); const isLastAttempt = attempt >= this.defiConfig.retryAttempts - 1; const isRetryableError = error instanceof Error && (error.name === "AbortError" || // Timeout error.message.includes("500") || // Server error error.message.includes("502") || // Bad Gateway error.message.includes("503") || // Service Unavailable error.message.includes("429")); if (!isLastAttempt && isRetryableError) { const baseDelay = this.defiConfig.retryDelay * Math.pow(2, attempt); const jitter = Math.random() * 1e3; const delay = baseDelay + jitter; logger.debug(`Retrying in ${delay}ms...`); await new Promise((resolve) => setTimeout(resolve, delay)); } else { const defiError = { code: "API_ERROR", message: "Failed to fetch data from DeFiLlama API", details: error, suggestions: [ "Try again later", "Check your internet connection", error instanceof Error && error.message.includes("404") ? "The requested resource may not exist" : "This may be a temporary service issue" ], fallbackAvailable: false }; throw new Error(JSON.stringify(defiError)); } } } } shouldRetryError(statusCode) { if (statusCode >= 400 && statusCode < 500) { return statusCode === 429; } if (statusCode >= 500) { return true; } return false; } parsePeriod(period) { const units = { "1d": 86400, "7d": 604800, "30d": 2592e3, "90d": 7776e3, "1y": 31536e3 }; return units[period] || 604800; } }; _DefiLlamaService.serviceType = "defillama"; var DefiLlamaService = _DefiLlamaService; // src/actions/protocolDataAction.ts import { logger as logger2, ModelType } from "@elizaos/core"; var extractProtocolsTemplate = `Extract protocol and analysis information from the user's request for DeFi protocol data. User request: "{{userMessage}}" IMPORTANT: Follow DeFiLlama API specification exactly: - Protocol responses use proper case names: "Ethereum", "Polygon", "Arbitrum", "Optimism", "BSC", "Avalanche" - Category names match API format: "Dexs", "Lending", "Liquid Staking", "Derivatives", "Yield", "CDP", "Bridge" - Chain filtering should use proper case as returned by /v2/chains endpoint The user might express protocol requests in various ways: - "What's Aave's TVL?" \u2192 specific protocol: "Aave" - "Compare Uniswap and SushiSwap" \u2192 multiple protocols: ["Uniswap", "SushiSwap"] - "Top lending protocols" \u2192 category: "lending" - "Show me DEX rankings" \u2192 category: "dex" - "Best yield farming protocols" \u2192 category: "yield" - "Ethereum DeFi protocols" \u2192 chain: "Ethereum" - "Protocol analysis for MakerDAO and Compound" \u2192 protocols: ["MakerDAO", "Compound"] - "TVL comparison of top 5 protocols" \u2192 analysis: "top_ranking", count: 5 Extract and return ONLY a JSON object following DeFiLlama API format: { "protocols": ["Protocol names if specifically mentioned"], "categories": ["Category names if mentioned: Dexs/Lending/Liquid Staking/Derivatives/Yield/CDP/Bridge"], "chains": ["Chain names as per /v2/chains endpoint: Ethereum/Polygon/Arbitrum/Optimism/BSC/Avalanche"], "analysisType": "specific/comparison/ranking/category_analysis/overview", "metrics": ["tvl/ranking/volume/fees/apy if specifically requested"], "count": number (if user wants top N protocols), "timeframe": "24h/7d/30d if mentioned for changes", "includeDetails": true/false (if user wants detailed breakdown) } Return only the JSON object, no other text.`; var protocolDataAction = { name: "PROTOCOL_DATA", similes: [ "GET_PROTOCOL", "PROTOCOL_INFO", "PROTOCOL_TVL", "PROTOCOL_STATS", "COMPARE_PROTOCOLS", "PROTOCOL_ANALYSIS", "PROTOCOL_METRICS", "PROTOCOL_RESEARCH", "COMPETITIVE_ANALYSIS", "PROTOCOL_INTELLIGENCE", "MARKET_POSITION", "PROTOCOL_HEALTH" ], description: `Simple DeFi protocol data fetcher. This action: 1. Extracts protocol names/categories from user queries using LLM 2. Fetches current protocol data from DeFiLlama API 3. Returns clean, structured protocol data for analysis or display 4. Supports multiple protocols and basic protocol information USE WHEN: User asks about specific protocols, TVL data, or protocol comparisons. RETURNS: Raw protocol data without extensive analysis - focused on data retrieval.`, validate: async (runtime, message, _state) => { const text = message.content.text?.toLowerCase() || ""; const protocolKeywords = [ "protocol", "tvl", "total value locked", "defi", "ranking", "compare", "aave", "compound", "uniswap", "curve", "makerdao", "top protocols", "market share", "liquidity", "dex", "dexs", "decentralized exchange", "trading", "swap", "swapping", "amm", "automated market maker", "pancakeswap", "sushiswap", "balancer", "1inch", "0x", "exchange" ]; return protocolKeywords.some((keyword) => text.includes(keyword)); }, handler: async (runtime, message, state, options, callback) => { try { logger2.info("[PROTOCOL_DATA] Starting protocol data fetch"); const defiLlamaService = runtime.getService( "defillama" ); if (!defiLlamaService) { throw new Error("DeFiLlama service not available"); } const userQuestion = message.content.text || ""; let extractedParams = {}; if (options?.protocolParams) { extractedParams = options.protocolParams; } else { const prompt = extractProtocolsTemplate.replace( "{{userMessage}}", userQuestion ); const response2 = await runtime.useModel(ModelType.TEXT_LARGE, { prompt }); if (response2) { try { const cleanedResponse = response2.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim(); const parsed = JSON.parse(cleanedResponse); extractedParams = { protocols: parsed.protocols || [], categories: parsed.categories || [], chains: parsed.chains || [], analysisType: parsed.analysisType || "overview", metrics: parsed.metrics || [], count: parsed.count || 10, timeframe: parsed.timeframe || "24h", includeDetails: parsed.includeDetails || false }; logger2.info( `[PROTOCOL_DATA] LLM extracted params: ${JSON.stringify(extractedParams)}` ); } catch (parseError) { logger2.warn( "Failed to parse LLM response, falling back to regex:", parseError ); extractedParams = { protocols: [], categories: [extractCategoryFromQueryLegacy(userQuestion)].filter( Boolean ), chains: [], analysisType: "overview", metrics: [], count: 10, timeframe: "24h", includeDetails: false }; } } else { logger2.warn( "[PROTOCOL_DATA] No LLM response received, falling back to regex" ); extractedParams = { protocols: [], categories: [extractCategoryFromQueryLegacy(userQuestion)].filter( Boolean ), chains: [], analysisType: "overview", metrics: [], count: 10, timeframe: "24h", includeDetails: false }; } } logger2.info( `[PROTOCOL_DATA] Processing request with categories: ${extractedParams.categories.join(", ")}, protocols: ${extractedParams.protocols.join(", ")}` ); const [protocols, chains] = await Promise.all([ defiLlamaService.getProtocols(), defiLlamaService.getChains() ]); const validatedProtocols = Array.isArray(protocols) ? protocols : []; if (validatedProtocols.length === 0) { throw new Error("No protocol data available from API"); } const targetProtocols = extractProtocolsFromDataLLM( extractedParams, validatedProtocols ); logger2.info( `[PROTOCOL_DATA] Found protocols: ${targetProtocols.map((p) => p.name).join(", ")}` ); const protocolData = buildProtocolData( targetProtocols, validatedProtocols, extractedParams.categories[0], // Use first category if multiple extractedParams.count ); const response = await runtime.useModel(ModelType.LARGE, { prompt: `Respond concisely with protocol analysis based on data: ${JSON.stringify(protocolData)} and user query: ${JSON.stringify(message.content)}` }); if (callback) { await callback({ text: response || "Unable to analyze protocol data at this time.", actions: ["PROTOCOL_DATA"], source: message.content.source }); } return { text: response, success: true, data: { actionName: "PROTOCOL_DATA", extractedParams, targetProtocols, protocolData, protocolDataFetched: true, protocolsFound: protocolData.protocols.length, timestamp: Date.now() } }; } catch (error) { logger2.error("[PROTOCOL_DATA] Error:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; if (callback) { await callback({ text: `\u274C Failed to fetch protocol data: ${errorMessage}`, actions: ["PROTOCOL_DATA"], source: message.content.source }); } return { text: `Error fetching protocol data: ${errorMessage}`, success: false, error: error instanceof Error ? error : new Error(errorMessage), data: { actionName: "PROTOCOL_DATA", error: errorMessage, protocolDataFetched: false, timestamp: Date.now() } }; } }, examples: [ [ { name: "{{user}}", content: { text: "What is Aave's current TVL and ranking?" } }, { name: "{{agent}}", content: { text: "Aave currently ranks #2 in DeFi with $12.8B TVL, up 2.3% over 24h. It's the leading lending protocol with deployments across 8 chains.", actions: ["PROTOCOL_DATA"] } } ], [ { name: "{{user}}", content: { text: "Compare Uniswap and SushiSwap TVL" } }, { name: "{{agent}}", content: { text: "Uniswap: $5.2B TVL (Rank #5), SushiSwap: $890M TVL (Rank #23). Uniswap leads with 5.8x higher TVL in the DEX category.", actions: ["PROTOCOL_DATA"] } } ] ] }; function extractProtocolsFromDataLLM(params, protocols) { let foundProtocols = []; if (params.protocols && params.protocols.length > 0) { for (const protocolName of params.protocols) { const protocol = findProtocolByName(protocolName, protocols); if (protocol) { foundProtocols.push(protocol); } } } if (params.categories && params.categories.length > 0) { for (const category of params.categories) { const categoryProtocols = protocols.filter( (p) => p.category && p.category.toLowerCase() === category.toLowerCase() ); foundProtocols.push(...categoryProtocols); } } if (params.chains && params.chains.length > 0) { const chainProtocols = protocols.filter( (p) => p.chains && params.chains.some( (chain) => p.chains.some( (pChain) => pChain.toLowerCase() === chain.toLowerCase() ) ) ); foundProtocols.push(...chainProtocols); } foundProtocols = foundProtocols.filter( (protocol, index, self) => index === self.findIndex((p) => p.slug === protocol.slug) ); if (foundProtocols.length === 0) { foundProtocols = protocols.slice(0, params.count || 10); } return foundProtocols; } function findProtocolByName(name, protocols) { const searchName = name.toLowerCase(); let protocol = protocols.find( (p) => p.name.toLowerCase() === searchName || p.slug.toLowerCase() === searchName || p.symbol?.toLowerCase() === searchName ); if (protocol) return protocol; protocol = protocols.find( (p) => p.name.toLowerCase().includes(searchName) || searchName.includes(p.name.toLowerCase()) || p.slug && p.slug.toLowerCase().includes(searchName) || p.symbol && searchName.includes(p.symbol.toLowerCase()) ); return protocol || null; } function extractCategoryFromQueryLegacy(query) { const searchText = query.toLowerCase(); const categoryMappings = { Dexs: [ "dex", "dexs", "decentralized exchange", "trading", "swap", "swapping", "amm", "automated market maker", "trading experience", "best trading", "trading platform", "exchange protocols" ], Lending: [ "lending", "borrow", "borrowing", "loan", "loans", "credit", "lending protocol", "borrowing platform", "lend" ], "Liquid Staking": [ "liquid staking", "staking", "stake", "validator", "liquid stake", "staking protocol", "staking platform" ], Derivatives: [ "derivatives", "perp", "perpetual", "futures", "options", "derivative protocol", "trading derivatives" ], Yield: [ "yield", "yield farming", "farming", "liquidity mining", "yield protocol", "yield platform", "yields" ], CDP: [ "cdp", "collateralized debt position", "collateral", "debt position", "cdp protocol", "maker", "makerdao" ], Bridge: [ "bridge", "cross-chain", "cross chain", "bridging", "bridge protocol" ] }; for (const [category, keywords] of Object.entries(categoryMappings)) { if (keywords.some((keyword) => searchText.includes(keyword))) { return category; } } return null; } function buildProtocolData(targetProtocols, allProtocols, categoryFilter = null, count = 10) { const data = { protocols: [], totalTvl: 0, categoryBreakdown: {}, timestamp: Date.now() }; let relevantProtocols = allProtocols; if (categoryFilter) { relevantProtocols = allProtocols.filter( (p) => p.category === categoryFilter ); } if (targetProtocols.length > 0) { relevantProtocols = targetProtocols; } relevantProtocols = relevantProtocols.sort((a, b) => (b.tvl || 0) - (a.tvl || 0)).slice(0, count); relevantProtocols.forEach((protocol, index) => { data.protocols.push({ name: protocol.name, slug: protocol.slug, tvl: protocol.tvl || 0, rank: calculateProtocolRank(protocol, allProtocols), category: protocol.category || "Unknown", chains: protocol.chains?.length || 0, change_1d: protocol.change_1d || 0, change_7d: protocol.change_7d || 0, change_30d: protocol.change_30d || 0, formatted: formatProtocolData(protocol) }); data.totalTvl += protocol.tvl || 0; }); data.categoryBreakdown = allProtocols.reduce((acc, p) => { const category = p.category || "Other"; acc[category] = (acc[category] || 0) + (p.tvl || 0); return acc; }, {}); return data; } function calculateProtocolRank(protocol, allProtocols) { const sorted = allProtocols.sort((a, b) => (b.tvl || 0) - (a.tvl || 0)); return sorted.findIndex((p) => p.slug === protocol.slug) + 1; } function formatProtocolData(protocol) { const tvl = formatTvl(protocol.tvl || 0); const change = formatChange(protocol.change_1d); return `${protocol.name}: $${tvl} TVL (${change} 24h)`; } function formatTvl(tvl) { if (tvl >= 1e9) return (tvl / 1e9).toFixed(1) + "B"; if (tvl >= 1e6) return (tvl / 1e6).toFixed(1) + "M"; if (tvl >= 1e3) return (tvl / 1e3).toFixed(1) + "K"; return tvl.toFixed(0); } function formatChange(change) { if (!change) return "N/A"; const sign = change >= 0 ? "+" : ""; return `${sign}${change.toFixed(1)}%`; } // src/actions/yieldSearchAction.ts import { logger as logger3, ModelType as ModelType2 } from "@elizaos/core"; var extractYieldTemplate = `Extract yield search criteria from the user's request for DeFi yield farming opportunities. User request: "{{userMessage}}" IMPORTANT: Follow DeFiLlama API specification exactly: - /pools endpoint returns data with chain names in proper case: "Ethereum", "Polygon", "Arbitrum", "Optimism", "BSC", "Avalanche" - Protocol names as returned by API: "Aave", "Compound", "Uniswap", "Curve", etc. - Asset symbols as in underlyingTokens/symbol fields: "USDC", "ETH", "WBTC", etc. The user might express yield requests in various ways: - "Best USDC yields" \u2192 asset: "USDC", sortBy: "apy" - "Aave staking opportunities" \u2192 protocol: "Aave" - "High APY farming above 15%" \u2192 minApy: 15, riskTolerance: "high" - "Safe stablecoin yields on Polygon" \u2192 chains: ["Polygon"], assets: ["stablecoins"], riskTolerance: "low" - "Ethereum lending yields under 10%" \u2192 chains: ["Ethereum"], maxApy: 10, categories: ["lending"] - "Auto-compounding WETH farms" \u2192 assets: ["WETH"], features: ["auto-compound"] Extract and return ONLY a JSON object following DeFiLlama API format: { "assets": ["Asset symbols if mentioned: USDC/ETH/WBTC/DAI/stablecoins"], "protocols": ["Protocol names if mentioned: Aave/Compound/Uniswap/Curve"], "chains": ["Chain names as per /pools endpoint: Ethereum/Polygon/Arbitrum/Optimism/BSC/Avalanche"], "minApy": number (minimum APY if specified), "maxApy": number (maximum APY if specified), "minTvl": number (minimum TVL in USD if specified), "riskTolerance": "low/medium/high/any (based on user preference)", "categories": ["lending/dex/staking/farming if mentioned"], "features": ["auto-compound/rewards/stable if mentioned"], "sortBy": "apy/tvl/project (how to sort results)", "limit": number (how many results, default 15) } Return only the JSON object, no other text.`; var yieldSearchAction = { name: "YIELD_SEARCH", similes: [ "FIND_YIELDS", "YIELD_FARMING", "BEST_APY", "STAKING_YIELDS", "FARM_SEARCH", "APY_SEARCH", "YIELD_OPPORTUNITIES", "FARMING_OPTIONS", "STAKE_REWARDS", "LIQUIDITY_MINING" ], description: `Simple yield opportunity finder. This action: 1. Extracts yield search criteria from user queries using LLM 2. Fetches current yield pool data from DeFiLlama API 3. Returns clean, structured yield data for analysis or display 4. Supports filtering by protocol, asset, chain, and APY ranges USE WHEN: User asks about yield farming, staking rewards, APY opportunities, or specific yield strategies. RETURNS: Raw yield data without extensive analysis - focused on data retrieval.`, validate: async (runtime, message, _state) => { const text = message.content.text?.toLowerCase() || ""; const yieldKeywords = [ "yield", "apy", "apr", "farming", "staking", "returns", "interest", "earn", "best yields", "high apy", "stable yields", "usdc yield", "eth yield", "stablecoin yield", "farm", "stake", "liquidity mining", "rewards", "yield opportunities", "passive income", "compound", "auto-compound" ]; return yieldKeywords.some((keyword) => text.includes(keyword)); }, handler: async (runtime, message, state, options, callback) => { try { logger3.info("[YIELD_SEARCH] Starting yield data fetch"); const defiLlamaService = runtime.getService( "defillama" ); if (!defiLlamaService) { throw new Error("DeFiLlama service not available"); } const userQuestion = message.content.text || ""; let yieldCriteria; if (options?.yieldParams) { yieldCriteria = options.yieldParams; } else { const prompt = extractYieldTemplate.replace( "{{userMessage}}", userQuestion ); const response2 = await runtime.useModel(ModelType2.TEXT_LARGE, { prompt }); if (response2) { try { const cleanedResponse = response2.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim(); const parsed = JSON.parse(cleanedResponse); yieldCriteria = { assets: parsed.assets || [], protocols: parsed.protocols || [], chains: parsed.chains || [], minApy: parsed.minApy || 0, maxApy: parsed.maxApy || 1e3, minTvl: parsed.minTvl || 0, riskTolerance: parsed.riskTolerance || "any", categories: parsed.categories || [], features: parsed.features || [], sortBy: parsed.sortBy || "apy", limit: parsed.limit || 15 }; logger3.info( `[YIELD_SEARCH] LLM extracted criteria: ${JSON.stringify(yieldCriteria)}` ); } catch (parseError) { logger3.warn( "Failed to parse LLM response, falling back to regex:", parseError ); yieldCriteria = extractYieldCriteriaLegacy(userQuestion); } } else { logger3.warn( "[YIELD_SEARCH] No LLM response received, falling back to regex" ); yieldCriteria = extractYieldCriteriaLegacy(userQuestion); } } logger3.info( `[YIELD_SEARCH] Fetching yields with criteria: ${JSON.stringify(yieldCriteria)}` ); const yields = await defiLlamaService.getYields(); const validatedYields = Array.isArray(yields) ? yields : []; if (validatedYields.length === 0) { throw new Error("No yield data available from API"); } const yieldData = buildYieldData(yieldCriteria, validatedYields); const response = await runtime.useModel(ModelType2.LARGE, { prompt: `Respond concisely with yield analysis based on data: ${JSON.stringify(yieldData)} and user query: ${JSON.stringify(message.content)}` }); if (callback) { await callback({ text: response || "Unable to fetch yield data at this time.", actions: ["YIELD_SEARCH"], source: message.content.source }); } return { text: response, success: true, data: { actionName: "YIELD_SEARCH", extractedCriteria: yieldCriteria, yieldData, yieldDataFetched: true, yieldsFound: yieldData.yields.length, timestamp: Date.now() } }; } catch (error) { logger3.error("[YIELD_SEARCH] Error:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; if (callback) { await callback({ text: `\u274C Failed to fetch yield data: ${errorMessage}`, actions: ["YIELD_SEARCH"], source: message.content.source }); } return { text: `Error fetching yield data: ${errorMessage}`, success: false, error: error instanceof Error ? error : new Error(errorMessage), data: { actionName: "YIELD_SEARCH", error: errorMessage, yieldDataFetched: false, timestamp: Date.now() } }; } }, examples: [ [ { name: "{{user}}", content: { text: "What are the best USDC yields right now?" } }, { name: "{{agent}}", content: { text: "Best USDC yields: Aave V3 Polygon 4.2% APY ($45M TVL), Compound V3 3.8% APY ($120M TVL), Venus BSC 5.1% APY ($12M TVL). All are stable lending yields with low risk.", actions: ["YIELD_SEARCH"] } } ], [ { name: "{{user}}", content: { text: "Find me high APY farming opportunities above 15%" } }, { name: "{{agent}}", content: { text: "High APY opportunities: PancakeSwap CAKE-BNB 18.5% APY, TraderJoe AVAX-USDC 22.1% APY, SpookySwap BOO-FTM 28.3% APY. Note: Higher APY carries increased impermanent loss and smart contract risks.", actions: ["YIELD_SEARCH"] } } ] ] }; function extractYieldCriteriaLegacy(query) { const searchText = query.toLowerCase(); const criteria = { assets: [], protocols: [], chains: [], minApy: 0, maxApy: 1e3, riskLevel: "any" }; const assetKeywords = { usdc: "usdc", usdt: "usdt", dai: "dai", eth: "eth", weth: "weth", btc: "btc", wbtc: "wbtc", matic: "matic", avax: "avax", bnb: "bnb", stablecoin: ["usdc", "usdt", "dai", "frax", "busd"] }; for (const [keyword, assets] of Object.entries(assetKeywords)) { if (searchText.includes(keyword)) { criteria.assets.push(...Array.isArray(assets) ? assets : [assets]); } } const protocolKeywords = { aave: "aave", compound: "compound", uniswap: "uniswap", sushiswap: "sushiswap", pancakeswap: "pancakeswap", curve: "curve", yearn: "yearn", convex: "convex", lido: "lido", maker: "maker" }; for (const [keyword, protocol] of Object.entries(protocolKeywords)) { if (searchText.includes(keyword)) { criteria.protocols.push(protocol); } } const chainKeywords = { ethereum: "Ethereum", polygon: "Polygon", arbitrum: "Arbitrum", optimism: "Optimism", avalanche: "Avalanche", bsc: "BSC", fantom: "Fantom", solana: "Solana" }; for (const [keyword, chain] of Object.entries(chainKeywords)) { if (searchText.includes(keyword)) { criteria.chains.push(chain); } } const apyMatch = searchText.match(/(\d+)%?\s*(apy|apr|yield)/i); if (apyMatch) { const apy = parseInt(apyMatch[1]); if (searchText.includes("above") || searchText.includes("over") || searchText.includes("high")) { criteria.minApy = apy; } else if (searchText.includes("under") || searchText.includes("below")) { criteria.maxApy = apy; } } if (searchText.includes("safe") || searchText.includes("stable") || searchText.includes("low risk")) { criteria.riskLevel = "low"; } else if (searchText.includes("high risk") || searchText.includes("aggressive")) { criteria.riskLevel = "high"; } return criteria; } function buildYieldData(criteria, allYields) { const data = { yields: [], totalTvl: 0, averageApy: 0, timestamp: Date.now() }; let filteredYields = allYields; if (criteria.assets && criteria.assets.length > 0) { filteredYields = filteredYields.filter((y) => { const symbol = y.symbol?.toLowerCase() || ""; const underlying = y.underlyingTokens?.map((t) => t.toLowerCase()) || []; return criteria.assets.some((asset) => { const searchAsset = asset.toLowerCase(); if (searchAsset === "stablecoins") { return ["usdc", "usdt", "dai", "frax", "busd"].some( (stable) => symbol.includes(stable) || underlying.some((u) => u.includes(stable)) ); } return symbol.includes(searchAsset) || underlying.some((u) => u.includes(searchAsset)); }); }); } if (criteria.protocols && criteria.protocols.length > 0) { filteredYields = filteredYields.filter( (y) => criteria.protocols.some( (protocol) => y.project?.toLowerCase().includes(protocol.toLowerCase()) ) ); } if (criteria.chains && criteria.chains.length > 0) { filteredYields = filteredYields.filter( (y) => criteria.chains.some( (chain) => y.chain?.toLowerCase().includes(chain.toLowerCase()) ) ); } filteredYields = filteredYields.filter((y) => { const apy = y.apy || 0; return apy >= (criteria.minApy || 0) && apy <= (criteria.maxApy || 1e3); }); if (criteria.minTvl && criteria.minTvl > 0) { filteredYields = filteredYields.filter( (y) => (y.tvlUsd || 0) >= criteria.minTvl ); } if (criteria.riskTolerance === "low") { filteredYields = filteredYields.filter((y) => { const apy = y.apy || 0; const tvl = y.tvlUsd || 0; return apy < 15 && tvl > 1e6; }); } else if (criteria.riskTolerance === "high") { filteredYields = filteredYields.filter((y) => { const apy = y.apy || 0; return apy > 20; }); } const sortBy = criteria.sortBy || "apy"; if (sortBy === "apy") { filteredYields = filteredYields.sort((a, b) => (b.apy || 0) - (a.apy || 0)); } else if (sortBy === "tvl") { filteredYields = filteredYields.sort( (a, b) => (b.tvlUsd || 0) - (a.tvlUsd || 0) ); } else if (sortBy === "project") { filteredYields = filteredYields.sort( (a, b) => (a.project || "").localeCompare(b.project || "") ); } filteredYields = filteredYields.slice(0, criteria.limit || 15); filteredYields.forEach((yieldPool) => { data.yields.push({ pool: yieldPool.pool, project: yieldPool.project || "Unknown", symbol: yieldPool.symbol || "Unknown", chain: yieldPool.chain || "Unknown", tvlUsd: yieldPool.tvlUsd || 0, apy: yieldPool.apy || 0, apyBase: yieldPool.apyBase || 0, apyReward: yieldPool.apyReward || 0, rewardTokens: yieldPool.rewardTokens || [], underlyingTokens: yieldPool.underlyingTokens || [], poolMeta: yieldPool.poolMeta || "", formatted: formatYieldData(yieldPool) }); data.totalTvl += yieldPool.tvlUsd || 0; }); if (data.yields.length > 0) { data.averageApy = data.yields.reduce((sum, y) => sum + y.apy, 0) / data.yields.length; } return data; } function formatYieldData(yieldPool) { const apy = (yieldPool.apy || 0).toFixed(1); const tvl = formatLargeNumber(yieldPool.tvlUsd || 0); return `${yieldPool.project} ${yieldPool.symbol}: ${apy}% APY, $${tvl} TVL`; } function formatLargeNumber(num) { if (num >= 1e9) return (num / 1e9).toFixed(1) + "B"; if (num >= 1e6) return (num / 1e6).toFixed(1) + "M"; if (num >= 1e3) return (num / 1e3).toFixed(1) + "K"; return num.toFixed(0); } // src/actions/marketTrendsAction.ts import { logger as logger4, ModelType as ModelType3 } from "@elizaos/core"; var extractMarketTrendsTemplate = `Extract market trends analysis parameters from the user's request for DeFi market trend data. User request: "{{userMessage}}" IMPORTANT: Follow DeFiLlama API specification exactly: - /protocols endpoint returns protocol data with tvl, change_1d, change_7d, category - /chains endpoint returns chain data with tvl (no change data available) - /overview/fees and /overview/dexs endpoints provide additional performance metrics - Protocol categories: "Dexs", "Lending", "Liquid Staking", "Derivatives", "Yield", "CDP", "Bridge" - Chain names in proper case: "Ethereum", "Polygon", "Arbitrum", "Optimism", "BSC", "Avalanche" The user might express market trends requests in various ways: - "Best performing DeFi protocols this week" \u2192 focus: "protocols", timeframe: "7d", direction: "winners" - "Chain performance trends last 30 days" \u2192 focus: "chains", timeframe: "30d", analysisType: "performance" - "DEX sector analysis" \u2192 focus: "categories", category: "Dexs", sortBy: "tvl" - "Top 5 lending protocols by TVL" \u2192 focus: "protocols", category: "Lending", sortBy: "tvl", limit: 5 - "Market losers today" \u2192 direction: "losers", timeframe: "1d" - "DeFi market overview" \u2192 focus: "all", analysisType: "overview" - "Volume leaders this month" \u2192 sortBy: "volume", timeframe: "30d" Extract and return ONLY a JSON object following DeFiLlama API format: { "focus": "protocols/chains/categories/all (what to analyze)", "timeframe": "1d/7d/30d (time period for analysis)", "direction": "winners/losers/all (performance filter)", "category": "Dexs/Lending/Liquid Staking/Derivatives if mentioned", "sortBy": "tvl/volume/change (how to rank results)", "analysisType": "performance/overview/ranking/comparison", "limit": number (how many results, default 10), "includeMetrics": ["tvl/volume/fees/change if specifically requested"], "marketScope": "overall/sector/chain if specified" } Return only the JSON object, no other text.`; var marketTrendsAction = { name: "MARKET_TRENDS", similes: [ "MARKET_ANALYSIS", "TREND_ANALYSIS", "MARKET_OVERVIEW", "DEFI_TRENDS", "SECTOR_PERFORMANCE", "CHAIN_TRENDS", "PROTOCOL_TRENDS", "TVL_TRENDS", "MARKET_SENTIMENT", "PERFORMANCE_ANALYSIS" ], description: `Simple market trends data fetcher. This action: 1. Extracts trend analysis criteria from user queries using LLM 2. Fetches current market trend data from DeFiLlama API 3. Returns clean, structured trend data for analysis or display 4. Supports filtering by time period, category, and specific metrics USE WHEN: User asks about market trends, sector performance, protocol rankings, or DeFi market analysis. RETURNS: Raw trend data without extensive analysis - focused on data retrieval.`, validate: async (runtime, message, _state) => { const text = message.content.text?.toLowerCase() || ""; const trendKeywords = [ "trend", "trends", "market", "performance", "growth", "decline", "sector", "category", "ranking", "winners", "losers", "best performing", "worst performing", "market overview", "defi market", "tvl growth", "volume trends", "chain performance", "protocol ranking" ]; return trendKeywords.some((keyword) => text.includes(keyword)); }, handler: async (runtime, message, state, options, callback) => { try { logger4.info("[MARKET_TRENDS] Starting market trends data fetch"); const defiLlamaService = runtime.getService( "defillama" ); if (!defiLlamaService) { throw new Error("DeFiLlama service not available"); } const userQuestion = message.content.text || ""; let extractedParams; if (options?.trendParams) { extractedParams = options.trendParams; } else { const prompt = extractMarketTrendsTemplate.replace( "{{userMessage}}", userQuestion ); const response2 = await runtime.useModel(ModelType3.TEXT_LARGE, { prompt }); if (response2) { try { const cleanedResponse = response2.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim(); const parsed = JSON.parse(cleanedResponse); extractedParams = { focus: parsed.focus || "protocols", timeframe: parsed.timeframe || "7d", direction: parsed.direction || "all", category: parsed.category || void 0, sortBy: parsed.sortBy || "tvl", analysisType: parsed.analysisType || "performance", limit: parsed.limit || 10, includeMetrics: parsed.includeMetrics || [], marketScope: parsed.marketScope || "overall" }; logger4.info( `[MARKET_TRENDS] LLM extracted params: ${JSON.stringify(extractedParams)}` ); } catch (parseError) { logger4.warn( "Failed to parse LLM response, falling back to regex:", parseError ); extractedParams = { ...extractTrendCriteriaLegacy(userQuestion), analysisType: "performance", includeMetrics: [], marketScope: "overall" }; } } else { logger4.warn( "[MARKET_TRENDS] No LLM response received, falling back to regex" ); extractedParams = { ...extractTrendCriteriaLegacy(userQuestion), analysisType: "performance", includeMetrics: [], marketScope: "overall" }; } } logger4.info( `[MARKET_TRENDS] Fetching trends with criteria: ${JSON.stringify(extractedParams)}` ); const [protocols, chains, dexVolumes, protocolFees] = await Promise.all([ defiLlamaService.getProtocols(), defiLlamaService.getChains(), defiLlamaService.getDexVolumes({ excludeTotalDataChart: true, excludeTotalDataChartBreakdown: true }).catch(() => null), defiLlamaService.getProtocolFees({ excludeTotalDataChart: true, excludeTotalDataChartBreakdown: true, dataType: "dailyFees" }).catch(() => null) ]); const validatedProtocols = Array.isArray(protocols) ? protocols : []; const validatedChains = Array.isArray(chains) ? chains : []; const validatedDexVolumes = Array.isArray(dexVolumes) ? dexVolumes : null; const validatedProtocolFees = Array.isArray(protocolFees) ? protocolFees : null; if (validatedProtocols.length === 0) { throw new Error("No market data available from API"); } const trendData = buildTrendDataLLM( extractedParams, validatedProtocols, validatedChains, validatedDexVolumes, validatedProtocolFees ); const response = await runtime.useModel(ModelType3.LARGE, { prompt: `Respond concisely with market trends analysis based on data: ${JSON.stringify(trendData)} and user query: ${JSON.stringify(message.content)}` }); if (callback) { await callback({ text: response || "Unable to analyze market trends at this time.", actions: ["MARKET_TRENDS"], source: message.content.source }); } return { text: response, success: true, data: { actionName: "MARKET_TRENDS", extractedParams, trendData, trendDataFetched: true, trendsFound: trendData.trends.length, timestamp: Date.now() } }; } catch (error) { logger4.error("[MARKET_TRENDS] Error:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; if (callback) { await callback({ text: `\u274C Failed to fetch trend data: ${errorMessage}`, actions: ["MARKET_TRENDS"], source: message.content.source }); } return { text: `Error fetching trend data: ${errorMessage}`, success: false, error: error instanceof Error ? err