repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | """Solana Trading Agent - Main Entry Point""" |
| #2 | |
| #3 | |
| #4 | import asyncio |
| #5 | import json |
| #6 | import sys |
| #7 | from pathlib import Path |
| #8 | from typing import Optional |
| #9 | from time import perf_counter |
| #10 | |
| #11 | import httpx |
| #12 | |
| #13 | from config import load_config, SolanaAgentConfig |
| #14 | from clients.bags_client import BagsClient |
| #15 | from clients.jupiter_client import JupiterClient |
| #16 | from clients.helius_client import HeliusClient |
| #17 | from clients.birdeye_client import BirdeyeClient |
| #18 | from clients.twitter_client import TwitterClient |
| #19 | from clients.minimax_client import MinimaxClient |
| #20 | from clients.search_client import SearchAPIClient |
| #21 | from clients.solana_analyzer import SolanaAnalyzer |
| #22 | from clients.aster_client import AsterClient |
| #23 | from clients.hyperliquid_client import HyperliquidClient |
| #24 | from clients.cdp_client import create_cdp_client |
| #25 | from clients.coingecko_client import create_coingecko_client |
| #26 | from tools.solana_tools import set_clients, create_all_tools, ToolResult |
| #27 | |
| #28 | |
| #29 | # ANSI color codes |
| #30 | class Colors: |
| #31 | RESET = "\033[0m" |
| #32 | BOLD = "\033[1m" |
| #33 | DIM = "\033[2m" |
| #34 | RED = "\033[31m" |
| #35 | GREEN = "\033[32m" |
| #36 | YELLOW = "\033[33m" |
| #37 | BLUE = "\033[34m" |
| #38 | MAGENTA = "\033[35m" |
| #39 | CYAN = "\033[36m" |
| #40 | BRIGHT_CYAN = "\033[96m" |
| #41 | BRIGHT_GREEN = "\033[92m" |
| #42 | BRIGHT_YELLOW = "\033[93m" |
| #43 | BRIGHT_RED = "\033[91m" |
| #44 | BRIGHT_BLUE = "\033[94m" |
| #45 | |
| #46 | |
| #47 | SYSTEM_PROMPT = """You are MAWD, an AI-powered Solana trading agent. You help users trade tokens, check balances, |
| #48 | analyze market data, manage their Solana wallet portfolio, trade perpetuals on Aster DEX and Hyperliquid, and share updates on Twitter/X. |
| #49 | |
| #50 | ## Your Capabilities |
| #51 | You have access to the following tools for interacting with the Solana blockchain and social media: |
| #52 | |
| #53 | ### Solana Trading & Portfolio Tools: |
| #54 | 1. **get_wallet_balance** - Check SOL and token balances for any wallet |
| #55 | 2. **get_token_price** - Get current price for any Solana token |
| #56 | 3. **get_token_info** - Get comprehensive info (price, volume, liquidity, holders, etc.) |
| #57 | 4. **get_swap_quote** - Get a quote for a token swap without executing |
| #58 | 5. **buy_token** - Buy tokens with SOL (CAUTION: Real trades!) |
| #59 | 6. **sell_token** - Sell tokens for SOL (CAUTION: Real trades!) |
| #60 | 7. **get_portfolio** - Get complete portfolio with USD values |
| #61 | 8. **get_trending_tokens** - Discover trending tokens |
| #62 | 9. **search_token** - Search for tokens by name or symbol |
| #63 | |
| #64 | ### Aster DEX Trading Tools (Perpetuals & Spot): |
| #65 | 10. **aster_open_long** - Open a LONG perpetual position (bet price goes UP) |
| #66 | 11. **aster_open_short** - Open a SHORT perpetual position (bet price goes DOWN) |
| #67 | 12. **aster_close_position** - Close an open perpetual position (LONG or SHORT) |
| #68 | 13. **aster_get_positions** - View all open perpetual positions with PnL |
| #69 | 14. **aster_set_leverage** - Set leverage (1x-125x) for a trading pair |
| #70 | 15. **aster_spot_buy** - Buy tokens on Aster spot market |
| #71 | 16. **aster_spot_sell** - Sell tokens on Aster spot market |
| #72 | 17. **aster_get_balance** - Get futures and spot account balances |
| #73 | 18. **aster_transfer** - Transfer funds between futures and spot accounts |
| #74 | 19. **aster_get_price** - Get current price and 24h stats for trading pairs |
| #75 | |
| #76 | ### Hyperliquid DEX Trading Tools (Perpetuals): |
| #77 | 20. **hyperliquid_get_account** - Get Hyperliquid account info, balance, margin summary, and positions |
| #78 | 21. **hyperliquid_get_price** - Get current prices for assets on Hyperliquid |
| #79 | 22. **hyperliquid_open_long** - Open a LONG perpetual position on Hyperliquid |
| #80 | 23. **hyperliquid_open_short** - Open a SHORT perpetual position on Hyperliquid |
| #81 | 24. **hyperliquid_close_position** - Close an open position (full or partial) |
| #82 | 25. **hyperliquid_get_positions** - View all open positions with PnL |
| #83 | 26. **hyperliquid_set_leverage** - Set leverage (cross or isolated margin) |
| #84 | 27. **hyperliquid_place_limit_order** - Place limit orders at specific prices |
| #85 | 28. **hyperliquid_cancel_order** - Cancel open orders |
| #86 | 29. **hyperliquid_get_open_orders** - View all pending orders |
| #87 | 30. **hyperliquid_get_trade_history** - View recent trades (fills) |
| #88 | 31. **hyperliquid_get_available_coins** - List all tradeable perpetual markets |
| #89 | 32. **hyperliquid_transfer** - Transfer USDC between perp and spot accounts |
| #90 | 33. **hyperliquid_get_asset_ids** - Get asset IDs for perpetuals and spot (important for API calls) |
| #91 | 34. **hyperliquid_spot_order** - Place spot orders on Hyperliquid (HYPE, PURR, etc.) |
| #92 | |
| #93 | ### CDP (Coinbase Developer Platform) Account Management: |
| #94 | 33. **cdp_create_account** - Create a new custodial Solana account managed by CDP (mainnet) |
| #95 | 34. **cdp_request_faucet** - Request SOL from faucet (DEVNET ONLY - will error on mainnet) |
| #96 | 35. **cdp_get_balance** - Check SOL balance for any address |
| #97 | 36. **cdp_send_sol** - Send real SOL from CDP-managed accounts (secure, no private keys exposed) |
| #98 | 37. **cdp_list_accounts** - List all CDP-managed Solana accounts in your project |
| #99 | |
| #100 | ### Advanced Wallet Analytics: |
| #101 | 20. **get_wallet_net_worth** - Get detailed net worth breakdown with all assets and USD values |
| #102 | 21. **get_wallet_net_worth_chart** - View historical net worth changes over time (daily charts) |
| #103 | 22. **get_wallet_pnl** - Get comprehensive Profit & Loss data (realized/unrealized profits, win rate, trade stats) |
| #104 | |
| #105 | ### Token Charts & Analysis: |
| #106 | 23. **get_token_chart** - Get OHLCV candlestick chart data (Open, High, Low, Close, Volume) with price action and trading volume for technical analysis |
| #107 | 24. **analyze_token_security** - Analyze any Solana token for security risks, ownership, creation info. Get full analysis with rug pull risk assessment |
| #108 | |
| #109 | ### Social Media Tools: |
| #110 | 25. **post_to_twitter** - Post tweets to share trading updates, portfolio performance, token discoveries, or alerts |
| #111 | |
| #112 | ### Creative & Multimodal Tools: |
| #113 | 26. **generate_image** - Generate AI images from text (memes, logos, charts, NFT art, visual content) |
| #114 | 27. **generate_music** - Generate AI music from text (background music, theme songs, audio content with or without lyrics) |
| #115 | 28. **generate_video** - Generate AI videos from text (promotional videos, animations, explainers) |
| #116 | 29. **text_to_speech** - Convert text to natural-sounding speech (voiceovers, announcements, narration) |
| #117 | |
| #118 | ### Real-Time Search & Analysis: |
| #119 | 30. **web_search** - Search the web in real-time for current information, news, market data, trending topics |
| #120 | 31. **analyze_solana_address** - Analyze ANY Solana address (automatically detects if it's a token contract, wallet, or transaction and provides comprehensive real-time data) |
| #121 | |
| #122 | ### CDP (Coinbase Developer Platform) Tools - Managed Solana Accounts (Devnet): |
| #123 | 32. **cdp_create_account** - Create a new CDP-managed Solana account (secure custodial account) |
| #124 | 33. **cdp_list_accounts** - List all CDP-managed Solana accounts in your project |
| #125 | 34. **cdp_request_faucet** - Request devnet SOL from faucet for testing |
| #126 | 35. **cdp_get_balance** - Get SOL balance for any Solana address |
| #127 | 36. **cdp_send_sol** - Send SOL from a CDP-managed account to another address |
| #128 | |
| #129 | ### CoinGecko Market Data Tools - Real-Time Cryptocurrency Analytics: |
| #130 | 37. **get_crypto_price** - Get current prices, market cap, volume, and 24hr change for cryptocurrencies (Bitcoin, Ethereum, Solana, etc.) |
| #131 | 38. **get_coin_market_data** - Get comprehensive market data with detailed metrics, rankings, and price change percentages |
| #132 | 39. **get_trending_cryptos** - Get trending cryptocurrencies based on search activity in the last 24 hours |
| #133 | 40. **search_crypto** - Search for cryptocurrencies by name or symbol to find coin IDs for other tools |
| #134 | 41. **get_global_crypto_stats** - Get global cryptocurrency market statistics (total market cap, volume, BTC dominance, etc.) |
| #135 | |
| #136 | ## Important Guidelines |
| #137 | |
| #138 | ### For Trading Operations: |
| #139 | - Always check balances before trading |
| #140 | - Get a quote first to show expected output before executing trades |
| #141 | - Warn users about price impact if it's high (>1%) |
| #142 | - Confirm with users before executing actual trades (buy_token, sell_token) |
| #143 | - Use appropriate slippage (default 3% = 300 bps, higher for volatile tokens) |
| #144 | |
| #145 | ### For Aster DEX Trading: |
| #146 | - **Perpetuals Trading**: High risk! Always warn about leverage and liquidation risks |
| #147 | - LONG = Bet price goes UP (buy to open, sell to close) |
| #148 | - SHORT = Bet price goes DOWN (sell to open, buy to close) |
| #149 | - Set leverage BEFORE opening positions (default is often 20x) |
| #150 | - Monitor positions with aster_get_positions to see unrealized PnL |
| #151 | - Common pairs: BTCUSDT, ETHUSDT, SOLUSDT |
| #152 | - **Spot Trading**: Traditional buy/sell on Aster's spot exchange |
| #153 | - MARKET orders execute immediately at current price |
| #154 | - LIMIT orders execute at your specified price (or better) |
| #155 | - Use aster_get_balance to see both futures and spot balances |
| #156 | - Transfer funds between futures/spot with aster_transfer |
| #157 | - **Risk Management**: |
| #158 | - Higher leverage = higher risk and faster liquidation |
| #159 | - Always check aster_get_price before placing orders |
| #160 | - Close losing positions before liquidation |
| #161 | - Never risk more than you can afford to lose |
| #162 | |
| #163 | ### For Hyperliquid DEX Trading: |
| #164 | - **Hyperliquid Overview**: Leading decentralized perpetuals exchange with deep liquidity |
| #165 | - Trade BTC, ETH, SOL, and many other perpetual contracts |
| #166 | - Cross margin (default) or isolated margin modes |
| #167 | - Uses USDC as collateral |
| #168 | - **Trading Flow**: |
| #169 | 1. Check account with **hyperliquid_get_account** to see balance and positions |
| #170 | 2. Check prices with **hyperliquid_get_price** before trading |
| #171 | 3. Set leverage with **hyperliquid_set_leverage** (cross margin recommended for beginners) |
| #172 | 4. Open positions with **hyperliquid_open_long** or **hyperliquid_open_short** |
| #173 | 5. Monitor positions with **hyperliquid_get_positions** |
| #174 | 6. Close with **hyperliquid_close_position** to take profit or cut losses |
| #175 | - **Order Types**: |
| #176 | - Market orders for immediate execution (hyperliquid_open_long/short) |
| #177 | - Limit orders for specific entry prices (hyperliquid_place_limit_order) |
| #178 | - Cancel pending orders with hyperliquid_cancel_order |
| #179 | - **Risk Management**: |
| #180 | - Use **cross margin** (is_cross=True) for better liquidation protection |
| #181 | - Use **isolated margin** (is_cross=False) to limit risk to specific position |
| #182 | - Check hyperliquid_get_positions regularly for unrealized PnL |
| #183 | - Close losing positions before liquidation |
| #184 | - Common coins: BTC, ETH, SOL, DOGE, WIF, PEPE, ARB, OP, SUI, SEI, TIA |
| #185 | |
| #186 | ### For CDP Managed Accounts (Mainnet): |
| #187 | - **CDP Overview**: Coinbase Developer Platform provides secure, custodial Solana accounts |
| #188 | - Accounts are managed by CDP - private keys never exposed |
| #189 | - Operates on Solana **mainnet** - real SOL and real transactions |
| #190 | - Create accounts and send transactions securely |
| #191 | - **Typical Workflow**: |
| #192 | 1. Create an account with **cdp_create_account** |
| #193 | 2. Fund the account by sending SOL to it from an external wallet |
| #194 | 3. Check balance with **cdp_get_balance** |
| #195 | 4. Send SOL with **cdp_send_sol** |
| #196 | 5. List all accounts with **cdp_list_accounts** |
| #197 | - **Important Notes**: |
| #198 | - CDP accounts are on **mainnet** - uses real SOL with real value |
| #199 | - Faucet (cdp_request_faucet) does NOT work on mainnet - fund manually |
| #200 | - Always verify addresses before sending transactions |
| #201 | - Transactions are irreversible on mainnet |
| #202 | |
| #203 | ### For CoinGecko Market Data: |
| #204 | - **CoinGecko Overview**: Real-time cryptocurrency market data from CoinGecko Pro API |
| #205 | - Get live prices, market caps, volumes, and price changes for 10,000+ cryptocurrencies |
| #206 | - Track trending coins and global market statistics |
| #207 | - Use coin IDs (e.g., 'bitcoin', 'ethereum', 'solana') not ticker symbols |
| #208 | - **Typical Usage**: |
| #209 | 1. Search coins with **search_crypto** to find exact coin IDs |
| #210 | 2. Get prices with **get_crypto_price** for quick price checks |
| #211 | 3. Get detailed data with **get_coin_market_data** for comprehensive analysis |
| #212 | 4. Check trends with **get_trending_cryptos** to discover hot coins |
| #213 | 5. Monitor markets with **get_global_crypto_stats** for big picture view |
| #214 | - **Coin ID Examples**: |
| #215 | - Bitcoin: 'bitcoin' (not 'BTC') |
| #216 | - Ethereum: 'ethereum' (not 'ETH') |
| #217 | - Solana: 'solana' (not 'SOL') |
| #218 | - Dogecoin: 'dogecoin' (not 'DOGE') |
| #219 | - Use search_crypto when unsure about the exact coin ID |
| #220 | |
| #221 | ### Unified Search & Analysis: |
| #222 | - **Web Search**: Use **web_search** for real-time information not in your knowledge (news, current prices, market trends, events) |
| #223 | - **Solana Address Analysis**: When users paste ANY Solana address: |
| #224 | - Use **analyze_solana_address** - it automatically detects the type and returns comprehensive data: |
| #225 | - Token contracts (32-44 chars): Full token info, price, market cap, security analysis, OHLCV charts |
| #226 | - Wallet addresses (32-44 chars): SOL balance, token holdings, NFTs, total assets |
| #227 | - Transaction signatures (88 chars): Transaction details, status, fees, involved accounts |
| #228 | - Example addresses: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" (token), "FZZFTXvg..." (wallet) |
| #229 | - The tool intelligently routes to the right API (Birdeye/Helius/DAS) based on detection |
| #230 | - SOL wrapped token: So11111111111111111111111111111111111111112 |
| #231 | |
| #232 | ### Best Practices: |
| #233 | - Be concise but informative |
| #234 | - Format numbers clearly (use commas for large numbers, limit decimals) |
| #235 | - Always show transaction signatures after trades |
| #236 | - Explain any failures clearly |
| #237 | |
| #238 | ### Safety: |
| #239 | - Never expose private keys |
| #240 | - Verify token addresses before trading |
| #241 | - Warn about rug pull risks for new/low liquidity tokens |
| #242 | - Check token security info when available |
| #243 | |
| #244 | ### Twitter Posting: |
| #245 | - Post updates about successful trades, portfolio gains, or interesting token discoveries |
| #246 | - Keep tweets under 280 characters |
| #247 | - Use emojis and relevant hashtags to increase engagement |
| #248 | - Share trading insights, market observations, or notable price movements |
| #249 | - Be enthusiastic but not overly promotional |
| #250 | - Examples: |
| #251 | * "Just bought 100 SOL of $BONK at $0.000015! 🚀 #Solana #MemeCoin" |
| #252 | * "Portfolio up 25% today! $WIF and $BONK carrying the team 📈💎" |
| #253 | * "Found a potential gem: $XYZ at $10M mcap, strong community and growing liquidity 🔍" |
| #254 | |
| #255 | ### Creative Content Generation: |
| #256 | - Generate images for memes, promotional content, charts, or visual explanations |
| #257 | - Create music for trading videos, celebration sounds, or background audio |
| #258 | - Generate videos for announcements, promotional content, or explainer videos |
| #259 | - Convert text to speech for voiceovers, audio alerts, or announcements |
| #260 | - These tools enable rich multimedia content creation for marketing and community engagement |
| #261 | |
| #262 | ### Chart Analysis & OHLCV Data: |
| #263 | - Use **get_token_chart** to provide technical analysis data |
| #264 | - OHLCV = Open, High, Low, Close, Volume candlestick data |
| #265 | - Available timeframes: 1m, 5m, 15m, 1H, 4H, 1D, 1W, 1M |
| #266 | - Charts help identify: trends, support/resistance, volume spikes, price patterns |
| #267 | - When analyzing a token, ALWAYS provide chart data alongside security analysis |
| #268 | - Explain what the price action shows (trending up/down, consolidating, volatile, etc.) |
| #269 | |
| #270 | ## Current Wallet |
| #271 | The agent is configured with your trading wallet. Use get_wallet_balance without arguments to check your balances. |
| #272 | |
| #273 | Let's start trading! What would you like to do? |
| #274 | """ |
| #275 | |
| #276 | |
| #277 | class Message: |
| #278 | """Simple message class for conversation history.""" |
| #279 | def __init__(self, role: str, content: str, tool_calls=None, tool_call_id=None, name=None): |
| #280 | self.role = role |
| #281 | self.content = content |
| #282 | self.tool_calls = tool_calls |
| #283 | self.tool_call_id = tool_call_id |
| #284 | self.name = name |
| #285 | |
| #286 | def to_dict(self) -> dict: |
| #287 | d = {"role": self.role, "content": self.content} |
| #288 | if self.tool_calls: |
| #289 | d["tool_calls"] = self.tool_calls |
| #290 | if self.tool_call_id: |
| #291 | d["tool_call_id"] = self.tool_call_id |
| #292 | if self.name: |
| #293 | d["name"] = self.name |
| #294 | return d |
| #295 | |
| #296 | |
| #297 | class OpenRouterClient: |
| #298 | """Simple client for OpenRouter API.""" |
| #299 | |
| #300 | def __init__(self, api_key: str, model: str = "anthropic/claude-sonnet-4"): |
| #301 | self.api_key = api_key |
| #302 | self.model = model |
| #303 | self.base_url = "https://openrouter.ai/api/v1" |
| #304 | self._client = httpx.AsyncClient( |
| #305 | base_url=self.base_url, |
| #306 | headers={ |
| #307 | "Authorization": f"Bearer {api_key}", |
| #308 | "Content-Type": "application/json", |
| #309 | }, |
| #310 | timeout=120.0 |
| #311 | ) |
| #312 | |
| #313 | async def generate(self, messages: list[Message], tools: list) -> dict: |
| #314 | """Generate a response from the LLM.""" |
| #315 | |
| #316 | # Convert messages to OpenAI format |
| #317 | formatted_messages = [] |
| #318 | for msg in messages: |
| #319 | if msg.role == "system": |
| #320 | formatted_messages.append({"role": "system", "content": msg.content}) |
| #321 | elif msg.role == "user": |
| #322 | formatted_messages.append({"role": "user", "content": msg.content}) |
| #323 | elif msg.role == "assistant": |
| #324 | m = {"role": "assistant", "content": msg.content or ""} |
| #325 | if msg.tool_calls: |
| #326 | m["tool_calls"] = msg.tool_calls |
| #327 | formatted_messages.append(m) |
| #328 | elif msg.role == "tool": |
| #329 | formatted_messages.append({ |
| #330 | "role": "tool", |
| #331 | "content": msg.content, |
| #332 | "tool_call_id": msg.tool_call_id, |
| #333 | "name": msg.name, |
| #334 | }) |
| #335 | |
| #336 | # Convert tools to OpenAI format |
| #337 | formatted_tools = [tool.to_openai_schema() for tool in tools] |
| #338 | |
| #339 | payload = { |
| #340 | "model": self.model, |
| #341 | "messages": formatted_messages, |
| #342 | "tools": formatted_tools, |
| #343 | "tool_choice": "auto", |
| #344 | } |
| #345 | |
| #346 | response = await self._client.post("/chat/completions", json=payload) |
| #347 | response.raise_for_status() |
| #348 | data = response.json() |
| #349 | |
| #350 | return data["choices"][0]["message"] |
| #351 | |
| #352 | async def close(self): |
| #353 | await self._client.aclose() |
| #354 | |
| #355 | |
| #356 | class OllamaClient: |
| #357 | """Simple client for Ollama API (OpenAI-compatible).""" |
| #358 | |
| #359 | def __init__(self, base_url: str = "http://localhost:11434/v1", model: str = "minimax-m2.1:cloud"): |
| #360 | self.model = model |
| #361 | self.base_url = base_url |
| #362 | self._client = httpx.AsyncClient( |
| #363 | base_url=self.base_url, |
| #364 | headers={ |
| #365 | "Content-Type": "application/json", |
| #366 | }, |
| #367 | timeout=120.0 |
| #368 | ) |
| #369 | |
| #370 | async def generate(self, messages: list[Message], tools: list) -> dict: |
| #371 | """Generate a response from the LLM.""" |
| #372 | |
| #373 | # Convert messages to OpenAI format |
| #374 | formatted_messages = [] |
| #375 | for msg in messages: |
| #376 | if msg.role == "system": |
| #377 | formatted_messages.append({"role": "system", "content": msg.content}) |
| #378 | elif msg.role == "user": |
| #379 | formatted_messages.append({"role": "user", "content": msg.content}) |
| #380 | elif msg.role == "assistant": |
| #381 | m = {"role": "assistant", "content": msg.content or ""} |
| #382 | if msg.tool_calls: |
| #383 | m["tool_calls"] = msg.tool_calls |
| #384 | formatted_messages.append(m) |
| #385 | elif msg.role == "tool": |
| #386 | formatted_messages.append({ |
| #387 | "role": "tool", |
| #388 | "content": msg.content, |
| #389 | "tool_call_id": msg.tool_call_id, |
| #390 | "name": msg.name, |
| #391 | }) |
| #392 | |
| #393 | # Convert tools to OpenAI format |
| #394 | formatted_tools = [tool.to_openai_schema() for tool in tools] |
| #395 | |
| #396 | payload = { |
| #397 | "model": self.model, |
| #398 | "messages": formatted_messages, |
| #399 | "tools": formatted_tools, |
| #400 | "tool_choice": "auto", |
| #401 | } |
| #402 | |
| #403 | response = await self._client.post("/chat/completions", json=payload) |
| #404 | response.raise_for_status() |
| #405 | data = response.json() |
| #406 | |
| #407 | return data["choices"][0]["message"] |
| #408 | |
| #409 | async def close(self): |
| #410 | await self._client.aclose() |
| #411 | |
| #412 | |
| #413 | class SolanaAgent: |
| #414 | """Main Solana Trading Agent.""" |
| #415 | |
| #416 | def __init__(self, config: SolanaAgentConfig): |
| #417 | self.config = config |
| #418 | self.bags_client = None |
| #419 | self.jupiter_client = None |
| #420 | self.helius_client = None |
| #421 | self.birdeye_client = None |
| #422 | self.twitter_client = None |
| #423 | self.minimax_client = None |
| #424 | self.aster_client = None |
| #425 | self.hyperliquid_client = None |
| #426 | self.cdp_client = None |
| #427 | self.coingecko_client = None |
| #428 | self.llm_client = None |
| #429 | self.messages = [] |
| #430 | self.max_steps = config.max_steps |
| #431 | |
| #432 | async def initialize(self): |
| #433 | """Initialize all clients and tools.""" |
| #434 | print(f"{Colors.CYAN}🚀 Initializing Solana Trading Agent...{Colors.RESET}") |
| #435 | |
| #436 | # Initialize Jupiter client (preferred) |
| #437 | if self.config.jupiter_api_key: |
| #438 | print(f"{Colors.DIM} → Connecting to Jupiter Ultra API...{Colors.RESET}") |
| #439 | from solders.keypair import Keypair |
| #440 | |
| #441 | # Initialize keypair if private key is available |
| #442 | keypair = None |
| #443 | if self.config.private_key: |
| #444 | try: |
| #445 | keypair = Keypair.from_base58_string(self.config.private_key) |
| #446 | except Exception as e: |
| #447 | print(f"{Colors.YELLOW} ⚠️ Could not load keypair: {e}{Colors.RESET}") |
| #448 | |
| #449 | self.jupiter_client = JupiterClient( |
| #450 | api_key=self.config.jupiter_api_key, |
| #451 | wallet_pubkey=self.config.wallet_address, |
| #452 | keypair=keypair, |
| #453 | referral_account=self.config.jupiter_referral_account, |
| #454 | ) |
| #455 | print(f"{Colors.GREEN} ✓ Jupiter Ultra initialized (preferred trading API){Colors.RESET}") |
| #456 | else: |
| #457 | print(f"{Colors.DIM} → Jupiter API not configured{Colors.RESET}") |
| #458 | |
| #459 | # Initialize Bags client (legacy, optional) |
| #460 | if self.config.bags_api_key and self.config.bags_config_key: |
| #461 | print(f"{Colors.DIM} → Connecting to Bags API (legacy)...{Colors.RESET}") |
| #462 | self.bags_client = BagsClient( |
| #463 | api_key=self.config.bags_api_key, |
| #464 | config_key=self.config.bags_config_key, |
| #465 | rpc_url=self.config.helius_rpc_url, |
| #466 | private_key=self.config.private_key, |
| #467 | ) |
| #468 | print(f"{Colors.GREEN} ✓ Bags client initialized (legacy trading API){Colors.RESET}") |
| #469 | else: |
| #470 | print(f"{Colors.DIM} → Bags API not configured (legacy){Colors.RESET}") |
| #471 | |
| #472 | # Initialize Helius client |
| #473 | print(f"{Colors.DIM} → Connecting to Helius RPC...{Colors.RESET}") |
| #474 | self.helius_client = HeliusClient( |
| #475 | api_key=self.config.helius_api_key, |
| #476 | rpc_url=self.config.helius_rpc_url, |
| #477 | wss_url=self.config.helius_wss_url, |
| #478 | ) |
| #479 | |
| #480 | # Initialize Birdeye client |
| #481 | print(f"{Colors.DIM} → Connecting to Birdeye API...{Colors.RESET}") |
| #482 | self.birdeye_client = BirdeyeClient( |
| #483 | api_key=self.config.birdeye_api_key, |
| #484 | ) |
| #485 | |
| #486 | # Initialize Twitter client (optional) |
| #487 | if (self.config.twitter_consumer_key and self.config.twitter_consumer_secret and |
| #488 | self.config.twitter_access_token and self.config.twitter_access_token_secret): |
| #489 | print(f"{Colors.DIM} → Connecting to Twitter API...{Colors.RESET}") |
| #490 | self.twitter_client = TwitterClient( |
| #491 | consumer_key=self.config.twitter_consumer_key, |
| #492 | consumer_secret=self.config.twitter_consumer_secret, |
| #493 | access_token=self.config.twitter_access_token, |
| #494 | access_token_secret=self.config.twitter_access_token_secret, |
| #495 | bearer_token=self.config.twitter_bearer_token, |
| #496 | ) |
| #497 | print(f"{Colors.GREEN} ✓ Twitter client initialized{Colors.RESET}") |
| #498 | else: |
| #499 | print(f"{Colors.DIM} → Twitter API credentials not configured (optional){Colors.RESET}") |
| #500 | |
| #501 | # Initialize MiniMax client (optional) |
| #502 | if self.config.minimax_api_key: |
| #503 | print(f"{Colors.DIM} → Connecting to MiniMax API...{Colors.RESET}") |
| #504 | self.minimax_client = MinimaxClient( |
| #505 | api_key=self.config.minimax_api_key, |
| #506 | ) |
| #507 | print(f"{Colors.GREEN} ✓ MiniMax client initialized (image, music, video, TTS){Colors.RESET}") |
| #508 | else: |
| #509 | print(f"{Colors.DIM} → MiniMax API credentials not configured (optional){Colors.RESET}") |
| #510 | |
| #511 | # Initialize Search client (optional) |
| #512 | if self.config.search_api_key: |
| #513 | print(f"{Colors.DIM} → Connecting to SearchAPI...{Colors.RESET}") |
| #514 | self.search_client = SearchAPIClient( |
| #515 | api_key=self.config.search_api_key, |
| #516 | ) |
| #517 | print(f"{Colors.GREEN} ✓ SearchAPI client initialized (real-time web search){Colors.RESET}") |
| #518 | else: |
| #519 | self.search_client = None |
| #520 | print(f"{Colors.DIM} → SearchAPI credentials not configured (optional){Colors.RESET}") |
| #521 | |
| #522 | # Initialize Solana Analyzer |
| #523 | print(f"{Colors.DIM} → Initializing Solana Analyzer...{Colors.RESET}") |
| #524 | self.solana_analyzer = SolanaAnalyzer( |
| #525 | birdeye_api_key=self.config.birdeye_api_key, |
| #526 | helius_api_key=self.config.helius_api_key, |
| #527 | helius_rpc_url=self.config.helius_rpc_url, |
| #528 | ) |
| #529 | print(f"{Colors.GREEN} ✓ Solana Analyzer initialized (contract/wallet/tx detection){Colors.RESET}") |
| #530 | |
| #531 | # Initialize Aster DEX client (optional) |
| #532 | if (self.config.aster_api_key and self.config.aster_user_address and |
| #533 | self.config.aster_signer_address and self.config.aster_private_key): |
| #534 | print(f"{Colors.DIM} → Connecting to Aster DEX...{Colors.RESET}") |
| #535 | self.aster_client = AsterClient( |
| #536 | user_address=self.config.aster_user_address, |
| #537 | signer_address=self.config.aster_signer_address, |
| #538 | private_key=self.config.aster_private_key, |
| #539 | ) |
| #540 | print(f"{Colors.GREEN} ✓ Aster DEX client initialized (perpetuals & spot trading){Colors.RESET}") |
| #541 | else: |
| #542 | self.aster_client = None |
| #543 | print(f"{Colors.DIM} → Aster DEX credentials not configured (optional){Colors.RESET}") |
| #544 | |
| #545 | # Initialize Hyperliquid DEX client (optional) |
| #546 | if self.config.hyperliquid_wallet and self.config.hyperliquid_private_key: |
| #547 | print(f"{Colors.DIM} → Connecting to Hyperliquid DEX...{Colors.RESET}") |
| #548 | try: |
| #549 | self.hyperliquid_client = HyperliquidClient( |
| #550 | wallet_address=self.config.hyperliquid_wallet, |
| #551 | private_key=self.config.hyperliquid_private_key, |
| #552 | use_testnet=self.config.hyperliquid_use_testnet, |
| #553 | ) |
| #554 | print(f"{Colors.GREEN} ✓ Hyperliquid DEX client initialized (perpetuals trading){Colors.RESET}") |
| #555 | except Exception as e: |
| #556 | self.hyperliquid_client = None |
| #557 | print(f"{Colors.YELLOW} ⚠️ Could not initialize Hyperliquid: {e}{Colors.RESET}") |
| #558 | else: |
| #559 | self.hyperliquid_client = None |
| #560 | print(f"{Colors.DIM} → Hyperliquid DEX credentials not configured (optional){Colors.RESET}") |
| #561 | |
| #562 | # Initialize CDP client (optional - for managed Solana accounts) |
| #563 | if self.config.cdp_api_key_id and self.config.cdp_api_key_secret: |
| #564 | network_name = "mainnet" if self.config.cdp_network == "solana-mainnet" else "devnet" |
| #565 | print(f"{Colors.DIM} → Connecting to CDP (Coinbase Developer Platform) on {network_name}...{Colors.RESET}") |
| #566 | try: |
| #567 | self.cdp_client = create_cdp_client( |
| #568 | api_key_id=self.config.cdp_api_key_id, |
| #569 | api_key_secret=self.config.cdp_api_key_secret, |
| #570 | wallet_secret=self.config.cdp_wallet_secret, |
| #571 | rpc_url=self.config.cdp_rpc_url, |
| #572 | network=self.config.cdp_network, |
| #573 | ) |
| #574 | if self.cdp_client: |
| #575 | print(f"{Colors.GREEN} ✓ CDP client initialized (managed Solana accounts on {network_name}){Colors.RESET}") |
| #576 | else: |
| #577 | print(f"{Colors.YELLOW} ⚠️ CDP SDK not available - install with: pip install cdp-sdk{Colors.RESET}") |
| #578 | except Exception as e: |
| #579 | self.cdp_client = None |
| #580 | print(f"{Colors.YELLOW} ⚠️ Could not initialize CDP: {e}{Colors.RESET}") |
| #581 | else: |
| #582 | self.cdp_client = None |
| #583 | print(f"{Colors.DIM} → CDP credentials not configured (optional){Colors.RESET}") |
| #584 | |
| #585 | # Initialize CoinGecko client (optional - for real-time crypto market data) |
| #586 | if self.config.coingecko_api_key: |
| #587 | print(f"{Colors.DIM} → Connecting to CoinGecko Pro API...{Colors.RESET}") |
| #588 | try: |
| #589 | self.coingecko_client = create_coingecko_client(api_key=self.config.coingecko_api_key) |
| #590 | if self.coingecko_client: |
| #591 | print(f"{Colors.GREEN} ✓ CoinGecko client initialized (real-time crypto market data){Colors.RESET}") |
| #592 | else: |
| #593 | print(f"{Colors.YELLOW} ⚠️ CoinGecko client not available{Colors.RESET}") |
| #594 | except Exception as e: |
| #595 | self.coingecko_client = None |
| #596 | print(f"{Colors.YELLOW} ⚠️ Could not initialize CoinGecko: {e}{Colors.RESET}") |
| #597 | else: |
| #598 | self.coingecko_client = None |
| #599 | print(f"{Colors.DIM} → CoinGecko API key not configured (optional){Colors.RESET}") |
| #600 | |
| #601 | # Set clients for tools |
| #602 | set_clients( |
| #603 | bags_client=self.bags_client, |
| #604 | jupiter_client=self.jupiter_client, |
| #605 | helius_client=self.helius_client, |
| #606 | birdeye_client=self.birdeye_client, |
| #607 | twitter_client=self.twitter_client, |
| #608 | minimax_client=self.minimax_client, |
| #609 | search_client=self.search_client, |
| #610 | solana_analyzer=self.solana_analyzer, |
| #611 | aster_client=self.aster_client, |
| #612 | hyperliquid_client=self.hyperliquid_client, |
| #613 | cdp_client=self.cdp_client, |
| #614 | coingecko_client=self.coingecko_client, |
| #615 | ) |
| #616 | |
| #617 | # Create tools |
| #618 | self.tools = create_all_tools() |
| #619 | print(f"{Colors.DIM} → Loaded {len(self.tools)} trading tools{Colors.RESET}") |
| #620 | |
| #621 | # Initialize LLM client based on provider setting |
| #622 | if self.config.llm_provider == "ollama": |
| #623 | print(f"{Colors.DIM} → Connecting to Ollama ({self.config.ollama_model})...{Colors.RESET}") |
| #624 | self.llm_client = OllamaClient( |
| #625 | base_url=self.config.ollama_base_url, |
| #626 | model=self.config.ollama_model, |
| #627 | ) |
| #628 | print(f"{Colors.GREEN} ✓ Ollama client initialized{Colors.RESET}") |
| #629 | elif self.config.openrouter_api_key: |
| #630 | print(f"{Colors.DIM} → Connecting to OpenRouter ({self.config.openrouter_model})...{Colors.RESET}") |
| #631 | self.llm_client = OpenRouterClient( |
| #632 | api_key=self.config.openrouter_api_key, |
| #633 | model=self.config.openrouter_model, |
| #634 | ) |
| #635 | print(f"{Colors.GREEN} ✓ OpenRouter client initialized{Colors.RESET}") |
| #636 | else: |
| #637 | print(f"{Colors.YELLOW} ⚠️ No LLM configured - running in tool-only mode{Colors.RESET}") |
| #638 | |
| #639 | # Initialize message history |
| #640 | self.messages = [Message(role="system", content=SYSTEM_PROMPT)] |
| #641 | |
| #642 | # Check wallet |
| #643 | wallet_pubkey = None |
| #644 | if self.jupiter_client and self.jupiter_client.wallet_pubkey: |
| #645 | wallet_pubkey = self.jupiter_client.wallet_pubkey |
| #646 | elif self.bags_client and self.bags_client.wallet_pubkey: |
| #647 | wallet_pubkey = self.bags_client.wallet_pubkey |
| #648 | |
| #649 | if wallet_pubkey: |
| #650 | print(f"{Colors.GREEN} ✓ Wallet: {wallet_pubkey}{Colors.RESET}") |
| #651 | try: |
| #652 | balance = await self.helius_client.get_sol_balance(wallet_pubkey) |
| #653 | print(f"{Colors.GREEN} ✓ Balance: {balance:.4f} SOL{Colors.RESET}") |
| #654 | except Exception as e: |
| #655 | print(f"{Colors.YELLOW} ⚠️ Could not fetch balance: {e}{Colors.RESET}") |
| #656 | |
| #657 | print(f"{Colors.BRIGHT_GREEN}✓ Agent initialized successfully!{Colors.RESET}\n") |
| #658 | |
| #659 | async def process_message(self, user_message: str) -> str: |
| #660 | """Process a user message and return the response.""" |
| #661 | |
| #662 | if not self.llm_client: |
| #663 | return "LLM not configured. Please set OPENROUTER_API_KEY." |
| #664 | |
| #665 | # Add user message |
| #666 | self.messages.append(Message(role="user", content=user_message)) |
| #667 | |
| #668 | step = 0 |
| #669 | while step < self.max_steps: |
| #670 | step += 1 |
| #671 | |
| #672 | print(f"\n{Colors.DIM}╭{'─' * 58}╮{Colors.RESET}") |
| #673 | print(f"{Colors.DIM}│{Colors.RESET} {Colors.BOLD}{Colors.BRIGHT_CYAN}💭 Step {step}/{self.max_steps}{Colors.RESET}{' ' * 40}{Colors.DIM}│{Colors.RESET}") |
| #674 | print(f"{Colors.DIM}╰{'─' * 58}╯{Colors.RESET}") |
| #675 | |
| #676 | # Get LLM response |
| #677 | start_time = perf_counter() |
| #678 | try: |
| #679 | response = await self.llm_client.generate(self.messages, self.tools) |
| #680 | except Exception as e: |
| #681 | print(f"{Colors.BRIGHT_RED}❌ LLM Error: {e}{Colors.RESET}") |
| #682 | return f"Error: {e}" |
| #683 | |
| #684 | elapsed = perf_counter() - start_time |
| #685 | |
| #686 | # Extract response content |
| #687 | content = response.get("content", "") |
| #688 | tool_calls = response.get("tool_calls", None) |
| #689 | |
| #690 | # Add assistant message |
| #691 | self.messages.append(Message( |
| #692 | role="assistant", |
| #693 | content=content, |
| #694 | tool_calls=tool_calls, |
| #695 | )) |
| #696 | |
| #697 | # Print response |
| #698 | if content: |
| #699 | print(f"\n{Colors.BOLD}{Colors.BRIGHT_BLUE}🤖 Assistant:{Colors.RESET}") |
| #700 | print(content) |
| #701 | |
| #702 | # If no tool calls, we're done |
| #703 | if not tool_calls: |
| #704 | print(f"\n{Colors.DIM}⏱️ Completed in {elapsed:.2f}s{Colors.RESET}") |
| #705 | return content |
| #706 | |
| #707 | # Execute tool calls |
| #708 | for tool_call in tool_calls: |
| #709 | tool_call_id = tool_call["id"] |
| #710 | function_name = tool_call["function"]["name"] |
| #711 | try: |
| #712 | arguments = json.loads(tool_call["function"]["arguments"]) |
| #713 | except json.JSONDecodeError: |
| #714 | arguments = {} |
| #715 | |
| #716 | print(f"\n{Colors.BRIGHT_YELLOW}🔧 Tool Call:{Colors.RESET} {Colors.BOLD}{Colors.CYAN}{function_name}{Colors.RESET}") |
| #717 | print(f"{Colors.DIM} Arguments: {json.dumps(arguments, indent=2)}{Colors.RESET}") |
| #718 | |
| #719 | # Find and execute tool |
| #720 | tool = None |
| #721 | for t in self.tools: |
| #722 | if t.name == function_name: |
| #723 | tool = t |
| #724 | break |
| #725 | |
| #726 | if tool is None: |
| #727 | result = ToolResult(success=False, error=f"Unknown tool: {function_name}") |
| #728 | else: |
| #729 | try: |
| #730 | result = await tool.execute(**arguments) |
| #731 | except Exception as e: |
| #732 | result = ToolResult(success=False, error=str(e)) |
| #733 | |
| #734 | # Print result |
| #735 | if result.success: |
| #736 | result_preview = result.content[:300] + "..." if len(result.content) > 300 else result.content |
| #737 | print(f"{Colors.BRIGHT_GREEN}✓ Result:{Colors.RESET} {result_preview}") |
| #738 | else: |
| #739 | print(f"{Colors.BRIGHT_RED}✗ Error:{Colors.RESET} {Colors.RED}{result.error}{Colors.RESET}") |
| #740 | |
| #741 | # Add tool result message |
| #742 | self.messages.append(Message( |
| #743 | role="tool", |
| #744 | content=result.content if result.success else f"Error: {result.error}", |
| #745 | tool_call_id=tool_call_id, |
| #746 | name=function_name, |
| #747 | )) |
| #748 | |
| #749 | print(f"\n{Colors.DIM}⏱️ Step {step} completed in {elapsed:.2f}s{Colors.RESET}") |
| #750 | |
| #751 | return "Max steps reached." |
| #752 | |
| #753 | async def run_interactive(self): |
| #754 | """Run the agent in interactive mode.""" |
| #755 | |
| #756 | print(f"\n{Colors.BOLD}{Colors.CYAN}{'═' * 60}{Colors.RESET}") |
| #757 | print(f"{Colors.BOLD}{Colors.CYAN} 🌊 MAWD - Solana Trading Agent{Colors.RESET}") |
| #758 | print(f"{Colors.BOLD}{Colors.CYAN}{'═' * 60}{Colors.RESET}") |
| #759 | print(f"{Colors.DIM}Type your message and press Enter. Type 'quit' to exit.{Colors.RESET}") |
| #760 | print(f"{Colors.DIM}Commands: /balance, /portfolio, /trending, /help{Colors.RESET}\n") |
| #761 | |
| #762 | while True: |
| #763 | try: |
| #764 | user_input = input(f"{Colors.BOLD}{Colors.GREEN}You:{Colors.RESET} ").strip() |
| #765 | |
| #766 | if not user_input: |
| #767 | continue |
| #768 | |
| #769 | if user_input.lower() in ["quit", "exit", "q"]: |
| #770 | print(f"\n{Colors.CYAN}👋 Goodbye!{Colors.RESET}") |
| #771 | break |
| #772 | |
| #773 | # Handle special commands |
| #774 | if user_input.startswith("/"): |
| #775 | command = user_input.lower() |
| #776 | if command == "/balance": |
| #777 | user_input = "Check my wallet balance" |
| #778 | elif command == "/portfolio": |
| #779 | user_input = "Show my complete portfolio with USD values" |
| #780 | elif command == "/trending": |
| #781 | user_input = "What are the trending tokens right now?" |
| #782 | elif command == "/help": |
| #783 | print(f""" |
| #784 | {Colors.CYAN}Available Commands:{Colors.RESET} |
| #785 | /balance - Check your wallet balance |
| #786 | /portfolio - Show your complete portfolio |
| #787 | /trending - Show trending tokens |
| #788 | /help - Show this help message |
| #789 | |
| #790 | {Colors.CYAN}Example Queries:{Colors.RESET} |
| #791 | "What's the price of BONK?" |
| #792 | "Search for tokens named PEPE" |
| #793 | "Get info on token <mint_address>" |
| #794 | "Buy 0.1 SOL worth of <token_mint>" |
| #795 | "Sell 1000 tokens of <token_mint>" |
| #796 | "Get a swap quote for 0.5 SOL to <token_mint>" |
| #797 | """) |
| #798 | continue |
| #799 | |
| #800 | # Process the message |
| #801 | response = await self.process_message(user_input) |
| #802 | |
| #803 | except KeyboardInterrupt: |
| #804 | print(f"\n\n{Colors.CYAN}👋 Goodbye!{Colors.RESET}") |
| #805 | break |
| #806 | except Exception as e: |
| #807 | print(f"{Colors.BRIGHT_RED}Error: {e}{Colors.RESET}") |
| #808 | |
| #809 | async def close(self): |
| #810 | """Clean up resources.""" |
| #811 | if self.bags_client: |
| #812 | await self.bags_client.close() |
| #813 | if self.jupiter_client: |
| #814 | await self.jupiter_client.close() |
| #815 | if self.helius_client: |
| #816 | await self.helius_client.close() |
| #817 | if self.birdeye_client: |
| #818 | await self.birdeye_client.close() |
| #819 | if self.aster_client: |
| #820 | await self.aster_client.close() |
| #821 | if self.cdp_client: |
| #822 | await self.cdp_client.close() |
| #823 | if self.coingecko_client: |
| #824 | await self.coingecko_client.close() |
| #825 | if self.llm_client: |
| #826 | await self.llm_client.close() |
| #827 | |
| #828 | |
| #829 | async def main(): |
| #830 | """Main entry point.""" |
| #831 | |
| #832 | # Load configuration |
| #833 | try: |
| #834 | # Look for .env.local in parent directory |
| #835 | env_path = Path(__file__).parent.parent / ".env.local" |
| #836 | if not env_path.exists(): |
| #837 | env_path = None |
| #838 | |
| #839 | config = load_config(str(env_path) if env_path else None) |
| #840 | except ValueError as e: |
| #841 | print(f"{Colors.BRIGHT_RED}Configuration Error: {e}{Colors.RESET}") |
| #842 | print(f"{Colors.DIM}Please ensure your .env.local file contains all required variables.{Colors.RESET}") |
| #843 | sys.exit(1) |
| #844 | |
| #845 | # Create and run agent |
| #846 | agent = SolanaAgent(config) |
| #847 | |
| #848 | try: |
| #849 | await agent.initialize() |
| #850 | await agent.run_interactive() |
| #851 | finally: |
| #852 | await agent.close() |
| #853 | |
| #854 | |
| #855 | if __name__ == "__main__": |
| #856 | asyncio.run(main()) |
| #857 |