├── examples ├── claude-desktop-config.json └── mcp.json ├── .github └── workflows │ └── publish.yml ├── package.json ├── gecko.md ├── src ├── constants.js ├── services │ ├── agService.js │ ├── coinGeckoApiService.js │ └── blockchainService.js ├── toolService.js └── index.js ├── .gitignore ├── loadCrypto.md ├── setup.js └── README.md /examples/claude-desktop-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "defi-trading-mcp": { 4 | "command": "npx", 5 | "args": ["defi-trading-mcp"], 6 | "env": { 7 | "USER_PRIVATE_KEY": "your_private_key_here", 8 | "USER_ADDRESS": "0xYourWalletAddress", 9 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 10 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '18' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install dependencies 21 | run: npm ci 22 | 23 | - name: Publish to NPM 24 | run: npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /examples/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "defi-trading-mcp": { 4 | "command": "npx", 5 | "args": ["defi-trading-mcp"], 6 | "env": { 7 | "USER_PRIVATE_KEY": "your_private_key_here", 8 | "USER_ADDRESS": "0xYourWalletAddress", 9 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 10 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 11 | }, 12 | "disabled": false, 13 | "autoApprove": [ 14 | "get_swap_price", 15 | "get_gasless_price", 16 | "get_portfolio_tokens", 17 | "get_portfolio_balances", 18 | "get_token_price", 19 | "get_supported_chains" 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "defi-trading-mcp", 4 | "version": "2.1.3", 5 | "description": "Transform your AI assistant into an autonomous crypto trading agent with real-time market analysis, portfolio management, and seamless trade execution across 17+ blockchains", 6 | "main": "src/index.js", 7 | "bin": { 8 | "defi-trading-mcp": "src/index.js" 9 | }, 10 | "files": [ 11 | "src/", 12 | "setup.js", 13 | ".env.example", 14 | "README.md" 15 | ], 16 | "homepage": "https://github.com/edkdev/defi-trading-mcp#readme", 17 | "bugs": { 18 | "url": "https://github.com/edkdev/defi-trading-mcp/issues" 19 | }, 20 | "scripts": { 21 | "start": "node src/index.js", 22 | "test": "node test.js", 23 | "setup": "node setup.js", 24 | "setup:dev": "node setup.js --dev" 25 | }, 26 | "keywords": [ 27 | "crypto-trading", 28 | "defi-trading", 29 | "trading-bot", 30 | "AI-agent", 31 | "memecoin-trader", 32 | "defi", 33 | "swap", 34 | "mcp", 35 | "protocol", 36 | "gasless", 37 | "portfolio", 38 | "ethereum", 39 | "blockchain" 40 | ], 41 | "author": "Ed", 42 | "license": "MIT", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/edkdev/defi-trading-mcp.git" 46 | }, 47 | "dependencies": { 48 | "@modelcontextprotocol/sdk": "^1.0.3", 49 | "dotenv": "^16.4.7", 50 | "ethers": "^6.15.0", 51 | "node-fetch": "^3.3.2" 52 | }, 53 | "engines": { 54 | "node": ">=18.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gecko.md: -------------------------------------------------------------------------------- 1 | # 🦎 How to Get Your CoinGecko API Key 2 | 3 | CoinGecko provides comprehensive cryptocurrency market data for the DeFi Trading Agent MCP. Follow these simple steps to get your free API key: 4 | 5 | ## 📋 **Step-by-Step Guide** 6 | 7 | ### **Step 1: Create a CoinGecko Account** 8 | 9 | 1. Visit [CoinGecko.com](https://www.coingecko.com/) 10 | 2. Click the **"Sign Up"** button in the top right corner 11 | 3. Complete the registration process. 12 | 13 | ### **Step 2: Access the Developer Dashboard** 14 | 15 | 1. Once logged in, navigate to the [Developer Dashboard](https://www.coingecko.com/developers/dashboard) 16 | 2. You'll see your API management interface 17 | 18 | ### **Step 3: Generate Your API Key** 19 | 20 | 1. Click **"Create API Key"** or **"Add New Key"** 21 | 2. Give your API key a descriptive name (e.g., "DeFi Trading") 22 | 3. Plan: 23 | - **Demo Plan**: Free, 30 calls/minute (good for starting out) 24 | 25 | ### **Step 4: Copy Your API Key** 26 | 27 | 1. Once generated, **copy your API key** 28 | 2. It will look something like: `CG-xxxxxxxxxxxxxxxxxxxxxxxxx` 29 | 3. **Keep it secure** - treat it like a password 30 | 31 | ## 🔧 **Using Your API Key** 32 | 33 | Add your CoinGecko API key to your MCP configuration: 34 | 35 | ```bash 36 | COINGECKO_API_KEY=CG-your_actual_api_key_here 37 | ``` 38 | 39 | ## 🔒 **Security Best Practices** 40 | 41 | - ✅ **Never share** your API key publicly 42 | - ✅ **Store securely** in environment variables 43 | - ✅ **Regenerate** if compromised 44 | - ✅ **Monitor usage** in the dashboard 45 | 46 | --- 47 | 48 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // src/constants.js 2 | export const TOOL_NAMES = { 3 | // Agg API Tools 4 | GET_SWAP_PRICE: "get_swap_price", 5 | GET_SWAP_QUOTE: "get_swap_quote", 6 | EXECUTE_SWAP: "execute_swap", 7 | GET_SUPPORTED_CHAINS: "get_supported_chains", 8 | GET_LIQUIDITY_SOURCES: "get_liquidity_sources", 9 | 10 | // Gasless Agg API Tools 11 | GET_GASLESS_PRICE: "get_gasless_price", 12 | GET_GASLESS_QUOTE: "get_gasless_quote", 13 | SUBMIT_GASLESS_SWAP: "submit_gasless_swap", 14 | GET_GASLESS_STATUS: "get_gasless_status", 15 | GET_GASLESS_CHAINS: "get_gasless_chains", 16 | GET_GASLESS_APPROVAL_TOKENS: "get_gasless_approval_tokens", 17 | 18 | // CoinGecko API Tools 19 | GET_TOKEN_PRICE: "get_token_price", 20 | GET_COINGECKO_NETWORKS: "get_coingecko_networks", 21 | GET_SUPPORTED_DEXES: "get_supported_dexes", 22 | GET_TRENDING_POOLS: "get_trending_pools", 23 | GET_TRENDING_POOLS_BY_NETWORK: "get_trending_pools_by_network", 24 | GET_MULTIPLE_POOLS_DATA: "get_multiple_pools_data", 25 | GET_TOP_POOLS_BY_DEX: "get_top_pools_by_dex", 26 | GET_NEW_POOLS: "get_new_pools", 27 | SEARCH_POOLS: "search_pools", 28 | 29 | // Additional CoinGecko API Tools 30 | GET_TOP_POOLS_BY_TOKEN: "get_top_pools_by_token", 31 | GET_TOKEN_DATA: "get_token_data", 32 | GET_MULTIPLE_TOKENS_DATA: "get_multiple_tokens_data", 33 | GET_TOKEN_INFO: "get_token_info", 34 | GET_RECENTLY_UPDATED_TOKENS: "get_recently_updated_tokens", 35 | GET_POOL_OHLCV: "get_pool_ohlcv", 36 | GET_POOL_TRADES: "get_pool_trades", 37 | 38 | // Portfolio API Tools 39 | GET_PORTFOLIO_TOKENS: "get_portfolio_tokens", 40 | GET_PORTFOLIO_BALANCES: "get_portfolio_balances", 41 | GET_PORTFOLIO_TRANSACTIONS: "get_portfolio_transactions", 42 | 43 | // Conversion Utility Tools 44 | CONVERT_WEI_TO_FORMATTED: "convert_wei_to_formatted", 45 | CONVERT_FORMATTED_TO_WEI: "convert_formatted_to_wei", 46 | }; 47 | 48 | // Aggregator Server Configuration 49 | export const AG_URL = "http://44.252.136.98"; 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables and secrets 2 | .env 3 | .env.local 4 | .env.development.local 5 | .env.test.local 6 | .env.production.local 7 | 8 | # Node.js dependencies 9 | node_modules/ 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage/ 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage 29 | .grunt 30 | 31 | # Bower dependency directory 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons 38 | build/Release 39 | 40 | # Dependency directories 41 | jspm_packages/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # parcel-bundler cache 62 | .cache 63 | .parcel-cache 64 | 65 | # Next.js build output 66 | .next 67 | 68 | # Nuxt.js build / generate output 69 | .nuxt 70 | dist 71 | 72 | # Gatsby files 73 | .cache/ 74 | public 75 | 76 | # Storybook build outputs 77 | .out 78 | .storybook-out 79 | 80 | # Temporary folders 81 | tmp/ 82 | temp/ 83 | 84 | # Logs 85 | logs 86 | *.log 87 | 88 | # Runtime data 89 | pids 90 | *.pid 91 | *.seed 92 | *.pid.lock 93 | 94 | # IDE and editor files 95 | .vscode/ 96 | .idea/ 97 | *.swp 98 | *.swo 99 | *~ 100 | 101 | # OS generated files 102 | .DS_Store 103 | .DS_Store? 104 | ._* 105 | .Spotlight-V100 106 | .Trashes 107 | ehthumbs.db 108 | Thumbs.db 109 | 110 | # Test files and coverage 111 | test-results/ 112 | coverage/ 113 | 114 | # Build artifacts 115 | build/ 116 | dist/ 117 | 118 | # Package manager lock files (keep package-lock.json but ignore others) 119 | yarn.lock 120 | pnpm-lock.yaml 121 | 122 | # MCP specific ignores 123 | examples/private/ 124 | test-private.js 125 | *.private.js 126 | 127 | # Test files 128 | test.js 129 | test-*.js 130 | simple-test.js 131 | *test*.js 132 | 133 | # Documentation files (keep main README.md) 134 | GASLESS_MCP_README.md 135 | *.txt 136 | coingeckoendpoints.txt 137 | coingeckoendpoints-2.txt 138 | 139 | # Backup files 140 | *.bak 141 | *.backup 142 | *.old 143 | 144 | # Local development files 145 | local/ 146 | dev/ 147 | scratch/ -------------------------------------------------------------------------------- /loadCrypto.md: -------------------------------------------------------------------------------- 1 | # 💰 How to Load Crypto into Your Wallet 2 | 3 | Before you can start trading with the DeFi Trading Agent, you'll need cryptocurrency in your wallet. Here are the most common ways to get started: 4 | 5 | ## 🏦 **Centralized Exchanges (CEX)** 6 | 7 | Purchase crypto from major exchanges and transfer to your wallet: 8 | 9 | ### **Popular Exchanges:** 10 | 11 | - **[Coinbase](https://coinbase.com)** - User-friendly, great for beginners 12 | - **[Binance](https://binance.com)** - Wide selection, competitive fees 13 | - **[Kraken](https://kraken.com)** - Advanced features, strong security 14 | 15 | ### **Steps:** 16 | 17 | 1. **Create account** and complete verification 18 | 2. **Buy cryptocurrency** using bank transfer, card, or wire 19 | 3. **Withdraw to your wallet** using the address from `--create-wallet` 20 | 21 | > ⚠️ **Important**: Always verify the **network/chain** when withdrawing (Ethereum, Base, Polygon, etc.) 22 | 23 | ## 💳 **Direct Wallet Purchases** 24 | 25 | Buy crypto directly within wallet applications: 26 | 27 | ### **Wallet Options:** 28 | 29 | - **[MetaMask](https://metamask.io)** - Built-in fiat-to-crypto conversion 30 | - **[Rainbow](https://rainbow.me)** - Simple mobile wallet with buying 31 | - **[Coinbase Wallet](https://wallet.coinbase.com)** - Direct purchase integration 32 | 33 | ### **Benefits:** 34 | 35 | - ✅ **Instant availability** - crypto goes directly to your wallet 36 | - ✅ **No transfer needed** - skip the withdrawal step 37 | - ✅ **Multiple payment methods** - card, bank, Apple Pay 38 | 39 | ## 🔄 **If You Already Have Crypto** 40 | 41 | ### **Option 1: Use Existing Wallet** 42 | 43 | If you can access your private key: 44 | 45 | 1. **Export private key** from your current wallet 46 | 2. **Add to MCP configuration** using the setup instructions 47 | 48 | ### **Option 2: Create New Wallet** 49 | 50 | For better security or organization: 51 | 52 | ```bash 53 | npx defi-trading-mcp --create-wallet 54 | ``` 55 | 56 | Then transfer your existing crypto to the new address. 57 | 58 | ## 💡 **Getting Started Tips** 59 | 60 | ### **Recommended Starting Amounts:** 61 | 62 | - **Testing**: $50-500 to learn the system 63 | - **Active Trading**: $500+ for meaningful opportunities 64 | - **Advanced Trading**: $1000+ for portfolio diversification 65 | 66 | ### **Multi-Chain Strategy:** 67 | 68 | - **Ethereum**: For established DeFi protocols 69 | - **Base**: For low-fee trading and new opportunities 70 | - **Polygon**: For high-frequency, low-cost trades 71 | - **Arbitrum**: For advanced DeFi strategies 72 | 73 | ## ⚠️ **Security Reminders** 74 | 75 | - ✅ **Double-check addresses** before sending crypto 76 | - ✅ **Verify the network** matches your intended chain 77 | - ✅ **Keep private keys secure** and never share them 78 | 79 | --- 80 | 81 | **Ready to trade?** Once you have crypto in your wallet, your DeFi Trading Agent can help you find opportunities, analyze markets, and execute trades across 17+ blockchain networks! 82 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import os from "os"; 6 | import { fileURLToPath } from "url"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | console.log("🚀 DeFi Trading MCP Server Setup\n"); 12 | 13 | // Check if running in development mode 14 | const isDev = process.argv.includes("--dev"); 15 | 16 | if (isDev) { 17 | console.log("📝 Setting up for local development...\n"); 18 | 19 | // Copy .env.example to .env if it doesn't exist 20 | const envExamplePath = path.join(__dirname, ".env.example"); 21 | const envPath = path.join(__dirname, ".env"); 22 | 23 | if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) { 24 | fs.copyFileSync(envExamplePath, envPath); 25 | console.log("✅ Created .env file from .env.example"); 26 | console.log( 27 | "📝 Please edit .env file with your actual API keys and private key\n" 28 | ); 29 | } else if (fs.existsSync(envPath)) { 30 | console.log("✅ .env file already exists\n"); 31 | } 32 | } else { 33 | console.log("📋 MCP Client Configuration Examples:\n"); 34 | 35 | console.log("🔧 For Kiro IDE (~/.kiro/settings/mcp.json):"); 36 | console.log( 37 | JSON.stringify( 38 | { 39 | mcpServers: { 40 | "defi-trading-mcp": { 41 | command: "npx", 42 | args: ["defi-trading-mcp"], 43 | env: { 44 | USER_PRIVATE_KEY: "your_private_key_here", 45 | USER_ADDRESS: "0xYourWalletAddress", 46 | COINGECKO_API_KEY: "CG-your_coingecko_api_key", 47 | ALCHEMY_API_KEY: "your_alchemy_api_key", 48 | }, 49 | }, 50 | }, 51 | }, 52 | null, 53 | 2 54 | ) 55 | ); 56 | 57 | console.log("\n🔧 For Claude Desktop (claude_desktop_config.json):"); 58 | console.log( 59 | JSON.stringify( 60 | { 61 | mcpServers: { 62 | "defi-trading-mcp": { 63 | command: "npx", 64 | args: ["defi-trading-mcp"], 65 | env: { 66 | USER_PRIVATE_KEY: "your_private_key_here", 67 | USER_ADDRESS: "0xYourWalletAddress", 68 | COINGECKO_API_KEY: "CG-your_coingecko_api_key", 69 | ALCHEMY_API_KEY: "your_alchemy_api_key" 70 | }, 71 | }, 72 | }, 73 | }, 74 | null, 75 | 2 76 | ) 77 | ); 78 | 79 | console.log("\n📋 Required Environment Variables:"); 80 | console.log("• USER_PRIVATE_KEY: Your Ethereum private key"); 81 | console.log("• USER_ADDRESS: Your Ethereum wallet address"); 82 | console.log("• COINGECKO_API_KEY: Your CoinGecko API key"); 83 | console.log("• ALCHEMY_API_KEY: Your Alchemy API key"); 84 | 85 | 86 | console.log("\n🔐 Security Note:"); 87 | console.log( 88 | "Your private key is only used locally for transaction signing and never sent to external servers." 89 | ); 90 | } 91 | 92 | console.log("\n📚 For more information, see the README.md file."); 93 | console.log( 94 | "🎯 Available tools: gasless swaps, portfolio tracking, DeFi data, and more!" 95 | ); 96 | -------------------------------------------------------------------------------- /src/services/agService.js: -------------------------------------------------------------------------------- 1 | // src/services/agService.js 2 | import fetch from 'node-fetch'; 3 | 4 | export class AgService { 5 | constructor(agUrl) { 6 | this.baseUrl = agUrl; 7 | } 8 | 9 | async getSwapPrice(params) { 10 | try { 11 | const queryParams = new URLSearchParams(params); 12 | const response = await fetch(`${this.baseUrl}/api/swap/price?${queryParams}`); 13 | 14 | if (!response.ok) { 15 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 16 | } 17 | 18 | const data = await response.json(); 19 | 20 | if (!data.success) { 21 | throw new Error(data.error || 'API request failed'); 22 | } 23 | 24 | return data.data; 25 | } catch (error) { 26 | throw new Error(`Failed to get swap price: ${error.message}`); 27 | } 28 | } 29 | 30 | async getSwapQuote(params) { 31 | try { 32 | const queryParams = new URLSearchParams(params); 33 | const response = await fetch(`${this.baseUrl}/api/swap/quote?${queryParams}`); 34 | 35 | if (!response.ok) { 36 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 37 | } 38 | 39 | const data = await response.json(); 40 | 41 | if (!data.success) { 42 | throw new Error(data.error || 'API request failed'); 43 | } 44 | 45 | return data.data; 46 | } catch (error) { 47 | throw new Error(`Failed to get swap quote: ${error.message}`); 48 | } 49 | } 50 | 51 | async executeSwap(swapData) { 52 | try { 53 | const response = await fetch(`${this.baseUrl}/api/swap/execute`, { 54 | method: 'POST', 55 | headers: { 56 | 'Content-Type': 'application/json' 57 | }, 58 | body: JSON.stringify(swapData) 59 | }); 60 | 61 | if (!response.ok) { 62 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 63 | } 64 | 65 | const data = await response.json(); 66 | 67 | if (!data.success) { 68 | throw new Error(data.error || 'Swap execution failed'); 69 | } 70 | 71 | return data.data; 72 | } catch (error) { 73 | throw new Error(`Failed to execute swap: ${error.message}`); 74 | } 75 | } 76 | 77 | async getSupportedChains() { 78 | try { 79 | const response = await fetch(`${this.baseUrl}/api/swap/chains`); 80 | 81 | if (!response.ok) { 82 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 83 | } 84 | 85 | const data = await response.json(); 86 | 87 | if (!data.success) { 88 | throw new Error(data.error || 'API request failed'); 89 | } 90 | 91 | return data.data; 92 | } catch (error) { 93 | throw new Error(`Failed to get supported chains: ${error.message}`); 94 | } 95 | } 96 | 97 | async getLiquiditySources(chainId) { 98 | try { 99 | const response = await fetch(`${this.baseUrl}/api/swap/sources?chainId=${chainId}`); 100 | 101 | if (!response.ok) { 102 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 103 | } 104 | 105 | const data = await response.json(); 106 | 107 | if (!data.success) { 108 | throw new Error(data.error || 'API request failed'); 109 | } 110 | 111 | return data.data; 112 | } catch (error) { 113 | throw new Error(`Failed to get liquidity sources: ${error.message}`); 114 | } 115 | } 116 | 117 | // Gasless API Methods 118 | async getGaslessPrice(params) { 119 | try { 120 | const queryParams = new URLSearchParams(params); 121 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/price?${queryParams}`); 122 | 123 | if (!response.ok) { 124 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 125 | } 126 | 127 | const data = await response.json(); 128 | 129 | if (!data.success) { 130 | throw new Error(data.error || 'Gasless price request failed'); 131 | } 132 | 133 | return data.data; 134 | } catch (error) { 135 | throw new Error(`Failed to get gasless price: ${error.message}`); 136 | } 137 | } 138 | 139 | async getGaslessQuote(params) { 140 | try { 141 | const queryParams = new URLSearchParams(params); 142 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/quote?${queryParams}`); 143 | 144 | if (!response.ok) { 145 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 146 | } 147 | 148 | const data = await response.json(); 149 | 150 | if (!data.success) { 151 | throw new Error(data.error || 'Gasless quote request failed'); 152 | } 153 | 154 | return data.data; 155 | } catch (error) { 156 | throw new Error(`Failed to get gasless quote: ${error.message}`); 157 | } 158 | } 159 | 160 | async submitGaslessSwap(swapData) { 161 | try { 162 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/submit`, { 163 | method: 'POST', 164 | headers: { 165 | 'Content-Type': 'application/json' 166 | }, 167 | body: JSON.stringify(swapData) 168 | }); 169 | 170 | if (!response.ok) { 171 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 172 | } 173 | 174 | const data = await response.json(); 175 | 176 | if (!data.success) { 177 | throw new Error(data.error || 'Gasless swap submission failed'); 178 | } 179 | 180 | return data.data; 181 | } catch (error) { 182 | throw new Error(`Failed to submit gasless swap: ${error.message}`); 183 | } 184 | } 185 | 186 | async getGaslessStatus(tradeHash, chainId) { 187 | try { 188 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/status/${tradeHash}?chainId=${chainId}`); 189 | 190 | if (!response.ok) { 191 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 192 | } 193 | 194 | const data = await response.json(); 195 | 196 | if (!data.success) { 197 | throw new Error(data.error || 'Gasless status request failed'); 198 | } 199 | 200 | return data.data; 201 | } catch (error) { 202 | throw new Error(`Failed to get gasless status: ${error.message}`); 203 | } 204 | } 205 | 206 | async getGaslessChains() { 207 | try { 208 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/chains`); 209 | 210 | if (!response.ok) { 211 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 212 | } 213 | 214 | const data = await response.json(); 215 | 216 | if (!data.success) { 217 | throw new Error(data.error || 'Gasless chains request failed'); 218 | } 219 | 220 | return data.data; 221 | } catch (error) { 222 | throw new Error(`Failed to get gasless chains: ${error.message}`); 223 | } 224 | } 225 | 226 | async getGaslessApprovalTokens(chainId) { 227 | try { 228 | const response = await fetch(`${this.baseUrl}/api/swap/gasless/approval-tokens?chainId=${chainId}`); 229 | 230 | if (!response.ok) { 231 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 232 | } 233 | 234 | const data = await response.json(); 235 | 236 | if (!data.success) { 237 | throw new Error(data.error || 'Gasless approval tokens request failed'); 238 | } 239 | 240 | return data.data; 241 | } catch (error) { 242 | throw new Error(`Failed to get gasless approval tokens: ${error.message}`); 243 | } 244 | } 245 | 246 | // Portfolio API Methods 247 | async getPortfolioTokens(addresses, options = {}) { 248 | try { 249 | const requestBody = { 250 | addresses, 251 | withMetadata: options.withMetadata, 252 | withPrices: options.withPrices, 253 | includeNativeTokens: options.includeNativeTokens 254 | }; 255 | 256 | const response = await fetch(`${this.baseUrl}/api/portfolio/tokens`, { 257 | method: 'POST', 258 | headers: { 259 | 'Content-Type': 'application/json' 260 | }, 261 | body: JSON.stringify(requestBody) 262 | }); 263 | 264 | if (!response.ok) { 265 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 266 | } 267 | 268 | const data = await response.json(); 269 | 270 | if (!data.success) { 271 | throw new Error(data.error || 'Portfolio tokens request failed'); 272 | } 273 | 274 | return data.data; 275 | } catch (error) { 276 | throw new Error(`Failed to get portfolio tokens: ${error.message}`); 277 | } 278 | } 279 | 280 | async getPortfolioBalances(addresses, options = {}) { 281 | try { 282 | const requestBody = { 283 | addresses, 284 | includeNativeTokens: options.includeNativeTokens 285 | }; 286 | 287 | const response = await fetch(`${this.baseUrl}/api/portfolio/balances`, { 288 | method: 'POST', 289 | headers: { 290 | 'Content-Type': 'application/json' 291 | }, 292 | body: JSON.stringify(requestBody) 293 | }); 294 | 295 | if (!response.ok) { 296 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 297 | } 298 | 299 | const data = await response.json(); 300 | 301 | if (!data.success) { 302 | throw new Error(data.error || 'Portfolio balances request failed'); 303 | } 304 | 305 | return data.data; 306 | } catch (error) { 307 | throw new Error(`Failed to get portfolio balances: ${error.message}`); 308 | } 309 | } 310 | 311 | async getPortfolioTransactions(addresses, options = {}) { 312 | try { 313 | const requestBody = { 314 | addresses, 315 | before: options.before, 316 | after: options.after, 317 | limit: options.limit 318 | }; 319 | 320 | // Remove undefined values 321 | Object.keys(requestBody).forEach(key => { 322 | if (requestBody[key] === undefined) { 323 | delete requestBody[key]; 324 | } 325 | }); 326 | 327 | const response = await fetch(`${this.baseUrl}/api/portfolio/transactions`, { 328 | method: 'POST', 329 | headers: { 330 | 'Content-Type': 'application/json' 331 | }, 332 | body: JSON.stringify(requestBody) 333 | }); 334 | 335 | if (!response.ok) { 336 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 337 | } 338 | 339 | const data = await response.json(); 340 | 341 | if (!data.success) { 342 | throw new Error(data.error || 'Portfolio transactions request failed'); 343 | } 344 | 345 | return data.data; 346 | } catch (error) { 347 | throw new Error(`Failed to get portfolio transactions: ${error.message}`); 348 | } 349 | } 350 | } -------------------------------------------------------------------------------- /src/services/coinGeckoApiService.js: -------------------------------------------------------------------------------- 1 | // src/services/coinGeckoApiService.js 2 | import fetch from 'node-fetch'; 3 | 4 | export class CoinGeckoApiService { 5 | constructor(apiKey) { 6 | this.baseUrl = 'https://api.coingecko.com/api/v3/onchain'; 7 | this.apiKey = apiKey; 8 | } 9 | 10 | async getTokenPrice(network, addresses, options = {}) { 11 | try { 12 | const queryParams = new URLSearchParams(); 13 | 14 | // Add optional parameters 15 | if (options.include_market_cap) queryParams.append('include_market_cap', options.include_market_cap); 16 | if (options.mcap_fdv_fallback) queryParams.append('mcap_fdv_fallback', options.mcap_fdv_fallback); 17 | if (options.include_24hr_vol) queryParams.append('include_24hr_vol', options.include_24hr_vol); 18 | if (options.include_24hr_price_change) queryParams.append('include_24hr_price_change', options.include_24hr_price_change); 19 | if (options.include_total_reserve_in_usd) queryParams.append('include_total_reserve_in_usd', options.include_total_reserve_in_usd); 20 | 21 | const url = `${this.baseUrl}/simple/networks/${network}/token_price/${addresses}${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 22 | 23 | const response = await fetch(url, { 24 | headers: { 25 | 'x-cg-demo-api-key': this.apiKey 26 | } 27 | }); 28 | 29 | if (!response.ok) { 30 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 31 | } 32 | 33 | return await response.json(); 34 | } catch (error) { 35 | throw new Error(`Failed to get token price: ${error.message}`); 36 | } 37 | } 38 | 39 | async getNetworks(page = 1) { 40 | try { 41 | const queryParams = new URLSearchParams(); 42 | if (page) queryParams.append('page', page); 43 | 44 | const url = `${this.baseUrl}/networks${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 45 | 46 | const response = await fetch(url, { 47 | headers: { 48 | 'x-cg-demo-api-key': this.apiKey 49 | } 50 | }); 51 | 52 | if (!response.ok) { 53 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 54 | } 55 | 56 | return await response.json(); 57 | } catch (error) { 58 | throw new Error(`Failed to get networks: ${error.message}`); 59 | } 60 | } 61 | 62 | async getSupportedDexes(network, page = 1) { 63 | try { 64 | const queryParams = new URLSearchParams(); 65 | if (page) queryParams.append('page', page); 66 | 67 | const url = `${this.baseUrl}/networks/${network}/dexes${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 68 | 69 | const response = await fetch(url, { 70 | headers: { 71 | 'x-cg-demo-api-key': this.apiKey 72 | } 73 | }); 74 | 75 | if (!response.ok) { 76 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 77 | } 78 | 79 | return await response.json(); 80 | } catch (error) { 81 | throw new Error(`Failed to get supported DEXes: ${error.message}`); 82 | } 83 | } 84 | 85 | async getTrendingPools(options = {}) { 86 | try { 87 | const queryParams = new URLSearchParams(); 88 | 89 | if (options.include) queryParams.append('include', options.include); 90 | if (options.page) queryParams.append('page', options.page); 91 | if (options.duration) queryParams.append('duration', options.duration); 92 | 93 | const url = `${this.baseUrl}/networks/trending_pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 94 | 95 | const response = await fetch(url, { 96 | headers: { 97 | 'x-cg-demo-api-key': this.apiKey 98 | } 99 | }); 100 | 101 | if (!response.ok) { 102 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 103 | } 104 | 105 | return await response.json(); 106 | } catch (error) { 107 | throw new Error(`Failed to get trending pools: ${error.message}`); 108 | } 109 | } 110 | 111 | async getTrendingPoolsByNetwork(network, options = {}) { 112 | try { 113 | const queryParams = new URLSearchParams(); 114 | 115 | if (options.include) queryParams.append('include', options.include); 116 | if (options.page) queryParams.append('page', options.page); 117 | if (options.duration) queryParams.append('duration', options.duration); 118 | 119 | const url = `${this.baseUrl}/networks/${network}/trending_pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 120 | 121 | const response = await fetch(url, { 122 | headers: { 123 | 'x-cg-demo-api-key': this.apiKey 124 | } 125 | }); 126 | 127 | if (!response.ok) { 128 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 129 | } 130 | 131 | return await response.json(); 132 | } catch (error) { 133 | throw new Error(`Failed to get trending pools by network: ${error.message}`); 134 | } 135 | } 136 | 137 | async getMultiplePoolsData(network, addresses, options = {}) { 138 | try { 139 | const queryParams = new URLSearchParams(); 140 | 141 | if (options.include) queryParams.append('include', options.include); 142 | if (options.include_volume_breakdown) queryParams.append('include_volume_breakdown', options.include_volume_breakdown); 143 | 144 | const url = `${this.baseUrl}/networks/${network}/pools/multi/${addresses}${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 145 | 146 | const response = await fetch(url, { 147 | headers: { 148 | 'x-cg-demo-api-key': this.apiKey 149 | } 150 | }); 151 | 152 | if (!response.ok) { 153 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 154 | } 155 | 156 | return await response.json(); 157 | } catch (error) { 158 | throw new Error(`Failed to get multiple pools data: ${error.message}`); 159 | } 160 | } 161 | 162 | async getTopPoolsByDex(network, dex, options = {}) { 163 | try { 164 | const queryParams = new URLSearchParams(); 165 | 166 | if (options.include) queryParams.append('include', options.include); 167 | if (options.page) queryParams.append('page', options.page); 168 | if (options.sort) queryParams.append('sort', options.sort); 169 | 170 | const url = `${this.baseUrl}/networks/${network}/dexes/${dex}/pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 171 | 172 | const response = await fetch(url, { 173 | headers: { 174 | 'x-cg-demo-api-key': this.apiKey 175 | } 176 | }); 177 | 178 | if (!response.ok) { 179 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 180 | } 181 | 182 | return await response.json(); 183 | } catch (error) { 184 | throw new Error(`Failed to get top pools by DEX: ${error.message}`); 185 | } 186 | } 187 | 188 | async getNewPools(options = {}) { 189 | try { 190 | const queryParams = new URLSearchParams(); 191 | 192 | if (options.include) queryParams.append('include', options.include); 193 | if (options.page) queryParams.append('page', options.page); 194 | 195 | const url = `${this.baseUrl}/networks/new_pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 196 | 197 | const response = await fetch(url, { 198 | headers: { 199 | 'x-cg-demo-api-key': this.apiKey 200 | } 201 | }); 202 | 203 | if (!response.ok) { 204 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 205 | } 206 | 207 | return await response.json(); 208 | } catch (error) { 209 | throw new Error(`Failed to get new pools: ${error.message}`); 210 | } 211 | } 212 | 213 | async searchPools(query, options = {}) { 214 | try { 215 | const queryParams = new URLSearchParams(); 216 | 217 | if (query) queryParams.append('query', query); 218 | if (options.network) queryParams.append('network', options.network); 219 | if (options.include) queryParams.append('include', options.include); 220 | if (options.page) queryParams.append('page', options.page); 221 | 222 | const url = `${this.baseUrl}/search/pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 223 | 224 | const response = await fetch(url, { 225 | headers: { 226 | 'x-cg-demo-api-key': this.apiKey 227 | } 228 | }); 229 | 230 | if (!response.ok) { 231 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 232 | } 233 | 234 | return await response.json(); 235 | } catch (error) { 236 | throw new Error(`Failed to search pools: ${error.message}`); 237 | } 238 | } 239 | 240 | // Additional endpoints from coingeckoendpoints-2.txt 241 | async getTopPoolsByToken(network, tokenAddress, options = {}) { 242 | try { 243 | const queryParams = new URLSearchParams(); 244 | 245 | if (options.include) queryParams.append('include', options.include); 246 | if (options.page) queryParams.append('page', options.page); 247 | if (options.sort) queryParams.append('sort', options.sort); 248 | 249 | const url = `${this.baseUrl}/networks/${network}/tokens/${tokenAddress}/pools${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 250 | 251 | const response = await fetch(url, { 252 | headers: { 253 | 'x-cg-demo-api-key': this.apiKey 254 | } 255 | }); 256 | 257 | if (!response.ok) { 258 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 259 | } 260 | 261 | return await response.json(); 262 | } catch (error) { 263 | throw new Error(`Failed to get top pools by token: ${error.message}`); 264 | } 265 | } 266 | 267 | async getTokenData(network, address, options = {}) { 268 | try { 269 | const queryParams = new URLSearchParams(); 270 | 271 | if (options.include) queryParams.append('include', options.include); 272 | 273 | const url = `${this.baseUrl}/networks/${network}/tokens/${address}${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 274 | 275 | const response = await fetch(url, { 276 | headers: { 277 | 'x-cg-demo-api-key': this.apiKey 278 | } 279 | }); 280 | 281 | if (!response.ok) { 282 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 283 | } 284 | 285 | return await response.json(); 286 | } catch (error) { 287 | throw new Error(`Failed to get token data: ${error.message}`); 288 | } 289 | } 290 | 291 | async getMultipleTokensData(network, addresses, options = {}) { 292 | try { 293 | const queryParams = new URLSearchParams(); 294 | 295 | if (options.include) queryParams.append('include', options.include); 296 | 297 | const url = `${this.baseUrl}/networks/${network}/tokens/multi/${addresses}${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 298 | 299 | const response = await fetch(url, { 300 | headers: { 301 | 'x-cg-demo-api-key': this.apiKey 302 | } 303 | }); 304 | 305 | if (!response.ok) { 306 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 307 | } 308 | 309 | return await response.json(); 310 | } catch (error) { 311 | throw new Error(`Failed to get multiple tokens data: ${error.message}`); 312 | } 313 | } 314 | 315 | async getTokenInfo(network, address) { 316 | try { 317 | const url = `${this.baseUrl}/networks/${network}/tokens/${address}/info`; 318 | 319 | const response = await fetch(url, { 320 | headers: { 321 | 'x-cg-demo-api-key': this.apiKey 322 | } 323 | }); 324 | 325 | if (!response.ok) { 326 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 327 | } 328 | 329 | return await response.json(); 330 | } catch (error) { 331 | throw new Error(`Failed to get token info: ${error.message}`); 332 | } 333 | } 334 | 335 | async getRecentlyUpdatedTokens(options = {}) { 336 | try { 337 | const queryParams = new URLSearchParams(); 338 | 339 | if (options.include) queryParams.append('include', options.include); 340 | if (options.network) queryParams.append('network', options.network); 341 | 342 | const url = `${this.baseUrl}/tokens/info_recently_updated${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 343 | 344 | const response = await fetch(url, { 345 | headers: { 346 | 'x-cg-demo-api-key': this.apiKey 347 | } 348 | }); 349 | 350 | if (!response.ok) { 351 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 352 | } 353 | 354 | return await response.json(); 355 | } catch (error) { 356 | throw new Error(`Failed to get recently updated tokens: ${error.message}`); 357 | } 358 | } 359 | 360 | async getPoolOHLCV(network, poolAddress, timeframe, options = {}) { 361 | try { 362 | const queryParams = new URLSearchParams(); 363 | 364 | if (options.aggregate) queryParams.append('aggregate', options.aggregate); 365 | if (options.before_timestamp) queryParams.append('before_timestamp', options.before_timestamp); 366 | if (options.limit) queryParams.append('limit', options.limit); 367 | if (options.currency) queryParams.append('currency', options.currency); 368 | if (options.token) queryParams.append('token', options.token); 369 | if (options.include_empty_intervals) queryParams.append('include_empty_intervals', options.include_empty_intervals); 370 | 371 | const url = `${this.baseUrl}/networks/${network}/pools/${poolAddress}/ohlcv/${timeframe}${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 372 | 373 | const response = await fetch(url, { 374 | headers: { 375 | 'x-cg-demo-api-key': this.apiKey 376 | } 377 | }); 378 | 379 | if (!response.ok) { 380 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 381 | } 382 | 383 | return await response.json(); 384 | } catch (error) { 385 | throw new Error(`Failed to get pool OHLCV: ${error.message}`); 386 | } 387 | } 388 | 389 | async getPoolTrades(network, poolAddress, options = {}) { 390 | try { 391 | const queryParams = new URLSearchParams(); 392 | 393 | if (options.trade_volume_in_usd_greater_than) queryParams.append('trade_volume_in_usd_greater_than', options.trade_volume_in_usd_greater_than); 394 | 395 | const url = `${this.baseUrl}/networks/${network}/pools/${poolAddress}/trades${queryParams.toString() ? '?' + queryParams.toString() : ''}`; 396 | 397 | const response = await fetch(url, { 398 | headers: { 399 | 'x-cg-demo-api-key': this.apiKey 400 | } 401 | }); 402 | 403 | if (!response.ok) { 404 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 405 | } 406 | 407 | return await response.json(); 408 | } catch (error) { 409 | throw new Error(`Failed to get pool trades: ${error.message}`); 410 | } 411 | } 412 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeFi Trading Agent MCP Server 2 | 3 | Transform your AI assistant into an autonomous crypto trading agent with real-time market analysis, portfolio management, and seamless trade execution across 17+ blockchains. 4 | 5 | ## 🎯 Starting Prompt Examples 6 | 7 | ### **Simple Quote** 8 | 9 | ``` 10 | Get me a quote for 0.1 eth to usdc on Base chain. 11 | ``` 12 | 13 | ### **Quote and Swap** 14 | 15 | ``` 16 | Get me a quote for 0.1 eth on ethereum chain and execute the swap. 17 | ``` 18 | 19 | ### **Memecoin Opportunity Scanner** 20 | 21 | ``` 22 | "Scan for newly launched memecoins on Base with >$100K liquidity, pick one or two tokens and analyze the best entry opportunities" 23 | ``` 24 | 25 | **Advanced Analysis Process:** 26 | 27 | 1. **Discovery Phase**: Uses `get_new_pools` to find tokens launched in last 24h 28 | 2. **Volume Filtering**: Identifies pools with >$100K liquidity and high trading activity 29 | 3. **Technical Analysis**: Pulls OHLCV data to analyze price patterns and momentum 30 | 4. **Risk Assessment**: Evaluates liquidity depth, holder concentration, and volatility 31 | 5. **Entry Strategy**: Determines optimal entry price, position size, and risk management 32 | 6. **Execution**: Places gasless swap with calculated slippage and stop-loss levels 33 | 34 | **Example AI Analysis:** 35 | 36 | ``` 37 | "Found 3 promising new tokens: 38 | 🚀 $ROCKET (0x123...): 2M volume, bullish OHLCV pattern, 85% liquidity locked 39 | 📈 Entry: $0.0001 (current support level) 40 | 💰 Size: 2% portfolio allocation 41 | 🛡️ Stop: $0.000085 (-15%) 42 | 🎯 Target: $0.00015 (+50%) 43 | Executing gasless swap now..." 44 | ``` 45 | 46 | ### **Risk Management Agent** 47 | 48 | ``` 49 | "Monitor my portfolio and alert me if any position drops more than 15%" 50 | ``` 51 | 52 | **Agent Actions:** 53 | 54 | 1. Continuously monitors portfolio values 55 | 2. Calculates position changes 56 | 3. Provides alerts and recommendations 57 | 4. Can execute protective trades 58 | 59 | ## 🚀 Quick Start 60 | 61 | ### **Installation** 62 | 63 | ```bash 64 | npm install -g defi-trading-mcp 65 | ``` 66 | 67 | ### **Create a New Wallet (Recommended)** 68 | 69 | ```bash 70 | npx defi-trading-mcp --create-wallet 71 | ``` 72 | 73 | This generates a new wallet with private key and address for secure trading. 74 | 75 | > 💰 **Need crypto?** See our guide: [How to Load Crypto into Your Wallet](./loadCrypto.md) 76 | 77 | ## ⚙️ Configuration 78 | 79 | ### **Required Keys** 80 | 81 | - `USER_PRIVATE_KEY`: Your private key (for signing transactions locally, stays local, never transmitted) 82 | - `USER_ADDRESS`: Your Ethereum wallet address 83 | - `COINGECKO_API_KEY`: CoinGecko API key for market data ([How to get your CoinGecko API key](./gecko.md)) 84 | 85 | ### **Optional Configuration** 86 | 87 | - `ALCHEMY_API_KEY`: Add an Alchemy API key to use your own RPCs, otherwise public rpcs will be used. 88 | 89 | ### **🔧 Premium RPC Integration** 90 | 91 | Your `ALCHEMY_API_KEY` automatically enables premium RPCs for: 92 | 93 | - **15 Major Chains**: Base, Polygon, Arbitrum, Optimism, BSC, Avalanche, Worldchain, Berachain, Blast, Linea, Scroll, Mantle, Ink, MonadTestnet 94 | - **Enhanced Performance**: Lower latency, better uptime 95 | - **Automatic Fallback**: Public RPCs for other chains 96 | 97 | ### **🔒 Security** 98 | 99 | - Private keys remain on your device 100 | - No sensitive data transmitted to external servers 101 | - Secure transaction signing locally 102 | 103 | ### **MEV Protection** 104 | 105 | - **Ethereum transactions** are protected from MEV attacks, sandwich attacks, and front-running 106 | - **Private mempool routing** ensures your trades aren't visible to MEV bots 107 | - **Fair pricing** without manipulation from malicious actors 108 | - **Automatic protection** - no additional configuration required 109 | 110 | ## 🔧 MCP Client Setup 111 | 112 | ### **Kiro IDE** 113 | 114 | **Step 1: Install the MCP** 115 | 116 | ```bash 117 | npm install -g defi-trading-mcp 118 | ``` 119 | 120 | Add to `~/.kiro/settings/mcp.json`: 121 | 122 | ```json 123 | { 124 | "mcpServers": { 125 | "defi-trading": { 126 | "command": "npx", 127 | "args": ["defi-trading-mcp"], 128 | "env": { 129 | "USER_PRIVATE_KEY": "your_private_key_here", 130 | "USER_ADDRESS": "0xYourWalletAddress", 131 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 132 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 133 | } 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | ### **Claude Code** 140 | 141 | Add the MCP to Claude Code using the command line: 142 | 143 | **Step 1: Install the MCP** 144 | 145 | ```bash 146 | npm install -g defi-trading-mcp 147 | ``` 148 | 149 | **Step 2: Add to Claude Code - Replace the placeholders with your environment variables** 150 | 151 | **For macOS/Linux/WSL:** 152 | 153 | ```bash 154 | claude mcp add defi-trading \ 155 | -e USER_PRIVATE_KEY=your_private_key_here \ 156 | -e USER_ADDRESS=0xYourWalletAddress \ 157 | -e COINGECKO_API_KEY=CG-your_coingecko_api_key \ 158 | -e ALCHEMY_API_KEY=your_alchemy_api_key \ 159 | -- npx defi-trading-mcp 160 | ``` 161 | 162 | **For Windows (native, not WSL):** 163 | 164 | ```bash 165 | claude mcp add defi-trading \ 166 | -e USER_PRIVATE_KEY=your_private_key_here \ 167 | -e USER_ADDRESS=0xYourWalletAddress \ 168 | -e COINGECKO_API_KEY=CG-your_coingecko_api_key \ 169 | -e ALCHEMY_API_KEY=your_alchemy_api_key \ 170 | -- cmd /c npx defi-trading-mcp 171 | ``` 172 | 173 | > **Windows Note**: The `cmd /c` wrapper is required on native Windows to prevent "Connection closed" errors when using npx. 174 | 175 | **Step 3: Verify the MCP is added** 176 | 177 | ```bash 178 | claude mcp list 179 | ``` 180 | 181 | **Step 4: Update wallet details (if needed)** 182 | If you need to update your private key or wallet address after initial setup: 183 | 184 | ```bash 185 | # Remove existing configuration 186 | claude mcp remove defi-trading 187 | 188 | # Add back with updated wallet details 189 | claude mcp add defi-trading \ 190 | -e USER_PRIVATE_KEY=your_new_private_key \ 191 | -e USER_ADDRESS=0xYourNewWalletAddress \ 192 | -e COINGECKO_API_KEY=CG-your_coingecko_api_key \ 193 | -e ALCHEMY_API_KEY=your_alchemy_api_key \ 194 | -- npx defi-trading-mcp 195 | ``` 196 | 197 | **Step 5: Start using the trading agent** 198 | Open Claude Code and start trading. 199 | Example Prompt: 200 | 201 | ``` 202 | "Check my portfolio across all chains and find trending memecoins on Base" 203 | ``` 204 | 205 | ### **Claude Desktop** 206 | 207 | **Step 1: Install the MCP** 208 | 209 | ```bash 210 | npm install -g defi-trading-mcp 211 | ``` 212 | 213 | Open Claude Desktop. 214 | Click the top left menu with the three dashes. 215 | Click Developer. 216 | Then click Open App Config File. 217 | Your config file will open. 218 | Then add the following. 219 | 220 | ```json 221 | { 222 | "mcpServers": { 223 | "defi-trading": { 224 | "command": "npx", 225 | "args": ["defi-trading-mcp"], 226 | "env": { 227 | "USER_PRIVATE_KEY": "your_private_key_here", 228 | "USER_ADDRESS": "0xYourWalletAddress", 229 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 230 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 231 | } 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | ### **Cursor** 238 | 239 | **Quick Setup (2 Steps)** 240 | 241 | **Step 1: Install the package** 242 | ```bash 243 | npm install -g defi-trading-mcp 244 | ``` 245 | 246 | **Step 2: Add to Cursor (One-Click)** 247 | 248 | Click the button below to automatically configure the MCP in Cursor: 249 | 250 | [![Add DeFi Trading MCP to Cursor](https://img.shields.io/badge/Add%20to%20Cursor-Configure%20MCP-blue?style=for-the-badge&logo=cursor)](cursor://anysphere.cursor-deeplink/mcp/install?name=defi-trading&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJkZWZpLXRyYWRpbmctbWNwIl0sImVudiI6eyJVU0VSX1BSSVZBVEVfS0VZIjoieW91cl9wcml2YXRlX2tleV9oZXJlIiwiVVNFUl9BRERSRVNTIjoiMHhZb3VyV2FsbGV0QWRkcmVzcyIsIkNPSU5HRUNLT19BUElfS0VZIjoiQ0cteW91cl9jb2luZ2Vja29fYXBpX2tleSIsIkFMQ0hFTVlfQVBJX0tFWSI6InlvdXJfYWxjaGVteV9hcGlfa2V5In19) 251 | 252 | > **Important**: The deeplink only configures Cursor - you must install the npm package first! 253 | 254 | **Manual Setup** 255 | 256 | **Step 1: Install the MCP** 257 | 258 | ```bash 259 | npm install -g defi-trading-mcp 260 | ``` 261 | 262 | **Step 2: Add to Cursor Configuration** 263 | 264 | 1. Open Cursor 265 | 2. Go to **Settings** → **Extensions** → **MCP Servers** 266 | 3. Add a new server with the following configuration: 267 | 268 | ```json 269 | { 270 | "defi-trading": { 271 | "command": "npx", 272 | "args": ["defi-trading-mcp"], 273 | "env": { 274 | "USER_PRIVATE_KEY": "your_private_key_here", 275 | "USER_ADDRESS": "0xYourWalletAddress", 276 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 277 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | **Step 3: Configure Environment Variables** 284 | 285 | Replace the placeholder values with your actual credentials: 286 | 287 | - `USER_PRIVATE_KEY`: Your wallet's private key 288 | - `USER_ADDRESS`: Your wallet address 289 | - `COINGECKO_API_KEY`: Your CoinGecko API key ([Get one here](./gecko.md)) 290 | - `ALCHEMY_API_KEY`: Your Alchemy API key (optional) 291 | 292 | **Step 4: Start Trading** 293 | 294 | Open Cursor and start using the DeFi Trading Agent: 295 | 296 | ``` 297 | "Get me a quote for 0.1 ETH to USDC on Base chain" 298 | ``` 299 | 300 | ### Other MCP Clients 301 | 302 | For other MCP clients like Github Copilot(mcp.json), Gemini Cli (settings.json), find equivalent file and use the same pattern with environment variables: 303 | 304 | ```json 305 | { 306 | "mcpServers": { 307 | "defi-trading": { 308 | "command": "npx", 309 | "args": ["defi-trading-mcp"], 310 | "env": { 311 | "USER_PRIVATE_KEY": "your_private_key_here", 312 | "USER_ADDRESS": "0xYourWalletAddress", 313 | "COINGECKO_API_KEY": "CG-your_coingecko_api_key", 314 | "ALCHEMY_API_KEY": "your_alchemy_api_key" 315 | } 316 | } 317 | } 318 | } 319 | ``` 320 | 321 | ## 🤖 Trading Agent Capabilities 322 | 323 | ### **Autonomous Portfolio Management** 324 | ### **Intelligent Market Analysis** 325 | ### **Advanced Trade Execution** 326 | ### **Risk Management & Security** 327 | 328 | ## 🛠️ Trading Agent Tools 329 | 330 | ### **Portfolio Management** 331 | 332 | - `get_portfolio_tokens` - Multi-chain portfolio analysis with prices and metadata 333 | - `get_portfolio_balances` - Fast balance checking across all chains 334 | - `get_portfolio_transactions` - Complete transaction history analysis 335 | 336 | ### **Market Intelligence & Analysis** 337 | 338 | - `get_trending_pools` - Identify hot trading opportunities with volume metrics 339 | - `get_new_pools` - Discover newly launched tokens and liquidity pools 340 | - `get_pool_ohlcv` - Technical analysis with OHLCV candlestick data 341 | - `get_pool_trades` - Analyze recent trading activity and whale movements 342 | - `get_token_price` - Real-time pricing with 24h change indicators 343 | - `get_token_data` - Deep token research with metadata and social links 344 | - `get_token_info` - Comprehensive token analysis including descriptions 345 | - `search_pools` - Find specific pools by token symbol or contract address 346 | 347 | ### **Smart Trading** 348 | 349 | - `get_swap_price` - Get best prices across all DEXes 350 | - `get_swap_quote` - Get executable quotes with transaction data 351 | - `execute_swap` - Execute trades with optimal routing 352 | - `get_supported_chains` - List all 17+ supported blockchains 353 | 354 | ### **Gasless Trading** 355 | 356 | - `get_gasless_price` - Get prices for gas-free trades 357 | - `get_gasless_quote` - Get gasless swap quotes 358 | - `submit_gasless_swap` - Execute trades without holding ETH 359 | - `get_gasless_status` - Monitor gasless transaction status 360 | 361 | ### **Utility Tools** 362 | 363 | - `convert_wei_to_formatted` - Convert blockchain units to human-readable 364 | - `convert_formatted_to_wei` - Convert amounts to blockchain format 365 | 366 | _Plus 25+ additional tools for comprehensive DeFi trading and analysis._ 367 | 368 | ## 🌐 Supported Networks 369 | 370 | **17+ Blockchain Networks:** 371 | 372 | - **Ethereum** - The original DeFi ecosystem 373 | - **Base** - Coinbase's L2 with low fees 374 | - **Polygon** - Fast and cheap transactions 375 | - **Arbitrum** - Leading Ethereum L2 376 | - **Optimism** - Optimistic rollup scaling 377 | - **BSC** - Binance Smart Chain 378 | - **Avalanche** - Fast. Scalable. Customizable 379 | - **Blast** - Native yield for ETH and stablecoins 380 | - **Linea** - ConsenSys zkEVM 381 | - **Scroll** - zkRollup technology 382 | - **Mantle** - Modular blockchain network 383 | - **Mode** - DeFi-focused L2 384 | - **Worldchain** - World ID integration 385 | - **Unichain** - Uniswap's dedicated chain 386 | - **Berachain** - Proof-of-liquidity consensus 387 | - **Ink** - Kraken's L2 solution 388 | - **MonadTestnet** - Next-gen parallel EVM 389 | 390 | _Use `get_supported_chains` for the complete current list._ 391 | 392 | ## 🔐 Security & Trust 393 | 394 | ### **Local Key Management** 395 | 396 | - Private keys never leave your device 397 | - All transaction signing happens locally 398 | - No sensitive data transmitted to servers 399 | 400 | ## 💡 Agent Use Cases 401 | 402 | ### **DeFi Portfolio Manager** 403 | 404 | ``` 405 | "Analyze my DeFi portfolio and suggest optimizations" 406 | ``` 407 | 408 | - Tracks performance across all chains 409 | - Identifies underperforming assets 410 | - Suggests rebalancing strategies 411 | - Executes optimization trades 412 | 413 | ### **Technical Analysis Expert** 414 | 415 | ``` 416 | "Analyze the OHLCV data for trending tokens and identify the best entry points" 417 | ``` 418 | 419 | **Advanced Technical Analysis:** 420 | 421 | - **Pattern Recognition**: Identifies bullish/bearish patterns in OHLCV data 422 | - **Support/Resistance**: Calculates key price levels using historical data 423 | - **Volume Analysis**: Analyzes trading volume for momentum confirmation 424 | - **Entry Timing**: Determines optimal entry points based on technical indicators 425 | - **Risk Management**: Sets stop-loss and take-profit levels automatically 426 | - **Position Sizing**: Calculates optimal allocation based on volatility and risk tolerance 427 | 428 | **Example Technical Analysis:** 429 | 430 | ``` 431 | "$TOKEN shows strong bullish momentum: 432 | 📊 OHLCV Analysis: Higher lows pattern, volume increasing 300% 433 | 📈 Support Level: $0.00085 (tested 3x, held strong) 434 | 📉 Resistance: $0.0012 (previous high, light volume) 435 | 💡 Strategy: Enter at $0.00095, Stop at $0.00082, Target $0.0015 436 | ⚖️ Risk/Reward: 1:4 ratio, recommended 1.5% portfolio allocation" 437 | ``` 438 | 439 | ### **Arbitrage Hunter** 440 | 441 | ``` 442 | "Look for arbitrage opportunities between chains" 443 | ``` 444 | 445 | - Compares prices across networks 446 | - Identifies profitable spreads 447 | - Calculates gas costs and slippage 448 | - Executes profitable arbitrage 449 | 450 | ### **Risk Monitor** 451 | 452 | ``` 453 | "Alert me if any of my positions drop more than 10%" 454 | ``` 455 | 456 | - Continuous portfolio monitoring 457 | - Real-time price alerts 458 | - Automatic stop-loss execution 459 | - Risk assessment reports 460 | 461 | ### **Advanced Market Analysis Agent** 462 | 463 | ``` 464 | "Analyze newly launched memecoins on Base with high volume and determine entry strategy" 465 | ``` 466 | 467 | **Comprehensive Analysis:** 468 | 469 | - **Trend Detection**: Identifies trending pools with unusual volume spikes 470 | - **Technical Analysis**: Uses OHLCV data to analyze price patterns and momentum 471 | - **Liquidity Assessment**: Evaluates pool depth and trading sustainability 472 | - **Risk Scoring**: Calculates risk metrics based on volatility and liquidity 473 | - **Entry Optimization**: Determines optimal entry points using technical indicators 474 | - **Position Sizing**: Recommends allocation based on portfolio risk management 475 | 476 | **Example Analysis Flow:** 477 | 478 | 1. **Discovery**: "Find new tokens with >1000% volume increase in last 24h" 479 | 2. **Research**: Pulls token metadata, social links, and trading history 480 | 3. **Technical Analysis**: Analyzes OHLCV patterns for support/resistance levels 481 | 4. **Risk Assessment**: Evaluates liquidity, holder distribution, and volatility 482 | 5. **Strategy**: "Enter 2% of portfolio at $0.0001 with stop-loss at $0.00008" 483 | 6. **Execution**: Places gasless swap with optimal slippage settings 484 | 485 | ## 🚀 Why Choose DeFi Trading Agent MCP? 486 | 487 | ### **For Traders** 488 | 489 | - **AI-Powered Analysis**: Advanced market intelligence with OHLCV technical analysis 490 | - **Memecoin Discovery**: Automated scanning for newly launched high-potential tokens 491 | - **Smart Entry Timing**: AI determines optimal entry points using multiple indicators 492 | - **Risk-Managed Trading**: Automated position sizing and stop-loss calculations 493 | - **Multi-chain Efficiency**: Trade across 17+ networks seamlessly 494 | - **Gas Optimization**: Gasless trades save on transaction costs 495 | - **Professional Grade**: Built for high-volume trading 496 | 497 | ## 💬 Community & Support 498 | 499 | ### **Join Our Community** 500 | 501 | - **[Telegram Group](https://t.me/+fC8GWO3zBe04NTY0)** - Get help, share strategies, and connect with other traders 502 | - **[GitHub Issues](https://github.com/edkdev/defi-trading-mcp/issues)** - Report bugs and request features 503 | - **[GitHub Discussions](https://github.com/edkdev/defi-trading-mcp/discussions)** - General questions and community chat 504 | 505 | ### **Need Help?** 506 | 507 | - 💬 **Quick questions**: Join our Telegram group for real-time support 508 | - 🐛 **Bug reports**: Create an issue on GitHub 509 | - 💡 **Feature requests**: Share your ideas in GitHub Discussions 510 | - 📚 **Documentation**: Check our guides for [CoinGecko API](./gecko.md) and [Loading Crypto](./loadCrypto.md) 511 | 512 | --- 513 | 514 | **Transform your AI into an autonomous crypto trading agent today.** 515 | -------------------------------------------------------------------------------- /src/services/blockchainService.js: -------------------------------------------------------------------------------- 1 | // src/services/blockchainService.js 2 | import { ethers } from "ethers"; 3 | 4 | export class BlockchainService { 5 | constructor(privateKey, alchemyApiKey) { 6 | this.wallet = null; 7 | this.providers = {}; 8 | this.alchemyApiKey = alchemyApiKey; // Store the Alchemy API key 9 | 10 | // Initialize wallet if private key is provided 11 | if (privateKey) { 12 | this.initializeWallet(privateKey); 13 | } 14 | 15 | // Initialize providers for supported chains 16 | this.initializeProviders(); 17 | } 18 | 19 | initializeWallet(privateKey) { 20 | try { 21 | this.wallet = new ethers.Wallet(privateKey); 22 | console.error("Wallet initialized:", this.wallet.address); 23 | } catch (error) { 24 | console.error("Failed to initialize wallet:", error.message); 25 | throw new Error("Invalid private key provided"); 26 | } 27 | } 28 | 29 | initializeProviders() { 30 | // Default RPC URLs for all supported chains (public/free RPCs) 31 | const defaultRpcUrls = { 32 | 1: "https://rpc.flashbots.net", // Ethereum 33 | 10: "https://mainnet.optimism.io", // Optimism 34 | 56: "https://bsc-dataseed.binance.org", // BSC 35 | 137: "https://polygon.llamarpc.com", // Polygon 36 | 8453: "https://mainnet.base.org", // Base 37 | 42161: "https://arb1.arbitrum.io/rpc", // Arbitrum 38 | 43114: "https://api.avax.network/ext/bc/C/rpc", // Avalanche 39 | 59144: "https://rpc.linea.build", // Linea 40 | 534352: "https://rpc.scroll.io", // Scroll 41 | 5000: "https://rpc.mantle.xyz", // Mantle 42 | 81457: "https://rpc.blast.io", // Blast 43 | 34443: "https://mainnet.mode.network", // Mode 44 | 480: "https://worldchain-mainnet.g.alchemy.com/public", // Worldchain 45 | 10143: "https://testnet1.monad.xyz", // MonadTestnet 46 | 130: "https://rpc.unichain.org", // Unichain 47 | 80094: "https://rpc.berachain.com", // Berachain 48 | 57073: "https://rpc-gel.inkonchain.com", // Ink 49 | }; 50 | 51 | // Alchemy RPC mapping (if ALCHEMY_API_KEY is provided) 52 | const alchemyChainNames = { 53 | 10: "opt-mainnet", // Optimism 54 | 56: "bnb-mainnet", // BSC 55 | 137: "polygon-mainnet", // Polygon 56 | 8453: "base-mainnet", // Base 57 | 42161: "arb-mainnet", // Arbitrum 58 | 43114: "avax-mainnet", // Avalanche 59 | 480: "worldchain-mainnet", // Worldchain 60 | 81457: "blast-mainnet", // Blast 61 | 59144: "linea-mainnet", // Linea 62 | 534352: "scroll-mainnet", // Scroll 63 | 5000: "mantle-mainnet", // Mantle 64 | 10143: "monad-testnet", // MonadTestnet 65 | 80094: "berachain-mainnet", // Berachain 66 | 57073: "ink-mainnet", // Ink 67 | // Note: Not all chains are supported by Alchemy 68 | }; 69 | 70 | // Check if Alchemy API key is provided 71 | const useAlchemy = !!this.alchemyApiKey; 72 | 73 | // Build final RPC URLs 74 | const rpcUrls = {}; 75 | for (const chainId of Object.keys(defaultRpcUrls)) { 76 | if (useAlchemy && alchemyChainNames[chainId]) { 77 | // Use Alchemy RPC if API key is provided and chain is supported 78 | rpcUrls[ 79 | chainId 80 | ] = `https://${alchemyChainNames[chainId]}.g.alchemy.com/v2/${this.alchemyApiKey}`; 81 | } else { 82 | // Fall back to default public RPC 83 | rpcUrls[chainId] = defaultRpcUrls[chainId]; 84 | } 85 | } 86 | 87 | for (const [chainId, rpcUrl] of Object.entries(rpcUrls)) { 88 | try { 89 | this.providers[chainId] = new ethers.JsonRpcProvider(rpcUrl); 90 | const rpcType = 91 | useAlchemy && alchemyChainNames[chainId] ? "Alchemy" : "default"; 92 | console.error( 93 | `Chain ${chainId} provider initialized (${rpcType}): ${rpcUrl.substring( 94 | 0, 95 | 50 96 | )}...` 97 | ); 98 | } catch (error) { 99 | console.warn( 100 | `Failed to initialize provider for chain ${chainId}:`, 101 | error.message 102 | ); 103 | } 104 | } 105 | } 106 | 107 | getProvider(chainId) { 108 | const provider = this.providers[chainId.toString()]; 109 | if (!provider) { 110 | throw new Error(`No provider configured for chain ID ${chainId}`); 111 | } 112 | return provider; 113 | } 114 | 115 | getConnectedWallet(chainId) { 116 | if (!this.wallet) { 117 | throw new Error("No private key configured for transaction signing"); 118 | } 119 | 120 | const provider = this.getProvider(chainId); 121 | return this.wallet.connect(provider); 122 | } 123 | 124 | async signEIP712Message(domain, types, message) { 125 | try { 126 | if (!this.wallet) { 127 | throw new Error("No private key configured for signing"); 128 | } 129 | 130 | console.log("🔐 Signing EIP-712 message:", { 131 | domain: domain.name, 132 | primaryType: Object.keys(types).find((key) => key !== "EIP712Domain"), 133 | messageKeys: Object.keys(message), 134 | }); 135 | 136 | const signature = await this.wallet.signTypedData(domain, types, message); 137 | 138 | console.log( 139 | "✅ EIP-712 signature created:", 140 | signature.substring(0, 20) + "..." 141 | ); 142 | 143 | return signature; 144 | } catch (error) { 145 | console.error("Failed to sign EIP-712 message:", error); 146 | throw new Error(`Failed to sign EIP-712 message: ${error.message}`); 147 | } 148 | } 149 | 150 | async signPermit2Message(permit2Data) { 151 | try { 152 | if (!permit2Data || !permit2Data.eip712) { 153 | throw new Error("Invalid permit2 data - missing EIP-712 structure"); 154 | } 155 | 156 | const { domain, types, message, primaryType } = permit2Data.eip712; 157 | 158 | 159 | const cleanTypes = { ...types }; 160 | delete cleanTypes.EIP712Domain; 161 | 162 | console.log( 163 | "🔐 Signing Permit2 with cleaned types:", 164 | Object.keys(cleanTypes) 165 | ); 166 | 167 | const signature = await this.wallet.signTypedData( 168 | domain, 169 | cleanTypes, 170 | message 171 | ); 172 | 173 | return { 174 | signature, 175 | hash: permit2Data.hash, 176 | eip712: permit2Data.eip712, 177 | }; 178 | } catch (error) { 179 | throw new Error(`Failed to sign Permit2 message: ${error.message}`); 180 | } 181 | } 182 | 183 | async signGaslessApproval(approvalData) { 184 | try { 185 | if (!approvalData || !approvalData.eip712) { 186 | throw new Error("Invalid approval data - missing EIP-712 structure"); 187 | } 188 | 189 | const { domain, types, message } = approvalData.eip712; 190 | const rawSignature = await this.signEIP712Message(domain, types, message); 191 | 192 | return { 193 | type: approvalData.type, 194 | eip712: approvalData.eip712, 195 | signature: rawSignature, // Return raw signature for aggregator to parse 196 | }; 197 | } catch (error) { 198 | throw new Error(`Failed to sign gasless approval: ${error.message}`); 199 | } 200 | } 201 | 202 | async signGaslessTrade(tradeData) { 203 | try { 204 | if (!tradeData || !tradeData.eip712) { 205 | throw new Error("Invalid trade data - missing EIP-712 structure"); 206 | } 207 | 208 | const { domain, types, message, primaryType } = tradeData.eip712; 209 | 210 | // Use explicit primary type for gasless trades 211 | const rawSignature = await this.signEIP712MessageWithPrimaryType( 212 | domain, 213 | types, 214 | message, 215 | primaryType 216 | ); 217 | 218 | return { 219 | type: tradeData.type, 220 | eip712: tradeData.eip712, 221 | signature: rawSignature, // Return raw signature for aggregator to parse 222 | }; 223 | } catch (error) { 224 | throw new Error(`Failed to sign gasless trade: ${error.message}`); 225 | } 226 | } 227 | 228 | async signEIP712MessageWithPrimaryType(domain, types, message, primaryType) { 229 | try { 230 | if (!this.wallet) { 231 | throw new Error("No private key configured for signing"); 232 | } 233 | 234 | console.log("🔐 Signing EIP-712 message with explicit primary type:", { 235 | domain: domain.name, 236 | primaryType: primaryType, 237 | messageKeys: Object.keys(message), 238 | }); 239 | 240 | const cleanTypes = { 241 | [primaryType]: types[primaryType], 242 | }; 243 | 244 | // Add any referenced types 245 | const addReferencedTypes = (typeName) => { 246 | const typeDefinition = types[typeName]; 247 | if (typeDefinition) { 248 | typeDefinition.forEach((field) => { 249 | const fieldType = field.type.replace("[]", ""); // Remove array notation 250 | if ( 251 | types[fieldType] && 252 | !cleanTypes[fieldType] && 253 | fieldType !== "EIP712Domain" 254 | ) { 255 | cleanTypes[fieldType] = types[fieldType]; 256 | addReferencedTypes(fieldType); // Recursively add referenced types 257 | } 258 | }); 259 | } 260 | }; 261 | 262 | addReferencedTypes(primaryType); 263 | 264 | console.log( 265 | "🔧 Using clean types (without EIP712Domain):", 266 | Object.keys(cleanTypes) 267 | ); 268 | 269 | // Now use the standard signTypedData with clean types 270 | const signature = await this.wallet.signTypedData( 271 | domain, 272 | cleanTypes, 273 | message 274 | ); 275 | 276 | console.log( 277 | "✅ EIP-712 signature created:", 278 | signature.substring(0, 20) + "..." 279 | ); 280 | 281 | return signature; 282 | } catch (error) { 283 | console.error("Failed to sign EIP-712 message:", error); 284 | throw new Error(`Failed to sign EIP-712 message: ${error.message}`); 285 | } 286 | } 287 | 288 | async getTransactionCount(chainId, address) { 289 | try { 290 | const provider = this.getProvider(chainId); 291 | const count = await provider.getTransactionCount(address, "pending"); 292 | return Number(count); // Convert BigInt to number 293 | } catch (error) { 294 | throw new Error( 295 | `Failed to get transaction count for chain ${chainId}: ${error.message}` 296 | ); 297 | } 298 | } 299 | 300 | async signAndBroadcastTransaction(chainId, quoteData) { 301 | try { 302 | if (!this.wallet) { 303 | throw new Error("No private key configured for transaction signing"); 304 | } 305 | 306 | const connectedWallet = this.getConnectedWallet(chainId); 307 | const { transaction, permit2, sellToken, sellAmount } = quoteData; 308 | 309 | if (!transaction) { 310 | throw new Error("No transaction data found in quote"); 311 | } 312 | 313 | console.log(`🚀 Processing transaction for chain ${chainId}:`, { 314 | hasTransaction: !!transaction, 315 | hasPermit2: !!permit2, 316 | transactionTo: transaction.to, 317 | transactionValue: transaction.value || "0", 318 | }); 319 | 320 | // Step 1: Handle token allowance for Permit2 (if needed) 321 | const PERMIT2_CONTRACT = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; 322 | let allowanceResult = null; 323 | 324 | if (permit2 && sellToken && sellAmount) { 325 | console.log("🔍 Checking token allowance for Permit2..."); 326 | try { 327 | allowanceResult = await this.ensureTokenAllowance( 328 | chainId, 329 | sellToken, 330 | PERMIT2_CONTRACT, 331 | sellAmount 332 | ); 333 | 334 | if (allowanceResult.approved) { 335 | console.log("✅ Token allowance set for Permit2 contract"); 336 | } else { 337 | console.log("✅ Sufficient token allowance already exists"); 338 | } 339 | } catch (error) { 340 | console.warn( 341 | "⚠️ Token allowance check failed, proceeding anyway:", 342 | error.message 343 | ); 344 | // Continue with swap execution even if allowance check fails 345 | // The transaction will fail if allowance is actually insufficient 346 | } 347 | } 348 | 349 | // Step 2: Prepare transaction data - start with original data 350 | let transactionData = transaction.data; 351 | 352 | // Step 3: Handle Permit2 signature if present 353 | let permit2Signature = null; 354 | if (permit2 && permit2.eip712) { 355 | console.log("🔐 Processing Permit2 signature..."); 356 | permit2Signature = await this.signPermit2Message(permit2); 357 | console.log("✅ Permit2 signature created"); 358 | 359 | 360 | const signature = permit2Signature.signature; 361 | 362 | // Ensure signature has 0x prefix and is properly formatted 363 | const cleanSignature = signature.startsWith("0x") ? signature : "0x" + signature; 364 | 365 | // Validate signature format (should be 65 bytes = 130 hex chars + 0x prefix = 132 total) 366 | if (cleanSignature.length !== 132) { 367 | throw new Error(`Invalid signature length: expected 132 chars (65 bytes), got ${cleanSignature.length}`); 368 | } 369 | 370 | // Calculate signature size in bytes (following viem's size() function logic) 371 | const signatureBytes = ethers.getBytes(cleanSignature); 372 | const signatureSize = signatureBytes.length; // Should be 65 bytes 373 | 374 | // Create signature length as 32-byte unsigned big-endian integer (following viem's numberToHex) 375 | const signatureLengthInHex = ethers.zeroPadValue( 376 | ethers.toBeHex(signatureSize), 377 | 32 378 | ); 379 | 380 | // Append signature length and signature data to transaction data (following viem's concat) 381 | transactionData = ethers.concat([ 382 | transaction.data, 383 | signatureLengthInHex, 384 | cleanSignature, 385 | ]); 386 | 387 | console.log("🔧 Permit2 signature embedded in transaction data:", { 388 | originalDataLength: transaction.data.length, 389 | signatureSize: signatureSize, 390 | newDataLength: ethers.hexlify(transactionData).length, 391 | signatureLengthHex: ethers.hexlify(signatureLengthInHex).substring(0, 20) + "...", 392 | signaturePreview: cleanSignature.substring(0, 20) + "...", 393 | }); 394 | } 395 | 396 | // Step 4: Validate quote data 397 | if (!transaction.gas) { 398 | throw new Error("No gas estimate found in quote data"); 399 | } 400 | if (!transaction.gasPrice) { 401 | throw new Error("No gasPrice found in quote data"); 402 | } 403 | 404 | // Step 5: Get nonce 405 | const nonce = await this.getTransactionCount( 406 | chainId, 407 | connectedWallet.address 408 | ); 409 | 410 | // Step 6: Create legacy transaction with embedded Permit2 signature 411 | const legacyTxData = { 412 | to: transaction.to, 413 | data: transactionData, // Use modified data with embedded signature 414 | value: transaction.value || "0", 415 | gasLimit: transaction.gas, 416 | gasPrice: transaction.gasPrice, 417 | nonce: nonce, 418 | type: 0, // Force legacy transaction 419 | }; 420 | 421 | console.log("🚀 Sending transaction with Permit2 signature embedded:", { 422 | to: legacyTxData.to, 423 | gasLimit: legacyTxData.gasLimit, 424 | gasPrice: legacyTxData.gasPrice, 425 | nonce: legacyTxData.nonce, 426 | type: "legacy", 427 | hasPermit2Embedded: !!permit2Signature, 428 | dataLength: transactionData.length, 429 | allowanceHandled: !!allowanceResult, 430 | }); 431 | 432 | // Step 7: Sign and send transaction 433 | console.log("🚀 Broadcasting transaction to network..."); 434 | const signedTx = await connectedWallet.sendTransaction(legacyTxData); 435 | console.log(`✅ Transaction broadcasted: ${signedTx.hash}`); 436 | 437 | // Return transaction details 438 | return { 439 | hash: signedTx.hash, 440 | from: signedTx.from, 441 | to: signedTx.to, 442 | value: signedTx.value?.toString(), 443 | gasLimit: signedTx.gasLimit?.toString(), 444 | gasPrice: signedTx.gasPrice?.toString(), 445 | nonce: signedTx.nonce?.toString(), 446 | chainId: signedTx.chainId?.toString(), 447 | type: signedTx.type?.toString(), 448 | permit2Signed: !!permit2Signature, 449 | permit2Hash: permit2Signature?.hash, 450 | permit2Embedded: !!permit2Signature, 451 | allowanceResult: allowanceResult, 452 | steps: [ 453 | "1. ✅ Token allowance checked/set for Permit2", 454 | "2. ✅ Permit2 EIP-712 message signed", 455 | "3. ✅ Signature embedded in transaction data", 456 | "4. ✅ Transaction broadcasted to network", 457 | ], 458 | }; 459 | } catch (error) { 460 | console.error("Transaction signing/broadcasting failed:", error); 461 | throw new Error( 462 | `Failed to sign and broadcast transaction: ${error.message}` 463 | ); 464 | } 465 | } 466 | 467 | async waitForTransaction( 468 | chainId, 469 | txHash, 470 | confirmations = 1, 471 | timeout = 300000 472 | ) { 473 | try { 474 | const provider = this.getProvider(chainId); 475 | 476 | console.log( 477 | `⏳ Waiting for transaction ${txHash} with ${confirmations} confirmations...` 478 | ); 479 | 480 | const receipt = await provider.waitForTransaction( 481 | txHash, 482 | confirmations, 483 | timeout 484 | ); 485 | 486 | if (!receipt) { 487 | throw new Error("Transaction receipt not found"); 488 | } 489 | 490 | return { 491 | hash: receipt.hash, 492 | blockNumber: receipt.blockNumber?.toString(), 493 | blockHash: receipt.blockHash, 494 | transactionIndex: receipt.transactionIndex?.toString(), 495 | from: receipt.from, 496 | to: receipt.to, 497 | gasUsed: receipt.gasUsed?.toString(), 498 | cumulativeGasUsed: receipt.cumulativeGasUsed?.toString(), 499 | effectiveGasPrice: receipt.effectiveGasPrice?.toString(), 500 | status: receipt.status, 501 | logs: receipt.logs?.map((log) => ({ 502 | ...log, 503 | blockNumber: log.blockNumber?.toString(), 504 | transactionIndex: log.transactionIndex?.toString(), 505 | logIndex: log.logIndex?.toString(), 506 | })), 507 | confirmations: (await receipt.confirmations())?.toString(), 508 | }; 509 | } catch (error) { 510 | throw new Error(`Failed to wait for transaction: ${error.message}`); 511 | } 512 | } 513 | 514 | async getTransactionStatus(chainId, txHash) { 515 | try { 516 | const provider = this.getProvider(chainId); 517 | 518 | const tx = await provider.getTransaction(txHash); 519 | if (!tx) { 520 | return { status: "not_found" }; 521 | } 522 | 523 | const receipt = await provider.getTransactionReceipt(txHash); 524 | 525 | if (!receipt) { 526 | return { 527 | status: "pending", 528 | transaction: { 529 | hash: tx.hash, 530 | from: tx.from, 531 | to: tx.to, 532 | value: tx.value?.toString(), 533 | gasLimit: tx.gasLimit?.toString(), 534 | gasPrice: tx.gasPrice?.toString(), 535 | nonce: tx.nonce?.toString(), 536 | }, 537 | }; 538 | } 539 | 540 | return { 541 | status: receipt.status === 1 ? "success" : "failed", 542 | transaction: { 543 | hash: tx.hash, 544 | from: tx.from, 545 | to: tx.to, 546 | value: tx.value?.toString(), 547 | gasLimit: tx.gasLimit?.toString(), 548 | gasPrice: tx.gasPrice?.toString(), 549 | nonce: tx.nonce?.toString(), 550 | }, 551 | receipt: { 552 | blockNumber: receipt.blockNumber?.toString(), 553 | blockHash: receipt.blockHash, 554 | gasUsed: receipt.gasUsed?.toString(), 555 | effectiveGasPrice: receipt.effectiveGasPrice?.toString(), 556 | status: receipt.status, 557 | confirmations: (await receipt.confirmations())?.toString(), 558 | }, 559 | }; 560 | } catch (error) { 561 | throw new Error(`Failed to get transaction status: ${error.message}`); 562 | } 563 | } 564 | 565 | async checkTokenAllowance( 566 | chainId, 567 | tokenAddress, 568 | ownerAddress, 569 | spenderAddress 570 | ) { 571 | try { 572 | const provider = this.getProvider(chainId); 573 | 574 | // ERC20 allowance function ABI 575 | const erc20Abi = [ 576 | "function allowance(address owner, address spender) view returns (uint256)", 577 | ]; 578 | 579 | const tokenContract = new ethers.Contract( 580 | tokenAddress, 581 | erc20Abi, 582 | provider 583 | ); 584 | const allowance = await tokenContract.allowance( 585 | ownerAddress, 586 | spenderAddress 587 | ); 588 | 589 | return allowance.toString(); 590 | } catch (error) { 591 | throw new Error(`Failed to check token allowance: ${error.message}`); 592 | } 593 | } 594 | 595 | async approveToken(chainId, tokenAddress, spenderAddress, amount) { 596 | try { 597 | if (!this.wallet) { 598 | throw new Error("No private key configured for token approval"); 599 | } 600 | 601 | const connectedWallet = this.getConnectedWallet(chainId); 602 | 603 | // ERC20 approve function ABI 604 | const erc20Abi = [ 605 | "function approve(address spender, uint256 amount) returns (bool)", 606 | ]; 607 | 608 | const tokenContract = new ethers.Contract( 609 | tokenAddress, 610 | erc20Abi, 611 | connectedWallet 612 | ); 613 | 614 | console.log( 615 | `🔐 Approving ${amount} tokens for spender ${spenderAddress}...` 616 | ); 617 | 618 | // Send approval transaction 619 | const tx = await tokenContract.approve(spenderAddress, amount); 620 | 621 | console.log(`✅ Approval transaction sent: ${tx.hash}`); 622 | 623 | // Wait for confirmation 624 | const receipt = await tx.wait(); 625 | 626 | console.log(`✅ Approval confirmed in block ${receipt.blockNumber}`); 627 | 628 | return { 629 | hash: tx.hash, 630 | blockNumber: receipt.blockNumber?.toString(), 631 | gasUsed: receipt.gasUsed?.toString(), 632 | status: receipt.status, 633 | }; 634 | } catch (error) { 635 | throw new Error(`Failed to approve token: ${error.message}`); 636 | } 637 | } 638 | 639 | async ensureTokenAllowance( 640 | chainId, 641 | tokenAddress, 642 | spenderAddress, 643 | requiredAmount 644 | ) { 645 | try { 646 | const ownerAddress = this.getWalletAddress(); 647 | if (!ownerAddress) { 648 | throw new Error("No wallet address available"); 649 | } 650 | 651 | console.log(`🔍 Checking token allowance for ${tokenAddress}...`); 652 | 653 | // Check current allowance 654 | const currentAllowance = await this.checkTokenAllowance( 655 | chainId, 656 | tokenAddress, 657 | ownerAddress, 658 | spenderAddress 659 | ); 660 | 661 | const currentAllowanceBN = ethers.getBigInt(currentAllowance); 662 | const requiredAmountBN = ethers.getBigInt(requiredAmount); 663 | 664 | console.log(`Current allowance: ${currentAllowance}`); 665 | console.log(`Required amount: ${requiredAmount}`); 666 | 667 | if (currentAllowanceBN >= requiredAmountBN) { 668 | console.log("✅ Sufficient allowance already exists"); 669 | return { 670 | approved: false, 671 | reason: "sufficient_allowance", 672 | currentAllowance: currentAllowance, 673 | requiredAmount: requiredAmount, 674 | }; 675 | } 676 | 677 | console.log("⚠️ Insufficient allowance, approving maximum amount..."); 678 | 679 | // Approve maximum amount (2^256 - 1) for convenience 680 | const maxAmount = ethers.MaxUint256; 681 | const approvalResult = await this.approveToken( 682 | chainId, 683 | tokenAddress, 684 | spenderAddress, 685 | maxAmount 686 | ); 687 | 688 | return { 689 | approved: true, 690 | reason: "insufficient_allowance", 691 | approvalTransaction: approvalResult, 692 | approvedAmount: maxAmount.toString(), 693 | previousAllowance: currentAllowance, 694 | }; 695 | } catch (error) { 696 | throw new Error(`Failed to ensure token allowance: ${error.message}`); 697 | } 698 | } 699 | 700 | getSupportedChains() { 701 | return Object.keys(this.providers).map((chainId) => parseInt(chainId)); 702 | } 703 | 704 | isChainSupported(chainId) { 705 | return this.getSupportedChains().includes(parseInt(chainId)); 706 | } 707 | 708 | getWalletAddress() { 709 | return this.wallet ? this.wallet.address : null; 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /src/toolService.js: -------------------------------------------------------------------------------- 1 | // src/toolService.js 2 | import { AgService } from "./services/agService.js"; 3 | import { CoinGeckoApiService } from "./services/coinGeckoApiService.js"; 4 | import { BlockchainService } from "./services/blockchainService.js"; 5 | import { ethers } from "ethers"; 6 | 7 | export class ToolService { 8 | constructor( 9 | agUrl, 10 | userPrivateKey, 11 | userAddress, 12 | coinGeckoApiKey, 13 | alchemyApiKey 14 | ) { 15 | this.agg = new AgService(agUrl); 16 | this.coinGeckoApi = new CoinGeckoApiService(coinGeckoApiKey); 17 | this.blockchain = new BlockchainService(userPrivateKey, alchemyApiKey); 18 | this.userPrivateKey = userPrivateKey; 19 | this.userAddress = userAddress; 20 | } 21 | 22 | async getSwapPrice(params) { 23 | // Validate required parameters 24 | const { chainId, buyToken, sellToken, sellAmount } = params; 25 | 26 | if (!chainId || !buyToken || !sellToken || !sellAmount) { 27 | throw new Error( 28 | "Missing required parameters: chainId, buyToken, sellToken, sellAmount" 29 | ); 30 | } 31 | 32 | const result = await this.agg.getSwapPrice(params); 33 | 34 | return { 35 | message: "Swap price retrieved successfully", 36 | data: result, 37 | }; 38 | } 39 | 40 | async getSwapQuote(params) { 41 | // Validate required parameters 42 | const { chainId, buyToken, sellToken, sellAmount } = params; 43 | 44 | if (!chainId || !buyToken || !sellToken || !sellAmount) { 45 | throw new Error( 46 | "Missing required parameters: chainId, buyToken, sellToken, sellAmount" 47 | ); 48 | } 49 | 50 | // Add taker address if not provided 51 | const quoteParams = { 52 | ...params, 53 | taker: params.taker || this.userAddress, 54 | }; 55 | 56 | const result = await this.agg.getSwapQuote(quoteParams); 57 | 58 | // Add chainId to the result for executeSwap to use 59 | result.chainId = chainId; 60 | 61 | return { 62 | message: "Swap quote retrieved successfully", 63 | data: result, 64 | nextSteps: [ 65 | "1. Review the quote details including fees and gas estimates", 66 | "2. Use execute_swap tool to execute this swap", 67 | "3. The permit2 signature will be handled automatically", 68 | ], 69 | }; 70 | } 71 | 72 | async executeSwap(quoteData) { 73 | if (!quoteData) { 74 | throw new Error("Quote data is required for swap execution"); 75 | } 76 | 77 | if (!this.userPrivateKey) { 78 | throw new Error("User private key is required for swap execution"); 79 | } 80 | 81 | try { 82 | // Extract chain ID from quote data 83 | const chainId = quoteData.chainId || quoteData.transaction?.chainId; 84 | if (!chainId) { 85 | throw new Error("Chain ID not found in quote data"); 86 | } 87 | 88 | console.log("🚀 Executing swap transaction..."); 89 | 90 | // Sign and broadcast the transaction using blockchain service 91 | const result = await this.blockchain.signAndBroadcastTransaction( 92 | chainId, 93 | quoteData 94 | ); 95 | 96 | return { 97 | message: "Swap executed successfully", 98 | data: result, 99 | nextSteps: [ 100 | "1. Transaction has been broadcasted to the blockchain", 101 | "2. Wait for confirmation (usually 1-3 minutes)", 102 | "3. Check transaction status on block explorer", 103 | `4. Transaction hash: ${result.hash}`, 104 | ], 105 | }; 106 | } catch (error) { 107 | throw new Error(`Swap execution failed: ${error.message}`); 108 | } 109 | } 110 | 111 | async getSupportedChains() { 112 | const result = await this.agg.getSupportedChains(); 113 | 114 | return { 115 | message: "Supported chains retrieved successfully", 116 | data: result, 117 | summary: `Found ${result.chains?.length || 0} supported chains`, 118 | }; 119 | } 120 | 121 | async getLiquiditySources(chainId) { 122 | if (!chainId) { 123 | throw new Error("chainId is required"); 124 | } 125 | 126 | const result = await this.agg.getLiquiditySources(chainId); 127 | 128 | return { 129 | message: `Liquidity sources for chain ${chainId} retrieved successfully`, 130 | data: result, 131 | summary: `Found ${result.sources?.length || 0} liquidity sources`, 132 | }; 133 | } 134 | 135 | // CoinGecko API Methods 136 | async getTokenPrice(network, addresses, options = {}) { 137 | if (!network || !addresses) { 138 | throw new Error("Missing required parameters: network, addresses"); 139 | } 140 | 141 | const result = await this.coinGeckoApi.getTokenPrice( 142 | network, 143 | addresses, 144 | options 145 | ); 146 | 147 | return { 148 | message: "Token prices retrieved successfully", 149 | data: result, 150 | summary: `Retrieved prices for ${ 151 | addresses.split(",").length 152 | } token(s) on ${network} network`, 153 | }; 154 | } 155 | 156 | async getCoinGeckoNetworks(page = 1) { 157 | const result = await this.coinGeckoApi.getNetworks(page); 158 | 159 | return { 160 | message: "CoinGecko networks retrieved successfully", 161 | data: result, 162 | summary: `Found ${result.data?.length || 0} networks on page ${page}`, 163 | }; 164 | } 165 | 166 | async getSupportedDexes(network, page = 1) { 167 | if (!network) { 168 | throw new Error("network is required"); 169 | } 170 | 171 | const result = await this.coinGeckoApi.getSupportedDexes(network, page); 172 | 173 | return { 174 | message: `Supported DEXes for ${network} retrieved successfully`, 175 | data: result, 176 | summary: `Found ${result.data?.length || 0} DEXes on ${network} network`, 177 | }; 178 | } 179 | 180 | async getTrendingPools(options = {}) { 181 | const result = await this.coinGeckoApi.getTrendingPools(options); 182 | 183 | return { 184 | message: "Trending pools retrieved successfully", 185 | data: result, 186 | summary: `Found ${result.data?.length || 0} trending pools`, 187 | duration: options.duration || "24h", 188 | }; 189 | } 190 | 191 | async getTrendingPoolsByNetwork(network, options = {}) { 192 | if (!network) { 193 | throw new Error("network is required"); 194 | } 195 | 196 | const result = await this.coinGeckoApi.getTrendingPoolsByNetwork( 197 | network, 198 | options 199 | ); 200 | 201 | return { 202 | message: `Trending pools for ${network} retrieved successfully`, 203 | data: result, 204 | summary: `Found ${result.data?.length || 0} trending pools on ${network}`, 205 | duration: options.duration || "24h", 206 | }; 207 | } 208 | 209 | async getMultiplePoolsData(network, addresses, options = {}) { 210 | if (!network || !addresses) { 211 | throw new Error("Missing required parameters: network, addresses"); 212 | } 213 | 214 | const result = await this.coinGeckoApi.getMultiplePoolsData( 215 | network, 216 | addresses, 217 | options 218 | ); 219 | 220 | return { 221 | message: "Multiple pools data retrieved successfully", 222 | data: result, 223 | summary: `Retrieved data for ${ 224 | addresses.split(",").length 225 | } pool(s) on ${network}`, 226 | }; 227 | } 228 | 229 | async getTopPoolsByDex(network, dex, options = {}) { 230 | if (!network || !dex) { 231 | throw new Error("Missing required parameters: network, dex"); 232 | } 233 | 234 | const result = await this.coinGeckoApi.getTopPoolsByDex( 235 | network, 236 | dex, 237 | options 238 | ); 239 | 240 | return { 241 | message: `Top pools for ${dex} on ${network} retrieved successfully`, 242 | data: result, 243 | summary: `Found ${result.data?.length || 0} pools on ${dex}`, 244 | sort: options.sort || "h24_tx_count_desc", 245 | }; 246 | } 247 | 248 | async getNewPools(options = {}) { 249 | const result = await this.coinGeckoApi.getNewPools(options); 250 | 251 | return { 252 | message: "New pools retrieved successfully", 253 | data: result, 254 | summary: `Found ${ 255 | result.data?.length || 0 256 | } new pools across all networks`, 257 | }; 258 | } 259 | 260 | async searchPools(query, options = {}) { 261 | if (!query) { 262 | throw new Error("query is required"); 263 | } 264 | 265 | const result = await this.coinGeckoApi.searchPools(query, options); 266 | 267 | return { 268 | message: `Pool search for "${query}" completed successfully`, 269 | data: result, 270 | summary: `Found ${result.data?.length || 0} pools matching "${query}"${ 271 | options.network ? ` on ${options.network}` : "" 272 | }`, 273 | }; 274 | } 275 | 276 | // Additional CoinGecko API Methods from coingeckoendpoints-2.txt 277 | async getTopPoolsByToken(network, tokenAddress, options = {}) { 278 | if (!network || !tokenAddress) { 279 | throw new Error("Missing required parameters: network, tokenAddress"); 280 | } 281 | 282 | const result = await this.coinGeckoApi.getTopPoolsByToken( 283 | network, 284 | tokenAddress, 285 | options 286 | ); 287 | 288 | return { 289 | message: `Top pools for token ${tokenAddress} on ${network} retrieved successfully`, 290 | data: result, 291 | summary: `Found ${ 292 | result.data?.length || 0 293 | } pools for token ${tokenAddress}`, 294 | sort: options.sort || "h24_volume_usd_liquidity_desc", 295 | }; 296 | } 297 | 298 | async getTokenData(network, address, options = {}) { 299 | if (!network || !address) { 300 | throw new Error("Missing required parameters: network, address"); 301 | } 302 | 303 | const result = await this.coinGeckoApi.getTokenData( 304 | network, 305 | address, 306 | options 307 | ); 308 | 309 | return { 310 | message: `Token data for ${address} on ${network} retrieved successfully`, 311 | data: result, 312 | summary: `Retrieved data for token ${ 313 | result.data?.attributes?.symbol || address 314 | }`, 315 | includes: options.include ? options.include.split(",") : [], 316 | }; 317 | } 318 | 319 | async getMultipleTokensData(network, addresses, options = {}) { 320 | if (!network || !addresses) { 321 | throw new Error("Missing required parameters: network, addresses"); 322 | } 323 | 324 | const result = await this.coinGeckoApi.getMultipleTokensData( 325 | network, 326 | addresses, 327 | options 328 | ); 329 | 330 | return { 331 | message: "Multiple tokens data retrieved successfully", 332 | data: result, 333 | summary: `Retrieved data for ${ 334 | addresses.split(",").length 335 | } token(s) on ${network}`, 336 | includes: options.include ? options.include.split(",") : [], 337 | }; 338 | } 339 | 340 | async getTokenInfo(network, address) { 341 | if (!network || !address) { 342 | throw new Error("Missing required parameters: network, address"); 343 | } 344 | 345 | const result = await this.coinGeckoApi.getTokenInfo(network, address); 346 | 347 | return { 348 | message: `Token info for ${address} on ${network} retrieved successfully`, 349 | data: result, 350 | summary: `Retrieved detailed info for token ${ 351 | result.data?.attributes?.symbol || address 352 | }`, 353 | note: "This endpoint provides additional token information like socials, websites, and description", 354 | }; 355 | } 356 | 357 | async getRecentlyUpdatedTokens(options = {}) { 358 | const result = await this.coinGeckoApi.getRecentlyUpdatedTokens(options); 359 | 360 | return { 361 | message: "Recently updated tokens retrieved successfully", 362 | data: result, 363 | summary: `Found ${result.data?.length || 0} recently updated tokens${ 364 | options.network ? ` on ${options.network}` : " across all networks" 365 | }`, 366 | includes: options.include ? options.include.split(",") : [], 367 | }; 368 | } 369 | 370 | async getPoolOHLCV(network, poolAddress, timeframe, options = {}) { 371 | if (!network || !poolAddress || !timeframe) { 372 | throw new Error( 373 | "Missing required parameters: network, poolAddress, timeframe" 374 | ); 375 | } 376 | 377 | const result = await this.coinGeckoApi.getPoolOHLCV( 378 | network, 379 | poolAddress, 380 | timeframe, 381 | options 382 | ); 383 | 384 | return { 385 | message: `OHLCV data for pool ${poolAddress} retrieved successfully`, 386 | data: result, 387 | summary: `Retrieved ${timeframe} OHLCV data for pool on ${network}`, 388 | timeframe: timeframe, 389 | aggregate: options.aggregate || "1", 390 | currency: options.currency || "usd", 391 | token: options.token || "base", 392 | }; 393 | } 394 | 395 | async getPoolTrades(network, poolAddress, options = {}) { 396 | if (!network || !poolAddress) { 397 | throw new Error("Missing required parameters: network, poolAddress"); 398 | } 399 | 400 | const result = await this.coinGeckoApi.getPoolTrades( 401 | network, 402 | poolAddress, 403 | options 404 | ); 405 | 406 | return { 407 | message: `Pool trades for ${poolAddress} on ${network} retrieved successfully`, 408 | data: result, 409 | summary: `Found ${result.data?.length || 0} trades for pool`, 410 | minVolumeFilter: options.trade_volume_in_usd_greater_than || "none", 411 | }; 412 | } 413 | 414 | // Gasless Aggregator API Methods 415 | async getGaslessPrice(params) { 416 | const { chainId, buyToken, sellToken, sellAmount } = params; 417 | 418 | if (!chainId || !buyToken || !sellToken || !sellAmount) { 419 | throw new Error( 420 | "Missing required parameters: chainId, buyToken, sellToken, sellAmount" 421 | ); 422 | } 423 | 424 | const result = await this.agg.getGaslessPrice(params); 425 | 426 | return { 427 | message: "Gasless swap price retrieved successfully", 428 | data: result, 429 | note: "This is a gasless swap - no ETH needed for gas fees", 430 | }; 431 | } 432 | 433 | async getGaslessQuote(params) { 434 | const { chainId, buyToken, sellToken, sellAmount } = params; 435 | 436 | if (!chainId || !buyToken || !sellToken || !sellAmount) { 437 | throw new Error( 438 | "Missing required parameters: chainId, buyToken, sellToken, sellAmount" 439 | ); 440 | } 441 | 442 | // Add taker address if not provided (required for gasless quotes) 443 | const quoteParams = { 444 | ...params, 445 | taker: params.taker || this.userAddress, 446 | }; 447 | 448 | if (!quoteParams.taker) { 449 | throw new Error("Taker address is required for gasless quotes"); 450 | } 451 | 452 | const result = await this.agg.getGaslessQuote(quoteParams); 453 | 454 | return { 455 | message: "Gasless swap quote retrieved successfully", 456 | data: result, 457 | nextSteps: [ 458 | "1. Review the quote details including approval and trade objects", 459 | "2. Use submit_gasless_swap tool to execute this gasless swap", 460 | "3. Both approval and trade signatures will be handled automatically", 461 | ], 462 | gaslessInfo: { 463 | hasApproval: !!result.approval, 464 | hasTrade: !!result.trade, 465 | approvalType: result.approval?.type, 466 | tradeType: result.trade?.type, 467 | }, 468 | }; 469 | } 470 | 471 | async submitGaslessSwap(params) { 472 | const { quoteData } = params; 473 | 474 | if (!quoteData) { 475 | throw new Error("Quote data from gasless quote is required"); 476 | } 477 | 478 | if (!this.userPrivateKey) { 479 | throw new Error( 480 | "User private key is required for gasless swap execution" 481 | ); 482 | } 483 | 484 | try { 485 | console.log("🚀 Processing gasless swap..."); 486 | 487 | // Prepare the submission data - extract chainId from trade domain 488 | const chainId = 489 | quoteData.trade?.eip712?.domain?.chainId || params.chainId; 490 | if (!chainId) { 491 | throw new Error("Chain ID not found in quote data or parameters"); 492 | } 493 | 494 | const submissionData = { 495 | chainId: chainId, 496 | }; 497 | 498 | // Sign approval if present 499 | if (quoteData.approval) { 500 | console.log("🔐 Signing gasless approval..."); 501 | const signedApproval = await this.blockchain.signGaslessApproval( 502 | quoteData.approval 503 | ); 504 | submissionData.approval = signedApproval; 505 | console.log("✅ Approval signed"); 506 | } 507 | 508 | // Sign trade (always required) 509 | if (!quoteData.trade) { 510 | throw new Error("Trade data is required in gasless quote"); 511 | } 512 | 513 | console.log("🔐 Signing gasless trade..."); 514 | const signedTrade = await this.blockchain.signGaslessTrade( 515 | quoteData.trade 516 | ); 517 | submissionData.trade = signedTrade; 518 | console.log("✅ Trade signed"); 519 | 520 | // Submit to Aggregator gasless API 521 | console.log("📤 Submitting gasless swap to Agg..."); 522 | const result = await this.agg.submitGaslessSwap(submissionData); 523 | 524 | return { 525 | message: "Gasless swap submitted successfully", 526 | data: result, 527 | nextSteps: [ 528 | "1. Gasless swap has been submitted to relayer", 529 | "2. Monitor status using get_gasless_status tool", 530 | "3. No gas fees required - relayer handles execution", 531 | `4. Trade hash: ${result.tradeHash}`, 532 | ], 533 | gaslessInfo: { 534 | tradeHash: result.tradeHash, 535 | approvalSigned: !!submissionData.approval, 536 | tradeSigned: !!submissionData.trade, 537 | relayerHandled: true, 538 | }, 539 | }; 540 | } catch (error) { 541 | throw new Error(`Gasless swap submission failed: ${error.message}`); 542 | } 543 | } 544 | 545 | async getGaslessStatus(params) { 546 | const { tradeHash, chainId } = params; 547 | 548 | if (!tradeHash || !chainId) { 549 | throw new Error("Missing required parameters: tradeHash, chainId"); 550 | } 551 | 552 | const result = await this.agg.getGaslessStatus(tradeHash, chainId); 553 | 554 | return { 555 | message: `Gasless swap status for ${tradeHash} retrieved successfully`, 556 | data: result, 557 | summary: `Status: ${result.status || "unknown"}`, 558 | gaslessInfo: { 559 | tradeHash, 560 | chainId, 561 | isGasless: true, 562 | relayerManaged: true, 563 | }, 564 | }; 565 | } 566 | 567 | async getGaslessChains() { 568 | const result = await this.agg.getGaslessChains(); 569 | 570 | return { 571 | message: "Gasless supported chains retrieved successfully", 572 | data: result, 573 | summary: `Found ${ 574 | result.chains?.length || 0 575 | } chains supporting gasless swaps`, 576 | note: "These chains support meta-transaction based gasless swaps", 577 | }; 578 | } 579 | 580 | async getGaslessApprovalTokens(params = {}) { 581 | // Default to Base chain if no chainId provided 582 | const chainId = params.chainId || 8453; 583 | 584 | const result = await this.agg.getGaslessApprovalTokens(chainId); 585 | 586 | return { 587 | message: "Gasless approval tokens retrieved successfully", 588 | data: result, 589 | summary: `Found ${ 590 | result.tokens?.length || 0 591 | } tokens supporting gasless approvals on chain ${chainId}`, 592 | note: "These tokens support EIP-2612 permit or meta-transaction approvals", 593 | chainId, 594 | }; 595 | } 596 | 597 | // Portfolio API Methods 598 | async getPortfolioTokens(params) { 599 | const { 600 | addresses, 601 | withMetadata, 602 | withPrices, 603 | includeNativeTokens, 604 | networks, 605 | } = params; 606 | 607 | // Use provided addresses or default to USER_ADDRESS with specified networks 608 | let targetAddresses; 609 | if (addresses && Array.isArray(addresses)) { 610 | targetAddresses = addresses; 611 | } else if (this.userAddress) { 612 | // Default to USER_ADDRESS with provided networks or common networks 613 | const defaultNetworks = networks || ["eth-mainnet", "base-mainnet"]; 614 | targetAddresses = [ 615 | { 616 | address: this.userAddress, 617 | networks: defaultNetworks, 618 | }, 619 | ]; 620 | } else { 621 | throw new Error( 622 | "Either addresses parameter or USER_ADDRESS environment variable is required" 623 | ); 624 | } 625 | 626 | const result = await this.agg.getPortfolioTokens(targetAddresses, { 627 | withMetadata, 628 | withPrices, 629 | includeNativeTokens, 630 | }); 631 | 632 | return { 633 | message: "Portfolio tokens retrieved successfully", 634 | data: result, 635 | summary: `Retrieved tokens for ${ 636 | targetAddresses.length 637 | } address(es) across ${targetAddresses.reduce( 638 | (total, addr) => total + addr.networks.length, 639 | 0 640 | )} network(s)`, 641 | addressUsed: targetAddresses[0].address, 642 | options: { 643 | withMetadata: withMetadata !== false, 644 | withPrices: withPrices !== false, 645 | includeNativeTokens: includeNativeTokens || false, 646 | }, 647 | }; 648 | } 649 | 650 | async getPortfolioBalances(params) { 651 | const { addresses, includeNativeTokens, networks } = params; 652 | 653 | // Use provided addresses or default to USER_ADDRESS with specified networks 654 | let targetAddresses; 655 | if (addresses && Array.isArray(addresses)) { 656 | targetAddresses = addresses; 657 | } else if (this.userAddress) { 658 | // Default to USER_ADDRESS with provided networks or common networks 659 | const defaultNetworks = networks || ["eth-mainnet", "base-mainnet"]; 660 | targetAddresses = [ 661 | { 662 | address: this.userAddress, 663 | networks: defaultNetworks, 664 | }, 665 | ]; 666 | } else { 667 | throw new Error( 668 | "Either addresses parameter or USER_ADDRESS environment variable is required" 669 | ); 670 | } 671 | 672 | const result = await this.agg.getPortfolioBalances(targetAddresses, { 673 | includeNativeTokens, 674 | }); 675 | 676 | return { 677 | message: "Portfolio balances retrieved successfully", 678 | data: result, 679 | summary: `Retrieved balances for ${ 680 | targetAddresses.length 681 | } address(es) across ${targetAddresses.reduce( 682 | (total, addr) => total + addr.networks.length, 683 | 0 684 | )} network(s)`, 685 | addressUsed: targetAddresses[0].address, 686 | note: "Balances only - no prices or metadata for faster response", 687 | options: { 688 | includeNativeTokens: includeNativeTokens || false, 689 | }, 690 | }; 691 | } 692 | 693 | async getPortfolioTransactions(params) { 694 | const { addresses, before, after, limit, networks } = params; 695 | 696 | // Use provided addresses or default to USER_ADDRESS with specified networks 697 | let targetAddresses; 698 | if (addresses && Array.isArray(addresses)) { 699 | targetAddresses = addresses; 700 | } else if (this.userAddress) { 701 | // Default to USER_ADDRESS with provided networks or BETA supported networks 702 | const defaultNetworks = networks || ["eth-mainnet", "base-mainnet"]; 703 | targetAddresses = [ 704 | { 705 | address: this.userAddress, 706 | networks: defaultNetworks, 707 | }, 708 | ]; 709 | } else { 710 | throw new Error( 711 | "Either addresses parameter or USER_ADDRESS environment variable is required" 712 | ); 713 | } 714 | 715 | if (targetAddresses.length !== 1) { 716 | throw new Error( 717 | "Transactions API currently supports only 1 address (BETA limitation)" 718 | ); 719 | } 720 | 721 | const result = await this.agg.getPortfolioTransactions(targetAddresses, { 722 | before, 723 | after, 724 | limit, 725 | }); 726 | 727 | return { 728 | message: "Portfolio transactions retrieved successfully", 729 | data: result, 730 | summary: `Retrieved ${ 731 | result.transactions?.length || 0 732 | } transactions for address ${targetAddresses[0].address}`, 733 | addressUsed: targetAddresses[0].address, 734 | pagination: { 735 | limit: limit || 25, 736 | before: result.before, 737 | after: result.after, 738 | totalCount: result.totalCount, 739 | }, 740 | beta: { 741 | limitations: 742 | "Currently supports 1 address and max 2 networks (eth-mainnet, base-mainnet)", 743 | note: "This endpoint is in BETA with limited functionality", 744 | }, 745 | }; 746 | } 747 | 748 | // Conversion utility methods 749 | async convertWeiToFormatted(params) { 750 | const { amount, decimals } = params; 751 | 752 | if (!amount) { 753 | throw new Error("amount is required"); 754 | } 755 | 756 | if (decimals === undefined || decimals === null) { 757 | throw new Error("decimals is required"); 758 | } 759 | 760 | try { 761 | // Convert the amount to a BigNumber and format it 762 | const formattedAmount = ethers.formatUnits(amount.toString(), decimals); 763 | 764 | return { 765 | message: "Wei to formatted conversion completed successfully", 766 | data: { 767 | originalAmount: amount.toString(), 768 | decimals: decimals, 769 | formattedAmount: formattedAmount, 770 | unit: decimals === 18 ? "ETH" : `${decimals} decimals`, 771 | }, 772 | summary: `Converted ${amount} wei to ${formattedAmount} (${decimals} decimals)`, 773 | }; 774 | } catch (error) { 775 | throw new Error(`Wei to formatted conversion failed: ${error.message}`); 776 | } 777 | } 778 | 779 | async convertFormattedToWei(params) { 780 | const { amount, decimals } = params; 781 | 782 | if (!amount) { 783 | throw new Error("amount is required"); 784 | } 785 | 786 | if (decimals === undefined || decimals === null) { 787 | throw new Error("decimals is required"); 788 | } 789 | 790 | try { 791 | // Convert the formatted amount to wei (BigNumber) 792 | const weiAmount = ethers.parseUnits(amount.toString(), decimals); 793 | 794 | return { 795 | message: "Formatted to wei conversion completed successfully", 796 | data: { 797 | originalAmount: amount.toString(), 798 | decimals: decimals, 799 | weiAmount: weiAmount.toString(), 800 | unit: decimals === 18 ? "ETH" : `${decimals} decimals`, 801 | }, 802 | summary: `Converted ${amount} (${decimals} decimals) to ${weiAmount.toString()} wei`, 803 | }; 804 | } catch (error) { 805 | throw new Error(`Formatted to wei conversion failed: ${error.message}`); 806 | } 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // src/index.js 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | ListToolsRequestSchema, 7 | CallToolRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { TOOL_NAMES, AG_URL } from "./constants.js"; 10 | import { ToolService } from "./toolService.js"; 11 | import { ethers } from "ethers"; 12 | import fs from "fs"; 13 | import path from "path"; 14 | import os from "os"; 15 | import "dotenv/config"; 16 | 17 | // Handle command line arguments 18 | const args = process.argv.slice(2); 19 | 20 | // Check for wallet creation command 21 | if (args.includes("--create-wallet")) { 22 | console.log("🔐 Creating new Ethereum wallet...\n"); 23 | 24 | // Generate a new random wallet 25 | const wallet = ethers.Wallet.createRandom(); 26 | 27 | console.log("✅ Wallet created successfully!\n"); 28 | console.log("📋 Your Wallet Details:"); 29 | console.log("=".repeat(50)); 30 | console.log(`Private Key: ${wallet.privateKey}`); 31 | console.log(`Wallet Address: ${wallet.address}`); 32 | console.log("=".repeat(50)); 33 | console.log("\n🔒 SECURITY WARNINGS:"); 34 | console.log("• Keep your private key SECRET and SECURE"); 35 | console.log("• Never share your private key with anyone"); 36 | console.log("• Store it in a secure password manager"); 37 | console.log("• This wallet has NO FUNDS - you need to deposit crypto"); 38 | console.log("\n💡 Next Steps:"); 39 | console.log("1. Copy the private key and wallet address above"); 40 | console.log("2. Update your MCP configuration with these values"); 41 | console.log("3. Send some ETH/tokens to your new wallet address"); 42 | console.log("4. Start trading with your DeFi Trading Agent!"); 43 | console.log("\n📚 For Claude Code users:"); 44 | console.log( 45 | "Run these commands to update your configuration, for other clients just update your config with the new address and private key:" 46 | ); 47 | console.log("claude mcp remove defi-trading"); 48 | console.log(`claude mcp add defi-trading \\`); 49 | console.log(` -e USER_PRIVATE_KEY=${wallet.privateKey} \\`); 50 | console.log(` -e USER_ADDRESS=${wallet.address} \\`); 51 | console.log(` -e COINGECKO_API_KEY=your_coingecko_api_key \\`); 52 | console.log(` -e ALCHEMY_API_KEY=your_alchemy_api_key \\`); 53 | console.log(` -- npx defi-trading-mcp`); 54 | 55 | process.exit(0); 56 | } 57 | 58 | // Load environment variables 59 | const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY; 60 | const USER_ADDRESS = process.env.USER_ADDRESS; 61 | const COINGECKO_API_KEY = process.env.COINGECKO_API_KEY; 62 | const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; 63 | 64 | // Initialize tool service with environment variables 65 | const toolService = new ToolService( 66 | AG_URL, 67 | USER_PRIVATE_KEY, 68 | USER_ADDRESS, 69 | COINGECKO_API_KEY, 70 | ALCHEMY_API_KEY 71 | ); 72 | 73 | const server = new Server( 74 | { 75 | name: "Defi-trading-mcp", 76 | version: "1.0.0", 77 | }, 78 | { 79 | capabilities: { 80 | tools: {}, 81 | }, 82 | } 83 | ); 84 | 85 | // Set up tool listing 86 | server.setRequestHandler(ListToolsRequestSchema, async () => { 87 | return { 88 | tools: [ 89 | { 90 | name: TOOL_NAMES.GET_SWAP_PRICE, 91 | description: 92 | "Get indicative price for a token swap using Aggregator Protocol", 93 | inputSchema: { 94 | type: "object", 95 | properties: { 96 | chainId: { 97 | type: "integer", 98 | description: "Blockchain ID (e.g., 1 for Ethereum)", 99 | }, 100 | buyToken: { 101 | type: "string", 102 | description: "Contract address of token to buy", 103 | }, 104 | sellToken: { 105 | type: "string", 106 | description: "Contract address of token to sell", 107 | }, 108 | sellAmount: { 109 | type: "string", 110 | description: "Amount of sellToken in base units", 111 | }, 112 | taker: { 113 | type: "string", 114 | description: "Address executing the trade (optional)", 115 | }, 116 | }, 117 | required: ["chainId", "buyToken", "sellToken", "sellAmount"], 118 | }, 119 | }, 120 | { 121 | name: TOOL_NAMES.GET_SWAP_QUOTE, 122 | description: 123 | "Get executable quote with transaction data for a token swap", 124 | inputSchema: { 125 | type: "object", 126 | properties: { 127 | chainId: { 128 | type: "integer", 129 | description: "Blockchain ID (e.g., 1 for Ethereum)", 130 | }, 131 | buyToken: { 132 | type: "string", 133 | description: "Contract address of token to buy", 134 | }, 135 | sellToken: { 136 | type: "string", 137 | description: "Contract address of token to sell", 138 | }, 139 | sellAmount: { 140 | type: "string", 141 | description: "Amount of sellToken in base units", 142 | }, 143 | taker: { 144 | type: "string", 145 | description: 146 | "Address executing the trade (optional, uses USER_ADDRESS from env)", 147 | }, 148 | slippageBps: { 149 | type: "integer", 150 | description: 151 | "Maximum acceptable slippage in basis points (optional, default: 100)", 152 | }, 153 | }, 154 | required: ["chainId", "buyToken", "sellToken", "sellAmount"], 155 | }, 156 | }, 157 | { 158 | name: TOOL_NAMES.EXECUTE_SWAP, 159 | description: "Execute a swap transaction (requires quote data)", 160 | inputSchema: { 161 | type: "object", 162 | properties: { 163 | quoteData: { 164 | type: "object", 165 | description: "Quote data from get_swap_quote", 166 | }, 167 | }, 168 | required: ["quoteData"], 169 | }, 170 | }, 171 | { 172 | name: TOOL_NAMES.GET_SUPPORTED_CHAINS, 173 | description: 174 | "Get list of blockchain networks supported by Aggregator Protocol", 175 | inputSchema: { 176 | type: "object", 177 | properties: {}, 178 | required: [], 179 | }, 180 | }, 181 | { 182 | name: TOOL_NAMES.GET_LIQUIDITY_SOURCES, 183 | description: 184 | "Get list of liquidity sources available on a specific chain", 185 | inputSchema: { 186 | type: "object", 187 | properties: { 188 | chainId: { 189 | type: "integer", 190 | description: "Blockchain ID to get sources for", 191 | }, 192 | }, 193 | required: ["chainId"], 194 | }, 195 | }, 196 | { 197 | name: TOOL_NAMES.GET_TOKEN_PRICE, 198 | description: 199 | "Get token prices by contract addresses using CoinGecko API", 200 | inputSchema: { 201 | type: "object", 202 | properties: { 203 | network: { 204 | type: "string", 205 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 206 | }, 207 | addresses: { 208 | type: "string", 209 | description: 210 | "Token contract addresses, comma-separated for multiple tokens", 211 | }, 212 | include_market_cap: { 213 | type: "boolean", 214 | description: "Include market capitalization (optional)", 215 | }, 216 | mcap_fdv_fallback: { 217 | type: "boolean", 218 | description: 219 | "Return FDV if market cap is not available (optional)", 220 | }, 221 | include_24hr_vol: { 222 | type: "boolean", 223 | description: "Include 24hr volume (optional)", 224 | }, 225 | include_24hr_price_change: { 226 | type: "boolean", 227 | description: "Include 24hr price change (optional)", 228 | }, 229 | include_total_reserve_in_usd: { 230 | type: "boolean", 231 | description: "Include total reserve in USD (optional)", 232 | }, 233 | }, 234 | required: ["network", "addresses"], 235 | }, 236 | }, 237 | { 238 | name: TOOL_NAMES.GET_COINGECKO_NETWORKS, 239 | description: 240 | "Get list of supported networks on CoinGecko/GeckoTerminal", 241 | inputSchema: { 242 | type: "object", 243 | properties: { 244 | page: { 245 | type: "integer", 246 | description: "Page number for pagination (optional, default: 1)", 247 | }, 248 | }, 249 | required: [], 250 | }, 251 | }, 252 | { 253 | name: TOOL_NAMES.GET_SUPPORTED_DEXES, 254 | description: "Get list of supported DEXes on a specific network", 255 | inputSchema: { 256 | type: "object", 257 | properties: { 258 | network: { 259 | type: "string", 260 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 261 | }, 262 | page: { 263 | type: "integer", 264 | description: "Page number for pagination (optional, default: 1)", 265 | }, 266 | }, 267 | required: ["network"], 268 | }, 269 | }, 270 | { 271 | name: TOOL_NAMES.GET_TRENDING_POOLS, 272 | description: "Get trending pools across all networks on GeckoTerminal", 273 | inputSchema: { 274 | type: "object", 275 | properties: { 276 | include: { 277 | type: "string", 278 | description: 279 | "Attributes to include: 'base_token', 'quote_token', 'dex', 'network' (comma-separated)", 280 | }, 281 | page: { 282 | type: "integer", 283 | description: "Page number for pagination (optional, default: 1)", 284 | }, 285 | duration: { 286 | type: "string", 287 | description: 288 | "Duration for trending: '5m', '1h', '6h', '24h' (optional, default: '24h')", 289 | enum: ["5m", "1h", "6h", "24h"], 290 | }, 291 | }, 292 | required: [], 293 | }, 294 | }, 295 | { 296 | name: TOOL_NAMES.GET_TRENDING_POOLS_BY_NETWORK, 297 | description: "Get trending pools on a specific network", 298 | inputSchema: { 299 | type: "object", 300 | properties: { 301 | network: { 302 | type: "string", 303 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 304 | }, 305 | include: { 306 | type: "string", 307 | description: 308 | "Attributes to include: 'base_token', 'quote_token', 'dex' (comma-separated)", 309 | }, 310 | page: { 311 | type: "integer", 312 | description: "Page number for pagination (optional, default: 1)", 313 | }, 314 | duration: { 315 | type: "string", 316 | description: 317 | "Duration for trending: '5m', '1h', '6h', '24h' (optional, default: '24h')", 318 | enum: ["5m", "1h", "6h", "24h"], 319 | }, 320 | }, 321 | required: ["network"], 322 | }, 323 | }, 324 | { 325 | name: TOOL_NAMES.GET_MULTIPLE_POOLS_DATA, 326 | description: "Get data for multiple pools by their contract addresses", 327 | inputSchema: { 328 | type: "object", 329 | properties: { 330 | network: { 331 | type: "string", 332 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 333 | }, 334 | addresses: { 335 | type: "string", 336 | description: 337 | "Pool contract addresses, comma-separated for multiple pools", 338 | }, 339 | include: { 340 | type: "string", 341 | description: 342 | "Attributes to include: 'base_token', 'quote_token', 'dex' (comma-separated)", 343 | }, 344 | include_volume_breakdown: { 345 | type: "boolean", 346 | description: 347 | "Include volume breakdown (optional, default: false)", 348 | }, 349 | }, 350 | required: ["network", "addresses"], 351 | }, 352 | }, 353 | { 354 | name: TOOL_NAMES.GET_TOP_POOLS_BY_DEX, 355 | description: "Get top pools on a specific DEX", 356 | inputSchema: { 357 | type: "object", 358 | properties: { 359 | network: { 360 | type: "string", 361 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 362 | }, 363 | dex: { 364 | type: "string", 365 | description: "DEX ID (e.g., 'uniswap_v3', 'sushiswap')", 366 | }, 367 | include: { 368 | type: "string", 369 | description: 370 | "Attributes to include: 'base_token', 'quote_token', 'dex' (comma-separated)", 371 | }, 372 | page: { 373 | type: "integer", 374 | description: "Page number for pagination (optional, default: 1)", 375 | }, 376 | sort: { 377 | type: "string", 378 | description: 379 | "Sort by: 'h24_tx_count_desc', 'h24_volume_usd_desc' (optional, default: 'h24_tx_count_desc')", 380 | enum: ["h24_tx_count_desc", "h24_volume_usd_desc"], 381 | }, 382 | }, 383 | required: ["network", "dex"], 384 | }, 385 | }, 386 | { 387 | name: TOOL_NAMES.GET_NEW_POOLS, 388 | description: "Get latest new pools across all networks", 389 | inputSchema: { 390 | type: "object", 391 | properties: { 392 | include: { 393 | type: "string", 394 | description: 395 | "Attributes to include: 'base_token', 'quote_token', 'dex', 'network' (comma-separated)", 396 | }, 397 | page: { 398 | type: "integer", 399 | description: "Page number for pagination (optional, default: 1)", 400 | }, 401 | }, 402 | required: [], 403 | }, 404 | }, 405 | { 406 | name: TOOL_NAMES.SEARCH_POOLS, 407 | description: 408 | "Search for pools by query (pool address, token address, or token symbol)", 409 | inputSchema: { 410 | type: "object", 411 | properties: { 412 | query: { 413 | type: "string", 414 | description: 415 | "Search query (pool address, token address, or token symbol)", 416 | }, 417 | network: { 418 | type: "string", 419 | description: 420 | "Network ID to search on (optional, e.g., 'eth', 'bsc', 'polygon_pos')", 421 | }, 422 | include: { 423 | type: "string", 424 | description: 425 | "Attributes to include: 'base_token', 'quote_token', 'dex' (comma-separated)", 426 | }, 427 | page: { 428 | type: "integer", 429 | description: "Page number for pagination (optional, default: 1)", 430 | }, 431 | }, 432 | required: ["query"], 433 | }, 434 | }, 435 | { 436 | name: TOOL_NAMES.GET_TOP_POOLS_BY_TOKEN, 437 | description: "Get top pools for a specific token by contract address", 438 | inputSchema: { 439 | type: "object", 440 | properties: { 441 | network: { 442 | type: "string", 443 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 444 | }, 445 | tokenAddress: { 446 | type: "string", 447 | description: "Token contract address", 448 | }, 449 | include: { 450 | type: "string", 451 | description: 452 | "Attributes to include: 'base_token', 'quote_token', 'dex' (comma-separated)", 453 | }, 454 | page: { 455 | type: "integer", 456 | description: "Page number for pagination (optional, default: 1)", 457 | }, 458 | sort: { 459 | type: "string", 460 | description: 461 | "Sort by: 'h24_volume_usd_liquidity_desc', 'h24_tx_count_desc', 'h24_volume_usd_desc' (optional, default: 'h24_volume_usd_liquidity_desc')", 462 | enum: [ 463 | "h24_volume_usd_liquidity_desc", 464 | "h24_tx_count_desc", 465 | "h24_volume_usd_desc", 466 | ], 467 | }, 468 | }, 469 | required: ["network", "tokenAddress"], 470 | }, 471 | }, 472 | { 473 | name: TOOL_NAMES.GET_TOKEN_DATA, 474 | description: "Get specific token data by contract address", 475 | inputSchema: { 476 | type: "object", 477 | properties: { 478 | network: { 479 | type: "string", 480 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 481 | }, 482 | address: { 483 | type: "string", 484 | description: "Token contract address", 485 | }, 486 | include: { 487 | type: "string", 488 | description: "Attributes to include: 'top_pools' (optional)", 489 | enum: ["top_pools"], 490 | }, 491 | }, 492 | required: ["network", "address"], 493 | }, 494 | }, 495 | { 496 | name: TOOL_NAMES.GET_MULTIPLE_TOKENS_DATA, 497 | description: "Get data for multiple tokens by their contract addresses", 498 | inputSchema: { 499 | type: "object", 500 | properties: { 501 | network: { 502 | type: "string", 503 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 504 | }, 505 | addresses: { 506 | type: "string", 507 | description: 508 | "Token contract addresses, comma-separated for multiple tokens", 509 | }, 510 | include: { 511 | type: "string", 512 | description: "Attributes to include: 'top_pools' (optional)", 513 | enum: ["top_pools"], 514 | }, 515 | }, 516 | required: ["network", "addresses"], 517 | }, 518 | }, 519 | { 520 | name: TOOL_NAMES.GET_TOKEN_INFO, 521 | description: 522 | "Get detailed token information including socials, websites, and description", 523 | inputSchema: { 524 | type: "object", 525 | properties: { 526 | network: { 527 | type: "string", 528 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 529 | }, 530 | address: { 531 | type: "string", 532 | description: "Token contract address", 533 | }, 534 | }, 535 | required: ["network", "address"], 536 | }, 537 | }, 538 | { 539 | name: TOOL_NAMES.GET_RECENTLY_UPDATED_TOKENS, 540 | description: "Get recently updated tokens with their information", 541 | inputSchema: { 542 | type: "object", 543 | properties: { 544 | include: { 545 | type: "string", 546 | description: "Attributes to include: 'network' (optional)", 547 | enum: ["network"], 548 | }, 549 | network: { 550 | type: "string", 551 | description: 552 | "Network ID to filter by (optional, e.g., 'eth', 'bsc', 'polygon_pos')", 553 | }, 554 | }, 555 | required: [], 556 | }, 557 | }, 558 | { 559 | name: TOOL_NAMES.GET_POOL_OHLCV, 560 | description: 561 | "Get OHLCV (Open, High, Low, Close, Volume) data for a pool", 562 | inputSchema: { 563 | type: "object", 564 | properties: { 565 | network: { 566 | type: "string", 567 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 568 | }, 569 | poolAddress: { 570 | type: "string", 571 | description: "Pool contract address", 572 | }, 573 | timeframe: { 574 | type: "string", 575 | description: "Timeframe for OHLCV data: 'day', 'hour', 'minute'", 576 | enum: ["day", "hour", "minute"], 577 | }, 578 | aggregate: { 579 | type: "string", 580 | description: "Aggregate interval (optional, default: '1')", 581 | }, 582 | before_timestamp: { 583 | type: "integer", 584 | description: "Get data before this timestamp (optional)", 585 | }, 586 | limit: { 587 | type: "integer", 588 | description: "Limit number of results (optional, max: 1000)", 589 | }, 590 | currency: { 591 | type: "string", 592 | description: 593 | "Currency for price data: 'usd', 'token' (optional, default: 'usd')", 594 | enum: ["usd", "token"], 595 | }, 596 | token: { 597 | type: "string", 598 | description: 599 | "Token for price data: 'base', 'quote' (optional, default: 'base')", 600 | enum: ["base", "quote"], 601 | }, 602 | include_empty_intervals: { 603 | type: "boolean", 604 | description: "Include empty intervals (optional, default: false)", 605 | }, 606 | }, 607 | required: ["network", "poolAddress", "timeframe"], 608 | }, 609 | }, 610 | { 611 | name: TOOL_NAMES.GET_POOL_TRADES, 612 | description: "Get recent trades for a specific pool", 613 | inputSchema: { 614 | type: "object", 615 | properties: { 616 | network: { 617 | type: "string", 618 | description: "Network ID (e.g., 'eth', 'bsc', 'polygon_pos')", 619 | }, 620 | poolAddress: { 621 | type: "string", 622 | description: "Pool contract address", 623 | }, 624 | trade_volume_in_usd_greater_than: { 625 | type: "number", 626 | description: 627 | "Filter trades with volume greater than this USD amount (optional)", 628 | }, 629 | }, 630 | required: ["network", "poolAddress"], 631 | }, 632 | }, 633 | { 634 | name: TOOL_NAMES.GET_GASLESS_PRICE, 635 | description: 636 | "Get indicative price for a gasless token swap (no gas fees required)", 637 | inputSchema: { 638 | type: "object", 639 | properties: { 640 | chainId: { 641 | type: "integer", 642 | description: "Blockchain ID (e.g., 8453 for Base)", 643 | }, 644 | buyToken: { 645 | type: "string", 646 | description: "Contract address of token to buy", 647 | }, 648 | sellToken: { 649 | type: "string", 650 | description: "Contract address of token to sell", 651 | }, 652 | sellAmount: { 653 | type: "string", 654 | description: "Amount of sellToken in base units", 655 | }, 656 | taker: { 657 | type: "string", 658 | description: "Address executing the trade (optional)", 659 | }, 660 | slippageBps: { 661 | type: "integer", 662 | description: 663 | "Maximum acceptable slippage in basis points (optional, min: 30)", 664 | }, 665 | }, 666 | required: ["chainId", "buyToken", "sellToken", "sellAmount"], 667 | }, 668 | }, 669 | { 670 | name: TOOL_NAMES.GET_GASLESS_QUOTE, 671 | description: 672 | "Get executable quote for a gasless token swap with EIP-712 signature data", 673 | inputSchema: { 674 | type: "object", 675 | properties: { 676 | chainId: { 677 | type: "integer", 678 | description: "Blockchain ID (e.g., 8453 for Base)", 679 | }, 680 | buyToken: { 681 | type: "string", 682 | description: "Contract address of token to buy", 683 | }, 684 | sellToken: { 685 | type: "string", 686 | description: "Contract address of token to sell", 687 | }, 688 | sellAmount: { 689 | type: "string", 690 | description: "Amount of sellToken in base units", 691 | }, 692 | taker: { 693 | type: "string", 694 | description: 695 | "Address executing the trade (required for gasless quotes, uses USER_ADDRESS from env if not provided)", 696 | }, 697 | slippageBps: { 698 | type: "integer", 699 | description: 700 | "Maximum acceptable slippage in basis points (optional, min: 30)", 701 | }, 702 | }, 703 | required: ["chainId", "buyToken", "sellToken", "sellAmount"], 704 | }, 705 | }, 706 | { 707 | name: TOOL_NAMES.SUBMIT_GASLESS_SWAP, 708 | description: 709 | "Submit a gasless swap by signing approval and trade messages (no gas fees required)", 710 | inputSchema: { 711 | type: "object", 712 | properties: { 713 | quoteData: { 714 | type: "object", 715 | description: "Quote data from get_gasless_quote", 716 | }, 717 | chainId: { 718 | type: "integer", 719 | description: "Blockchain ID (optional if included in quoteData)", 720 | }, 721 | }, 722 | required: ["quoteData"], 723 | }, 724 | }, 725 | { 726 | name: TOOL_NAMES.GET_GASLESS_STATUS, 727 | description: "Get the status of a submitted gasless swap", 728 | inputSchema: { 729 | type: "object", 730 | properties: { 731 | tradeHash: { 732 | type: "string", 733 | description: "Trade hash from gasless swap submission", 734 | }, 735 | chainId: { 736 | type: "integer", 737 | description: "Blockchain ID where the trade was submitted", 738 | }, 739 | }, 740 | required: ["tradeHash", "chainId"], 741 | }, 742 | }, 743 | { 744 | name: TOOL_NAMES.GET_GASLESS_CHAINS, 745 | description: 746 | "Get list of blockchain networks that support gasless swaps", 747 | inputSchema: { 748 | type: "object", 749 | properties: {}, 750 | required: [], 751 | }, 752 | }, 753 | { 754 | name: TOOL_NAMES.GET_GASLESS_APPROVAL_TOKENS, 755 | description: 756 | "Get list of tokens that support gasless approvals (EIP-2612 permit)", 757 | inputSchema: { 758 | type: "object", 759 | properties: { 760 | chainId: { 761 | type: "integer", 762 | description: 763 | "Blockchain ID (e.g., 8453 for Base, defaults to 8453 if not provided)", 764 | }, 765 | }, 766 | required: [], 767 | }, 768 | }, 769 | { 770 | name: TOOL_NAMES.GET_PORTFOLIO_TOKENS, 771 | description: 772 | "Get tokens with balances, prices, and metadata for wallet addresses (uses USER_ADDRESS from env if addresses not provided)", 773 | inputSchema: { 774 | type: "object", 775 | properties: { 776 | addresses: { 777 | type: "array", 778 | description: 779 | "Array of address and networks pairs (max 3 addresses, max 20 networks each). Optional - uses USER_ADDRESS from env if not provided", 780 | items: { 781 | type: "object", 782 | properties: { 783 | address: { 784 | type: "string", 785 | description: "Wallet address", 786 | }, 787 | networks: { 788 | type: "array", 789 | items: { 790 | type: "string", 791 | }, 792 | description: 793 | "Network identifiers (e.g., 'eth-mainnet', 'base-mainnet')", 794 | }, 795 | }, 796 | required: ["address", "networks"], 797 | }, 798 | }, 799 | networks: { 800 | type: "array", 801 | items: { 802 | type: "string", 803 | }, 804 | description: 805 | "Network identifiers to use with USER_ADDRESS (e.g., 'eth-mainnet', 'base-mainnet'). Only used when addresses not provided. Defaults to ['eth-mainnet', 'base-mainnet']", 806 | }, 807 | withMetadata: { 808 | type: "boolean", 809 | description: "Include token metadata (optional, default: true)", 810 | }, 811 | withPrices: { 812 | type: "boolean", 813 | description: "Include token prices (optional, default: true)", 814 | }, 815 | includeNativeTokens: { 816 | type: "boolean", 817 | description: 818 | "Include native tokens like ETH (optional, default: false)", 819 | }, 820 | }, 821 | required: [], 822 | }, 823 | }, 824 | { 825 | name: TOOL_NAMES.GET_PORTFOLIO_BALANCES, 826 | description: 827 | "Get token balances for wallet addresses (faster, no prices/metadata, uses USER_ADDRESS from env if addresses not provided)", 828 | inputSchema: { 829 | type: "object", 830 | properties: { 831 | addresses: { 832 | type: "array", 833 | description: 834 | "Array of address and networks pairs (max 3 addresses, max 20 networks each). Optional - uses USER_ADDRESS from env if not provided", 835 | items: { 836 | type: "object", 837 | properties: { 838 | address: { 839 | type: "string", 840 | description: "Wallet address", 841 | }, 842 | networks: { 843 | type: "array", 844 | items: { 845 | type: "string", 846 | }, 847 | description: 848 | "Network identifiers (e.g., 'eth-mainnet', 'base-mainnet')", 849 | }, 850 | }, 851 | required: ["address", "networks"], 852 | }, 853 | }, 854 | networks: { 855 | type: "array", 856 | items: { 857 | type: "string", 858 | }, 859 | description: 860 | "Network identifiers to use with USER_ADDRESS (e.g., 'eth-mainnet', 'base-mainnet'). Only used when addresses not provided. Defaults to ['eth-mainnet', 'base-mainnet']", 861 | }, 862 | includeNativeTokens: { 863 | type: "boolean", 864 | description: 865 | "Include native tokens like ETH (optional, default: false)", 866 | }, 867 | }, 868 | required: [], 869 | }, 870 | }, 871 | { 872 | name: TOOL_NAMES.GET_PORTFOLIO_TRANSACTIONS, 873 | description: 874 | "Get transaction history for a wallet address (BETA: 1 address, ETH/BASE only, uses USER_ADDRESS from env if addresses not provided)", 875 | inputSchema: { 876 | type: "object", 877 | properties: { 878 | addresses: { 879 | type: "array", 880 | description: 881 | "Array with single address and networks (BETA limitation: 1 address, max 2 networks). Optional - uses USER_ADDRESS from env if not provided", 882 | items: { 883 | type: "object", 884 | properties: { 885 | address: { 886 | type: "string", 887 | description: "Wallet address", 888 | }, 889 | networks: { 890 | type: "array", 891 | items: { 892 | type: "string", 893 | enum: ["eth-mainnet", "base-mainnet"], 894 | }, 895 | description: 896 | "Network identifiers (BETA: only eth-mainnet and base-mainnet supported)", 897 | }, 898 | }, 899 | required: ["address", "networks"], 900 | }, 901 | maxItems: 1, 902 | }, 903 | networks: { 904 | type: "array", 905 | items: { 906 | type: "string", 907 | enum: ["eth-mainnet", "base-mainnet"], 908 | }, 909 | description: 910 | "Network identifiers to use with USER_ADDRESS (BETA: only eth-mainnet and base-mainnet supported). Only used when addresses not provided. Defaults to ['eth-mainnet', 'base-mainnet']", 911 | }, 912 | before: { 913 | type: "string", 914 | description: 915 | "Cursor for pagination - get results before this cursor (optional)", 916 | }, 917 | after: { 918 | type: "string", 919 | description: 920 | "Cursor for pagination - get results after this cursor (optional)", 921 | }, 922 | limit: { 923 | type: "integer", 924 | description: 925 | "Number of transactions to return (optional, default: 25, max: 50)", 926 | minimum: 1, 927 | maximum: 50, 928 | }, 929 | }, 930 | required: [], 931 | }, 932 | }, 933 | { 934 | name: TOOL_NAMES.CONVERT_WEI_TO_FORMATTED, 935 | description: 936 | "Convert wei amounts to human-readable format using ethers.js", 937 | inputSchema: { 938 | type: "object", 939 | properties: { 940 | amount: { 941 | type: "string", 942 | description: "Amount in wei (as string to handle large numbers)", 943 | }, 944 | decimals: { 945 | type: "integer", 946 | description: 947 | "Number of decimal places for the token (e.g., 18 for ETH, 6 for USDC)", 948 | }, 949 | }, 950 | required: ["amount", "decimals"], 951 | }, 952 | }, 953 | { 954 | name: TOOL_NAMES.CONVERT_FORMATTED_TO_WEI, 955 | description: "Convert formatted amounts to wei using ethers.js", 956 | inputSchema: { 957 | type: "object", 958 | properties: { 959 | amount: { 960 | type: "string", 961 | description: "Formatted amount (e.g., '1.5' for 1.5 ETH)", 962 | }, 963 | decimals: { 964 | type: "integer", 965 | description: 966 | "Number of decimal places for the token (e.g., 18 for ETH, 6 for USDC)", 967 | }, 968 | }, 969 | required: ["amount", "decimals"], 970 | }, 971 | }, 972 | ], 973 | }; 974 | }); 975 | 976 | // Set up tool execution 977 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 978 | const { name, arguments: args } = request.params; 979 | 980 | try { 981 | let result; 982 | 983 | switch (name) { 984 | case TOOL_NAMES.GET_SWAP_PRICE: 985 | result = await toolService.getSwapPrice(args); 986 | break; 987 | 988 | case TOOL_NAMES.GET_SWAP_QUOTE: 989 | result = await toolService.getSwapQuote(args); 990 | break; 991 | 992 | case TOOL_NAMES.EXECUTE_SWAP: 993 | result = await toolService.executeSwap(args.quoteData); 994 | break; 995 | 996 | case TOOL_NAMES.GET_SUPPORTED_CHAINS: 997 | result = await toolService.getSupportedChains(); 998 | break; 999 | 1000 | case TOOL_NAMES.GET_LIQUIDITY_SOURCES: 1001 | result = await toolService.getLiquiditySources(args.chainId); 1002 | break; 1003 | 1004 | case TOOL_NAMES.GET_TOKEN_PRICE: 1005 | result = await toolService.getTokenPrice(args.network, args.addresses, { 1006 | include_market_cap: args.include_market_cap, 1007 | mcap_fdv_fallback: args.mcap_fdv_fallback, 1008 | include_24hr_vol: args.include_24hr_vol, 1009 | include_24hr_price_change: args.include_24hr_price_change, 1010 | include_total_reserve_in_usd: args.include_total_reserve_in_usd, 1011 | }); 1012 | break; 1013 | 1014 | case TOOL_NAMES.GET_COINGECKO_NETWORKS: 1015 | result = await toolService.getCoinGeckoNetworks(args.page); 1016 | break; 1017 | 1018 | case TOOL_NAMES.GET_SUPPORTED_DEXES: 1019 | result = await toolService.getSupportedDexes(args.network, args.page); 1020 | break; 1021 | 1022 | case TOOL_NAMES.GET_TRENDING_POOLS: 1023 | result = await toolService.getTrendingPools({ 1024 | include: args.include, 1025 | page: args.page, 1026 | duration: args.duration, 1027 | }); 1028 | break; 1029 | 1030 | case TOOL_NAMES.GET_TRENDING_POOLS_BY_NETWORK: 1031 | result = await toolService.getTrendingPoolsByNetwork(args.network, { 1032 | include: args.include, 1033 | page: args.page, 1034 | duration: args.duration, 1035 | }); 1036 | break; 1037 | 1038 | case TOOL_NAMES.GET_MULTIPLE_POOLS_DATA: 1039 | result = await toolService.getMultiplePoolsData( 1040 | args.network, 1041 | args.addresses, 1042 | { 1043 | include: args.include, 1044 | include_volume_breakdown: args.include_volume_breakdown, 1045 | } 1046 | ); 1047 | break; 1048 | 1049 | case TOOL_NAMES.GET_TOP_POOLS_BY_DEX: 1050 | result = await toolService.getTopPoolsByDex(args.network, args.dex, { 1051 | include: args.include, 1052 | page: args.page, 1053 | sort: args.sort, 1054 | }); 1055 | break; 1056 | 1057 | case TOOL_NAMES.GET_NEW_POOLS: 1058 | result = await toolService.getNewPools({ 1059 | include: args.include, 1060 | page: args.page, 1061 | }); 1062 | break; 1063 | 1064 | case TOOL_NAMES.SEARCH_POOLS: 1065 | result = await toolService.searchPools(args.query, { 1066 | network: args.network, 1067 | include: args.include, 1068 | page: args.page, 1069 | }); 1070 | break; 1071 | 1072 | case TOOL_NAMES.GET_TOP_POOLS_BY_TOKEN: 1073 | result = await toolService.getTopPoolsByToken( 1074 | args.network, 1075 | args.tokenAddress, 1076 | { 1077 | include: args.include, 1078 | page: args.page, 1079 | sort: args.sort, 1080 | } 1081 | ); 1082 | break; 1083 | 1084 | case TOOL_NAMES.GET_TOKEN_DATA: 1085 | result = await toolService.getTokenData(args.network, args.address, { 1086 | include: args.include, 1087 | }); 1088 | break; 1089 | 1090 | case TOOL_NAMES.GET_MULTIPLE_TOKENS_DATA: 1091 | result = await toolService.getMultipleTokensData( 1092 | args.network, 1093 | args.addresses, 1094 | { 1095 | include: args.include, 1096 | } 1097 | ); 1098 | break; 1099 | 1100 | case TOOL_NAMES.GET_TOKEN_INFO: 1101 | result = await toolService.getTokenInfo(args.network, args.address); 1102 | break; 1103 | 1104 | case TOOL_NAMES.GET_RECENTLY_UPDATED_TOKENS: 1105 | result = await toolService.getRecentlyUpdatedTokens({ 1106 | include: args.include, 1107 | network: args.network, 1108 | }); 1109 | break; 1110 | 1111 | case TOOL_NAMES.GET_POOL_OHLCV: 1112 | result = await toolService.getPoolOHLCV( 1113 | args.network, 1114 | args.poolAddress, 1115 | args.timeframe, 1116 | { 1117 | aggregate: args.aggregate, 1118 | before_timestamp: args.before_timestamp, 1119 | limit: args.limit, 1120 | currency: args.currency, 1121 | token: args.token, 1122 | include_empty_intervals: args.include_empty_intervals, 1123 | } 1124 | ); 1125 | break; 1126 | 1127 | case TOOL_NAMES.GET_POOL_TRADES: 1128 | result = await toolService.getPoolTrades( 1129 | args.network, 1130 | args.poolAddress, 1131 | { 1132 | trade_volume_in_usd_greater_than: 1133 | args.trade_volume_in_usd_greater_than, 1134 | } 1135 | ); 1136 | break; 1137 | 1138 | case TOOL_NAMES.GET_GASLESS_PRICE: 1139 | result = await toolService.getGaslessPrice(args); 1140 | break; 1141 | 1142 | case TOOL_NAMES.GET_GASLESS_QUOTE: 1143 | result = await toolService.getGaslessQuote(args); 1144 | break; 1145 | 1146 | case TOOL_NAMES.SUBMIT_GASLESS_SWAP: 1147 | result = await toolService.submitGaslessSwap(args); 1148 | break; 1149 | 1150 | case TOOL_NAMES.GET_GASLESS_STATUS: 1151 | result = await toolService.getGaslessStatus(args); 1152 | break; 1153 | 1154 | case TOOL_NAMES.GET_GASLESS_CHAINS: 1155 | result = await toolService.getGaslessChains(); 1156 | break; 1157 | 1158 | case TOOL_NAMES.GET_GASLESS_APPROVAL_TOKENS: 1159 | result = await toolService.getGaslessApprovalTokens(args); 1160 | break; 1161 | 1162 | case TOOL_NAMES.GET_PORTFOLIO_TOKENS: 1163 | result = await toolService.getPortfolioTokens(args); 1164 | break; 1165 | 1166 | case TOOL_NAMES.GET_PORTFOLIO_BALANCES: 1167 | result = await toolService.getPortfolioBalances(args); 1168 | break; 1169 | 1170 | case TOOL_NAMES.GET_PORTFOLIO_TRANSACTIONS: 1171 | result = await toolService.getPortfolioTransactions(args); 1172 | break; 1173 | 1174 | case TOOL_NAMES.CONVERT_WEI_TO_FORMATTED: 1175 | result = await toolService.convertWeiToFormatted(args); 1176 | break; 1177 | 1178 | case TOOL_NAMES.CONVERT_FORMATTED_TO_WEI: 1179 | result = await toolService.convertFormattedToWei(args); 1180 | break; 1181 | 1182 | default: 1183 | throw new Error(`Unknown tool: ${name}`); 1184 | } 1185 | 1186 | // Format the result for MCP protocol 1187 | return { 1188 | content: [ 1189 | { 1190 | type: "text", 1191 | text: 1192 | typeof result === "string" 1193 | ? result 1194 | : JSON.stringify(result, null, 2), 1195 | }, 1196 | ], 1197 | }; 1198 | } catch (error) { 1199 | console.error(`Tool execution failed:`, error); 1200 | throw new Error(`Tool execution failed: ${error.message}`); 1201 | } 1202 | }); 1203 | 1204 | // Start the server 1205 | const transport = new StdioServerTransport(); 1206 | await server.connect(transport); 1207 | console.error("DeFi Trading MCP Server running on stdio"); 1208 | --------------------------------------------------------------------------------