├── .gitignore ├── .env.test ├── .DS_Store ├── .npmignore ├── images ├── logo.jpg └── banner.jpg ├── src ├── .DS_Store ├── constants.ts ├── types.ts ├── templates │ ├── trending.ts │ ├── trendingPools.ts │ ├── newCoins.ts │ ├── networkNewPools.ts │ ├── networkTrendingPools.ts │ ├── gainersLosers.ts │ ├── markets.ts │ ├── priceAddress.ts │ └── price.ts ├── environment.ts ├── index.ts ├── providers │ ├── categoriesProvider.ts │ ├── coinsProvider.ts │ └── networkProvider.ts └── actions │ ├── getNewlyListed.ts │ ├── getPricePerAddress.ts │ ├── getTrending.ts │ ├── getTrendingPools.ts │ ├── getTopGainersLosers.ts │ ├── getNetworkNewPools.ts │ ├── getNetworkTrendingPools.ts │ ├── getMarkets.ts │ └── getPrice.ts ├── dist └── index.d.ts ├── vitest.config.ts ├── tsup.config.ts ├── __tests__ ├── setup.ts └── actions │ ├── getPrice.test.ts │ ├── getTrending.test.ts │ ├── getTopGainersLosers.test.ts │ └── getMarkets.test.ts ├── tsconfig.json ├── biome.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | COINGECKO_API_KEY=your_test_api_key_here -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-coingecko/HEAD/.DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !dist/** 4 | !package.json 5 | !readme.md 6 | !tsup.config.ts 7 | -------------------------------------------------------------------------------- /images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-coingecko/HEAD/images/logo.jpg -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-coingecko/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-coingecko/HEAD/images/banner.jpg -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@elizaos/core'; 2 | 3 | declare const coingeckoPlugin: Plugin; 4 | 5 | export { coingeckoPlugin, coingeckoPlugin as default }; 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_URLS = { 2 | FREE: 'https://api.coingecko.com/api/v3', 3 | PRO: 'https://pro-api.coingecko.com/api/v3' 4 | } as const; 5 | 6 | // We'll determine which URL to use based on API key validation/usage 7 | export const DEFAULT_BASE_URL = API_URLS.FREE; -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | setupFiles: ['./__tests__/setup.ts'], 8 | include: ['**/__tests__/**/*.test.ts'], 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | outDir: "dist", 6 | sourcemap: true, 7 | clean: true, 8 | format: ["esm"], 9 | dts: true, 10 | external: [ "@elizaos/core", "@elizaos/core","dotenv", "fs", "path", "https", "http"], 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | import { elizaLogger } from '@elizaos/core'; 3 | 4 | // Mock elizaLogger 5 | vi.mock('@elizaos/core', () => ({ 6 | elizaLogger: { 7 | log: vi.fn(), 8 | error: vi.fn(), 9 | warn: vi.fn(), 10 | info: vi.fn(), 11 | generateObject: vi.fn(), 12 | } 13 | })); 14 | 15 | // Mock fetch 16 | global.fetch = vi.fn(); 17 | 18 | beforeEach(() => { 19 | vi.clearAllMocks(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for CoinGecko plugin 2 | 3 | export interface CoinGeckoConfig { 4 | apiKey: string; 5 | baseUrl?: string; 6 | } 7 | 8 | export interface PriceResponse { 9 | [key: string]: { 10 | [currency: string]: number; 11 | }; 12 | } 13 | 14 | export interface MarketData { 15 | id: string; 16 | symbol: string; 17 | name: string; 18 | current_price: number; 19 | market_cap: number; 20 | market_cap_rank: number; 21 | price_change_percentage_24h: number; 22 | total_volume: number; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "lib": ["ESNext", "dom"], 6 | "module": "Preserve", 7 | "moduleResolution": "Bundler", 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": false, 12 | "allowImportingTsExtensions": true, 13 | "declaration": true, 14 | "emitDeclarationOnly": true, 15 | "resolveJsonModule": true, 16 | "noImplicitAny": false, 17 | "allowJs": true, 18 | "checkJs": false, 19 | "noEmitOnError": false, 20 | "moduleDetection": "force", 21 | "allowArbitraryExtensions": true 22 | }, 23 | "include": ["src/**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "organizeImports": { 4 | "enabled": false 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "correctness": { 11 | "noUnusedVariables": "error" 12 | }, 13 | "suspicious": { 14 | "noExplicitAny": "error" 15 | }, 16 | "style": { 17 | "useConst": "error", 18 | "useImportType": "off" 19 | } 20 | } 21 | }, 22 | "formatter": { 23 | "enabled": true, 24 | "indentStyle": "space", 25 | "indentWidth": 4, 26 | "lineWidth": 100 27 | }, 28 | "javascript": { 29 | "formatter": { 30 | "quoteStyle": "single", 31 | "trailingCommas": "es5" 32 | } 33 | }, 34 | "files": { 35 | "ignore": [ 36 | "dist/**/*", 37 | "extra/**/*", 38 | "node_modules/**/*" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /src/templates/trending.ts: -------------------------------------------------------------------------------- 1 | export const getTrendingTemplate = ` 2 | Extract the following parameters for trending data: 3 | - **include_nfts** (boolean): Whether to include NFTs in the response (default: true) 4 | - **include_categories** (boolean): Whether to include categories in the response (default: true) 5 | 6 | Provide the values in the following JSON format: 7 | 8 | \`\`\`json 9 | { 10 | "include_nfts": true, 11 | "include_categories": true 12 | } 13 | \`\`\` 14 | 15 | Example request: "What's trending in crypto?" 16 | Example response: 17 | \`\`\`json 18 | { 19 | "include_nfts": true, 20 | "include_categories": true 21 | } 22 | \`\`\` 23 | 24 | Example request: "Show me trending coins only" 25 | Example response: 26 | \`\`\`json 27 | { 28 | "include_nfts": false, 29 | "include_categories": false 30 | } 31 | \`\`\` 32 | 33 | Here are the recent user messages for context: 34 | {{recentMessages}} 35 | 36 | Based on the conversation above, if the request is for trending market data, extract the appropriate parameters and respond with a JSON object. If the request is not related to trending data, respond with null.`; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elizaos-plugins/plugin-coingecko", 3 | "version": "0.1.9", 4 | "main": "dist/index.js", 5 | "type": "module", 6 | "types": "dist/index.d.ts", 7 | "dependencies": { 8 | "axios": "^1.6.7", 9 | "tsup": "^8.3.5" 10 | }, 11 | "devDependencies": { 12 | "@biomejs/biome": "1.9.4", 13 | "@vitest/coverage-v8": "^1.2.2", 14 | "vitest": "^1.2.2" 15 | }, 16 | "scripts": { 17 | "build": "tsup --format esm --dts", 18 | "dev": "tsup --format esm --dts --watch", 19 | "test": "vitest run", 20 | "test:watch": "vitest watch", 21 | "test:coverage": "vitest run --coverage", 22 | "clean": "rm -rf dist", 23 | "lint": "biome lint .", 24 | "lint:fix": "biome check --apply .", 25 | "format": "biome format .", 26 | "format:fix": "biome format --write ." 27 | }, 28 | "agentConfig": { 29 | "pluginType": "elizaos:client:1.0.0", 30 | "pluginParameters": { 31 | "COINGECKO_API_KEY": { 32 | "type": "string" 33 | }, 34 | "COINGECKO_PRO_API_KEY": { 35 | "type": "string", 36 | "optional": true 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/templates/trendingPools.ts: -------------------------------------------------------------------------------- 1 | export const getTrendingPoolsTemplate = `Determine if this is a trending pools request. If it is one of the specified situations, perform the corresponding action: 2 | 3 | Situation 1: "Get all trending pools" 4 | - Message contains: phrases like "all trending pools", "show all pools", "list all pools" 5 | - Example: "Show me all trending pools" or "List all pools" 6 | - Action: Return with limit=100 7 | 8 | Situation 2: "Get specific number of pools" 9 | - Message contains: number followed by "pools" or "top" followed by number and "pools" 10 | - Example: "Show top 5 pools" or "Get me 20 trending pools" 11 | - Action: Return with limit=specified number 12 | 13 | Situation 3: "Default trending pools request" 14 | - Message contains: general phrases like "trending pools", "hot pools", "popular pools" 15 | - Example: "What are the trending pools?" or "Show me hot pools" 16 | - Action: Return with limit=10 17 | 18 | For all situations, respond with a JSON object in the format: 19 | \`\`\`json 20 | { 21 | "limit": number 22 | } 23 | \`\`\` 24 | 25 | Previous conversation for context: 26 | {{conversation}} 27 | 28 | You are replying to: {{message}} 29 | `; -------------------------------------------------------------------------------- /src/templates/newCoins.ts: -------------------------------------------------------------------------------- 1 | export const getNewCoinsTemplate = `Determine if this is a new coins request. If it is one of the specified situations, perform the corresponding action: 2 | 3 | Situation 1: "Get all new coins" 4 | - Message contains: phrases like "all new coins", "all recent listings", "all latest coins" 5 | - Example: "Show me all new coin listings" or "List all recently added coins" 6 | - Action: Return with limit=50 7 | 8 | Situation 2: "Get specific number of new coins" 9 | - Message contains: number followed by "new coins" or "latest" followed by number and "coins" 10 | - Example: "Show me 5 new coins" or "Get the latest 20 coins" 11 | - Action: Return with limit=specified number 12 | 13 | Situation 3: "Default new coins request" 14 | - Message contains: general phrases like "new coins", "recent listings", "latest coins" 15 | - Example: "What are the newest coins?" or "Show me recent listings" 16 | - Action: Return with limit=10 17 | 18 | For all situations, respond with a JSON object in the format: 19 | \`\`\`json 20 | { 21 | "limit": number 22 | } 23 | \`\`\` 24 | 25 | Previous conversation for context: 26 | {{conversation}} 27 | 28 | You are replying to: {{message}} 29 | `; -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | import type { IAgentRuntime } from "@elizaos/core"; 2 | import { z } from "zod"; 3 | 4 | const coingeckoConfigSchema = z.object({ 5 | COINGECKO_API_KEY: z.string().nullable(), 6 | COINGECKO_PRO_API_KEY: z.string().nullable(), 7 | }).refine(data => data.COINGECKO_API_KEY || data.COINGECKO_PRO_API_KEY, { 8 | message: "Either COINGECKO_API_KEY or COINGECKO_PRO_API_KEY must be provided" 9 | }); 10 | 11 | export type CoingeckoConfig = z.infer; 12 | 13 | export async function validateCoingeckoConfig(runtime: IAgentRuntime): Promise { 14 | const config = { 15 | COINGECKO_API_KEY: runtime.getSetting("COINGECKO_API_KEY"), 16 | COINGECKO_PRO_API_KEY: runtime.getSetting("COINGECKO_PRO_API_KEY"), 17 | }; 18 | 19 | return coingeckoConfigSchema.parse(config); 20 | } 21 | 22 | export function getApiConfig(config: CoingeckoConfig) { 23 | const isPro = !!config.COINGECKO_PRO_API_KEY; 24 | return { 25 | baseUrl: isPro ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3", 26 | apiKey: isPro ? config.COINGECKO_PRO_API_KEY : config.COINGECKO_API_KEY, 27 | headerKey: isPro ? "x-cg-pro-api-key" : "x-cg-demo-api-key" 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "@elizaos/core"; 2 | import getMarkets from "./actions/getMarkets"; 3 | import getPrice from "./actions/getPrice"; 4 | import getPricePerAddress from "./actions/getPricePerAddress"; 5 | import getTopGainersLosers from "./actions/getTopGainersLosers"; 6 | import getTrending from "./actions/getTrending"; 7 | import getTrendingPools from "./actions/getTrendingPools"; 8 | import getNewlyListed from "./actions/getNewlyListed"; 9 | import getNetworkTrendingPools from "./actions/getNetworkTrendingPools"; 10 | import getNetworkNewPools from "./actions/getNetworkNewPools"; 11 | import { categoriesProvider } from "./providers/categoriesProvider"; 12 | import { coinsProvider } from "./providers/coinsProvider"; 13 | import { networksProvider } from "./providers/networkProvider"; 14 | 15 | export const coingeckoPlugin: Plugin = { 16 | name: "coingecko", 17 | description: "CoinGecko Plugin for Eliza", 18 | actions: [ 19 | getPrice, 20 | getPricePerAddress, 21 | getTrending, 22 | getTrendingPools, 23 | getMarkets, 24 | getTopGainersLosers, 25 | getNewlyListed, 26 | getNetworkTrendingPools, 27 | getNetworkNewPools, 28 | ], 29 | evaluators: [], 30 | providers: [categoriesProvider, coinsProvider, networksProvider], 31 | }; 32 | 33 | export default coingeckoPlugin; 34 | -------------------------------------------------------------------------------- /src/templates/networkNewPools.ts: -------------------------------------------------------------------------------- 1 | export const getNetworkNewPoolsTemplate = `Determine if this is a network-specific new pools request. If it is one of the specified situations, extract the network ID and limit: 2 | 3 | Situation 1: "Get network new pools" 4 | - Message contains: network name AND phrases about new/recent/latest pools 5 | - Example: "Show new pools on Ethereum" or "What are the latest pools on BSC?" 6 | - Action: Extract network ID and use default limit 7 | 8 | Situation 2: "Get specific number of new pools" 9 | - Message contains: number AND network name AND new/recent/latest pools reference 10 | - Example: "Show 5 newest pools on Polygon" or "Get 20 latest pools on Avalanche" 11 | - Action: Extract network ID and specific limit 12 | 13 | Situation 3: "Get all new pools" 14 | - Message contains: "all" AND network name AND new/recent/latest pools reference 15 | - Example: "Show all new pools on BSC" or "List all recent pools on Ethereum" 16 | - Action: Extract network ID and set maximum limit 17 | 18 | Network ID mappings: 19 | - "solana", "sol" => "solana" 20 | - "ethereum", "eth" => "eth" 21 | - "binance smart chain", "bsc", "bnb chain" => "bsc" 22 | - "polygon", "matic" => "polygon_pos" 23 | - "avalanche", "avax" => "avax" 24 | 25 | For all situations, respond with a JSON object in the format: 26 | \`\`\`json 27 | { 28 | "networkId": string, 29 | "limit": number 30 | } 31 | \`\`\` 32 | 33 | Previous conversation for context: 34 | {{conversation}} 35 | 36 | You are replying to: {{message}} 37 | `; 38 | -------------------------------------------------------------------------------- /src/templates/networkTrendingPools.ts: -------------------------------------------------------------------------------- 1 | export const getNetworkTrendingPoolsTemplate = `Determine if this is a network-specific trending pools request. If it is one of the specified situations, extract the network ID and limit: 2 | 3 | Situation 1: "Get network trending pools" 4 | - Message contains: network name (e.g., "solana", "ethereum", "bsc") AND phrases about pools 5 | - Example: "Show trending pools on Solana" or "What are the hot pools on ETH?" 6 | - Action: Extract network ID and use default limit 7 | 8 | Situation 2: "Get specific number of network pools" 9 | - Message contains: number AND network name AND pools reference 10 | - Example: "Show top 5 pools on BSC" or "Get 20 trending pools on Ethereum" 11 | - Action: Extract network ID and specific limit 12 | 13 | Situation 3: "Get all network pools" 14 | - Message contains: "all" AND network name AND pools reference 15 | - Example: "Show all trending pools on Polygon" or "List all hot pools on Avalanche" 16 | - Action: Extract network ID and set maximum limit 17 | 18 | Network ID mappings: 19 | - "solana", "sol" => "solana" 20 | - "ethereum", "eth" => "eth" 21 | - "binance smart chain", "bsc", "bnb chain" => "bsc" 22 | - "polygon", "matic" => "polygon_pos" 23 | - "avalanche", "avax" => "avax" 24 | 25 | For all situations, respond with a JSON object in the format: 26 | \`\`\`json 27 | { 28 | "networkId": string, 29 | "limit": number 30 | } 31 | \`\`\` 32 | 33 | Previous conversation for context: 34 | {{conversation}} 35 | 36 | You are replying to: {{message}} 37 | `; 38 | -------------------------------------------------------------------------------- /src/templates/gainersLosers.ts: -------------------------------------------------------------------------------- 1 | export const getTopGainersLosersTemplate = ` 2 | Extract the following parameters for top gainers and losers data: 3 | - **vs_currency** (string): The target currency to display prices in (e.g., "usd", "eur") - defaults to "usd" 4 | - **duration** (string): Time range for price changes - one of "24h", "7d", "14d", "30d", "60d", "1y" - defaults to "24h" 5 | - **top_coins** (string): Filter by market cap ranking (e.g., "100", "1000") - defaults to "1000" 6 | 7 | Provide the values in the following JSON format: 8 | 9 | \`\`\`json 10 | { 11 | "vs_currency": "usd", 12 | "duration": "24h", 13 | "top_coins": "1000" 14 | } 15 | \`\`\` 16 | 17 | Example request: "Show me the biggest gainers and losers today" 18 | Example response: 19 | \`\`\`json 20 | { 21 | "vs_currency": "usd", 22 | "duration": "24h", 23 | "top_coins": "1000" 24 | } 25 | \`\`\` 26 | 27 | Example request: "What are the top movers in EUR for the past week?" 28 | Example response: 29 | \`\`\`json 30 | { 31 | "vs_currency": "eur", 32 | "duration": "7d", 33 | "top_coins": "300" 34 | } 35 | \`\`\` 36 | 37 | Example request: "Show me monthly performance of top 100 coins" 38 | Example response: 39 | \`\`\`json 40 | { 41 | "vs_currency": "usd", 42 | "duration": "30d", 43 | "top_coins": "100" 44 | } 45 | \`\`\` 46 | 47 | Here are the recent user messages for context: 48 | {{recentMessages}} 49 | 50 | Based on the conversation above, if the request is for top gainers and losers data, extract the appropriate parameters and respond with a JSON object. If the request is not related to top movers data, respond with null.`; -------------------------------------------------------------------------------- /src/templates/markets.ts: -------------------------------------------------------------------------------- 1 | export const getMarketsTemplate = ` 2 | Extract the following parameters for market listing: 3 | - **vs_currency** (string): Target currency for price data (default: "usd") 4 | - **category** (string, optional): Specific category ID from the available categories 5 | - **per_page** (number): Number of results to return (1-250, default: 20) 6 | - **order** (string): Sort order for results, one of: 7 | - market_cap_desc: Highest market cap first 8 | - market_cap_asc: Lowest market cap first 9 | - volume_desc: Highest volume first 10 | - volume_asc: Lowest volume first 11 | 12 | Available Categories: 13 | {{categories}} 14 | 15 | Provide the values in the following JSON format: 16 | 17 | \`\`\`json 18 | { 19 | "vs_currency": "", 20 | "category": "", 21 | "per_page": , 22 | "order": "", 23 | "page": 1, 24 | "sparkline": false 25 | } 26 | \`\`\` 27 | 28 | Example request: "Show me the top 10 gaming cryptocurrencies" 29 | Example response: 30 | \`\`\`json 31 | { 32 | "vs_currency": "usd", 33 | "category": "gaming", 34 | "per_page": 10, 35 | "order": "market_cap_desc", 36 | "page": 1, 37 | "sparkline": false 38 | } 39 | \`\`\` 40 | 41 | Example request: "What are the best performing coins by volume?" 42 | Example response: 43 | \`\`\`json 44 | { 45 | "vs_currency": "usd", 46 | "per_page": 20, 47 | "order": "volume_desc", 48 | "page": 1, 49 | "sparkline": false 50 | } 51 | \`\`\` 52 | 53 | Here are the recent user messages for context: 54 | {{recentMessages}} 55 | 56 | Based on the conversation above, if the request is for a market listing/ranking, extract the appropriate parameters and respond with a JSON object. If the request is for specific coins only, respond with null.`; -------------------------------------------------------------------------------- /src/templates/priceAddress.ts: -------------------------------------------------------------------------------- 1 | export const getPriceByAddressTemplate = ` 2 | Extract the following parameters for token price data: 3 | - **chainId** (string): The blockchain network ID (e.g., "ethereum", "polygon", "binance-smart-chain") 4 | - **tokenAddress** (string): The contract address of the token 5 | - **include_market_cap** (boolean): Whether to include market cap data - defaults to true 6 | 7 | Normalize chain IDs to lowercase names: ethereum, polygon, binance-smart-chain, avalanche, fantom, arbitrum, optimism, etc. 8 | Token address should be the complete address string, maintaining its original case. 9 | 10 | Provide the values in the following JSON format: 11 | 12 | \`\`\`json 13 | { 14 | "chainId": "ethereum", 15 | "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 16 | "include_market_cap": true 17 | } 18 | \`\`\` 19 | 20 | Example request: "What's the price of USDC on Ethereum? Address: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 21 | Example response: 22 | \`\`\`json 23 | { 24 | "chainId": "ethereum", 25 | "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 26 | "include_market_cap": true 27 | } 28 | \`\`\` 29 | 30 | Example request: "Check the price for this token on Polygon: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" 31 | Example response: 32 | \`\`\`json 33 | { 34 | "chainId": "polygon", 35 | "tokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 36 | "include_market_cap": true 37 | } 38 | \`\`\` 39 | 40 | Example request: "Get price for BONK token on Solana with address HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC" 41 | Example response: 42 | \`\`\`json 43 | { 44 | "chainId": "solana", 45 | "tokenAddress": "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC" 46 | } 47 | \`\`\` 48 | 49 | Here are the recent user messages for context: 50 | {{recentMessages}} 51 | 52 | Based on the conversation above, use last question made and if the request is for token price data and includes both a chain and address, extract the appropriate parameters and respond with a JSON object. If the request is not related to token price data or missing required information, respond with null.`; 53 | -------------------------------------------------------------------------------- /src/templates/price.ts: -------------------------------------------------------------------------------- 1 | export const getPriceTemplate = ` 2 | Extract the following parameters for cryptocurrency price data: 3 | - **coinIds** (string | string[]): The ID(s) of the cryptocurrency/cryptocurrencies to get prices for (e.g., "bitcoin" or ["bitcoin", "ethereum"]) 4 | - **currency** (string | string[]): The currency/currencies to display prices in (e.g., "usd" or ["usd", "eur", "jpy"]) - defaults to ["usd"] 5 | - **include_market_cap** (boolean): Whether to include market cap data - defaults to false 6 | - **include_24hr_vol** (boolean): Whether to include 24h volume data - defaults to false 7 | - **include_24hr_change** (boolean): Whether to include 24h price change data - defaults to false 8 | - **include_last_updated_at** (boolean): Whether to include last update timestamp - defaults to false 9 | 10 | Provide the values in the following JSON format: 11 | 12 | \`\`\`json 13 | { 14 | "coinIds": "bitcoin", 15 | "currency": ["usd"], 16 | "include_market_cap": false, 17 | "include_24hr_vol": false, 18 | "include_24hr_change": false, 19 | "include_last_updated_at": false 20 | } 21 | \`\`\` 22 | 23 | Example request: "What's the current price of Bitcoin?" 24 | Example response: 25 | \`\`\`json 26 | { 27 | "coinIds": "bitcoin", 28 | "currency": ["usd"], 29 | "include_market_cap": false, 30 | "include_24hr_vol": false, 31 | "include_24hr_change": false, 32 | "include_last_updated_at": false 33 | } 34 | \`\`\` 35 | 36 | Example request: "Show me ETH price and market cap in EUR with last update time" 37 | Example response: 38 | \`\`\`json 39 | { 40 | "coinIds": "ethereum", 41 | "currency": ["eur"], 42 | "include_market_cap": true, 43 | "include_24hr_vol": false, 44 | "include_24hr_change": false, 45 | "include_last_updated_at": true 46 | } 47 | \`\`\` 48 | 49 | Example request: "What's the current price of Bitcoin in USD, JPY and EUR?" 50 | Example response: 51 | \`\`\`json 52 | { 53 | "coinIds": "bitcoin", 54 | "currency": ["usd", "jpy", "eur"], 55 | "include_market_cap": false, 56 | "include_24hr_vol": false, 57 | "include_24hr_change": false, 58 | "include_last_updated_at": false 59 | } 60 | \`\`\` 61 | 62 | Here are the recent user messages for context: 63 | {{recentMessages}} 64 | 65 | Based on the conversation above, if the request is for cryptocurrency price data, extract the appropriate parameters and respond with a JSON object. If the request is not related to price data, respond with null.`; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plugin CoinGecko 2 | 3 | A plugin for fetching cryptocurrency price data from the CoinGecko API. 4 | 5 | ## Overview 6 | 7 | The Plugin CoinGecko provides a simple interface to get real-time cryptocurrency data. It integrates with CoinGecko's API to fetch current prices, market data, trending coins, and top gainers/losers for various cryptocurrencies in different fiat currencies. 8 | 9 | This plugin uses the [CoinGecko Pro API](https://docs.coingecko.com/reference/introduction). Please refer to their documentation for detailed information about rate limits, available endpoints, and response formats. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pnpm add @elizaos/plugin-coingecko 15 | ``` 16 | 17 | ## Configuration 18 | 19 | Set up your environment with the required CoinGecko API key: 20 | 21 | | Variable Name | Description | 22 | | ------------------- | ---------------------- | 23 | | `COINGECKO_API_KEY` | Your CoinGecko Pro API key | 24 | | `COINGECKO_PRO_API_KEY` | Your CoinGecko Pro API key | 25 | 26 | ## Usage 27 | 28 | ```typescript 29 | import { coingeckoPlugin } from "@elizaos/plugin-coingecko"; 30 | 31 | // Initialize the plugin 32 | const plugin = coingeckoPlugin; 33 | ``` 34 | 35 | ## Actions 36 | 37 | ### GET_PRICE 38 | 39 | Fetches the current price and market data for one or more cryptocurrencies. 40 | 41 | Features: 42 | - Multiple currency support (e.g., USD, EUR, JPY) 43 | - Optional market cap data 44 | - Optional 24h volume data 45 | - Optional 24h price change data 46 | - Optional last update timestamp 47 | 48 | Examples: 49 | - "What's the current price of Bitcoin?" 50 | - "Check ETH price in EUR with market cap" 51 | - "Show me BTC and ETH prices in USD and EUR" 52 | - "What's USDC worth with 24h volume and price change?" 53 | 54 | ### GET_TRENDING 55 | 56 | Fetches the current trending cryptocurrencies on CoinGecko. 57 | 58 | Features: 59 | - Includes trending coins with market data 60 | - Optional NFT inclusion 61 | - Optional category inclusion 62 | 63 | Examples: 64 | - "What's trending in crypto?" 65 | - "Show me trending coins only" 66 | - "What are the hot cryptocurrencies right now?" 67 | 68 | ### GET_TOP_GAINERS_LOSERS 69 | 70 | Fetches the top gaining and losing cryptocurrencies by price change. 71 | 72 | Features: 73 | - Customizable time range (1h, 24h, 7d, 14d, 30d, 60d, 1y) 74 | - Configurable number of top coins to include 75 | - Multiple currency support 76 | - Market cap ranking included 77 | 78 | Examples: 79 | - "Show me the biggest gainers and losers today" 80 | - "What are the top movers in EUR for the past week?" 81 | - "Show me monthly performance of top 100 coins" 82 | 83 | ## Response Format 84 | 85 | All actions return structured data including: 86 | - Formatted text for easy reading 87 | - Raw data for programmatic use 88 | - Request parameters used 89 | - Error details when applicable 90 | 91 | ## Error Handling 92 | 93 | The plugin handles various error scenarios: 94 | - Rate limiting 95 | - API key validation 96 | - Invalid parameters 97 | - Network issues 98 | - Pro plan requirements -------------------------------------------------------------------------------- /src/providers/categoriesProvider.ts: -------------------------------------------------------------------------------- 1 | import { type IAgentRuntime, type Memory, type Provider, type State, elizaLogger } from "@elizaos/core"; 2 | import axios from 'axios'; 3 | import { getApiConfig, validateCoingeckoConfig } from '../environment'; 4 | 5 | interface CategoryItem { 6 | category_id: string; 7 | name: string; 8 | } 9 | 10 | const CACHE_KEY = 'coingecko:categories'; 11 | const CACHE_TTL = 5 * 60; // 5 minutes 12 | const MAX_RETRIES = 3; 13 | 14 | async function fetchCategories(runtime: IAgentRuntime): Promise { 15 | const config = await validateCoingeckoConfig(runtime); 16 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 17 | 18 | const response = await axios.get( 19 | `${baseUrl}/coins/categories/list`, 20 | { 21 | headers: { 22 | 'accept': 'application/json', 23 | [headerKey]: apiKey 24 | }, 25 | timeout: 5000 // 5 second timeout 26 | } 27 | ); 28 | 29 | if (!response.data?.length) { 30 | throw new Error("Invalid categories data received"); 31 | } 32 | 33 | return response.data; 34 | } 35 | 36 | async function fetchWithRetry(runtime: IAgentRuntime): Promise { 37 | let lastError: Error | null = null; 38 | 39 | for (let i = 0; i < MAX_RETRIES; i++) { 40 | try { 41 | return await fetchCategories(runtime); 42 | } catch (error) { 43 | lastError = error; 44 | elizaLogger.error(`Categories fetch attempt ${i + 1} failed:`, error); 45 | await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); 46 | } 47 | } 48 | 49 | throw lastError || new Error("Failed to fetch categories after multiple attempts"); 50 | } 51 | 52 | async function getCategories(runtime: IAgentRuntime): Promise { 53 | try { 54 | // Try to get from cache first 55 | const cached = await runtime.cacheManager.get(CACHE_KEY); 56 | if (cached) { 57 | return cached; 58 | } 59 | 60 | // Fetch fresh data 61 | const categories = await fetchWithRetry(runtime); 62 | 63 | // Cache the result 64 | await runtime.cacheManager.set(CACHE_KEY, categories, { expires: CACHE_TTL }); 65 | 66 | return categories; 67 | } catch (error) { 68 | elizaLogger.error("Error fetching categories:", error); 69 | throw error; 70 | } 71 | } 72 | 73 | function formatCategoriesContext(categories: CategoryItem[]): string { 74 | const popularCategories = [ 75 | 'layer-1', 'defi', 'meme', 'ai-meme-coins', 76 | 'artificial-intelligence', 'gaming', 'metaverse' 77 | ]; 78 | 79 | const popular = categories 80 | .filter(c => popularCategories.includes(c.category_id)) 81 | .map(c => `${c.name} (${c.category_id})`); 82 | 83 | return ` 84 | Available cryptocurrency categories: 85 | 86 | Popular categories: 87 | ${popular.map(c => `- ${c}`).join('\n')} 88 | 89 | Total available categories: ${categories.length} 90 | 91 | You can use these category IDs when filtering cryptocurrency market data. 92 | `.trim(); 93 | } 94 | 95 | export const categoriesProvider: Provider = { 96 | // eslint-disable-next-line 97 | get: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { 98 | try { 99 | const categories = await getCategories(runtime); 100 | return formatCategoriesContext(categories); 101 | } catch (error) { 102 | elizaLogger.error("Categories provider error:", error); 103 | return "Cryptocurrency categories are temporarily unavailable. Please try again later."; 104 | } 105 | } 106 | }; 107 | 108 | // Helper function for actions to get raw categories data 109 | export async function getCategoriesData(runtime: IAgentRuntime): Promise { 110 | return getCategories(runtime); 111 | } 112 | -------------------------------------------------------------------------------- /src/providers/coinsProvider.ts: -------------------------------------------------------------------------------- 1 | import { type IAgentRuntime, type Memory, type Provider, type State, elizaLogger } from "@elizaos/core"; 2 | import axios from 'axios'; 3 | import { getApiConfig, validateCoingeckoConfig } from '../environment'; 4 | 5 | interface CoinItem { 6 | id: string; 7 | symbol: string; 8 | name: string; 9 | } 10 | 11 | const CACHE_KEY = 'coingecko:coins'; 12 | const CACHE_TTL = 5 * 60; // 5 minutes 13 | const MAX_RETRIES = 3; 14 | 15 | async function fetchCoins(runtime: IAgentRuntime, includePlatform = false): Promise { 16 | const config = await validateCoingeckoConfig(runtime); 17 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 18 | 19 | const response = await axios.get( 20 | `${baseUrl}/coins/list`, 21 | { 22 | params: { 23 | include_platform: includePlatform 24 | }, 25 | headers: { 26 | 'accept': 'application/json', 27 | [headerKey]: apiKey 28 | }, 29 | timeout: 5000 // 5 second timeout 30 | } 31 | ); 32 | 33 | if (!response.data?.length) { 34 | throw new Error("Invalid coins data received"); 35 | } 36 | 37 | return response.data; 38 | } 39 | 40 | async function fetchWithRetry(runtime: IAgentRuntime, includePlatform = false): Promise { 41 | let lastError: Error | null = null; 42 | 43 | for (let i = 0; i < MAX_RETRIES; i++) { 44 | try { 45 | return await fetchCoins(runtime, includePlatform); 46 | } catch (error) { 47 | lastError = error; 48 | elizaLogger.error(`Coins fetch attempt ${i + 1} failed:`, error); 49 | await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); 50 | } 51 | } 52 | 53 | throw lastError || new Error("Failed to fetch coins after multiple attempts"); 54 | } 55 | 56 | async function getCoins(runtime: IAgentRuntime, includePlatform = false): Promise { 57 | try { 58 | // Try to get from cache first 59 | const cached = await runtime.cacheManager.get(CACHE_KEY); 60 | if (cached) { 61 | return cached; 62 | } 63 | 64 | // Fetch fresh data 65 | const coins = await fetchWithRetry(runtime, includePlatform); 66 | 67 | // Cache the result 68 | await runtime.cacheManager.set(CACHE_KEY, coins, { expires: CACHE_TTL }); 69 | 70 | return coins; 71 | } catch (error) { 72 | elizaLogger.error("Error fetching coins:", error); 73 | throw error; 74 | } 75 | } 76 | 77 | function formatCoinsContext(coins: CoinItem[]): string { 78 | const popularCoins = [ 79 | 'bitcoin', 'ethereum', 'binancecoin', 'ripple', 80 | 'cardano', 'solana', 'polkadot', 'dogecoin' 81 | ]; 82 | 83 | const popular = coins 84 | .filter(c => popularCoins.includes(c.id)) 85 | .map(c => `${c.name} (${c.symbol.toUpperCase()}) - ID: ${c.id}`); 86 | 87 | return ` 88 | Available cryptocurrencies: 89 | 90 | Popular coins: 91 | ${popular.map(c => `- ${c}`).join('\n')} 92 | 93 | Total available coins: ${coins.length} 94 | 95 | You can use these coin IDs when querying specific cryptocurrency data. 96 | `.trim(); 97 | } 98 | 99 | export const coinsProvider: Provider = { 100 | // eslint-disable-next-line 101 | get: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { 102 | try { 103 | const coins = await getCoins(runtime); 104 | return formatCoinsContext(coins); 105 | } catch (error) { 106 | elizaLogger.error("Coins provider error:", error); 107 | return "Cryptocurrency list is temporarily unavailable. Please try again later."; 108 | } 109 | } 110 | }; 111 | 112 | // Helper function for actions to get raw coins data 113 | export async function getCoinsData(runtime: IAgentRuntime, includePlatform = false): Promise { 114 | return getCoins(runtime, includePlatform); 115 | } 116 | -------------------------------------------------------------------------------- /src/providers/networkProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type IAgentRuntime, 3 | type Memory, 4 | type Provider, 5 | type State, 6 | elizaLogger, 7 | } from "@elizaos/core"; 8 | import axios from "axios"; 9 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 10 | 11 | interface NetworkAttributes { 12 | name: string; 13 | coingecko_asset_platform_id: string; 14 | } 15 | 16 | interface NetworkItem { 17 | id: string; 18 | type: string; 19 | attributes: NetworkAttributes; 20 | } 21 | 22 | interface NetworksResponse { 23 | data: NetworkItem[]; 24 | } 25 | 26 | const CACHE_KEY = "coingecko:networks"; 27 | const CACHE_TTL = 30 * 60; // 30 minutes 28 | const MAX_RETRIES = 3; 29 | 30 | async function fetchNetworks(runtime: IAgentRuntime): Promise { 31 | const config = await validateCoingeckoConfig(runtime); 32 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 33 | 34 | const response = await axios.get( 35 | `${baseUrl}/onchain/networks`, 36 | { 37 | headers: { 38 | accept: "application/json", 39 | [headerKey]: apiKey, 40 | }, 41 | timeout: 5000, // 5 second timeout 42 | } 43 | ); 44 | 45 | if (!response.data?.data?.length) { 46 | throw new Error("Invalid networks data received"); 47 | } 48 | 49 | return response.data.data; 50 | } 51 | 52 | async function fetchWithRetry(runtime: IAgentRuntime): Promise { 53 | let lastError: Error | null = null; 54 | 55 | for (let i = 0; i < MAX_RETRIES; i++) { 56 | try { 57 | return await fetchNetworks(runtime); 58 | } catch (error) { 59 | lastError = error; 60 | elizaLogger.error(`Networks fetch attempt ${i + 1} failed:`, error); 61 | await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); 62 | } 63 | } 64 | 65 | throw ( 66 | lastError || 67 | new Error("Failed to fetch networks after multiple attempts") 68 | ); 69 | } 70 | 71 | async function getNetworks(runtime: IAgentRuntime): Promise { 72 | try { 73 | // Try to get from cache first 74 | const cached = await runtime.cacheManager.get(CACHE_KEY); 75 | if (cached) { 76 | return cached; 77 | } 78 | 79 | // Fetch fresh data 80 | const networks = await fetchWithRetry(runtime); 81 | 82 | // Cache the result 83 | await runtime.cacheManager.set(CACHE_KEY, networks, { 84 | expires: CACHE_TTL, 85 | }); 86 | 87 | return networks; 88 | } catch (error) { 89 | elizaLogger.error("Error fetching networks:", error); 90 | throw error; 91 | } 92 | } 93 | 94 | function formatNetworksContext(networks: NetworkItem[]): string { 95 | const mainNetworks = ["eth", "bsc", "polygon_pos", "avax", "solana"]; 96 | 97 | const popular = networks 98 | .filter((n) => mainNetworks.includes(n.id)) 99 | .map((n) => `${n.attributes.name} - ID: ${n.id}`); 100 | 101 | return ` 102 | Available blockchain networks: 103 | 104 | Major networks: 105 | ${popular.map((n) => `- ${n}`).join("\n")} 106 | 107 | Total available networks: ${networks.length} 108 | 109 | You can use these network IDs when querying network-specific data. 110 | `.trim(); 111 | } 112 | 113 | export const networksProvider: Provider = { 114 | // eslint-disable-next-line 115 | get: async ( 116 | runtime: IAgentRuntime, 117 | message: Memory, 118 | state?: State 119 | ): Promise => { 120 | try { 121 | const networks = await getNetworks(runtime); 122 | return formatNetworksContext(networks); 123 | } catch (error) { 124 | elizaLogger.error("Networks provider error:", error); 125 | return "Blockchain networks list is temporarily unavailable. Please try again later."; 126 | } 127 | }, 128 | }; 129 | 130 | // Helper function for actions to get raw networks data 131 | export async function getNetworksData( 132 | runtime: IAgentRuntime 133 | ): Promise { 134 | return getNetworks(runtime); 135 | } 136 | -------------------------------------------------------------------------------- /src/actions/getNewlyListed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getNewCoinsTemplate } from "../templates/newCoins"; 18 | 19 | interface NewCoin { 20 | id: string; 21 | symbol: string; 22 | name: string; 23 | activated_at: number; 24 | } 25 | 26 | interface NewCoinsResponse extends Array {} 27 | 28 | export const GetNewCoinsSchema = z.object({ 29 | limit: z.number().min(1).max(50).default(10) 30 | }); 31 | 32 | export type GetNewCoinsContent = z.infer & Content; 33 | 34 | export const isGetNewCoinsContent = (obj: unknown): obj is GetNewCoinsContent => { 35 | return GetNewCoinsSchema.safeParse(obj).success; 36 | }; 37 | 38 | export default { 39 | name: "GET_NEW_COINS", 40 | similes: [ 41 | "NEW_COINS", 42 | "RECENTLY_ADDED", 43 | "NEW_LISTINGS", 44 | "LATEST_COINS", 45 | ], 46 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 47 | await validateCoingeckoConfig(runtime); 48 | return true; 49 | }, 50 | description: "Get list of recently added coins from CoinGecko", 51 | handler: async ( 52 | runtime: IAgentRuntime, 53 | message: Memory, 54 | state: State, 55 | _options: { [key: string]: unknown }, 56 | callback?: HandlerCallback 57 | ): Promise => { 58 | elizaLogger.log("Starting CoinGecko GET_NEW_COINS handler..."); 59 | 60 | // Initialize or update state 61 | let currentState = state; 62 | if (!currentState) { 63 | currentState = (await runtime.composeState(message)) as State; 64 | } else { 65 | currentState = await runtime.updateRecentMessageState(currentState); 66 | } 67 | 68 | 69 | try { 70 | elizaLogger.log("Composing new coins context..."); 71 | const newCoinsContext = composeContext({ 72 | state: currentState, 73 | template: getNewCoinsTemplate, 74 | }); 75 | 76 | const result = await generateObject({ 77 | runtime, 78 | context: newCoinsContext, 79 | modelClass: ModelClass.LARGE, 80 | schema: GetNewCoinsSchema 81 | }); 82 | 83 | if (!isGetNewCoinsContent(result.object)) { 84 | elizaLogger.error("Invalid new coins request format"); 85 | return false; 86 | } 87 | 88 | // Fetch new coins data from CoinGecko 89 | const config = await validateCoingeckoConfig(runtime); 90 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 91 | 92 | elizaLogger.log("Fetching new coins data..."); 93 | 94 | const response = await axios.get( 95 | `${baseUrl}/coins/list/new`, 96 | { 97 | headers: { 98 | [headerKey]: apiKey 99 | } 100 | } 101 | ); 102 | 103 | if (!response.data) { 104 | throw new Error("No data received from CoinGecko API"); 105 | } 106 | 107 | const formattedData = response.data 108 | .slice(0, result.object.limit) 109 | .map(coin => ({ 110 | name: coin.name, 111 | symbol: coin.symbol.toUpperCase(), 112 | activatedAt: new Date(coin.activated_at * 1000).toLocaleString('en-US', { 113 | year: 'numeric', 114 | month: 'long', 115 | day: 'numeric', 116 | hour: '2-digit', 117 | minute: '2-digit' 118 | }) 119 | })); 120 | 121 | const responseText = [ 122 | 'Recently Added Coins:', 123 | '', 124 | ...formattedData.map((coin, index) => 125 | `${index + 1}. ${coin.name} (${coin.symbol})\n Listed: ${coin.activatedAt}` 126 | ) 127 | ].join('\n'); 128 | 129 | elizaLogger.success("New coins data retrieved successfully!"); 130 | 131 | if (callback) { 132 | callback({ 133 | text: responseText, 134 | content: { 135 | newCoins: formattedData, 136 | timestamp: new Date().toISOString() 137 | } 138 | }); 139 | } 140 | 141 | return true; 142 | } catch (error) { 143 | elizaLogger.error("Error in GET_NEW_COINS handler:", error); 144 | 145 | const errorMessage = error.response?.status === 429 ? 146 | "Rate limit exceeded. Please try again later." : 147 | `Error fetching new coins data: ${error.message}`; 148 | 149 | if (callback) { 150 | callback({ 151 | text: errorMessage, 152 | content: { 153 | error: error.message, 154 | statusCode: error.response?.status 155 | }, 156 | }); 157 | } 158 | return false; 159 | } 160 | }, 161 | 162 | examples: [ 163 | [ 164 | { 165 | user: "{{user1}}", 166 | content: { 167 | text: "What are the newest coins listed?", 168 | }, 169 | }, 170 | { 171 | user: "{{agent}}", 172 | content: { 173 | text: "I'll check the recently added coins for you.", 174 | action: "GET_NEW_COINS", 175 | }, 176 | }, 177 | { 178 | user: "{{agent}}", 179 | content: { 180 | text: "Here are the recently added coins:\n1. Verb Ai (VERB)\n Listed: January 20, 2025, 12:31 PM\n{{dynamic}}", 181 | }, 182 | }, 183 | ], 184 | ] as ActionExample[][], 185 | } as Action; -------------------------------------------------------------------------------- /__tests__/actions/getPrice.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { elizaLogger, ModelClass, generateObject, composeContext } from '@elizaos/core'; 3 | import getPriceAction from '../../src/actions/getPrice'; 4 | import axios from 'axios'; 5 | import * as environment from '../../src/environment'; 6 | import * as coinsProvider from '../../src/providers/coinsProvider'; 7 | 8 | vi.mock('axios'); 9 | vi.mock('@elizaos/core', () => ({ 10 | elizaLogger: { 11 | log: vi.fn(), 12 | error: vi.fn(), 13 | warn: vi.fn(), 14 | info: vi.fn(), 15 | success: vi.fn(), 16 | }, 17 | generateObject: vi.fn(), 18 | composeContext: vi.fn(), 19 | ModelClass: { LARGE: 'LARGE' } 20 | })); 21 | vi.mock('../../src/environment', () => ({ 22 | validateCoingeckoConfig: vi.fn(), 23 | getApiConfig: vi.fn() 24 | })); 25 | vi.mock('../../src/providers/coinsProvider'); 26 | 27 | describe('getPrice action', () => { 28 | const mockRuntime = { 29 | composeState: vi.fn(), 30 | updateRecentMessageState: vi.fn(), 31 | getPluginConfig: vi.fn(), 32 | }; 33 | 34 | const mockMessage = {}; 35 | const mockState = {}; 36 | const mockCallback = vi.fn(); 37 | const mockConfig = { 38 | COINGECKO_API_KEY: 'test-api-key', 39 | COINGECKO_PRO_API_KEY: null 40 | }; 41 | 42 | beforeEach(() => { 43 | vi.clearAllMocks(); 44 | 45 | // Mock environment validation 46 | vi.mocked(environment.validateCoingeckoConfig).mockResolvedValue(mockConfig); 47 | vi.mocked(environment.getApiConfig).mockReturnValue({ 48 | baseUrl: 'https://api.coingecko.com/api/v3', 49 | apiKey: 'test-api-key', 50 | headerKey: 'x-cg-demo-api-key' 51 | }); 52 | 53 | // Mock runtime functions 54 | mockRuntime.composeState.mockResolvedValue(mockState); 55 | mockRuntime.updateRecentMessageState.mockResolvedValue(mockState); 56 | mockRuntime.getPluginConfig.mockResolvedValue({ 57 | apiKey: 'test-api-key', 58 | baseUrl: 'https://api.coingecko.com/api/v3' 59 | }); 60 | 61 | // Mock the core functions 62 | vi.mocked(elizaLogger.log).mockImplementation(() => {}); 63 | vi.mocked(elizaLogger.error).mockImplementation(() => {}); 64 | vi.mocked(elizaLogger.success).mockImplementation(() => {}); 65 | vi.mocked(composeContext).mockReturnValue({}); 66 | }); 67 | 68 | it('should validate coingecko config', async () => { 69 | await getPriceAction.validate(mockRuntime, mockMessage); 70 | expect(environment.validateCoingeckoConfig).toHaveBeenCalledWith(mockRuntime); 71 | }); 72 | 73 | it('should fetch and format price data for a single coin', async () => { 74 | const mockPriceResponse = { 75 | data: { 76 | bitcoin: { 77 | usd: 50000, 78 | eur: 42000 79 | } 80 | } 81 | }; 82 | 83 | const mockCoinsData = [{ 84 | id: 'bitcoin', 85 | name: 'Bitcoin', 86 | symbol: 'btc' 87 | }]; 88 | 89 | vi.mocked(axios.get).mockResolvedValueOnce(mockPriceResponse); 90 | vi.mocked(coinsProvider.getCoinsData).mockResolvedValueOnce(mockCoinsData); 91 | 92 | // Mock the content generation 93 | vi.mocked(generateObject).mockResolvedValueOnce({ 94 | object: { 95 | coinIds: 'bitcoin', 96 | currency: ['usd', 'eur'], 97 | include_market_cap: false, 98 | include_24hr_vol: false, 99 | include_24hr_change: false, 100 | include_last_updated_at: false 101 | }, 102 | modelClass: ModelClass.LARGE 103 | }); 104 | 105 | await getPriceAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 106 | 107 | expect(axios.get).toHaveBeenCalledWith( 108 | 'https://api.coingecko.com/api/v3/simple/price', 109 | expect.any(Object) 110 | ); 111 | 112 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 113 | text: expect.stringContaining('Bitcoin (BTC)') 114 | })); 115 | }); 116 | 117 | it('should handle API errors gracefully', async () => { 118 | vi.mocked(axios.get).mockRejectedValueOnce(new Error('API Error')); 119 | 120 | // Mock the content generation 121 | vi.mocked(generateObject).mockResolvedValueOnce({ 122 | object: { 123 | coinIds: 'invalid-coin', 124 | currency: ['usd'], 125 | include_market_cap: false, 126 | include_24hr_vol: false, 127 | include_24hr_change: false, 128 | include_last_updated_at: false 129 | }, 130 | modelClass: ModelClass.LARGE 131 | }); 132 | 133 | await getPriceAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 134 | 135 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 136 | content: expect.objectContaining({ 137 | error: expect.stringContaining('API Error') 138 | }) 139 | })); 140 | }); 141 | 142 | it('should handle empty response data', async () => { 143 | vi.mocked(axios.get).mockResolvedValueOnce({ data: {} }); 144 | 145 | // Mock the content generation 146 | vi.mocked(generateObject).mockResolvedValueOnce({ 147 | object: { 148 | coinIds: 'non-existent-coin', 149 | currency: ['usd'], 150 | include_market_cap: false, 151 | include_24hr_vol: false, 152 | include_24hr_change: false, 153 | include_last_updated_at: false 154 | }, 155 | modelClass: ModelClass.LARGE 156 | }); 157 | 158 | await getPriceAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 159 | 160 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 161 | content: expect.objectContaining({ 162 | error: expect.stringContaining('No price data available') 163 | }) 164 | })); 165 | }); 166 | 167 | it('should include additional market data when requested', async () => { 168 | const mockPriceResponse = { 169 | data: { 170 | ethereum: { 171 | usd: 3000, 172 | usd_market_cap: 350000000000, 173 | usd_24h_vol: 20000000000, 174 | usd_24h_change: 5.5, 175 | last_updated_at: 1643673600 176 | } 177 | } 178 | }; 179 | 180 | const mockCoinsData = [{ 181 | id: 'ethereum', 182 | name: 'Ethereum', 183 | symbol: 'eth' 184 | }]; 185 | 186 | vi.mocked(axios.get).mockResolvedValueOnce(mockPriceResponse); 187 | vi.mocked(coinsProvider.getCoinsData).mockResolvedValueOnce(mockCoinsData); 188 | 189 | // Mock the content generation 190 | vi.mocked(generateObject).mockResolvedValueOnce({ 191 | object: { 192 | coinIds: 'ethereum', 193 | currency: ['usd'], 194 | include_market_cap: true, 195 | include_24hr_vol: true, 196 | include_24hr_change: true, 197 | include_last_updated_at: true 198 | }, 199 | modelClass: ModelClass.LARGE 200 | }); 201 | 202 | await getPriceAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 203 | 204 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 205 | text: expect.stringContaining('Market Cap') 206 | })); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /src/actions/getPricePerAddress.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action, 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getPriceByAddressTemplate } from "../templates/priceAddress"; 18 | 19 | // Schema definition for the token price request 20 | export const GetTokenPriceSchema = z.object({ 21 | chainId: z.string(), 22 | tokenAddress: z.string(), 23 | }); 24 | 25 | export type GetTokenPriceContent = z.infer & 26 | Content; 27 | 28 | export const isGetTokenPriceContent = ( 29 | obj: unknown 30 | ): obj is GetTokenPriceContent => { 31 | return GetTokenPriceSchema.safeParse(obj).success; 32 | }; 33 | 34 | interface TokenResponse { 35 | id: string; 36 | symbol: string; 37 | name: string; 38 | market_data: { 39 | current_price: { 40 | usd: number; 41 | }; 42 | market_cap: { 43 | usd: number; 44 | }; 45 | }; 46 | } 47 | 48 | export default { 49 | name: "GET_TOKEN_PRICE_BY_ADDRESS", 50 | similes: [ 51 | "FETCH_TOKEN_PRICE_BY_ADDRESS", 52 | "CHECK_TOKEN_PRICE_BY_ADDRESS", 53 | "LOOKUP_TOKEN_BY_ADDRESS", 54 | ], 55 | // eslint-disable-next-line 56 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 57 | await validateCoingeckoConfig(runtime); 58 | return true; 59 | }, 60 | description: 61 | "Get the current USD price for a token using its blockchain address", 62 | handler: async ( 63 | runtime: IAgentRuntime, 64 | message: Memory, 65 | state: State, 66 | _options: { [key: string]: unknown }, 67 | callback?: HandlerCallback 68 | ): Promise => { 69 | elizaLogger.log("Starting GET_TOKEN_PRICE_BY_ADDRESS handler..."); 70 | 71 | // Initialize or update state 72 | let currentState = state; 73 | if (!currentState) { 74 | currentState = (await runtime.composeState(message)) as State; 75 | } else { 76 | currentState = await runtime.updateRecentMessageState(currentState); 77 | } 78 | 79 | 80 | try { 81 | elizaLogger.log("Composing token price context..."); 82 | const context = composeContext({ 83 | state: currentState, 84 | template: getPriceByAddressTemplate, 85 | }); 86 | 87 | elizaLogger.log("Generating content from template..."); 88 | const result = await generateObject({ 89 | runtime, 90 | context, 91 | modelClass: ModelClass.SMALL, 92 | schema: GetTokenPriceSchema, 93 | }); 94 | 95 | if (!isGetTokenPriceContent(result.object)) { 96 | elizaLogger.error("Invalid token price request format"); 97 | return false; 98 | } 99 | 100 | const content = result.object; 101 | elizaLogger.log("Generated content:", content); 102 | 103 | // Get API configuration 104 | const config = await validateCoingeckoConfig(runtime); 105 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 106 | 107 | // Fetch token data 108 | elizaLogger.log("Fetching token data..."); 109 | const response = await axios.get( 110 | `${baseUrl}/coins/${content.chainId}/contract/${content.tokenAddress}`, 111 | { 112 | headers: { 113 | accept: "application/json", 114 | [headerKey]: apiKey, 115 | }, 116 | } 117 | ); 118 | 119 | const tokenData = response.data; 120 | if (!tokenData.market_data?.current_price?.usd) { 121 | throw new Error( 122 | `No price data available for token ${content.tokenAddress} on ${content.chainId}` 123 | ); 124 | } 125 | 126 | // Format response 127 | const parts = [ 128 | `${tokenData.name} (${tokenData.symbol.toUpperCase()})`, 129 | `Address: ${content.tokenAddress}`, 130 | `Chain: ${content.chainId}`, 131 | `Price: $${tokenData.market_data.current_price.usd.toFixed(6)} USD`, 132 | ]; 133 | 134 | if (tokenData.market_data.market_cap?.usd) { 135 | parts.push( 136 | `Market Cap: $${tokenData.market_data.market_cap.usd.toLocaleString()} USD` 137 | ); 138 | } 139 | 140 | const responseText = parts.join("\n"); 141 | elizaLogger.success("Token price data retrieved successfully!"); 142 | 143 | if (callback) { 144 | callback({ 145 | text: responseText, 146 | content: { 147 | token: { 148 | name: tokenData.name, 149 | symbol: tokenData.symbol, 150 | address: content.tokenAddress, 151 | chain: content.chainId, 152 | price: tokenData.market_data.current_price.usd, 153 | marketCap: tokenData.market_data.market_cap?.usd, 154 | }, 155 | }, 156 | }); 157 | } 158 | 159 | return true; 160 | } catch (error) { 161 | elizaLogger.error( 162 | "Error in GET_TOKEN_PRICE_BY_ADDRESS handler:", 163 | error 164 | ); 165 | 166 | let errorMessage: string; 167 | if (error.response?.status === 429) { 168 | errorMessage = "Rate limit exceeded. Please try again later."; 169 | } else if (error.response?.status === 403) { 170 | errorMessage = 171 | "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; 172 | } else if (error.response?.status === 400) { 173 | errorMessage = 174 | "Invalid request parameters. Please check your input."; 175 | } else { 176 | errorMessage = 177 | "Failed to fetch token price. Please try again later."; 178 | } 179 | 180 | if (callback) { 181 | callback({ 182 | text: errorMessage, 183 | content: { 184 | error: error.message, 185 | statusCode: error.response?.status, 186 | requiresProPlan: error.response?.status === 403, 187 | }, 188 | }); 189 | } 190 | return false; 191 | } 192 | }, 193 | 194 | examples: [ 195 | [ 196 | { 197 | user: "{{user1}}", 198 | content: { 199 | text: "What's the price of the USDC token on Ethereum? The address is 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 200 | }, 201 | }, 202 | { 203 | user: "{{agent}}", 204 | content: { 205 | text: "I'll check the USDC token price for you.", 206 | action: "GET_TOKEN_PRICE_BY_ADDRESS", 207 | }, 208 | }, 209 | { 210 | user: "{{agent}}", 211 | content: { 212 | text: "USD Coin (USDC)\nAddress: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\nChain: ethereum\nPrice: {{dynamic}} USD\nMarket Cap: ${{dynamic}} USD", 213 | }, 214 | }, 215 | ], 216 | ] as ActionExample[][], 217 | } as Action; 218 | -------------------------------------------------------------------------------- /__tests__/actions/getTrending.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { elizaLogger, ModelClass, generateObject, composeContext } from '@elizaos/core'; 3 | import getTrendingAction from '../../src/actions/getTrending'; 4 | import axios from 'axios'; 5 | import * as environment from '../../src/environment'; 6 | 7 | vi.mock('axios'); 8 | vi.mock('@elizaos/core', () => ({ 9 | elizaLogger: { 10 | log: vi.fn(), 11 | error: vi.fn(), 12 | warn: vi.fn(), 13 | info: vi.fn(), 14 | success: vi.fn(), 15 | }, 16 | generateObject: vi.fn(), 17 | composeContext: vi.fn(), 18 | ModelClass: { LARGE: 'LARGE' } 19 | })); 20 | vi.mock('../../src/environment', () => ({ 21 | validateCoingeckoConfig: vi.fn(), 22 | getApiConfig: vi.fn() 23 | })); 24 | 25 | describe('getTrending action', () => { 26 | const mockRuntime = { 27 | composeState: vi.fn(), 28 | updateRecentMessageState: vi.fn(), 29 | getPluginConfig: vi.fn(), 30 | }; 31 | 32 | const mockMessage = {}; 33 | const mockState = {}; 34 | const mockCallback = vi.fn(); 35 | const mockConfig = { 36 | COINGECKO_API_KEY: 'test-api-key', 37 | COINGECKO_PRO_API_KEY: null 38 | }; 39 | 40 | beforeEach(() => { 41 | vi.clearAllMocks(); 42 | 43 | // Mock environment validation 44 | vi.mocked(environment.validateCoingeckoConfig).mockResolvedValue(mockConfig); 45 | vi.mocked(environment.getApiConfig).mockReturnValue({ 46 | baseUrl: 'https://api.coingecko.com/api/v3', 47 | apiKey: 'test-api-key', 48 | headerKey: 'x-cg-demo-api-key' 49 | }); 50 | 51 | // Mock runtime functions 52 | mockRuntime.composeState.mockResolvedValue(mockState); 53 | mockRuntime.updateRecentMessageState.mockResolvedValue(mockState); 54 | mockRuntime.getPluginConfig.mockResolvedValue({ 55 | apiKey: 'test-api-key', 56 | baseUrl: 'https://api.coingecko.com/api/v3' 57 | }); 58 | 59 | // Mock the core functions 60 | vi.mocked(elizaLogger.log).mockImplementation(() => {}); 61 | vi.mocked(elizaLogger.error).mockImplementation(() => {}); 62 | vi.mocked(elizaLogger.success).mockImplementation(() => {}); 63 | vi.mocked(composeContext).mockReturnValue({}); 64 | }); 65 | 66 | it('should validate coingecko config', async () => { 67 | await getTrendingAction.validate(mockRuntime, mockMessage); 68 | expect(environment.validateCoingeckoConfig).toHaveBeenCalledWith(mockRuntime); 69 | }); 70 | 71 | it('should fetch and format trending data', async () => { 72 | const mockTrendingResponse = { 73 | data: { 74 | coins: [ 75 | { 76 | item: { 77 | id: 'bitcoin', 78 | name: 'Bitcoin', 79 | symbol: 'btc', 80 | market_cap_rank: 1, 81 | thumb: 'thumb_url', 82 | large: 'large_url' 83 | } 84 | } 85 | ], 86 | nfts: [ 87 | { 88 | id: 'bored-ape', 89 | name: 'Bored Ape Yacht Club', 90 | symbol: 'BAYC', 91 | thumb: 'thumb_url' 92 | } 93 | ], 94 | categories: [ 95 | { 96 | id: 'defi', 97 | name: 'DeFi' 98 | } 99 | ], 100 | exchanges: [], 101 | icos: [] 102 | } 103 | }; 104 | 105 | vi.mocked(axios.get).mockResolvedValueOnce(mockTrendingResponse); 106 | 107 | // Mock the content generation 108 | vi.mocked(generateObject).mockResolvedValueOnce({ 109 | object: { 110 | include_nfts: true, 111 | include_categories: true 112 | }, 113 | modelClass: ModelClass.LARGE 114 | }); 115 | 116 | await getTrendingAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 117 | 118 | expect(axios.get).toHaveBeenCalledWith( 119 | 'https://api.coingecko.com/api/v3/search/trending', 120 | expect.any(Object) 121 | ); 122 | 123 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 124 | text: expect.stringContaining('Bitcoin (BTC)'), 125 | content: expect.objectContaining({ 126 | trending: expect.objectContaining({ 127 | coins: expect.arrayContaining([ 128 | expect.objectContaining({ 129 | name: 'Bitcoin', 130 | symbol: 'BTC', 131 | marketCapRank: 1 132 | }) 133 | ]), 134 | nfts: expect.arrayContaining([ 135 | expect.objectContaining({ 136 | name: 'Bored Ape Yacht Club', 137 | symbol: 'BAYC' 138 | }) 139 | ]), 140 | categories: expect.arrayContaining([ 141 | expect.objectContaining({ 142 | name: 'DeFi' 143 | }) 144 | ]) 145 | }) 146 | }) 147 | })); 148 | }); 149 | 150 | it('should handle API errors gracefully', async () => { 151 | vi.mocked(axios.get).mockRejectedValueOnce(new Error('API Error')); 152 | 153 | // Mock the content generation 154 | vi.mocked(generateObject).mockResolvedValueOnce({ 155 | object: { 156 | include_nfts: true, 157 | include_categories: true 158 | }, 159 | modelClass: ModelClass.LARGE 160 | }); 161 | 162 | await getTrendingAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 163 | 164 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 165 | text: expect.stringContaining('Error fetching trending data'), 166 | content: expect.objectContaining({ 167 | error: expect.stringContaining('API Error') 168 | }) 169 | })); 170 | }); 171 | 172 | it('should handle rate limit errors', async () => { 173 | const rateLimitError = new Error('Rate limit exceeded'); 174 | Object.assign(rateLimitError, { 175 | response: { status: 429 } 176 | }); 177 | vi.mocked(axios.get).mockRejectedValueOnce(rateLimitError); 178 | 179 | // Mock the content generation 180 | vi.mocked(generateObject).mockResolvedValueOnce({ 181 | object: { 182 | include_nfts: true, 183 | include_categories: true 184 | }, 185 | modelClass: ModelClass.LARGE 186 | }); 187 | 188 | await getTrendingAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 189 | 190 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 191 | text: expect.stringContaining('Rate limit exceeded'), 192 | content: expect.objectContaining({ 193 | error: expect.stringContaining('Rate limit exceeded'), 194 | statusCode: 429 195 | }) 196 | })); 197 | }); 198 | 199 | it('should handle empty response data', async () => { 200 | vi.mocked(axios.get).mockResolvedValueOnce({ data: null }); 201 | 202 | // Mock the content generation 203 | vi.mocked(generateObject).mockResolvedValueOnce({ 204 | object: { 205 | include_nfts: true, 206 | include_categories: true 207 | }, 208 | modelClass: ModelClass.LARGE 209 | }); 210 | 211 | await getTrendingAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 212 | 213 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 214 | text: expect.stringContaining('Error fetching trending data'), 215 | content: expect.objectContaining({ 216 | error: expect.stringContaining('No data received') 217 | }) 218 | })); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/actions/getTrending.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getTrendingTemplate } from "../templates/trending"; 18 | 19 | interface TrendingCoinItem { 20 | id: string; 21 | name: string; 22 | api_symbol: string; 23 | symbol: string; 24 | market_cap_rank: number; 25 | thumb: string; 26 | large: string; 27 | } 28 | 29 | interface TrendingExchange { 30 | id: string; 31 | name: string; 32 | market_type: string; 33 | thumb: string; 34 | large: string; 35 | } 36 | 37 | interface TrendingCategory { 38 | id: string; 39 | name: string; 40 | } 41 | 42 | interface TrendingNFT { 43 | id: string; 44 | name: string; 45 | symbol: string; 46 | thumb: string; 47 | } 48 | 49 | interface TrendingResponse { 50 | coins: Array<{ item: TrendingCoinItem }>; 51 | exchanges: TrendingExchange[]; 52 | categories: TrendingCategory[]; 53 | nfts: TrendingNFT[]; 54 | icos: string[]; 55 | } 56 | 57 | export const GetTrendingSchema = z.object({ 58 | include_nfts: z.boolean().default(true), 59 | include_categories: z.boolean().default(true) 60 | }); 61 | 62 | export type GetTrendingContent = z.infer & Content; 63 | 64 | export const isGetTrendingContent = (obj: unknown): obj is GetTrendingContent => { 65 | return GetTrendingSchema.safeParse(obj).success; 66 | }; 67 | 68 | export default { 69 | name: "GET_TRENDING", 70 | similes: [ 71 | "TRENDING_COINS", 72 | "TRENDING_CRYPTO", 73 | "HOT_COINS", 74 | "POPULAR_COINS", 75 | "TRENDING_SEARCH", 76 | ], 77 | // eslint-disable-next-line 78 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 79 | await validateCoingeckoConfig(runtime); 80 | return true; 81 | }, 82 | description: "Get list of trending cryptocurrencies, NFTs, and categories from CoinGecko", 83 | handler: async ( 84 | runtime: IAgentRuntime, 85 | message: Memory, 86 | state: State, 87 | _options: { [key: string]: unknown }, 88 | callback?: HandlerCallback 89 | ): Promise => { 90 | elizaLogger.log("Starting CoinGecko GET_TRENDING handler..."); 91 | 92 | // Initialize or update state 93 | let currentState = state; 94 | if (!currentState) { 95 | currentState = (await runtime.composeState(message)) as State; 96 | } else { 97 | currentState = await runtime.updateRecentMessageState(currentState); 98 | } 99 | 100 | 101 | try { 102 | // Compose trending context 103 | elizaLogger.log("Composing trending context..."); 104 | const trendingContext = composeContext({ 105 | state: currentState, 106 | template: getTrendingTemplate, 107 | }); 108 | 109 | const result = await generateObject({ 110 | runtime, 111 | context: trendingContext, 112 | modelClass: ModelClass.LARGE, 113 | schema: GetTrendingSchema 114 | }); 115 | 116 | if (!isGetTrendingContent(result.object)) { 117 | elizaLogger.error("Invalid trending request format"); 118 | return false; 119 | } 120 | 121 | // Fetch trending data from CoinGecko 122 | const config = await validateCoingeckoConfig(runtime); 123 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 124 | 125 | elizaLogger.log("Fetching trending data..."); 126 | 127 | const response = await axios.get( 128 | `${baseUrl}/search/trending`, 129 | { 130 | headers: { 131 | [headerKey]: apiKey 132 | } 133 | } 134 | ); 135 | 136 | if (!response.data) { 137 | throw new Error("No data received from CoinGecko API"); 138 | } 139 | 140 | const formattedData = { 141 | coins: response.data.coins.map(({ item }) => ({ 142 | name: item.name, 143 | symbol: item.symbol.toUpperCase(), 144 | marketCapRank: item.market_cap_rank, 145 | id: item.id, 146 | thumbnail: item.thumb, 147 | largeImage: item.large 148 | })), 149 | nfts: response.data.nfts.map(nft => ({ 150 | name: nft.name, 151 | symbol: nft.symbol, 152 | id: nft.id, 153 | thumbnail: nft.thumb 154 | })), 155 | categories: response.data.categories.map(category => ({ 156 | name: category.name, 157 | id: category.id 158 | })) 159 | }; 160 | 161 | const responseText = [ 162 | 'Trending Coins:', 163 | ...formattedData.coins.map((coin, index) => 164 | `${index + 1}. ${coin.name} (${coin.symbol})${coin.marketCapRank ? ` - Rank #${coin.marketCapRank}` : ''}` 165 | ), 166 | '', 167 | 'Trending NFTs:', 168 | ...(formattedData.nfts.length ? 169 | formattedData.nfts.map((nft, index) => `${index + 1}. ${nft.name} (${nft.symbol})`) : 170 | ['No trending NFTs available']), 171 | '', 172 | 'Trending Categories:', 173 | ...(formattedData.categories.length ? 174 | formattedData.categories.map((category, index) => `${index + 1}. ${category.name}`) : 175 | ['No trending categories available']) 176 | ].join('\n'); 177 | 178 | elizaLogger.success("Trending data retrieved successfully!"); 179 | 180 | if (callback) { 181 | callback({ 182 | text: responseText, 183 | content: { 184 | trending: formattedData, 185 | timestamp: new Date().toISOString() 186 | } 187 | }); 188 | } 189 | 190 | return true; 191 | } catch (error) { 192 | elizaLogger.error("Error in GET_TRENDING handler:", error); 193 | 194 | // Enhanced error handling 195 | const errorMessage = error.response?.status === 429 ? 196 | "Rate limit exceeded. Please try again later." : 197 | `Error fetching trending data: ${error.message}`; 198 | 199 | if (callback) { 200 | callback({ 201 | text: errorMessage, 202 | content: { 203 | error: error.message, 204 | statusCode: error.response?.status 205 | }, 206 | }); 207 | } 208 | return false; 209 | } 210 | }, 211 | 212 | examples: [ 213 | [ 214 | { 215 | user: "{{user1}}", 216 | content: { 217 | text: "What are the trending cryptocurrencies?", 218 | }, 219 | }, 220 | { 221 | user: "{{agent}}", 222 | content: { 223 | text: "I'll check the trending cryptocurrencies for you.", 224 | action: "GET_TRENDING", 225 | }, 226 | }, 227 | { 228 | user: "{{agent}}", 229 | content: { 230 | text: "Here are the trending cryptocurrencies:\n1. Bitcoin (BTC) - Rank #1\n2. Ethereum (ETH) - Rank #2\n{{dynamic}}", 231 | }, 232 | }, 233 | ], 234 | [ 235 | { 236 | user: "{{user1}}", 237 | content: { 238 | text: "Show me what's hot in crypto right now", 239 | }, 240 | }, 241 | { 242 | user: "{{agent}}", 243 | content: { 244 | text: "I'll fetch the current trending cryptocurrencies.", 245 | action: "GET_TRENDING", 246 | }, 247 | }, 248 | { 249 | user: "{{agent}}", 250 | content: { 251 | text: "Here are the trending cryptocurrencies:\n{{dynamic}}", 252 | }, 253 | }, 254 | ], 255 | ] as ActionExample[][], 256 | } as Action; -------------------------------------------------------------------------------- /src/actions/getTrendingPools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action, 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getTrendingPoolsTemplate } from "../templates/trendingPools"; 18 | 19 | interface TrendingPool { 20 | id: string; 21 | type: string; 22 | attributes: { 23 | name: string; 24 | market_cap_usd: string; 25 | fdv_usd: string; 26 | reserve_in_usd: string; 27 | pool_created_at: string; 28 | }; 29 | } 30 | 31 | interface TrendingPoolsResponse { 32 | data: TrendingPool[]; 33 | } 34 | 35 | export const GetTrendingPoolsSchema = z.object({ 36 | limit: z.number().min(1).max(100).default(10), 37 | }); 38 | 39 | export type GetTrendingPoolsContent = z.infer & 40 | Content; 41 | 42 | export const isGetTrendingPoolsContent = ( 43 | obj: unknown, 44 | ): obj is GetTrendingPoolsContent => { 45 | return GetTrendingPoolsSchema.safeParse(obj).success; 46 | }; 47 | 48 | export default { 49 | name: "GET_TRENDING_POOLS", 50 | similes: ["TRENDING_POOLS", "HOT_POOLS", "POPULAR_POOLS", "TOP_POOLS"], 51 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 52 | await validateCoingeckoConfig(runtime); 53 | return true; 54 | }, 55 | description: "Get list of trending pools from CoinGecko's onchain data", 56 | handler: async ( 57 | runtime: IAgentRuntime, 58 | message: Memory, 59 | state: State, 60 | _options: { [key: string]: unknown }, 61 | callback?: HandlerCallback, 62 | ): Promise => { 63 | elizaLogger.log("Starting CoinGecko GET_TRENDING_POOLS handler..."); 64 | 65 | // Initialize or update state 66 | let currentState = state; 67 | if (!currentState) { 68 | currentState = (await runtime.composeState(message)) as State; 69 | } else { 70 | currentState = await runtime.updateRecentMessageState(currentState); 71 | } 72 | 73 | 74 | try { 75 | elizaLogger.log("Composing trending pools context..."); 76 | const trendingContext = composeContext({ 77 | state: currentState, 78 | template: getTrendingPoolsTemplate, 79 | }); 80 | 81 | const result = await generateObject({ 82 | runtime, 83 | context: trendingContext, 84 | modelClass: ModelClass.LARGE, 85 | schema: GetTrendingPoolsSchema, 86 | }); 87 | 88 | if (!isGetTrendingPoolsContent(result.object)) { 89 | elizaLogger.error("Invalid trending pools request format"); 90 | return false; 91 | } 92 | 93 | // Fetch trending pools data from CoinGecko 94 | const config = await validateCoingeckoConfig(runtime); 95 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 96 | 97 | elizaLogger.log("Fetching trending pools data..."); 98 | 99 | const response = await axios.get( 100 | `${baseUrl}/onchain/networks/trending_pools?include=base_token,dex`, 101 | { 102 | headers: { 103 | [headerKey]: apiKey, 104 | }, 105 | }, 106 | ); 107 | 108 | if (!response.data) { 109 | throw new Error("No data received from CoinGecko API"); 110 | } 111 | 112 | const formattedData = response.data.data.map((pool) => ({ 113 | name: pool.attributes.name, 114 | marketCap: Number( 115 | pool.attributes.market_cap_usd, 116 | ).toLocaleString("en-US", { 117 | style: "currency", 118 | currency: "USD", 119 | }), 120 | fdv: Number(pool.attributes.fdv_usd).toLocaleString("en-US", { 121 | style: "currency", 122 | currency: "USD", 123 | }), 124 | reserveUSD: Number( 125 | pool.attributes.reserve_in_usd, 126 | ).toLocaleString("en-US", { 127 | style: "currency", 128 | currency: "USD", 129 | }), 130 | createdAt: new Date( 131 | pool.attributes.pool_created_at, 132 | ).toLocaleDateString(), 133 | })); 134 | 135 | const responseText = [ 136 | "Trending Pools Overview:", 137 | "", 138 | ...formattedData.map((pool, index) => 139 | [ 140 | `${index + 1}. ${pool.name}`, 141 | ` Market Cap: ${pool.marketCap}`, 142 | ` FDV: ${pool.fdv}`, 143 | ` Reserve: ${pool.reserveUSD}`, 144 | ` Created: ${pool.createdAt}`, 145 | "", 146 | ].join("\n"), 147 | ), 148 | ].join("\n"); 149 | 150 | elizaLogger.success("Trending pools data retrieved successfully!"); 151 | 152 | if (callback) { 153 | callback({ 154 | text: responseText, 155 | content: { 156 | trendingPools: formattedData, 157 | timestamp: new Date().toISOString(), 158 | }, 159 | }); 160 | } 161 | 162 | return true; 163 | } catch (error) { 164 | elizaLogger.error("Error in GET_TRENDING_POOLS handler:", error); 165 | 166 | const errorMessage = 167 | error.response?.status === 429 168 | ? "Rate limit exceeded. Please try again later." 169 | : `Error fetching trending pools data: ${error.message}`; 170 | 171 | if (callback) { 172 | callback({ 173 | text: errorMessage, 174 | content: { 175 | error: error.message, 176 | statusCode: error.response?.status, 177 | }, 178 | }); 179 | } 180 | return false; 181 | } 182 | }, 183 | 184 | examples: [ 185 | [ 186 | { 187 | user: "{{user1}}", 188 | content: { 189 | text: "Show me trending liquidity pools", 190 | }, 191 | }, 192 | { 193 | user: "{{agent}}", 194 | content: { 195 | text: "I'll check the trending liquidity pools for you.", 196 | action: "GET_TRENDING_POOLS", 197 | }, 198 | }, 199 | { 200 | user: "{{agent}}", 201 | content: { 202 | text: "Here are the trending liquidity pools:\n1. MELANIA / USDC\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025\n2. TRUMP / USDC\n Market Cap: $8,844,297,825\n FDV: $43,874,068,484\n Reserve: $718,413,745\n Created: 1/17/2025", 203 | }, 204 | }, 205 | ], 206 | [ 207 | { 208 | user: "{{user1}}", 209 | content: { 210 | text: "What are the top hottest dex pools?", 211 | }, 212 | }, 213 | { 214 | user: "{{agent}}", 215 | content: { 216 | text: "I'll fetch the top hottest DEX pools for you.", 217 | action: "GET_TRENDING_POOLS", 218 | }, 219 | }, 220 | { 221 | user: "{{agent}}", 222 | content: { 223 | text: "Here are the top 5 hottest DEX pools:\n1. TRUMP / USDC\n Market Cap: $8,844,297,825\n FDV: $43,874,068,484\n Reserve: $718,413,745\n Created: 1/17/2025\n2. MELANIA / USDC\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025", 224 | }, 225 | }, 226 | ], 227 | [ 228 | { 229 | user: "{{user1}}", 230 | content: { 231 | text: "List all trading pools with highest volume", 232 | }, 233 | }, 234 | { 235 | user: "{{agent}}", 236 | content: { 237 | text: "I'll get all the trending trading pools for you.", 238 | action: "GET_TRENDING_POOLS", 239 | }, 240 | }, 241 | { 242 | user: "{{agent}}", 243 | content: { 244 | text: "Here are all trending trading pools:\n1. MELANIA / USDC\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025\n2. TRUMP / USDC\n Market Cap: $8,844,297,825\n FDV: $43,874,068,484\n Reserve: $718,413,745\n Created: 1/17/2025", 245 | }, 246 | }, 247 | ], 248 | ] as ActionExample[][], 249 | } as Action; 250 | -------------------------------------------------------------------------------- /__tests__/actions/getTopGainersLosers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { elizaLogger, ModelClass, generateObject, composeContext } from '@elizaos/core'; 3 | import getTopGainersLosersAction from '../../src/actions/getTopGainersLosers'; 4 | import axios from 'axios'; 5 | import * as environment from '../../src/environment'; 6 | 7 | vi.mock('axios'); 8 | vi.mock('@elizaos/core', () => ({ 9 | elizaLogger: { 10 | log: vi.fn(), 11 | error: vi.fn(), 12 | warn: vi.fn(), 13 | info: vi.fn(), 14 | success: vi.fn(), 15 | }, 16 | generateObject: vi.fn(), 17 | composeContext: vi.fn(), 18 | ModelClass: { LARGE: 'LARGE' } 19 | })); 20 | vi.mock('../../src/environment', () => ({ 21 | validateCoingeckoConfig: vi.fn(), 22 | getApiConfig: vi.fn() 23 | })); 24 | 25 | describe('getTopGainersLosers action', () => { 26 | const mockRuntime = { 27 | composeState: vi.fn(), 28 | updateRecentMessageState: vi.fn(), 29 | getPluginConfig: vi.fn(), 30 | }; 31 | 32 | const mockMessage = {}; 33 | const mockState = {}; 34 | const mockCallback = vi.fn(); 35 | const mockConfig = { 36 | COINGECKO_API_KEY: 'test-api-key', 37 | COINGECKO_PRO_API_KEY: null 38 | }; 39 | 40 | beforeEach(() => { 41 | vi.clearAllMocks(); 42 | 43 | // Mock environment validation 44 | vi.mocked(environment.validateCoingeckoConfig).mockResolvedValue(mockConfig); 45 | vi.mocked(environment.getApiConfig).mockReturnValue({ 46 | baseUrl: 'https://api.coingecko.com/api/v3', 47 | apiKey: 'test-api-key', 48 | headerKey: 'x-cg-demo-api-key' 49 | }); 50 | 51 | // Mock runtime functions 52 | mockRuntime.composeState.mockResolvedValue(mockState); 53 | mockRuntime.updateRecentMessageState.mockResolvedValue(mockState); 54 | mockRuntime.getPluginConfig.mockResolvedValue({ 55 | apiKey: 'test-api-key', 56 | baseUrl: 'https://api.coingecko.com/api/v3' 57 | }); 58 | 59 | // Mock the core functions 60 | vi.mocked(elizaLogger.log).mockImplementation(() => {}); 61 | vi.mocked(elizaLogger.error).mockImplementation(() => {}); 62 | vi.mocked(elizaLogger.success).mockImplementation(() => {}); 63 | vi.mocked(composeContext).mockReturnValue({}); 64 | }); 65 | 66 | it('should validate coingecko config', async () => { 67 | await getTopGainersLosersAction.validate(mockRuntime, mockMessage); 68 | expect(environment.validateCoingeckoConfig).toHaveBeenCalledWith(mockRuntime); 69 | }); 70 | 71 | it('should fetch and format top gainers and losers data', async () => { 72 | const mockResponse = { 73 | data: { 74 | top_gainers: [ 75 | { 76 | id: 'bitcoin', 77 | symbol: 'btc', 78 | name: 'Bitcoin', 79 | image: 'image_url', 80 | market_cap_rank: 1, 81 | usd: 50000, 82 | usd_24h_vol: 30000000000, 83 | usd_24h_change: 5.5 84 | } 85 | ], 86 | top_losers: [ 87 | { 88 | id: 'ethereum', 89 | symbol: 'eth', 90 | name: 'Ethereum', 91 | image: 'image_url', 92 | market_cap_rank: 2, 93 | usd: 2500, 94 | usd_24h_vol: 20000000000, 95 | usd_24h_change: -3.2 96 | } 97 | ] 98 | } 99 | }; 100 | 101 | vi.mocked(axios.get).mockResolvedValueOnce(mockResponse); 102 | 103 | // Mock the content generation 104 | vi.mocked(generateObject).mockResolvedValueOnce({ 105 | object: { 106 | vs_currency: 'usd', 107 | duration: '24h', 108 | top_coins: '1000' 109 | }, 110 | modelClass: ModelClass.LARGE 111 | }); 112 | 113 | await getTopGainersLosersAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 114 | 115 | expect(axios.get).toHaveBeenCalledWith( 116 | 'https://api.coingecko.com/api/v3/coins/top_gainers_losers', 117 | expect.objectContaining({ 118 | params: { 119 | vs_currency: 'usd', 120 | duration: '24h', 121 | top_coins: '1000' 122 | } 123 | }) 124 | ); 125 | 126 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 127 | text: expect.stringContaining('Bitcoin (BTC)'), 128 | content: expect.objectContaining({ 129 | data: expect.objectContaining({ 130 | top_gainers: expect.arrayContaining([ 131 | expect.objectContaining({ 132 | name: 'Bitcoin', 133 | symbol: 'btc', 134 | usd_24h_change: 5.5 135 | }) 136 | ]), 137 | top_losers: expect.arrayContaining([ 138 | expect.objectContaining({ 139 | name: 'Ethereum', 140 | symbol: 'eth', 141 | usd_24h_change: -3.2 142 | }) 143 | ]) 144 | }) 145 | }) 146 | })); 147 | }); 148 | 149 | it('should handle API errors gracefully', async () => { 150 | vi.mocked(axios.get).mockRejectedValueOnce(new Error('API Error')); 151 | 152 | // Mock the content generation 153 | vi.mocked(generateObject).mockResolvedValueOnce({ 154 | object: { 155 | vs_currency: 'usd', 156 | duration: '24h', 157 | top_coins: '1000' 158 | }, 159 | modelClass: ModelClass.LARGE 160 | }); 161 | 162 | await getTopGainersLosersAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 163 | 164 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 165 | text: expect.stringContaining('Error fetching top gainers/losers data'), 166 | content: expect.objectContaining({ 167 | error: expect.stringContaining('API Error') 168 | }) 169 | })); 170 | }); 171 | 172 | it('should handle rate limit errors', async () => { 173 | const rateLimitError = new Error('Rate limit exceeded'); 174 | Object.assign(rateLimitError, { 175 | response: { status: 429 } 176 | }); 177 | vi.mocked(axios.get).mockRejectedValueOnce(rateLimitError); 178 | 179 | // Mock the content generation 180 | vi.mocked(generateObject).mockResolvedValueOnce({ 181 | object: { 182 | vs_currency: 'usd', 183 | duration: '24h', 184 | top_coins: '1000' 185 | }, 186 | modelClass: ModelClass.LARGE 187 | }); 188 | 189 | await getTopGainersLosersAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 190 | 191 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 192 | text: expect.stringContaining('Rate limit exceeded'), 193 | content: expect.objectContaining({ 194 | error: expect.stringContaining('Rate limit exceeded'), 195 | statusCode: 429 196 | }) 197 | })); 198 | }); 199 | 200 | it('should handle pro plan requirement errors', async () => { 201 | const proPlanError = new Error('Pro plan required'); 202 | Object.assign(proPlanError, { 203 | response: { status: 403 } 204 | }); 205 | vi.mocked(axios.get).mockRejectedValueOnce(proPlanError); 206 | 207 | // Mock the content generation 208 | vi.mocked(generateObject).mockResolvedValueOnce({ 209 | object: { 210 | vs_currency: 'usd', 211 | duration: '24h', 212 | top_coins: '1000' 213 | }, 214 | modelClass: ModelClass.LARGE 215 | }); 216 | 217 | await getTopGainersLosersAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 218 | 219 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 220 | text: expect.stringContaining('requires a CoinGecko Pro API key'), 221 | content: expect.objectContaining({ 222 | error: expect.stringContaining('Pro plan required'), 223 | statusCode: 403, 224 | requiresProPlan: true 225 | }) 226 | })); 227 | }); 228 | 229 | it('should handle empty response data', async () => { 230 | vi.mocked(axios.get).mockResolvedValueOnce({ data: null }); 231 | 232 | // Mock the content generation 233 | vi.mocked(generateObject).mockResolvedValueOnce({ 234 | object: { 235 | vs_currency: 'usd', 236 | duration: '24h', 237 | top_coins: '1000' 238 | }, 239 | modelClass: ModelClass.LARGE 240 | }); 241 | 242 | await getTopGainersLosersAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 243 | 244 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 245 | text: expect.stringContaining('No data received'), 246 | content: expect.objectContaining({ 247 | error: expect.stringContaining('No data received') 248 | }) 249 | })); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/actions/getTopGainersLosers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getTopGainersLosersTemplate } from "../templates/gainersLosers"; 18 | 19 | interface TopGainerLoserItem { 20 | id: string; 21 | symbol: string; 22 | name: string; 23 | image: string; 24 | market_cap_rank: number; 25 | usd: number; 26 | usd_24h_vol: number; 27 | usd_1h_change?: number; 28 | usd_24h_change?: number; 29 | usd_7d_change?: number; 30 | usd_14d_change?: number; 31 | usd_30d_change?: number; 32 | usd_60d_change?: number; 33 | usd_1y_change?: number; 34 | } 35 | 36 | interface TopGainersLosersResponse { 37 | top_gainers: TopGainerLoserItem[]; 38 | top_losers: TopGainerLoserItem[]; 39 | } 40 | 41 | const DurationEnum = z.enum(["1h", "24h", "7d", "14d", "30d", "60d", "1y"]); 42 | //type Duration = z.infer; 43 | 44 | export const GetTopGainersLosersSchema = z.object({ 45 | vs_currency: z.string().default("usd"), 46 | duration: DurationEnum.default("24h"), 47 | top_coins: z.string().default("1000") 48 | }); 49 | 50 | export type GetTopGainersLosersContent = z.infer & Content; 51 | 52 | export const isGetTopGainersLosersContent = (obj: unknown): obj is GetTopGainersLosersContent => { 53 | return GetTopGainersLosersSchema.safeParse(obj).success; 54 | }; 55 | 56 | export default { 57 | name: "GET_TOP_GAINERS_LOSERS", 58 | similes: [ 59 | "TOP_MOVERS", 60 | "BIGGEST_GAINERS", 61 | "BIGGEST_LOSERS", 62 | "PRICE_CHANGES", 63 | "BEST_WORST_PERFORMERS", 64 | ], 65 | // eslint-disable-next-line 66 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 67 | await validateCoingeckoConfig(runtime); 68 | return true; 69 | }, 70 | description: "Get list of top gaining and losing cryptocurrencies by price change", 71 | handler: async ( 72 | runtime: IAgentRuntime, 73 | message: Memory, 74 | state: State, 75 | _options: { [key: string]: unknown }, 76 | callback?: HandlerCallback 77 | ): Promise => { 78 | elizaLogger.log("Starting CoinGecko GET_TOP_GAINERS_LOSERS handler..."); 79 | 80 | // Initialize or update state 81 | let currentState = state; 82 | if (!currentState) { 83 | currentState = (await runtime.composeState(message)) as State; 84 | } else { 85 | currentState = await runtime.updateRecentMessageState(currentState); 86 | } 87 | 88 | 89 | try { 90 | elizaLogger.log("Composing gainers/losers context..."); 91 | const context = composeContext({ 92 | state: currentState, 93 | template: getTopGainersLosersTemplate, 94 | }); 95 | 96 | elizaLogger.log("Generating content from template..."); 97 | const result = await generateObject({ 98 | runtime, 99 | context, 100 | modelClass: ModelClass.LARGE, 101 | schema: GetTopGainersLosersSchema 102 | }); 103 | 104 | if (!isGetTopGainersLosersContent(result.object)) { 105 | elizaLogger.error("Invalid gainers/losers request format"); 106 | return false; 107 | } 108 | 109 | const content = result.object; 110 | elizaLogger.log("Generated content:", content); 111 | 112 | // Fetch data from CoinGecko 113 | const config = await validateCoingeckoConfig(runtime); 114 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 115 | 116 | elizaLogger.log("Fetching top gainers/losers data..."); 117 | elizaLogger.log("API request params:", { 118 | vs_currency: content.vs_currency, 119 | duration: content.duration, 120 | top_coins: content.top_coins 121 | }); 122 | 123 | const response = await axios.get( 124 | `${baseUrl}/coins/top_gainers_losers`, 125 | { 126 | headers: { 127 | 'accept': 'application/json', 128 | [headerKey]: apiKey 129 | }, 130 | params: { 131 | vs_currency: content.vs_currency, 132 | duration: content.duration, 133 | top_coins: content.top_coins 134 | } 135 | } 136 | ); 137 | 138 | if (!response.data) { 139 | throw new Error("No data received from CoinGecko API"); 140 | } 141 | 142 | // Format the response text 143 | const responseText = [ 144 | 'Top Gainers:', 145 | ...response.data.top_gainers.map((coin, index) => { 146 | const changeKey = `usd_${content.duration}_change` as keyof TopGainerLoserItem; 147 | const change = coin[changeKey] as number; 148 | return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()})` + 149 | ` | $${coin.usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}` + 150 | ` | ${change >= 0 ? '+' : ''}${change.toFixed(2)}%` + 151 | `${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ''}`; 152 | }), 153 | '', 154 | 'Top Losers:', 155 | ...response.data.top_losers.map((coin, index) => { 156 | const changeKey = `usd_${content.duration}_change` as keyof TopGainerLoserItem; 157 | const change = coin[changeKey] as number; 158 | return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()})` + 159 | ` | $${coin.usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}` + 160 | ` | ${change >= 0 ? '+' : ''}${change.toFixed(2)}%` + 161 | `${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ''}`; 162 | }) 163 | ].join('\n'); 164 | 165 | if (callback) { 166 | callback({ 167 | text: responseText, 168 | content: { 169 | data: response.data, 170 | params: { 171 | vs_currency: content.vs_currency, 172 | duration: content.duration, 173 | top_coins: content.top_coins 174 | } 175 | } 176 | }); 177 | } 178 | 179 | return true; 180 | } catch (error) { 181 | elizaLogger.error("Error in GET_TOP_GAINERS_LOSERS handler:", error); 182 | 183 | let errorMessage: string; 184 | if (error.response?.status === 429) { 185 | errorMessage = "Rate limit exceeded. Please try again later."; 186 | } else if (error.response?.status === 403) { 187 | errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; 188 | } else if (error.response?.status === 400) { 189 | errorMessage = "Invalid request parameters. Please check your input."; 190 | } else { 191 | errorMessage = `Error fetching top gainers/losers data: ${error.message}`; 192 | } 193 | 194 | if (callback) { 195 | callback({ 196 | text: errorMessage, 197 | content: { 198 | error: error.message, 199 | statusCode: error.response?.status, 200 | params: error.config?.params, 201 | requiresProPlan: error.response?.status === 403 202 | }, 203 | }); 204 | } 205 | return false; 206 | } 207 | }, 208 | 209 | examples: [ 210 | [ 211 | { 212 | user: "{{user1}}", 213 | content: { 214 | text: "What are the top gaining and losing cryptocurrencies?", 215 | }, 216 | }, 217 | { 218 | user: "{{agent}}", 219 | content: { 220 | text: "I'll check the top gainers and losers for you.", 221 | action: "GET_TOP_GAINERS_LOSERS", 222 | }, 223 | }, 224 | { 225 | user: "{{agent}}", 226 | content: { 227 | text: "Here are the top gainers and losers:\nTop Gainers:\n1. Bitcoin (BTC) | $45,000 | +5.2% | Rank #1\n{{dynamic}}", 228 | }, 229 | }, 230 | ], 231 | [ 232 | { 233 | user: "{{user1}}", 234 | content: { 235 | text: "Show me the best and worst performing crypto today", 236 | }, 237 | }, 238 | { 239 | user: "{{agent}}", 240 | content: { 241 | text: "I'll fetch the current top movers in the crypto market.", 242 | action: "GET_TOP_GAINERS_LOSERS", 243 | }, 244 | }, 245 | { 246 | user: "{{agent}}", 247 | content: { 248 | text: "Here are today's best and worst performers:\n{{dynamic}}", 249 | }, 250 | }, 251 | ], 252 | ] as ActionExample[][], 253 | } as Action; -------------------------------------------------------------------------------- /__tests__/actions/getMarkets.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { elizaLogger, ModelClass, generateObject, composeContext } from '@elizaos/core'; 3 | import getMarketsAction, { formatCategory } from '../../src/actions/getMarkets'; 4 | import axios from 'axios'; 5 | import * as environment from '../../src/environment'; 6 | import * as categoriesProvider from '../../src/providers/categoriesProvider'; 7 | 8 | vi.mock('axios'); 9 | vi.mock('@elizaos/core', () => ({ 10 | elizaLogger: { 11 | log: vi.fn(), 12 | error: vi.fn(), 13 | warn: vi.fn(), 14 | info: vi.fn(), 15 | success: vi.fn(), 16 | }, 17 | generateObject: vi.fn(), 18 | composeContext: vi.fn(), 19 | ModelClass: { LARGE: 'LARGE', SMALL: 'SMALL' } 20 | })); 21 | vi.mock('../../src/environment', () => ({ 22 | validateCoingeckoConfig: vi.fn(), 23 | getApiConfig: vi.fn() 24 | })); 25 | vi.mock('../../src/providers/categoriesProvider'); 26 | 27 | describe('getMarkets action', () => { 28 | const mockRuntime = { 29 | composeState: vi.fn(), 30 | updateRecentMessageState: vi.fn(), 31 | getPluginConfig: vi.fn(), 32 | }; 33 | 34 | const mockMessage = {}; 35 | const mockState = {}; 36 | const mockCallback = vi.fn(); 37 | const mockConfig = { 38 | COINGECKO_API_KEY: 'test-api-key', 39 | COINGECKO_PRO_API_KEY: null 40 | }; 41 | 42 | const mockCategories = [ 43 | { category_id: 'defi', name: 'DeFi' }, 44 | { category_id: 'nft', name: 'NFT' } 45 | ]; 46 | 47 | beforeEach(() => { 48 | vi.clearAllMocks(); 49 | 50 | // Mock environment validation 51 | vi.mocked(environment.validateCoingeckoConfig).mockResolvedValue(mockConfig); 52 | vi.mocked(environment.getApiConfig).mockReturnValue({ 53 | baseUrl: 'https://api.coingecko.com/api/v3', 54 | apiKey: 'test-api-key', 55 | headerKey: 'x-cg-demo-api-key' 56 | }); 57 | 58 | // Mock categories provider 59 | vi.mocked(categoriesProvider.getCategoriesData).mockResolvedValue(mockCategories); 60 | 61 | // Mock runtime functions 62 | mockRuntime.composeState.mockResolvedValue(mockState); 63 | mockRuntime.updateRecentMessageState.mockResolvedValue(mockState); 64 | mockRuntime.getPluginConfig.mockResolvedValue({ 65 | apiKey: 'test-api-key', 66 | baseUrl: 'https://api.coingecko.com/api/v3' 67 | }); 68 | 69 | // Mock the core functions 70 | vi.mocked(elizaLogger.log).mockImplementation(() => {}); 71 | vi.mocked(elizaLogger.error).mockImplementation(() => {}); 72 | vi.mocked(elizaLogger.success).mockImplementation(() => {}); 73 | vi.mocked(composeContext).mockReturnValue({}); 74 | }); 75 | 76 | describe('formatCategory', () => { 77 | it('should return undefined for undefined input', () => { 78 | expect(formatCategory(undefined, mockCategories)).toBeUndefined(); 79 | }); 80 | 81 | it('should find exact match by category_id', () => { 82 | expect(formatCategory('defi', mockCategories)).toBe('defi'); 83 | }); 84 | 85 | it('should find match by name', () => { 86 | expect(formatCategory('DeFi', mockCategories)).toBe('defi'); 87 | }); 88 | 89 | it('should find partial match', () => { 90 | expect(formatCategory('nf', mockCategories)).toBe('nft'); 91 | }); 92 | 93 | it('should return undefined for no match', () => { 94 | expect(formatCategory('invalid-category', mockCategories)).toBeUndefined(); 95 | }); 96 | }); 97 | 98 | it('should validate coingecko config', async () => { 99 | await getMarketsAction.validate(mockRuntime, mockMessage); 100 | expect(environment.validateCoingeckoConfig).toHaveBeenCalledWith(mockRuntime); 101 | }); 102 | 103 | it('should fetch and format market data', async () => { 104 | const mockResponse = { 105 | data: [ 106 | { 107 | id: 'bitcoin', 108 | symbol: 'btc', 109 | name: 'Bitcoin', 110 | image: 'image_url', 111 | current_price: 50000, 112 | market_cap: 1000000000000, 113 | market_cap_rank: 1, 114 | fully_diluted_valuation: 1100000000000, 115 | total_volume: 30000000000, 116 | high_24h: 51000, 117 | low_24h: 49000, 118 | price_change_24h: 1000, 119 | price_change_percentage_24h: 2, 120 | market_cap_change_24h: 20000000000, 121 | market_cap_change_percentage_24h: 2, 122 | circulating_supply: 19000000, 123 | total_supply: 21000000, 124 | max_supply: 21000000, 125 | ath: 69000, 126 | ath_change_percentage: -27.5, 127 | ath_date: '2021-11-10T14:24:11.849Z', 128 | atl: 67.81, 129 | atl_change_percentage: 73623.12, 130 | atl_date: '2013-07-06T00:00:00.000Z', 131 | last_updated: '2024-01-31T23:00:00.000Z' 132 | } 133 | ] 134 | }; 135 | 136 | vi.mocked(axios.get).mockResolvedValueOnce(mockResponse); 137 | 138 | // Mock the content generation 139 | vi.mocked(generateObject).mockResolvedValueOnce({ 140 | object: { 141 | vs_currency: 'usd', 142 | category: 'defi', 143 | order: 'market_cap_desc', 144 | per_page: 20, 145 | page: 1, 146 | sparkline: false 147 | }, 148 | modelClass: ModelClass.SMALL 149 | }); 150 | 151 | await getMarketsAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 152 | 153 | expect(axios.get).toHaveBeenCalledWith( 154 | 'https://api.coingecko.com/api/v3/coins/markets', 155 | expect.objectContaining({ 156 | params: { 157 | vs_currency: 'usd', 158 | category: 'defi', 159 | order: 'market_cap_desc', 160 | per_page: 20, 161 | page: 1, 162 | sparkline: false 163 | } 164 | }) 165 | ); 166 | 167 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 168 | text: expect.stringContaining('Bitcoin (BTC)'), 169 | content: expect.objectContaining({ 170 | markets: expect.arrayContaining([ 171 | expect.objectContaining({ 172 | name: 'Bitcoin', 173 | symbol: 'BTC', 174 | marketCapRank: 1, 175 | currentPrice: 50000 176 | }) 177 | ]) 178 | }) 179 | })); 180 | }); 181 | 182 | it('should handle invalid category', async () => { 183 | vi.mocked(generateObject).mockResolvedValueOnce({ 184 | object: { 185 | vs_currency: 'usd', 186 | category: 'invalid-category', 187 | order: 'market_cap_desc', 188 | per_page: 20, 189 | page: 1, 190 | sparkline: false 191 | }, 192 | modelClass: ModelClass.SMALL 193 | }); 194 | 195 | await getMarketsAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 196 | 197 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 198 | text: expect.stringContaining('Invalid category'), 199 | error: expect.objectContaining({ 200 | message: expect.stringContaining('Invalid category') 201 | }) 202 | })); 203 | }); 204 | 205 | it('should handle API errors gracefully', async () => { 206 | vi.mocked(axios.get).mockRejectedValueOnce(new Error('API Error')); 207 | 208 | vi.mocked(generateObject).mockResolvedValueOnce({ 209 | object: { 210 | vs_currency: 'usd', 211 | order: 'market_cap_desc', 212 | per_page: 20, 213 | page: 1, 214 | sparkline: false 215 | }, 216 | modelClass: ModelClass.SMALL 217 | }); 218 | 219 | await getMarketsAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 220 | 221 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 222 | text: expect.stringContaining('Error fetching market data'), 223 | error: expect.objectContaining({ 224 | message: expect.stringContaining('API Error') 225 | }) 226 | })); 227 | }); 228 | 229 | it('should handle rate limit errors', async () => { 230 | const rateLimitError = new Error('Rate limit exceeded'); 231 | Object.assign(rateLimitError, { 232 | response: { status: 429 } 233 | }); 234 | vi.mocked(axios.get).mockRejectedValueOnce(rateLimitError); 235 | 236 | vi.mocked(generateObject).mockResolvedValueOnce({ 237 | object: { 238 | vs_currency: 'usd', 239 | order: 'market_cap_desc', 240 | per_page: 20, 241 | page: 1, 242 | sparkline: false 243 | }, 244 | modelClass: ModelClass.SMALL 245 | }); 246 | 247 | await getMarketsAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 248 | 249 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 250 | text: expect.stringContaining('Rate limit exceeded'), 251 | error: expect.objectContaining({ 252 | message: expect.stringContaining('Rate limit exceeded'), 253 | statusCode: 429 254 | }) 255 | })); 256 | }); 257 | 258 | it('should handle empty response data', async () => { 259 | vi.mocked(axios.get).mockResolvedValueOnce({ data: [] }); 260 | 261 | vi.mocked(generateObject).mockResolvedValueOnce({ 262 | object: { 263 | vs_currency: 'usd', 264 | order: 'market_cap_desc', 265 | per_page: 20, 266 | page: 1, 267 | sparkline: false 268 | }, 269 | modelClass: ModelClass.SMALL 270 | }); 271 | 272 | await getMarketsAction.handler(mockRuntime, mockMessage, mockState, {}, mockCallback); 273 | 274 | expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ 275 | text: expect.stringContaining('No market data received'), 276 | error: expect.objectContaining({ 277 | message: expect.stringContaining('No market data received') 278 | }) 279 | })); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /src/actions/getNetworkNewPools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action, 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getNetworkNewPoolsTemplate } from "../templates/networkNewPools"; 18 | import { getNetworksData } from "../providers/networkProvider"; 19 | 20 | interface NewPool { 21 | id: string; 22 | type: string; 23 | attributes: { 24 | name: string; 25 | market_cap_usd: string; 26 | fdv_usd: string; 27 | reserve_in_usd: string; 28 | pool_created_at: string; 29 | }; 30 | } 31 | 32 | interface NewPoolsResponse { 33 | data: NewPool[]; 34 | } 35 | 36 | export const GetNetworkNewPoolsSchema = z.object({ 37 | networkId: z.string(), 38 | limit: z.number().min(1).max(100).default(10), 39 | }); 40 | 41 | export type GetNetworkNewPoolsContent = z.infer< 42 | typeof GetNetworkNewPoolsSchema 43 | > & 44 | Content; 45 | 46 | export const isGetNetworkNewPoolsContent = ( 47 | obj: unknown 48 | ): obj is GetNetworkNewPoolsContent => { 49 | return GetNetworkNewPoolsSchema.safeParse(obj).success; 50 | }; 51 | 52 | export default { 53 | name: "GET_NETWORK_NEW_POOLS", 54 | similes: [ 55 | "NETWORK_NEW_POOLS", 56 | "CHAIN_NEW_POOLS", 57 | "NEW_POOLS_BY_NETWORK", 58 | "RECENT_POOLS", 59 | "LATEST_POOLS", 60 | ], 61 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 62 | await validateCoingeckoConfig(runtime); 63 | return true; 64 | }, 65 | description: 66 | "Get list of newly created pools for a specific network from CoinGecko's onchain data", 67 | handler: async ( 68 | runtime: IAgentRuntime, 69 | message: Memory, 70 | state: State, 71 | _options: { [key: string]: unknown }, 72 | callback?: HandlerCallback 73 | ): Promise => { 74 | elizaLogger.log("Starting CoinGecko GET_NETWORK_NEW_POOLS handler..."); 75 | 76 | let currentState = state; 77 | if (!currentState) { 78 | currentState = (await runtime.composeState(message)) as State; 79 | } else { 80 | currentState = await runtime.updateRecentMessageState(currentState); 81 | } 82 | 83 | try { 84 | elizaLogger.log("Composing network new pools context..."); 85 | const newPoolsContext = composeContext({ 86 | state: currentState, 87 | template: getNetworkNewPoolsTemplate, 88 | }); 89 | 90 | const result = await generateObject({ 91 | runtime, 92 | context: newPoolsContext, 93 | modelClass: ModelClass.LARGE, 94 | schema: GetNetworkNewPoolsSchema, 95 | }); 96 | 97 | if (!isGetNetworkNewPoolsContent(result.object)) { 98 | elizaLogger.error("Invalid network new pools request format"); 99 | return false; 100 | } 101 | 102 | // Fetch networks data first 103 | const networks = await getNetworksData(runtime); 104 | 105 | // Fetch networks data first 106 | const networksResponse = await getNetworksData(runtime); 107 | 108 | // Find the matching network from the data array 109 | const network = networksResponse.find((n) => { 110 | const searchTerm = ( 111 | result.object as { networkId: string } 112 | ).networkId.toLowerCase(); 113 | return ( 114 | n.id.toLowerCase() === searchTerm || 115 | n.attributes.name.toLowerCase().includes(searchTerm) || 116 | n.attributes.coingecko_asset_platform_id.toLowerCase() === 117 | searchTerm 118 | ); 119 | }); 120 | 121 | if (!network) { 122 | throw new Error( 123 | `Network ${result.object.networkId} not found in available networks` 124 | ); 125 | } 126 | 127 | const config = await validateCoingeckoConfig(runtime); 128 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 129 | 130 | elizaLogger.log( 131 | `Fetching new pools data for network: ${network.id}` 132 | ); 133 | 134 | const response = await axios.get( 135 | `${baseUrl}/onchain/networks/${network.id}/new_pools?include=base_token,dex`, 136 | { 137 | headers: { 138 | [headerKey]: apiKey, 139 | }, 140 | } 141 | ); 142 | 143 | if (!response.data) { 144 | throw new Error("No data received from CoinGecko API"); 145 | } 146 | 147 | const formattedData = response.data.data 148 | .slice(0, result.object.limit) 149 | .map((pool) => ({ 150 | name: pool.attributes.name, 151 | marketCap: Number( 152 | pool.attributes.market_cap_usd 153 | ).toLocaleString("en-US", { 154 | style: "currency", 155 | currency: "USD", 156 | }), 157 | fdv: Number(pool.attributes.fdv_usd).toLocaleString( 158 | "en-US", 159 | { 160 | style: "currency", 161 | currency: "USD", 162 | } 163 | ), 164 | reserveUSD: Number( 165 | pool.attributes.reserve_in_usd 166 | ).toLocaleString("en-US", { 167 | style: "currency", 168 | currency: "USD", 169 | }), 170 | createdAt: new Date( 171 | pool.attributes.pool_created_at 172 | ).toLocaleDateString(), 173 | })); 174 | 175 | const responseText = [ 176 | `New Pools Overview for ${network.attributes.name}:`, 177 | "", 178 | ...formattedData.map((pool, index) => 179 | [ 180 | `${index + 1}. ${pool.name}`, 181 | ` Market Cap: ${pool.marketCap}`, 182 | ` FDV: ${pool.fdv}`, 183 | ` Reserve: ${pool.reserveUSD}`, 184 | ` Created: ${pool.createdAt}`, 185 | "", 186 | ].join("\n") 187 | ), 188 | ].join("\n"); 189 | 190 | elizaLogger.success( 191 | "Network new pools data retrieved successfully!" 192 | ); 193 | 194 | if (callback) { 195 | callback({ 196 | text: responseText, 197 | content: { 198 | networkId: network.id, 199 | networkName: network.attributes.name, 200 | newPools: formattedData, 201 | timestamp: new Date().toISOString(), 202 | }, 203 | }); 204 | } 205 | 206 | return true; 207 | } catch (error) { 208 | elizaLogger.error("Error in GET_NETWORK_NEW_POOLS handler:", error); 209 | 210 | const errorMessage = 211 | error.response?.status === 429 212 | ? "Rate limit exceeded. Please try again later." 213 | : `Error fetching new pools data: ${error.message}`; 214 | 215 | if (callback) { 216 | callback({ 217 | text: errorMessage, 218 | content: { 219 | error: error.message, 220 | statusCode: error.response?.status, 221 | }, 222 | }); 223 | } 224 | return false; 225 | } 226 | }, 227 | 228 | examples: [ 229 | [ 230 | { 231 | user: "{{user1}}", 232 | content: { 233 | text: "Show me new liquidity pools on Ethereum", 234 | }, 235 | }, 236 | { 237 | user: "{{agent}}", 238 | content: { 239 | text: "I'll check the new Ethereum liquidity pools for you.", 240 | action: "GET_NETWORK_NEW_POOLS", 241 | }, 242 | }, 243 | { 244 | user: "{{agent}}", 245 | content: { 246 | text: "Here are the new pools on ETHEREUM:\n1. PEPE / WETH\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025\n2. SUSHI / WETH\n Market Cap: $8,844,297,825\n FDV: $43,874,068,484\n Reserve: $718,413,745\n Created: 1/17/2025", 247 | }, 248 | }, 249 | ], 250 | [ 251 | { 252 | user: "{{user1}}", 253 | content: { 254 | text: "What are the 5 latest pools on BSC?", 255 | }, 256 | }, 257 | { 258 | user: "{{agent}}", 259 | content: { 260 | text: "I'll fetch the 5 latest pools on BSC for you.", 261 | action: "GET_NETWORK_NEW_POOLS", 262 | }, 263 | }, 264 | { 265 | user: "{{agent}}", 266 | content: { 267 | text: "Here are the 5 newest pools on BSC:\n1. CAKE / WBNB\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025", 268 | }, 269 | }, 270 | ], 271 | [ 272 | { 273 | user: "{{user1}}", 274 | content: { 275 | text: "List all recent pools on Polygon", 276 | }, 277 | }, 278 | { 279 | user: "{{agent}}", 280 | content: { 281 | text: "I'll get all the recently added pools on Polygon for you.", 282 | action: "GET_NETWORK_NEW_POOLS", 283 | }, 284 | }, 285 | { 286 | user: "{{agent}}", 287 | content: { 288 | text: "Here are all new pools on POLYGON:\n1. MATIC / USDC\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025", 289 | }, 290 | }, 291 | ], 292 | ] as ActionExample[][], 293 | } as Action; 294 | -------------------------------------------------------------------------------- /src/actions/getNetworkTrendingPools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action, 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getNetworkTrendingPoolsTemplate } from "../templates/networkTrendingPools"; 18 | import { getNetworksData } from "../providers/networkProvider"; 19 | 20 | interface TrendingPool { 21 | id: string; 22 | type: string; 23 | attributes: { 24 | name: string; 25 | market_cap_usd: string; 26 | fdv_usd: string; 27 | reserve_in_usd: string; 28 | pool_created_at: string; 29 | }; 30 | } 31 | 32 | interface TrendingPoolsResponse { 33 | data: TrendingPool[]; 34 | } 35 | 36 | export const GetNetworkTrendingPoolsSchema = z.object({ 37 | networkId: z.string(), 38 | limit: z.number().min(1).max(100).default(10), 39 | }); 40 | 41 | export type GetNetworkTrendingPoolsContent = z.infer< 42 | typeof GetNetworkTrendingPoolsSchema 43 | > & 44 | Content; 45 | 46 | export const isGetNetworkTrendingPoolsContent = ( 47 | obj: unknown 48 | ): obj is GetNetworkTrendingPoolsContent => { 49 | return GetNetworkTrendingPoolsSchema.safeParse(obj).success; 50 | }; 51 | 52 | export default { 53 | name: "GET_NETWORK_TRENDING_POOLS", 54 | similes: [ 55 | "NETWORK_TRENDING_POOLS", 56 | "CHAIN_HOT_POOLS", 57 | "BLOCKCHAIN_POPULAR_POOLS", 58 | ], 59 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 60 | await validateCoingeckoConfig(runtime); 61 | return true; 62 | }, 63 | description: 64 | "Get list of trending pools for a specific network from CoinGecko's onchain data", 65 | handler: async ( 66 | runtime: IAgentRuntime, 67 | message: Memory, 68 | state: State, 69 | _options: { [key: string]: unknown }, 70 | callback?: HandlerCallback 71 | ): Promise => { 72 | elizaLogger.log( 73 | "Starting CoinGecko GET_NETWORK_TRENDING_POOLS handler..." 74 | ); 75 | 76 | let currentState = state; 77 | if (!currentState) { 78 | currentState = (await runtime.composeState(message)) as State; 79 | } else { 80 | currentState = await runtime.updateRecentMessageState(currentState); 81 | } 82 | 83 | try { 84 | elizaLogger.log("Composing network trending pools context..."); 85 | const trendingContext = composeContext({ 86 | state: currentState, 87 | template: getNetworkTrendingPoolsTemplate, 88 | }); 89 | 90 | const result = await generateObject({ 91 | runtime, 92 | context: trendingContext, 93 | modelClass: ModelClass.LARGE, 94 | schema: GetNetworkTrendingPoolsSchema, 95 | }); 96 | 97 | if (!isGetNetworkTrendingPoolsContent(result.object)) { 98 | elizaLogger.error( 99 | "Invalid network trending pools request format" 100 | ); 101 | return false; 102 | } 103 | 104 | // Fetch networks data first 105 | const networks = await getNetworksData(runtime); 106 | 107 | // Find the matching network 108 | const network = networks.find((n) => { 109 | const searchTerm = ( 110 | result.object as { networkId: string } 111 | ).networkId.toLowerCase(); 112 | return ( 113 | n.id.toLowerCase() === searchTerm || 114 | n.attributes.name.toLowerCase().includes(searchTerm) || 115 | n.attributes.coingecko_asset_platform_id.toLowerCase() === 116 | searchTerm 117 | ); 118 | }); 119 | 120 | if (!network) { 121 | throw new Error( 122 | `Network ${result.object.networkId} not found in available networks` 123 | ); 124 | } 125 | 126 | const config = await validateCoingeckoConfig(runtime); 127 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 128 | 129 | elizaLogger.log( 130 | `Fetching trending pools data for network: ${network.id}` 131 | ); 132 | 133 | const response = await axios.get( 134 | `${baseUrl}/onchain/networks/${network.id}/trending_pools?include=base_token,dex`, 135 | { 136 | headers: { 137 | [headerKey]: apiKey, 138 | }, 139 | } 140 | ); 141 | 142 | if (!response.data) { 143 | throw new Error("No data received from CoinGecko API"); 144 | } 145 | 146 | const formattedData = response.data.data 147 | .slice(0, result.object.limit) 148 | .map((pool) => ({ 149 | name: pool.attributes.name, 150 | marketCap: Number( 151 | pool.attributes.market_cap_usd 152 | ).toLocaleString("en-US", { 153 | style: "currency", 154 | currency: "USD", 155 | }), 156 | fdv: Number(pool.attributes.fdv_usd).toLocaleString( 157 | "en-US", 158 | { 159 | style: "currency", 160 | currency: "USD", 161 | } 162 | ), 163 | reserveUSD: Number( 164 | pool.attributes.reserve_in_usd 165 | ).toLocaleString("en-US", { 166 | style: "currency", 167 | currency: "USD", 168 | }), 169 | createdAt: new Date( 170 | pool.attributes.pool_created_at 171 | ).toLocaleDateString(), 172 | })); 173 | 174 | const responseText = [ 175 | `Trending Pools Overview for ${network.attributes.name}:`, 176 | "", 177 | ...formattedData.map((pool, index) => 178 | [ 179 | `${index + 1}. ${pool.name}`, 180 | ` Market Cap: ${pool.marketCap}`, 181 | ` FDV: ${pool.fdv}`, 182 | ` Reserve: ${pool.reserveUSD}`, 183 | ` Created: ${pool.createdAt}`, 184 | "", 185 | ].join("\n") 186 | ), 187 | ].join("\n"); 188 | 189 | elizaLogger.success( 190 | "Network trending pools data retrieved successfully!" 191 | ); 192 | 193 | if (callback) { 194 | callback({ 195 | text: responseText, 196 | content: { 197 | networkId: network.id, 198 | networkName: network.attributes.name, 199 | trendingPools: formattedData, 200 | timestamp: new Date().toISOString(), 201 | }, 202 | }); 203 | } 204 | 205 | return true; 206 | } catch (error) { 207 | elizaLogger.error( 208 | "Error in GET_NETWORK_TRENDING_POOLS handler:", 209 | error 210 | ); 211 | 212 | const errorMessage = 213 | error.response?.status === 429 214 | ? "Rate limit exceeded. Please try again later." 215 | : `Error fetching trending pools data: ${error.message}`; 216 | 217 | if (callback) { 218 | callback({ 219 | text: errorMessage, 220 | content: { 221 | error: error.message, 222 | statusCode: error.response?.status, 223 | }, 224 | }); 225 | } 226 | return false; 227 | } 228 | }, 229 | 230 | examples: [ 231 | [ 232 | { 233 | user: "{{user1}}", 234 | content: { 235 | text: "Show me trending liquidity pools on Solana", 236 | }, 237 | }, 238 | { 239 | user: "{{agent}}", 240 | content: { 241 | text: "I'll check the trending Solana liquidity pools for you.", 242 | action: "GET_NETWORK_TRENDING_POOLS", 243 | }, 244 | }, 245 | { 246 | user: "{{agent}}", 247 | content: { 248 | text: "Here are the trending pools on SOLANA:\n1. MELANIA / USDC\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025\n2. TRUMP / USDC\n Market Cap: $8,844,297,825\n FDV: $43,874,068,484\n Reserve: $718,413,745\n Created: 1/17/2025", 249 | }, 250 | }, 251 | ], 252 | [ 253 | { 254 | user: "{{user1}}", 255 | content: { 256 | text: "What are the top 5 hottest pools on Ethereum?", 257 | }, 258 | }, 259 | { 260 | user: "{{agent}}", 261 | content: { 262 | text: "I'll fetch the top 5 hottest pools on Ethereum for you.", 263 | action: "GET_NETWORK_TRENDING_POOLS", 264 | }, 265 | }, 266 | { 267 | user: "{{agent}}", 268 | content: { 269 | text: "Here are the top 5 trending pools on ETHEREUM:\n1. PEPE / WETH\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025", 270 | }, 271 | }, 272 | ], 273 | [ 274 | { 275 | user: "{{user1}}", 276 | content: { 277 | text: "List all BSC pools with highest volume", 278 | }, 279 | }, 280 | { 281 | user: "{{agent}}", 282 | content: { 283 | text: "I'll get all the trending pools on BSC for you.", 284 | action: "GET_NETWORK_TRENDING_POOLS", 285 | }, 286 | }, 287 | { 288 | user: "{{agent}}", 289 | content: { 290 | text: "Here are all trending pools on BSC:\n1. CAKE / WBNB\n Market Cap: $954,636,707\n FDV: $6,402,478,508\n Reserve: $363,641,037\n Created: 1/19/2025", 291 | }, 292 | }, 293 | ], 294 | ] as ActionExample[][], 295 | } as Action; 296 | -------------------------------------------------------------------------------- /src/actions/getMarkets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getCategoriesData } from '../providers/categoriesProvider'; 18 | import { getMarketsTemplate } from "../templates/markets"; 19 | 20 | interface CategoryItem { 21 | category_id: string; 22 | name: string; 23 | } 24 | 25 | export function formatCategory(category: string | undefined, categories: CategoryItem[]): string | undefined { 26 | if (!category) return undefined; 27 | 28 | const normalizedInput = category.toLowerCase().trim(); 29 | 30 | // First try to find exact match by category_id 31 | const exactMatch = categories.find(c => c.category_id === normalizedInput); 32 | if (exactMatch) { 33 | return exactMatch.category_id; 34 | } 35 | 36 | // Then try to find match by name 37 | const nameMatch = categories.find(c => 38 | c.name.toLowerCase() === normalizedInput || 39 | c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') === normalizedInput 40 | ); 41 | if (nameMatch) { 42 | return nameMatch.category_id; 43 | } 44 | 45 | // Try to find partial matches 46 | const partialMatch = categories.find(c => 47 | c.name.toLowerCase().includes(normalizedInput) || 48 | c.category_id.includes(normalizedInput) 49 | ); 50 | if (partialMatch) { 51 | return partialMatch.category_id; 52 | } 53 | 54 | return undefined; 55 | } 56 | 57 | /** 58 | * Interface for CoinGecko /coins/markets endpoint response 59 | * @see https://docs.coingecko.com/reference/coins-markets 60 | */ 61 | export interface CoinMarketData { 62 | id: string; 63 | symbol: string; 64 | name: string; 65 | image: string; 66 | current_price: number; 67 | market_cap: number; 68 | market_cap_rank: number; 69 | fully_diluted_valuation: number; 70 | total_volume: number; 71 | high_24h: number; 72 | low_24h: number; 73 | price_change_24h: number; 74 | price_change_percentage_24h: number; 75 | market_cap_change_24h: number; 76 | market_cap_change_percentage_24h: number; 77 | circulating_supply: number; 78 | total_supply: number; 79 | max_supply: number; 80 | ath: number; 81 | ath_change_percentage: number; 82 | ath_date: string; 83 | atl: number; 84 | atl_change_percentage: number; 85 | atl_date: string; 86 | last_updated: string; 87 | } 88 | 89 | export const GetMarketsSchema = z.object({ 90 | vs_currency: z.string().default('usd'), 91 | category: z.string().optional(), 92 | order: z.enum(['market_cap_desc', 'market_cap_asc', 'volume_desc', 'volume_asc']).default('market_cap_desc'), 93 | per_page: z.number().min(1).max(250).default(20), 94 | page: z.number().min(1).default(1), 95 | sparkline: z.boolean().default(false) 96 | }); 97 | 98 | export type GetMarketsContent = z.infer & Content; 99 | 100 | export const isGetMarketsContent = (obj: unknown): obj is GetMarketsContent => { 101 | return GetMarketsSchema.safeParse(obj).success; 102 | }; 103 | 104 | export default { 105 | name: "GET_MARKETS", 106 | similes: [ 107 | "MARKET_OVERVIEW", 108 | "TOP_RANKINGS", 109 | "MARKET_LEADERBOARD", 110 | "CRYPTO_RANKINGS", 111 | "BEST_PERFORMING_COINS", 112 | "TOP_MARKET_CAPS" 113 | ], 114 | // eslint-disable-next-line 115 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 116 | await validateCoingeckoConfig(runtime); 117 | return true; 118 | }, 119 | // Comprehensive endpoint for market rankings, supports up to 250 coins per request 120 | description: "Get ranked list of top cryptocurrencies sorted by market metrics (without specifying coins)", 121 | handler: async ( 122 | runtime: IAgentRuntime, 123 | message: Memory, 124 | state: State, 125 | _options: { [key: string]: unknown }, 126 | callback?: HandlerCallback 127 | ): Promise => { 128 | elizaLogger.log("Starting CoinGecko GET_MARKETS handler..."); 129 | 130 | // Initialize or update state 131 | let currentState = state; 132 | if (!currentState) { 133 | currentState = (await runtime.composeState(message)) as State; 134 | } else { 135 | currentState = await runtime.updateRecentMessageState(currentState); 136 | } 137 | 138 | 139 | try { 140 | const config = await validateCoingeckoConfig(runtime); 141 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 142 | 143 | // Get categories through the provider 144 | const categories = await getCategoriesData(runtime); 145 | 146 | // Compose markets context with categories 147 | const marketsContext = composeContext({ 148 | state: currentState, 149 | template: getMarketsTemplate.replace('{{categories}}', 150 | categories.map(c => `- ${c.name} (ID: ${c.category_id})`).join('\n') 151 | ), 152 | }); 153 | 154 | const result = await generateObject({ 155 | runtime, 156 | context: marketsContext, 157 | modelClass: ModelClass.SMALL, 158 | schema: GetMarketsSchema 159 | }); 160 | 161 | if (!isGetMarketsContent(result.object)) { 162 | elizaLogger.error("Invalid market data format received"); 163 | return false; 164 | } 165 | 166 | const content = result.object; 167 | elizaLogger.log("Content from template:", content); 168 | 169 | // If template returns null, this is not a markets request 170 | if (!content) { 171 | return false; 172 | } 173 | 174 | const formattedCategory = formatCategory(content.category, categories); 175 | if (content.category && !formattedCategory) { 176 | throw new Error(`Invalid category: ${content.category}. Please choose from the available categories.`); 177 | } 178 | 179 | elizaLogger.log("Making API request with params:", { 180 | url: `${baseUrl}/coins/markets`, 181 | category: formattedCategory, 182 | vs_currency: content.vs_currency, 183 | order: content.order, 184 | per_page: content.per_page, 185 | page: content.page 186 | }); 187 | 188 | const response = await axios.get( 189 | `${baseUrl}/coins/markets`, 190 | { 191 | headers: { 192 | 'accept': 'application/json', 193 | [headerKey]: apiKey 194 | }, 195 | params: { 196 | vs_currency: content.vs_currency, 197 | category: formattedCategory, 198 | order: content.order, 199 | per_page: content.per_page, 200 | page: content.page, 201 | sparkline: content.sparkline 202 | } 203 | } 204 | ); 205 | 206 | if (!response.data?.length) { 207 | throw new Error("No market data received from CoinGecko API"); 208 | } 209 | 210 | const formattedData = response.data.map(coin => ({ 211 | name: coin.name, 212 | symbol: coin.symbol.toUpperCase(), 213 | marketCapRank: coin.market_cap_rank, 214 | currentPrice: coin.current_price, 215 | priceChange24h: coin.price_change_24h, 216 | priceChangePercentage24h: coin.price_change_percentage_24h, 217 | marketCap: coin.market_cap, 218 | volume24h: coin.total_volume, 219 | high24h: coin.high_24h, 220 | low24h: coin.low_24h, 221 | circulatingSupply: coin.circulating_supply, 222 | totalSupply: coin.total_supply, 223 | maxSupply: coin.max_supply, 224 | lastUpdated: coin.last_updated 225 | })); 226 | 227 | const categoryDisplay = content.category ? 228 | `${categories.find(c => c.category_id === formattedCategory)?.name.toUpperCase() || content.category.toUpperCase()} ` : ''; 229 | 230 | const responseText = [ 231 | `Top ${formattedData.length} ${categoryDisplay}Cryptocurrencies by ${content.order === 'volume_desc' || content.order === 'volume_asc' ? 'Volume' : 'Market Cap'}:`, 232 | ...formattedData.map((coin, index) => 233 | `${index + 1}. ${coin.name} (${coin.symbol})` + 234 | ` | $${coin.currentPrice.toLocaleString()}` + 235 | ` | ${coin.priceChangePercentage24h.toFixed(2)}%` + 236 | ` | MCap: $${(coin.marketCap / 1e9).toFixed(2)}B` 237 | ) 238 | ].join('\n'); 239 | 240 | elizaLogger.success("Market data retrieved successfully!"); 241 | 242 | if (callback) { 243 | callback({ 244 | text: responseText, 245 | content: { 246 | markets: formattedData, 247 | params: { 248 | vs_currency: content.vs_currency, 249 | category: content.category, 250 | order: content.order, 251 | per_page: content.per_page, 252 | page: content.page 253 | }, 254 | timestamp: new Date().toISOString() 255 | } 256 | }); 257 | } 258 | 259 | return true; 260 | } catch (error) { 261 | elizaLogger.error("Error in GET_MARKETS handler:", error); 262 | 263 | let errorMessage: string; 264 | if (error.response?.status === 429) { 265 | errorMessage = "Rate limit exceeded. Please try again later."; 266 | } else if (error.response?.status === 403) { 267 | errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; 268 | } else if (error.response?.status === 400) { 269 | errorMessage = "Invalid request parameters. Please check your input."; 270 | } else { 271 | errorMessage = `Error fetching market data: ${error.message}`; 272 | } 273 | 274 | if (callback) { 275 | callback({ 276 | text: errorMessage, 277 | error: { 278 | message: error.message, 279 | statusCode: error.response?.status, 280 | params: error.config?.params, 281 | requiresProPlan: error.response?.status === 403 282 | } 283 | }); 284 | } 285 | return false; 286 | } 287 | }, 288 | 289 | examples: [ 290 | [ 291 | { 292 | user: "{{user1}}", 293 | content: { 294 | text: "Show me the top cryptocurrencies by market cap", 295 | }, 296 | }, 297 | { 298 | user: "{{agent}}", 299 | content: { 300 | text: "I'll fetch the current market data for top cryptocurrencies.", 301 | action: "GET_MARKETS", 302 | }, 303 | }, 304 | { 305 | user: "{{agent}}", 306 | content: { 307 | text: "Here are the top cryptocurrencies:\n1. Bitcoin (BTC) | $45,000 | +2.5% | MCap: $870.5B\n{{dynamic}}", 308 | }, 309 | }, 310 | ], 311 | ] as ActionExample[][], 312 | } as Action; 313 | -------------------------------------------------------------------------------- /src/actions/getPrice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionExample, 3 | composeContext, 4 | type Content, 5 | elizaLogger, 6 | generateObject, 7 | type HandlerCallback, 8 | type IAgentRuntime, 9 | type Memory, 10 | ModelClass, 11 | type State, 12 | type Action, 13 | } from "@elizaos/core"; 14 | import axios from "axios"; 15 | import { z } from "zod"; 16 | import { getApiConfig, validateCoingeckoConfig } from "../environment"; 17 | import { getCoinsData } from "../providers/coinsProvider"; 18 | import { getPriceTemplate } from "../templates/price"; 19 | 20 | interface CurrencyData { 21 | [key: string]: number; 22 | usd?: number; 23 | eur?: number; 24 | usd_market_cap?: number; 25 | eur_market_cap?: number; 26 | usd_24h_vol?: number; 27 | eur_24h_vol?: number; 28 | usd_24h_change?: number; 29 | eur_24h_change?: number; 30 | last_updated_at?: number; 31 | } 32 | 33 | interface PriceResponse { 34 | [coinId: string]: CurrencyData; 35 | } 36 | 37 | export const GetPriceSchema = z.object({ 38 | coinIds: z.union([z.string(), z.array(z.string())]), 39 | currency: z.union([z.string(), z.array(z.string())]).default(["usd"]), 40 | include_market_cap: z.boolean().default(false), 41 | include_24hr_vol: z.boolean().default(false), 42 | include_24hr_change: z.boolean().default(false), 43 | include_last_updated_at: z.boolean().default(false) 44 | }); 45 | 46 | export type GetPriceContent = z.infer & Content; 47 | 48 | export const isGetPriceContent = (obj: unknown): obj is GetPriceContent => { 49 | return GetPriceSchema.safeParse(obj).success; 50 | }; 51 | 52 | function formatCoinIds(input: string | string[]): string { 53 | if (Array.isArray(input)) { 54 | return input.join(','); 55 | } 56 | return input; 57 | } 58 | 59 | export default { 60 | name: "GET_PRICE", 61 | similes: [ 62 | "COIN_PRICE_CHECK", 63 | "SPECIFIC_COINS_PRICE", 64 | "COIN_PRICE_LOOKUP", 65 | "SELECTED_COINS_PRICE", 66 | "PRICE_DETAILS", 67 | "COIN_PRICE_DATA" 68 | ], 69 | // eslint-disable-next-line 70 | validate: async (runtime: IAgentRuntime, _message: Memory) => { 71 | await validateCoingeckoConfig(runtime); 72 | return true; 73 | }, 74 | description: "Get price and basic market data for one or more specific cryptocurrencies (by name/symbol)", 75 | handler: async ( 76 | runtime: IAgentRuntime, 77 | message: Memory, 78 | state: State, 79 | _options: { [key: string]: unknown }, 80 | callback?: HandlerCallback 81 | ): Promise => { 82 | elizaLogger.log("Starting CoinGecko GET_PRICE handler..."); 83 | 84 | // Initialize or update state 85 | let currentState = state; 86 | if (!currentState) { 87 | currentState = (await runtime.composeState(message)) as State; 88 | } else { 89 | currentState = await runtime.updateRecentMessageState(currentState); 90 | } 91 | 92 | 93 | try { 94 | elizaLogger.log("Composing price context..."); 95 | const priceContext = composeContext({ 96 | state: currentState, 97 | template: getPriceTemplate, 98 | }); 99 | 100 | elizaLogger.log("Generating content from template..."); 101 | const result = await generateObject({ 102 | runtime, 103 | context: priceContext, 104 | modelClass: ModelClass.LARGE, 105 | schema: GetPriceSchema 106 | }); 107 | 108 | if (!isGetPriceContent(result.object)) { 109 | elizaLogger.error("Invalid price request format"); 110 | return false; 111 | } 112 | 113 | const content = result.object; 114 | elizaLogger.log("Generated content:", content); 115 | 116 | // Format currencies for API request 117 | const currencies = Array.isArray(content.currency) ? content.currency : [content.currency]; 118 | const vs_currencies = currencies.join(',').toLowerCase(); 119 | 120 | // Format coin IDs for API request 121 | const coinIds = formatCoinIds(content.coinIds); 122 | 123 | elizaLogger.log("Formatted request parameters:", { coinIds, vs_currencies }); 124 | 125 | // Fetch price from CoinGecko 126 | const config = await validateCoingeckoConfig(runtime); 127 | const { baseUrl, apiKey, headerKey } = getApiConfig(config); 128 | 129 | elizaLogger.log(`Fetching prices for ${coinIds} in ${vs_currencies}...`); 130 | elizaLogger.log("API request URL:", `${baseUrl}/simple/price`); 131 | elizaLogger.log("API request params:", { 132 | ids: coinIds, 133 | vs_currencies, 134 | include_market_cap: content.include_market_cap, 135 | include_24hr_vol: content.include_24hr_vol, 136 | include_24hr_change: content.include_24hr_change, 137 | include_last_updated_at: content.include_last_updated_at 138 | }); 139 | 140 | const response = await axios.get( 141 | `${baseUrl}/simple/price`, 142 | { 143 | params: { 144 | ids: coinIds, 145 | vs_currencies, 146 | include_market_cap: content.include_market_cap, 147 | include_24hr_vol: content.include_24hr_vol, 148 | include_24hr_change: content.include_24hr_change, 149 | include_last_updated_at: content.include_last_updated_at 150 | }, 151 | headers: { 152 | 'accept': 'application/json', 153 | [headerKey]: apiKey 154 | } 155 | } 156 | ); 157 | 158 | if (Object.keys(response.data).length === 0) { 159 | throw new Error("No price data available for the specified coins and currency"); 160 | } 161 | 162 | // Get coins data for formatting 163 | const coins = await getCoinsData(runtime); 164 | 165 | // Format response text for each coin 166 | const formattedResponse = Object.entries(response.data).map(([coinId, data]) => { 167 | const coin = coins.find(c => c.id === coinId); 168 | const coinName = coin ? `${coin.name} (${coin.symbol.toUpperCase()})` : coinId; 169 | const parts = [`${coinName}:`]; 170 | 171 | // Add price for each requested currency 172 | for (const currency of currencies) { 173 | const upperCurrency = currency.toUpperCase(); 174 | if (data[currency]) { 175 | parts.push(` ${upperCurrency}: ${data[currency].toLocaleString(undefined, { 176 | style: 'currency', 177 | currency: currency 178 | })}`); 179 | } 180 | 181 | // Add market cap if requested and available 182 | if (content.include_market_cap) { 183 | const marketCap = data[`${currency}_market_cap`]; 184 | if (marketCap !== undefined) { 185 | parts.push(` Market Cap (${upperCurrency}): ${marketCap.toLocaleString(undefined, { 186 | style: 'currency', 187 | currency: currency, 188 | maximumFractionDigits: 0 189 | })}`); 190 | } 191 | } 192 | 193 | // Add 24h volume if requested and available 194 | if (content.include_24hr_vol) { 195 | const volume = data[`${currency}_24h_vol`]; 196 | if (volume !== undefined) { 197 | parts.push(` 24h Volume (${upperCurrency}): ${volume.toLocaleString(undefined, { 198 | style: 'currency', 199 | currency: currency, 200 | maximumFractionDigits: 0 201 | })}`); 202 | } 203 | } 204 | 205 | // Add 24h change if requested and available 206 | if (content.include_24hr_change) { 207 | const change = data[`${currency}_24h_change`]; 208 | if (change !== undefined) { 209 | const changePrefix = change >= 0 ? '+' : ''; 210 | parts.push(` 24h Change (${upperCurrency}): ${changePrefix}${change.toFixed(2)}%`); 211 | } 212 | } 213 | } 214 | 215 | // Add last updated if requested 216 | if (content.include_last_updated_at && data.last_updated_at) { 217 | const lastUpdated = new Date(data.last_updated_at * 1000).toLocaleString(); 218 | parts.push(` Last Updated: ${lastUpdated}`); 219 | } 220 | 221 | return parts.join('\n'); 222 | }).filter(Boolean); 223 | 224 | if (formattedResponse.length === 0) { 225 | throw new Error("Failed to format price data for the specified coins"); 226 | } 227 | 228 | const responseText = formattedResponse.join('\n\n'); 229 | elizaLogger.success("Price data retrieved successfully!"); 230 | 231 | if (callback) { 232 | callback({ 233 | text: responseText, 234 | content: { 235 | prices: Object.entries(response.data).reduce((acc, [coinId, data]) => { 236 | const coinPrices = currencies.reduce((currencyAcc, currency) => { 237 | const currencyData = { 238 | price: data[currency], 239 | marketCap: data[`${currency}_market_cap`], 240 | volume24h: data[`${currency}_24h_vol`], 241 | change24h: data[`${currency}_24h_change`], 242 | lastUpdated: data.last_updated_at, 243 | }; 244 | Object.assign(currencyAcc, { [currency]: currencyData }); 245 | return currencyAcc; 246 | }, {}); 247 | Object.assign(acc, { [coinId]: coinPrices }); 248 | return acc; 249 | }, {}), 250 | params: { 251 | currencies: currencies.map(c => c.toUpperCase()), 252 | include_market_cap: content.include_market_cap, 253 | include_24hr_vol: content.include_24hr_vol, 254 | include_24hr_change: content.include_24hr_change, 255 | include_last_updated_at: content.include_last_updated_at 256 | } 257 | } 258 | }); 259 | } 260 | 261 | return true; 262 | } catch (error) { 263 | elizaLogger.error("Error in GET_PRICE handler:", error); 264 | 265 | let errorMessage: string; 266 | if (error.response?.status === 429) { 267 | errorMessage = "Rate limit exceeded. Please try again later."; 268 | } else if (error.response?.status === 403) { 269 | errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; 270 | } else if (error.response?.status === 400) { 271 | errorMessage = "Invalid request parameters. Please check your input."; 272 | } 273 | 274 | if (callback) { 275 | callback({ 276 | text: errorMessage, 277 | content: { 278 | error: error.message, 279 | statusCode: error.response?.status, 280 | params: error.config?.params, 281 | requiresProPlan: error.response?.status === 403 282 | }, 283 | }); 284 | } 285 | return false; 286 | } 287 | }, 288 | 289 | examples: [ 290 | [ 291 | { 292 | user: "{{user1}}", 293 | content: { 294 | text: "What's the current price of Bitcoin?", 295 | }, 296 | }, 297 | { 298 | user: "{{agent}}", 299 | content: { 300 | text: "I'll check the current Bitcoin price for you.", 301 | action: "GET_PRICE", 302 | }, 303 | }, 304 | { 305 | user: "{{agent}}", 306 | content: { 307 | text: "The current price of Bitcoin is {{dynamic}} USD", 308 | }, 309 | }, 310 | ], 311 | [ 312 | { 313 | user: "{{user1}}", 314 | content: { 315 | text: "Check ETH and BTC prices in EUR with market cap", 316 | }, 317 | }, 318 | { 319 | user: "{{agent}}", 320 | content: { 321 | text: "I'll check the current prices with market cap data.", 322 | action: "GET_PRICE", 323 | }, 324 | }, 325 | { 326 | user: "{{agent}}", 327 | content: { 328 | text: "Bitcoin: EUR {{dynamic}} | Market Cap: €{{dynamic}}\nEthereum: EUR {{dynamic}} | Market Cap: €{{dynamic}}", 329 | }, 330 | }, 331 | ], 332 | ] as ActionExample[][], 333 | } as Action; 334 | --------------------------------------------------------------------------------