├── active_tokens.json ├── purchased_tokens.json ├── sold_positions.json ├── screen.jpg ├── public ├── logo.png └── index.html ├── package.json ├── config.js ├── websocket-client.js ├── server.js ├── README.md ├── scanner.js └── trader.js /active_tokens.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /purchased_tokens.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /sold_positions.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamblingTerminal/gt-bot/HEAD/screen.jpg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamblingTerminal/gt-bot/HEAD/public/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gamblingterminal", 3 | "version": "1.0.0", 4 | "description": "Solana token trading bot", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js & node scanner.js & node trader.js", 8 | "scanner": "node scanner.js", 9 | "trader": "node trader.js", 10 | "web": "node server.js" 11 | }, 12 | "dependencies": { 13 | "@solana/web3.js": "^1.87.1", 14 | "bs58": "^5.0.0", 15 | "express": "^4.18.2", 16 | "node-fetch": "^2.6.7", 17 | "ws": "^8.14.2" 18 | } 19 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Your wallet's private key (keep this secure!) 3 | PRIVATE_KEY: "", 4 | 5 | // Amount of SOL to spend 6 | AMOUNT_TO_SPEND: 0.1, 7 | 8 | // Slippage tolerance in basis points (1 bp = 0.01%, 100 bp = 1%) 9 | SLIPPAGE_BPS: 2500, // 25% slippage 10 | 11 | // Priority fee in SOL (increased for faster execution) 12 | PRIORITY_FEE_SOL: 0.0015, 13 | 14 | // List of RPC endpoints to use (will try them in order) 15 | RPC_ENDPOINTS: [ 16 | 'https://api.mainnet-beta.solana.com', 17 | // Add more RPC endpoints here for more stability 18 | // https://instantnodes.io/ for examples (need account) 19 | ], 20 | 21 | // Keep as fallback 22 | RPC_URL: 'https://api.mainnet-beta.solana.com', 23 | 24 | // Price monitoring settings 25 | PRICE_CHECK_INTERVAL: 10, // seconds 26 | STOP_LOSS_PERCENTAGE: 30, // sell if price drops by 30% 27 | TAKE_PROFIT_PERCENTAGE: 100, // sell if price increases by 100% 28 | 29 | // Auto-sell settings 30 | SELL_SLIPPAGE_BPS: 3000, // 30% slippage for selling 31 | SELL_PRIORITY_FEE_SOL: 0.0015, // Increased priority fee for selling 32 | 33 | // Scanner settings 34 | SCAN_INTERVAL_MINUTES: 10 // Time between token scans 35 | } -------------------------------------------------------------------------------- /websocket-client.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | let ws; 3 | 4 | function connect() { 5 | ws = new WebSocket('ws://localhost:3005'); 6 | 7 | ws.on('open', () => { 8 | console.log('Connected to web interface'); 9 | }); 10 | 11 | ws.on('error', (error) => { 12 | console.error('WebSocket error:', error.message); 13 | }); 14 | 15 | ws.on('close', () => { 16 | console.log('Disconnected from web interface, attempting to reconnect...'); 17 | setTimeout(connect, 5000); 18 | }); 19 | } 20 | 21 | function sendToWeb(type, data) { 22 | if (ws && ws.readyState === WebSocket.OPEN) { 23 | ws.send(JSON.stringify({ type, data })); 24 | } 25 | } 26 | 27 | function updateMonitoringInfo(data) { 28 | if (ws && ws.readyState === WebSocket.OPEN) { 29 | const message = { 30 | type: 'monitoring_update', 31 | data: { 32 | positions: data.positions || [], 33 | soldPositions: data.soldPositions || [], 34 | walletBalanceSOL: data.walletBalanceSOL, 35 | walletBalanceUSD: data.walletBalanceUSD, 36 | lastUpdateTime: data.lastUpdateTime 37 | } 38 | }; 39 | ws.send(JSON.stringify(message)); 40 | console.log(`Positions updated (${data.positions.length} active, ${data.soldPositions.length} sold)`); 41 | } else { 42 | console.log('WebSocket not ready. State:', ws ? ws.readyState : 'no websocket'); 43 | } 44 | } 45 | 46 | module.exports = { 47 | connect, 48 | sendToWeb, 49 | updateMonitoringInfo 50 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const http = require('http'); 3 | const WebSocket = require('ws'); 4 | const path = require('path'); 5 | 6 | const app = express(); 7 | const server = http.createServer(app); 8 | const wss = new WebSocket.Server({ server }); 9 | 10 | // Serve static files from 'public' directory 11 | app.use(express.static('public')); 12 | 13 | // Store latest data 14 | let scriptLogs = []; 15 | let monitoringInfo = null; 16 | 17 | // WebSocket connection handling 18 | wss.on('connection', (ws) => { 19 | console.log('Client connected to server'); 20 | 21 | // Send initial data if available 22 | if (scriptLogs.length > 0) { 23 | console.log('Sending initial logs to client'); 24 | ws.send(JSON.stringify({ 25 | type: 'log', 26 | data: scriptLogs.join('\n') 27 | })); 28 | } 29 | if (monitoringInfo) { 30 | console.log('Sending initial monitoring info to client'); 31 | ws.send(JSON.stringify({ 32 | type: 'monitoring_update', 33 | data: monitoringInfo 34 | })); 35 | } 36 | 37 | // Handle incoming messages 38 | ws.on('message', (message) => { 39 | try { 40 | const data = JSON.parse(message.toString()); 41 | console.log('Received message from client:', message.toString()); 42 | 43 | if (data.type === 'log') { 44 | scriptLogs.push(data.data); 45 | broadcastUpdate('log', data.data); 46 | } else if (data.type === 'monitoring_update') { 47 | monitoringInfo = data.data; 48 | broadcastUpdate('monitoring_update', data.data); 49 | } 50 | } catch (error) { 51 | console.error('Error processing message:', error); 52 | } 53 | }); 54 | 55 | ws.on('error', (error) => { 56 | console.error('WebSocket error:', error); 57 | }); 58 | 59 | ws.on('close', () => { 60 | console.log('Client disconnected'); 61 | }); 62 | }); 63 | 64 | function broadcastUpdate(type, data) { 65 | console.log(`Broadcasting ${type} update to ${wss.clients.size} clients`); 66 | wss.clients.forEach((client) => { 67 | if (client.readyState === WebSocket.OPEN) { 68 | try { 69 | client.send(JSON.stringify({ type, data })); 70 | } catch (error) { 71 | console.error('Error sending to client:', error); 72 | } 73 | } 74 | }); 75 | } 76 | 77 | // Start server 78 | const PORT = 3005; 79 | server.listen(PORT, () => { 80 | console.log(`Server running on http://localhost:${PORT}`); 81 | }); 82 | 83 | module.exports = { 84 | broadcastUpdate 85 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Trading Terminal 2 | 3 | A high-frequency trading terminal for Solana tokens with real-time monitoring and automated trading strategies. 4 | 5 | ![Terminal Screenshot](screen.jpg) 6 | 7 | ## Features 8 | 9 | - 🚀 Real-time token discovery and analysis 10 | - 💹 Automated trading with configurable parameters 11 | - 📊 Live position monitoring and P&L tracking 12 | - 🔄 Automatic stop-loss and take-profit execution 13 | - 💼 Wallet balance monitoring 14 | - 📈 Historical trades tracking 15 | 16 | ## Requirements 17 | 18 | - Node.js v16 or higher 19 | - npm or yarn 20 | - Solana wallet with SOL for trading 21 | - Access to a Solana RPC endpoint 22 | 23 | ## Installation 24 | 25 | 1. Clone the repository: 26 | ```bash 27 | git clone https://github.com/GamblingTerminal/gt-bot.git 28 | cd solana-trading-terminal 29 | ``` 30 | 31 | 2. Install dependencies: 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | 3. Edit a `config.js` file in the root directory with your settings: 37 | ```javascript 38 | module.exports = { 39 | // Required settings 40 | PRIVATE_KEY: "your_wallet_private_key", 41 | RPC_ENDPOINTS: [ 42 | "https://your-rpc-endpoint.com" 43 | ], 44 | 45 | // Trading parameters 46 | AMOUNT_TO_SPEND: 0.1, // Amount of SOL to spend per trade 47 | TAKE_PROFIT_PERCENTAGE: 100, // Take profit trigger (100%) 48 | STOP_LOSS_PERCENTAGE: -20, // Stop loss trigger (-20%) 49 | 50 | // Advanced settings 51 | SLIPPAGE_BPS: 1000, // Trade slippage in basis points (1000 = 10%) 52 | SELL_SLIPPAGE_BPS: 2000, // Sell slippage in basis points (2000 = 20%) 53 | PRIORITY_FEE_SOL: 0.000001, // Priority fee for transactions 54 | MIN_LIQUIDITY_USD: 1000, // Minimum liquidity requirement 55 | SCAN_INTERVAL: 120000 // Token scanning interval (2 minutes) 56 | } 57 | ``` 58 | 59 | ## Configuration Settings 60 | 61 | All settings are configured in `config.js`: 62 | 63 | ### Required Settings 64 | - `PRIVATE_KEY`: Your wallet's private key 65 | - `RPC_ENDPOINTS`: Array of Solana RPC endpoints (can specify multiple for fallback) 66 | 67 | ### Trading Parameters 68 | - `AMOUNT_TO_SPEND`: Amount of SOL to spend per trade (default: 0.1) 69 | - `TAKE_PROFIT_PERCENTAGE`: Take profit trigger (default: 100%) 70 | - `STOP_LOSS_PERCENTAGE`: Stop loss trigger (default: -20%) 71 | 72 | ### Advanced Settings 73 | - `SLIPPAGE_BPS`: Trade slippage tolerance in basis points (1000 = 10%) 74 | - `SELL_SLIPPAGE_BPS`: Sell slippage tolerance in basis points (2000 = 20%) 75 | - `PRIORITY_FEE_SOL`: Transaction priority fee in SOL 76 | - `MIN_LIQUIDITY_USD`: Minimum token liquidity requirement in USD 77 | - `SCAN_INTERVAL`: Time between token scans in milliseconds 78 | 79 | ## Usage 80 | 81 | 1. Start the terminal: 82 | ```bash 83 | npm start 84 | ``` 85 | 86 | 2. Open your browser and navigate to: 87 | ``` 88 | http://localhost:3005 89 | ``` 90 | 91 | 3. The terminal will automatically: 92 | - Scan for new tokens 93 | - Execute trades based on criteria 94 | - Monitor positions 95 | - Apply stop-loss and take-profit orders 96 | 97 | ## Interface Components 98 | 99 | ### Position Monitor 100 | - Displays active trading positions 101 | - Shows token symbols and current values 102 | - Tracks profit/loss percentages 103 | - Lists historical (sold) positions 104 | 105 | ### Wallet Info 106 | - Shows current SOL balance 107 | - Displays USD equivalent 108 | - Updates in real-time 109 | 110 | ### Trading Log 111 | - Real-time trading activity 112 | - Token discovery notifications 113 | - Transaction confirmations 114 | - Error messages 115 | 116 | ## Trading Strategy 117 | 118 | The terminal implements the following strategy: 119 | 120 | 1. **Token Discovery** 121 | - Continuously scans for new tokens 122 | - Analyzes market data and liquidity 123 | - Filters based on minimum liquidity requirements 124 | 125 | 2. **Buy Conditions** 126 | - Minimum liquidity threshold 127 | - Price momentum indicators 128 | - Market cap requirements 129 | - Trading volume analysis 130 | 131 | 3. **Position Management** 132 | - Automatic stop-loss execution 133 | - Take-profit order management 134 | - Real-time position monitoring 135 | - Price change tracking 136 | 137 | 4. **Sell Conditions** 138 | - Stop-loss hit (-20% default) 139 | - Take-profit reached (+100% default) 140 | - Liquidity drop protection 141 | - Automatic execution 142 | 143 | ## Data Storage 144 | 145 | The terminal maintains several JSON files for data persistence: 146 | 147 | - `active_tokens.json`: Currently held positions 148 | - `sold_positions.json`: Historical trades with P&L 149 | - `selected_token.json`: Currently selected token for purchase 150 | 151 | ## Error Handling 152 | 153 | The terminal includes robust error handling for: 154 | 155 | - Network connectivity issues 156 | - RPC endpoint failures 157 | - Transaction errors 158 | - Invalid token data 159 | - Insufficient liquidity 160 | - Failed trades 161 | 162 | ## Security Considerations 163 | 164 | 1. **Private Key Storage** 165 | - Store your private key securely 166 | - Never commit .env file to version control 167 | - Use environment variables for sensitive data 168 | 169 | 2. **RPC Endpoints** 170 | - Use reliable RPC providers 171 | - Consider using multiple fallback endpoints 172 | - Monitor RPC rate limits 173 | 174 | ## Troubleshooting 175 | 176 | Common issues and solutions: 177 | 178 | 1. **Transaction Failures** 179 | - Check SOL balance for fees 180 | - Verify RPC endpoint status 181 | - Ensure sufficient slippage tolerance 182 | 183 | 2. **Position Not Updating** 184 | - Check network connectivity 185 | - Verify token contract status 186 | - Confirm transaction confirmations 187 | 188 | 3. **Token Discovery Issues** 189 | - Check RPC endpoint connectivity 190 | - Verify minimum liquidity settings 191 | - Review token filtering criteria 192 | 193 | ## Contributing 194 | 195 | 1. Fork the repository 196 | 2. Create your feature branch 197 | 3. Commit your changes 198 | 4. Push to the branch 199 | 5. Create a new Pull Request 200 | 201 | ## License 202 | 203 | This project is licensed under the MIT License. 204 | 205 | ## Disclaimer 206 | 207 | Trading cryptocurrencies carries significant risk. This software is for educational purposes only. Always do your own research and trade responsibly. 208 | -------------------------------------------------------------------------------- /scanner.js: -------------------------------------------------------------------------------- 1 | const { Connection, PublicKey } = require('@solana/web3.js'); 2 | const fetch = require('node-fetch'); 3 | const config = require('./config'); 4 | const wsClient = require('./websocket-client'); 5 | const fs = require('fs'); 6 | 7 | // Set to store previously purchased tokens 8 | const purchasedTokens = new Set(); 9 | 10 | // Load previously purchased tokens from file if it exists 11 | try { 12 | if (fs.existsSync('purchased_tokens.json')) { 13 | const data = fs.readFileSync('purchased_tokens.json', 'utf8'); 14 | const tokens = JSON.parse(data); 15 | tokens.forEach(token => purchasedTokens.add(token)); 16 | } 17 | } catch (error) { 18 | console.error('Error loading purchased tokens:', error.message); 19 | } 20 | 21 | // Function to save purchased tokens to file 22 | function savePurchasedTokens() { 23 | try { 24 | fs.writeFileSync('purchased_tokens.json', JSON.stringify(Array.from(purchasedTokens), null, 2)); 25 | } catch (error) { 26 | console.error('Error saving purchased tokens:', error.message); 27 | } 28 | } 29 | 30 | // Connect to WebSocket server 31 | wsClient.connect(); 32 | 33 | // Store original console methods 34 | const originalConsole = { 35 | log: console.log, 36 | error: console.error 37 | }; 38 | 39 | // Override console.log to send to web interface 40 | console.log = function() { 41 | const text = Array.from(arguments).join(' '); 42 | originalConsole.log.apply(console, arguments); 43 | wsClient.sendToWeb('log', text); 44 | }; 45 | 46 | // Override console.error to send to web interface 47 | console.error = function() { 48 | const text = Array.from(arguments).join(' '); 49 | originalConsole.error.apply(console, arguments); 50 | wsClient.sendToWeb('log', 'Error: ' + text); 51 | }; 52 | 53 | async function getTokenProfiles() { 54 | try { 55 | console.log('Fetching latest token profiles...'); 56 | const url = 'https://api.dexscreener.com/token-profiles/latest/v1'; 57 | console.log('Request URL:', url); 58 | 59 | const response = await fetch(url, { 60 | headers: { 61 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 62 | 'Accept': 'application/json' 63 | } 64 | }); 65 | 66 | if (!response.ok) { 67 | console.error('Response status:', response.status); 68 | console.error('Response headers:', response.headers); 69 | const text = await response.text(); 70 | console.error('Response text:', text.substring(0, 200) + '...'); // First 200 chars 71 | throw new Error(`HTTP error! status: ${response.status}`); 72 | } 73 | 74 | const jsonData = await response.json(); 75 | 76 | if (!Array.isArray(jsonData) || jsonData.length === 0) { 77 | console.error("No valid token data found"); 78 | return []; 79 | } 80 | 81 | let allPairs = []; 82 | console.log("Fetching detailed token data..."); 83 | 84 | for (const token of jsonData) { 85 | // Skip if token was already purchased 86 | if (purchasedTokens.has(token.tokenAddress)) { 87 | console.log(`Skipping ${token.tokenAddress} - already purchased before`); 88 | continue; 89 | } 90 | 91 | const searchUrl = `https://api.dexscreener.com/latest/dex/search?q=${token.tokenAddress}`; 92 | const searchResponse = await fetch(searchUrl, { 93 | headers: { 94 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 95 | 'Accept': 'application/json' 96 | } 97 | }); 98 | 99 | if (!searchResponse.ok) continue; 100 | 101 | const searchResult = await searchResponse.json(); 102 | 103 | if (searchResult?.pairs?.[0]?.chainId === 'solana' && 104 | searchResult.pairs[0].marketCap && 105 | (searchResult.pairs[0].quoteToken.symbol === 'USDC' || 106 | searchResult.pairs[0].quoteToken.symbol === 'SOL')) { 107 | 108 | const pair = searchResult.pairs[0]; 109 | const tokenData = { 110 | address: token.tokenAddress, 111 | name: pair.baseToken.name, 112 | symbol: pair.baseToken.symbol, 113 | liquidity: pair.liquidity?.usd || 0, 114 | volume24h: pair.volume?.h24 || 0, 115 | marketCap: pair.marketCap || 0, 116 | priceUsd: parseFloat(pair.priceUsd) || 0, 117 | createdAt: pair.pairCreatedAt, 118 | description: token.description 119 | }; 120 | 121 | allPairs.push(tokenData); 122 | console.log(`Found promising token: ${tokenData.name} (${tokenData.symbol})`); 123 | console.log(`• Address: ${tokenData.address}`); 124 | console.log(`• Market Cap: $${tokenData.marketCap.toLocaleString()}`); 125 | console.log(`• Liquidity: $${tokenData.liquidity.toLocaleString()}`); 126 | console.log(''); 127 | } 128 | } 129 | 130 | // Filter and sort tokens 131 | const solanaTokens = allPairs 132 | .filter(token => token.marketCap >= 20000 && token.marketCap < 2000000 && token.liquidity >= 10000) 133 | .sort((a, b) => (b.liquidity || 0) - (a.liquidity || 0)); 134 | 135 | console.log(`Found ${solanaTokens.length} suitable Solana tokens\n`); 136 | 137 | return solanaTokens; 138 | } catch (error) { 139 | console.error('Error fetching token profiles:', error.message); 140 | return []; 141 | } 142 | } 143 | 144 | async function getTokenOrders(tokenAddress) { 145 | try { 146 | const response = await fetch(`https://api.dexscreener.com/orders/v1/solana/${tokenAddress}`); 147 | const data = await response.json(); 148 | return data; 149 | } catch (error) { 150 | console.error(`Error fetching orders for ${tokenAddress}:`, error.message); 151 | return null; 152 | } 153 | } 154 | 155 | async function getDexScreenerInfo(tokenAddress) { 156 | try { 157 | const response = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`, { 158 | headers: { 159 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 160 | 'Accept': 'application/json' 161 | } 162 | }); 163 | 164 | if (!response.ok) { 165 | throw new Error(`HTTP error! status: ${response.status}`); 166 | } 167 | 168 | const data = await response.json(); 169 | 170 | if (!data.pairs || data.pairs.length === 0) { 171 | console.log(`No pairs found for token ${tokenAddress}`); 172 | return null; 173 | } 174 | 175 | // Get the pair with highest liquidity and USDC or SOL as quote token 176 | const validPairs = data.pairs.filter(pair => 177 | pair.quoteToken.symbol === 'USDC' || pair.quoteToken.symbol === 'SOL' 178 | ); 179 | 180 | if (validPairs.length === 0) { 181 | console.log(`No valid pairs found for token ${tokenAddress}`); 182 | return null; 183 | } 184 | 185 | const mainPair = validPairs.sort((a, b) => { 186 | const liquidityA = a.liquidity?.usd || 0; 187 | const liquidityB = b.liquidity?.usd || 0; 188 | return liquidityB - liquidityA; 189 | })[0]; 190 | 191 | return { 192 | marketCap: mainPair.marketCap || 0, 193 | priceUsd: parseFloat(mainPair.priceUsd) || 0, 194 | liquidity: mainPair.liquidity?.usd || 0, 195 | volume24h: mainPair.volume?.h24 || 0, 196 | priceChange24h: mainPair.priceChange?.h24 || 0, 197 | createdAt: mainPair.pairCreatedAt, 198 | dexId: mainPair.dexId, 199 | quoteToken: mainPair.quoteToken.symbol 200 | }; 201 | } catch (error) { 202 | console.error('Error fetching DexScreener info:', error.message); 203 | return null; 204 | } 205 | } 206 | 207 | function analyzeToken(tokenInfo) { 208 | let score = 0; 209 | 210 | // Market Cap Score (0-30 points) 211 | if (tokenInfo.marketCap >= 1000000) score += 30; 212 | else if (tokenInfo.marketCap >= 500000) score += 20; 213 | else if (tokenInfo.marketCap >= 100000) score += 10; 214 | 215 | // Liquidity Score (0-30 points) 216 | if (tokenInfo.liquidity >= 100000) score += 30; 217 | else if (tokenInfo.liquidity >= 50000) score += 20; 218 | else if (tokenInfo.liquidity >= 10000) score += 10; 219 | 220 | // Volume Score (0-20 points) 221 | if (tokenInfo.volume24h >= tokenInfo.marketCap * 0.2) score += 20; 222 | else if (tokenInfo.volume24h >= tokenInfo.marketCap * 0.1) score += 10; 223 | 224 | // Volatility Score (0-20 points) 225 | const absChange = Math.abs(tokenInfo.priceChange24h); 226 | if (absChange >= 5 && absChange <= 50) score += 20; 227 | else if (absChange > 50) score += 10; 228 | 229 | return score; 230 | } 231 | 232 | async function findBestToken() { 233 | console.log('\nStarting token analysis...'); 234 | 235 | const tokens = await getTokenProfiles(); 236 | let tokenMetrics = []; 237 | 238 | console.log(`\nAnalyzing ${tokens.length} tokens for trading opportunities...`); 239 | 240 | for (const token of tokens) { 241 | // Get fresh market data 242 | const tokenInfo = await getDexScreenerInfo(token.address); 243 | if (!tokenInfo) { 244 | console.log('No market data available, skipping...'); 245 | continue; 246 | } 247 | 248 | // Calculate age in hours 249 | const createdAt = new Date(tokenInfo.createdAt); 250 | const now = new Date(); 251 | const ageInHours = (now - createdAt) / (1000 * 60 * 60); 252 | 253 | // Skip tokens older than 48 hours 254 | if (ageInHours > 48) { 255 | console.log(`Token too old (${Math.round(ageInHours)} hours), skipping...`); 256 | continue; 257 | } 258 | 259 | // Calculate per hour metrics 260 | const marketCapPerHour = tokenInfo.marketCap / ageInHours; 261 | const liquidityPerHour = tokenInfo.liquidity / ageInHours; 262 | const volumePerHour = tokenInfo.volume24h / Math.min(ageInHours, 24); 263 | 264 | // Calculate score with emphasis on recent tokens 265 | const score = ( 266 | (marketCapPerHour * 0.3) + 267 | (liquidityPerHour * 0.3) + 268 | (volumePerHour * 0.2) + 269 | ((48 - ageInHours) * 1000) // Bonus for newer tokens 270 | ); 271 | 272 | console.log('\nToken Analysis:'); 273 | console.log(`Name: ${token.name} (${token.symbol})`); 274 | console.log(`Address: ${token.address}`); 275 | console.log(`Age: ${ageInHours < 1 ? `${Math.round(ageInHours * 60)} minutes` : `${Math.round(ageInHours)} hours`}`); 276 | console.log(`Market Cap: $${tokenInfo.marketCap.toLocaleString()} ($${Math.round(marketCapPerHour).toLocaleString()}/hour)`); 277 | console.log(`Liquidity: $${tokenInfo.liquidity.toLocaleString()} ($${Math.round(liquidityPerHour).toLocaleString()}/hour)`); 278 | console.log(`24h Volume: $${tokenInfo.volume24h.toLocaleString()} ($${Math.round(volumePerHour).toLocaleString()}/hour)`); 279 | console.log(`Score: ${Math.round(score).toLocaleString()}`); 280 | if (token.description) { 281 | console.log(`Description: ${token.description}`); 282 | } 283 | 284 | tokenMetrics.push({ 285 | token: { 286 | address: token.address, 287 | name: token.name, 288 | symbol: token.symbol, 289 | description: token.description 290 | }, 291 | info: tokenInfo, 292 | ageInHours, 293 | score, 294 | marketCapPerHour, 295 | liquidityPerHour, 296 | volumePerHour 297 | }); 298 | } 299 | 300 | if (tokenMetrics.length === 0) { 301 | console.log('No suitable tokens found'); 302 | return null; 303 | } 304 | 305 | // Sort by score and get top 3 306 | const topTokens = tokenMetrics 307 | .sort((a, b) => b.score - a.score) 308 | .slice(0, 3); 309 | 310 | console.log('\n=== Top 3 Most Interesting Tokens ==='); 311 | topTokens.forEach((item, index) => { 312 | const ageText = item.ageInHours < 1 313 | ? `${Math.round(item.ageInHours * 60)} minutes` 314 | : `${Math.round(item.ageInHours)} hours`; 315 | 316 | console.log(`\n${index + 1}. ${item.token.name} (${item.token.symbol})`); 317 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); 318 | console.log(`• Age: ${ageText}`); 319 | console.log(`• Market Cap: $${item.info.marketCap.toLocaleString()} ($${Math.round(item.marketCapPerHour).toLocaleString()}/hour)`); 320 | console.log(`• Liquidity: $${item.info.liquidity.toLocaleString()} ($${Math.round(item.liquidityPerHour).toLocaleString()}/hour)`); 321 | console.log(`• Volume 24h: $${item.info.volume24h.toLocaleString()} ($${Math.round(item.volumePerHour).toLocaleString()}/hour)`); 322 | console.log(`• Created: ${new Date(item.info.createdAt).toLocaleString()}`); 323 | console.log(`• Address: ${item.token.address}`); 324 | if (item.token.description) { 325 | console.log(`• Description: ${item.token.description}`); 326 | } 327 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); 328 | }); 329 | 330 | // Select the newest token from top 3 331 | const selectedToken = topTokens.reduce((newest, current) => 332 | current.ageInHours < newest.ageInHours ? current : newest 333 | , topTokens[0]); 334 | 335 | if (selectedToken) { 336 | console.log('\n=== Selected Token for Purchase ==='); 337 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); 338 | console.log(`Name: ${selectedToken.token.name} (${selectedToken.token.symbol})`); 339 | console.log(`Address: ${selectedToken.token.address}`); 340 | console.log(`Market Cap: $${selectedToken.info.marketCap.toLocaleString()}`); 341 | console.log(`Liquidity: $${selectedToken.info.liquidity.toLocaleString()}`); 342 | console.log(`24h Volume: $${selectedToken.info.volume24h.toLocaleString()}`); 343 | if (selectedToken.token.description) { 344 | console.log(`Description: ${selectedToken.token.description}`); 345 | } 346 | console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); 347 | 348 | // Add token to purchased set and save 349 | purchasedTokens.add(selectedToken.token.address); 350 | savePurchasedTokens(); 351 | 352 | // Save to file for trader.js 353 | const bestToken = { 354 | address: selectedToken.token.address, 355 | symbol: selectedToken.token.symbol, 356 | name: selectedToken.token.name, 357 | info: selectedToken.info 358 | }; 359 | 360 | console.log('\nSaving selected token to file for trader...'); 361 | fs.writeFileSync('selected_token.json', JSON.stringify(bestToken, null, 2)); 362 | console.log('Token information saved successfully'); 363 | } 364 | 365 | return selectedToken; 366 | } 367 | 368 | async function main() { 369 | try { 370 | while (true) { 371 | await findBestToken(); 372 | console.log(`\nWaiting ${config.SCAN_INTERVAL_MINUTES} minutes before next scan...`); 373 | await new Promise(resolve => setTimeout(resolve, config.SCAN_INTERVAL_MINUTES * 60 * 1000)); 374 | } 375 | } catch (error) { 376 | console.error('Error in main loop:', error.message); 377 | } 378 | } 379 | 380 | main(); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Gambling Terminal 7 | 8 | 9 | 10 | 246 | 247 | 248 |
249 |
250 |
251 | 252 |
GAMBLING terminal
253 |
254 |
Waiting for script to start...
255 |
256 | 257 |
258 |
259 |

Active positions:

260 | 261 | 262 | 263 | 264 | 265 | 266 |
Loading...
267 | 268 |

Sold positions:

269 | 270 | 271 | 272 | 273 | 274 | 275 |
Loading...
276 |
277 | 278 |
279 | 280 | 281 | 284 | 287 | 288 |
282 | Balance: 283 | 285 | Loading... 286 |
289 |
290 |
291 |
292 |
293 |
294 |

About Project

295 |
296 |

This innovative gambling terminal is designed to provide degens with a thrilling demonstration of the risks and rewards of aggressive trading. The terminal automatically buys and sells meme coins based on real-time market conditions, showcasing the unpredictable nature of fast-paced crypto trading. 297 |

298 |

Its primary goal? To illustrate just how profitable—or devastating—such impulsive trading strategies can be. Users can watch as the terminal navigates the chaotic world of meme coins, making decisions without emotional hesitation, and revealing the raw volatility of the market.

299 |

300 | Will the terminal score massive gains or crash into losses? The results serve as both an adrenaline rush and a valuable lesson for anyone tempted by the allure of high-risk, high-reward crypto speculation.

301 |
302 |
303 |
304 |

Links

305 | 310 |
311 |
312 | 313 |
314 |
315 |

How It Works

316 |
317 |
318 |
Step 1
319 |
Token Discovery
320 |
321 | The system continuously scans the Solana network for newly created tokens, analyzing their market data, liquidity, and trading patterns in real-time. 322 |
323 |
324 |
325 |
Step 2
326 |
Analysis & Trading
327 |
328 | When a potential opportunity is identified, the terminal automatically executes trades based on predefined criteria, managing risk through stop-loss and take-profit orders. 329 |
330 |
331 |
332 |
Step 3
333 |
Position Management
334 |
335 | Active positions are continuously monitored for optimal exit points, while the system tracks performance metrics and maintains a history of completed trades. 336 |
337 |
338 |
339 |
340 |
341 | 342 |
343 |
344 |

How to Use It

345 |
346 |
347 |
Installation
348 |
349 |
    350 |
  1. Clone the repository:
    git clone https://github.com/yourusername/project.git
  2. 351 |
  3. Install dependencies:
    npm install
  4. 352 |
  5. Create a .env file in the root directory
  6. 353 |
354 |
355 |
356 |
357 |
Configuration
358 |
359 |

Add the following to your .env file:

360 | 361 | RPC_URL=your_rpc_url
362 | PRIVATE_KEY=your_wallet_private_key
363 | AMOUNT_SOL=0.1
364 | TAKE_PROFIT=20
365 | STOP_LOSS=10 366 |
367 |

Adjust the values according to your trading preferences.

368 |
369 |
370 |
371 |
Running
372 |
373 |
    374 |
  1. Start the server:
    npm run start
  2. 375 |
  3. Open your browser and navigate to:
    http://localhost:3000
  4. 376 |
  5. Monitor the terminal for trading activity
  6. 377 |
378 |
379 |
380 |
381 |
382 |
383 | 384 | 626 | 627 | 628 | -------------------------------------------------------------------------------- /trader.js: -------------------------------------------------------------------------------- 1 | const { Connection, PublicKey, Transaction, sendAndConfirmTransaction, ComputeBudgetProgram, VersionedTransaction, TransactionMessage } = require('@solana/web3.js'); 2 | const { Keypair } = require('@solana/web3.js'); 3 | const bs58 = require('bs58'); 4 | const fetch = require('node-fetch'); 5 | const fs = require('fs'); 6 | const config = require('./config'); 7 | const wsClient = require('./websocket-client'); 8 | 9 | // Connect to WebSocket server 10 | wsClient.connect(); 11 | 12 | // Store original console methods 13 | const originalConsole = { 14 | log: console.log, 15 | error: console.error 16 | }; 17 | 18 | // Override console.log to send to web interface 19 | console.log = function() { 20 | const text = Array.from(arguments).join(' '); 21 | originalConsole.log.apply(console, arguments); 22 | wsClient.sendToWeb('log', text); 23 | }; 24 | 25 | // Override console.error to send to web interface 26 | console.error = function() { 27 | const text = Array.from(arguments).join(' '); 28 | originalConsole.error.apply(console, arguments); 29 | wsClient.sendToWeb('log', 'Error: ' + text); 30 | }; 31 | 32 | // Store active trading positions 33 | let activeTokens = new Map(); 34 | 35 | // Add tracking file management 36 | function saveActiveTokens() { 37 | const tokensData = Array.from(activeTokens.entries()).map(([address, data]) => ({ 38 | address, 39 | name: data.name, 40 | symbol: data.symbol, 41 | initialPrice: data.initialPrice, 42 | initialLiquidity: data.initialLiquidity, 43 | purchaseTime: data.purchaseTime, 44 | purchaseAmount: data.purchaseAmount 45 | })); 46 | 47 | fs.writeFileSync('active_tokens.json', JSON.stringify(tokensData, null, 2)); 48 | console.log('Active tokens saved'); 49 | } 50 | 51 | function loadActiveTokens() { 52 | try { 53 | if (fs.existsSync('active_tokens.json')) { 54 | const data = fs.readFileSync('active_tokens.json', 'utf8'); 55 | const tokensData = JSON.parse(data); 56 | activeTokens.clear(); 57 | tokensData.forEach(token => { 58 | activeTokens.set(token.address, { 59 | name: token.name, 60 | symbol: token.symbol, 61 | initialPrice: token.initialPrice, 62 | initialLiquidity: token.initialLiquidity, 63 | purchaseTime: token.purchaseTime, 64 | purchaseAmount: token.purchaseAmount, 65 | address: token.address 66 | }); 67 | }); 68 | console.log(`Loaded ${activeTokens.size} active tokens`); 69 | } 70 | } catch (error) { 71 | console.error('Error loading active tokens:', error.message); 72 | } 73 | } 74 | 75 | // Load active tokens at startup 76 | loadActiveTokens(); 77 | 78 | let lastSuccessfulPriceApi = null; 79 | let lastPriceCheck = 0; 80 | let cachedSolPrice = null; 81 | 82 | // Remove hardcoded RPC endpoints and use config 83 | const RPC_ENDPOINTS = config.RPC_ENDPOINTS; 84 | 85 | let currentRpcIndex = 0; 86 | 87 | // Add token removal tracking 88 | const tokenRemovalCandidates = new Map(); // Map to track tokens that might need to be removed 89 | const tokenAccountCache = new Map(); // Cache to store token account data 90 | 91 | // Add sold tokens tracking 92 | const soldTokensWithPnL = new Map(); 93 | 94 | function saveSoldTokensWithPnL() { 95 | try { 96 | const soldTokensData = Array.from(soldTokensWithPnL.entries()).map(([address, data]) => ({ 97 | address, 98 | symbol: data.symbol, 99 | name: data.name, 100 | profitLoss: data.profitLoss, 101 | soldAt: data.soldAt 102 | })); 103 | fs.writeFileSync('sold_positions.json', JSON.stringify(soldTokensData, null, 2)); 104 | console.log('Saved sold positions data'); 105 | } catch (error) { 106 | console.error('Error saving sold positions:', error); 107 | } 108 | } 109 | 110 | function loadSoldTokensWithPnL() { 111 | try { 112 | if (fs.existsSync('sold_positions.json')) { 113 | const data = fs.readFileSync('sold_positions.json', 'utf8'); 114 | const tokens = JSON.parse(data); 115 | soldTokensWithPnL.clear(); 116 | tokens.forEach(token => { 117 | soldTokensWithPnL.set(token.address, { 118 | symbol: token.symbol, 119 | name: token.name, 120 | profitLoss: token.profitLoss, 121 | soldAt: token.soldAt 122 | }); 123 | }); 124 | console.log(`Loaded ${soldTokensWithPnL.size} sold positions`); 125 | } 126 | } catch (error) { 127 | console.error('Error loading sold positions:', error); 128 | } 129 | } 130 | 131 | // Load sold positions at startup 132 | loadSoldTokensWithPnL(); 133 | 134 | async function createConnection() { 135 | const currentEndpoint = RPC_ENDPOINTS[currentRpcIndex]; 136 | // console.log(`Creating connection using RPC endpoint [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${currentEndpoint}`); 137 | 138 | const options = { 139 | commitment: 'confirmed', 140 | confirmTransactionInitialTimeout: 1000, 141 | wsEndpoint: currentEndpoint.startsWith('https://') ? 142 | currentEndpoint.replace('https://', 'wss://') : 143 | undefined 144 | }; 145 | 146 | return new Connection(currentEndpoint, options); 147 | } 148 | 149 | async function getWorkingConnection() { 150 | let connection; 151 | let attempts = 0; 152 | const maxAttempts = RPC_ENDPOINTS.length * 2; // Try each endpoint twice 153 | 154 | while (attempts < maxAttempts) { 155 | try { 156 | connection = await createConnection(); 157 | // Test the connection 158 | await connection.getSlot(); 159 | console.log(`Successfully connected to RPC [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${RPC_ENDPOINTS[currentRpcIndex]}`); 160 | return connection; 161 | } catch (error) { 162 | // console.error(`RPC connection failed [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}] (${RPC_ENDPOINTS[currentRpcIndex]}):`, error.message); 163 | // Switch to next RPC endpoint 164 | currentRpcIndex = (currentRpcIndex + 1) % RPC_ENDPOINTS.length; 165 | attempts++; 166 | 167 | if (attempts < maxAttempts) { 168 | // console.log(`Switching to next RPC endpoint [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${RPC_ENDPOINTS[currentRpcIndex]}`); 169 | } 170 | } 171 | } 172 | throw new Error('All RPC endpoints failed'); 173 | } 174 | 175 | async function withRpcRetry(operation) { 176 | let attempts = 0; 177 | const maxAttempts = RPC_ENDPOINTS.length * 2; 178 | 179 | while (attempts < maxAttempts) { 180 | try { 181 | return await operation(); 182 | } catch (error) { 183 | if (error.message.includes('429') || error.message.includes('Too many requests')) { 184 | // console.log(`RPC rate limit hit [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}] (${RPC_ENDPOINTS[currentRpcIndex]}), switching endpoint...`); 185 | currentRpcIndex = (currentRpcIndex + 1) % RPC_ENDPOINTS.length; 186 | console.log(`Switched to RPC [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${RPC_ENDPOINTS[currentRpcIndex]}`); 187 | const connection = await createConnection(); 188 | attempts++; 189 | 190 | if (attempts < maxAttempts) { 191 | await new Promise(resolve => setTimeout(resolve, 1000)); 192 | continue; 193 | } 194 | } 195 | throw error; 196 | } 197 | } 198 | throw new Error('Operation failed after all RPC retries'); 199 | } 200 | 201 | async function getTokenMetadata(mintAddress) { 202 | try { 203 | const response = await fetch(`https://tokens.jup.ag/token/${mintAddress}`); 204 | const token = await response.json(); 205 | 206 | if (token) { 207 | return { 208 | name: token.name, 209 | symbol: token.symbol, 210 | decimals: token.decimals, 211 | tags: token.tags, 212 | dailyVolume: token.daily_volume 213 | }; 214 | } 215 | return null; 216 | } catch (error) { 217 | console.error('Error fetching token metadata:', error.message); 218 | return null; 219 | } 220 | } 221 | 222 | async function getTokenDecimals(connection, mintAddress) { 223 | try { 224 | const info = await connection.getParsedAccountInfo(new PublicKey(mintAddress)); 225 | return info.value?.data?.parsed?.info?.decimals ?? null; 226 | } catch (error) { 227 | console.error('Error fetching token decimals:', error.message); 228 | return null; 229 | } 230 | } 231 | 232 | async function getSolPrice() { 233 | const CACHE_DURATION = 60000; // 1 minute cache 234 | const currentTime = Date.now(); 235 | 236 | // Return cached price if available and not expired 237 | if (cachedSolPrice && lastPriceCheck && (currentTime - lastPriceCheck < CACHE_DURATION)) { 238 | return cachedSolPrice; 239 | } 240 | 241 | const APIs = [ 242 | { 243 | name: 'CoinGecko', 244 | url: 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd', 245 | handler: (data) => data?.solana?.usd 246 | }, 247 | { 248 | name: 'Jupiter', 249 | url: 'https://price.jup.ag/v4/price?ids=SOL', 250 | handler: (data) => data?.data?.SOL?.price 251 | }, 252 | { 253 | name: 'Birdeye', 254 | url: 'https://public-api.birdeye.so/public/price?address=So11111111111111111111111111111111111111112', 255 | handler: (data) => data?.data?.value 256 | } 257 | ]; 258 | 259 | // Try last successful API first 260 | if (lastSuccessfulPriceApi) { 261 | try { 262 | const response = await fetch(lastSuccessfulPriceApi.url, { 263 | headers: { 264 | 'Accept': 'application/json', 265 | 'User-Agent': 'Mozilla/5.0' 266 | } 267 | }); 268 | 269 | if (response.ok) { 270 | const data = await response.json(); 271 | const price = lastSuccessfulPriceApi.handler(data); 272 | 273 | if (price && !isNaN(price) && price > 0) { 274 | // console.log(`Got SOL price from ${lastSuccessfulPriceApi.name}: $${price}`); 275 | cachedSolPrice = price; 276 | lastPriceCheck = currentTime; 277 | return price; 278 | } 279 | } 280 | } catch (error) { 281 | // console.log(`Previous successful API (${lastSuccessfulPriceApi.name}) failed, trying others...`); 282 | } 283 | } 284 | 285 | // Try other APIs 286 | for (const api of APIs) { 287 | // Skip if this was the last successful API we just tried 288 | if (lastSuccessfulPriceApi && api.name === lastSuccessfulPriceApi.name) { 289 | continue; 290 | } 291 | 292 | try { 293 | const response = await fetch(api.url, { 294 | headers: { 295 | 'Accept': 'application/json', 296 | 'User-Agent': 'Mozilla/5.0' 297 | }, 298 | timeout: 5000 // 5 second timeout 299 | }); 300 | 301 | if (!response.ok) { 302 | throw new Error(`HTTP error! status: ${response.status}`); 303 | } 304 | 305 | const data = await response.json(); 306 | const price = api.handler(data); 307 | 308 | if (price && !isNaN(price) && price > 0) { 309 | // console.log(`Got SOL price from ${api.name}: $${price}`); 310 | lastSuccessfulPriceApi = api; 311 | cachedSolPrice = price; 312 | lastPriceCheck = currentTime; 313 | return price; 314 | } 315 | } catch (error) { 316 | console.log(`${api.name} API failed: ${error.message}`); 317 | } 318 | } 319 | 320 | // If we have a cached price, use it even if expired 321 | if (cachedSolPrice) { 322 | console.log(`Using last known price: $${cachedSolPrice}`); 323 | return cachedSolPrice; 324 | } 325 | 326 | // Last resort: use default price 327 | console.log('All price APIs failed and no cached price available, using default price'); 328 | return 20; 329 | } 330 | 331 | async function getDexScreenerInfo(tokenAddress) { 332 | try { 333 | const response = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`); 334 | const data = await response.json(); 335 | 336 | if (data.pairs && data.pairs.length > 0) { 337 | const mainPair = data.pairs.sort((a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0))[0]; 338 | return { 339 | marketCap: mainPair.marketCap || 0, 340 | priceUsd: parseFloat(mainPair.priceUsd) || 0, 341 | liquidity: mainPair.liquidity?.usd || 0, 342 | volume24h: mainPair.volume?.h24 || 0, 343 | priceChange24h: mainPair.priceChange?.h24 || 0, 344 | createdAt: mainPair.createdAt, 345 | dexId: mainPair.dexId 346 | }; 347 | } 348 | return null; 349 | } catch (error) { 350 | console.error('Error fetching DexScreener info:', error.message); 351 | return null; 352 | } 353 | } 354 | 355 | async function checkTransactionStatus(connection, signature) { 356 | let retries = 30; 357 | while (retries > 0) { 358 | try { 359 | const status = await connection.getSignatureStatus(signature); 360 | if (status.value?.confirmationStatus === 'confirmed' || status.value?.confirmationStatus === 'finalized') { 361 | return true; 362 | } 363 | } catch (error) { 364 | console.error('Error checking transaction status:', error.message); 365 | } 366 | await new Promise(resolve => setTimeout(resolve, 1000)); 367 | retries--; 368 | } 369 | return false; 370 | } 371 | 372 | async function customSendAndConfirmTransaction(connection, transaction, wallet, retries = 3) { 373 | for (let i = 0; i < retries; i++) { 374 | try { 375 | // Get latest blockhash 376 | const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); 377 | 378 | // Sign the transaction 379 | transaction.sign([wallet]); 380 | 381 | // Send raw transaction 382 | const rawTransaction = transaction.serialize(); 383 | const txid = await connection.sendRawTransaction(rawTransaction, { 384 | skipPreflight: true, 385 | maxRetries: 2 386 | }); 387 | 388 | // console.log(`Transaction sent via RPC [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${RPC_ENDPOINTS[currentRpcIndex]}`); 389 | // console.log(`Signature: ${txid}`); 390 | // console.log(`View in Explorer: https://solscan.io/tx/${txid}`); 391 | 392 | // Start async confirmation check 393 | checkTransactionConfirmation(connection, txid, blockhash, lastValidBlockHeight); 394 | 395 | // Return signature immediately 396 | return txid; 397 | } catch (error) { 398 | console.error(`Transaction attempt ${i + 1} failed:`, error.message); 399 | if (i < retries - 1) { 400 | currentRpcIndex = (currentRpcIndex + 1) % RPC_ENDPOINTS.length; 401 | // console.log(`Switching to RPC [${currentRpcIndex + 1}/${RPC_ENDPOINTS.length}]: ${RPC_ENDPOINTS[currentRpcIndex]}`); 402 | connection = await createConnection(); 403 | await new Promise(resolve => setTimeout(resolve, 500)); 404 | } 405 | } 406 | } 407 | throw new Error('Transaction failed after all retries'); 408 | } 409 | 410 | async function checkTransactionConfirmation(connection, signature, blockhash, lastValidBlockHeight) { 411 | try { 412 | const confirmation = await connection.confirmTransaction({ 413 | signature: signature, 414 | blockhash: blockhash, 415 | lastValidBlockHeight: lastValidBlockHeight 416 | }, 'confirmed'); 417 | 418 | if (confirmation?.value?.err) { 419 | console.error(`Transaction failed: ${confirmation.value.err}`); 420 | } else { 421 | console.log(`Transaction confirmed: ${signature}`); 422 | } 423 | } catch (error) { 424 | console.error('Error confirming transaction:', error.message); 425 | } 426 | } 427 | 428 | async function buyToken(connection, wallet, token) { 429 | try { 430 | // Check if we already have this token 431 | if (activeTokens.has(token.address)) { 432 | console.log(`Token ${token.symbol} is already in active positions`); 433 | fs.unlinkSync('selected_token.json'); 434 | return false; 435 | } 436 | 437 | // Delete selected_token.json before attempting purchase to prevent double buys 438 | if (fs.existsSync('selected_token.json')) { 439 | fs.unlinkSync('selected_token.json'); 440 | } 441 | 442 | console.log(`\n=== Buying ${token.name} (${token.symbol}) ===`); 443 | // console.log(`• Address: ${token.address}`); 444 | // console.log(`• Price: $${token.info.priceUsd}`); 445 | // console.log(`• Liquidity: $${token.info.liquidity.toLocaleString()}`); 446 | 447 | const inputMint = "So11111111111111111111111111111111111111112"; // SOL 448 | const outputMint = token.address; 449 | 450 | // Validate token address 451 | try { 452 | new PublicKey(outputMint); 453 | } catch (error) { 454 | console.error('Invalid token address'); 455 | return false; 456 | } 457 | 458 | // Validate token liquidity and market data 459 | const tokenInfo = await getDexScreenerInfo(outputMint); 460 | if (!tokenInfo) { 461 | console.error('Could not fetch token market data'); 462 | return false; 463 | } 464 | 465 | // Basic validation checks 466 | if (!tokenInfo.liquidity || tokenInfo.liquidity < 1000) { 467 | console.error('Token has insufficient liquidity'); 468 | return false; 469 | } 470 | 471 | // Get token metadata to verify symbol and name 472 | const metadata = await getTokenMetadata(outputMint); 473 | if (metadata) { 474 | console.log('Token metadata from Jupiter:', metadata); 475 | // Verify token metadata matches 476 | if (metadata.symbol && metadata.symbol.toLowerCase() !== token.symbol.toLowerCase()) { 477 | console.error(`Token symbol mismatch. Expected: ${token.symbol}, Got: ${metadata.symbol}`); 478 | return false; 479 | } 480 | } 481 | 482 | console.log(`\nInitiating purchase of ${token.name} (${token.symbol})...`); 483 | // console.log('Token details:'); 484 | // console.log(`- Address: ${outputMint}`); 485 | // console.log(`- Symbol: ${token.symbol}`); 486 | // console.log(`- Name: ${token.name}`); 487 | // console.log(`- Liquidity: $${tokenInfo.liquidity.toLocaleString()}`); 488 | 489 | // 1. Get quote from Jupiter with proper URL encoding 490 | const quoteUrl = new URL('https://quote-api.jup.ag/v6/quote'); 491 | quoteUrl.searchParams.append('inputMint', inputMint); 492 | quoteUrl.searchParams.append('outputMint', outputMint); 493 | quoteUrl.searchParams.append('amount', Math.floor(config.AMOUNT_TO_SPEND * 1e9).toString()); 494 | quoteUrl.searchParams.append('slippageBps', config.SLIPPAGE_BPS.toString()); 495 | quoteUrl.searchParams.append('onlyDirectRoutes', 'false'); 496 | quoteUrl.searchParams.append('asLegacyTransaction', 'false'); 497 | 498 | console.log('Requesting quote with URL:', quoteUrl.toString()); 499 | 500 | const quoteResponse = await fetch(quoteUrl.toString(), { 501 | headers: { 'Content-Type': 'application/json' }, 502 | timeout: 10000 503 | }); 504 | 505 | if (!quoteResponse.ok) { 506 | const errorText = await quoteResponse.text(); 507 | console.error('Quote API Error Response:', errorText); 508 | try { 509 | const errorJson = JSON.parse(errorText); 510 | console.error('Quote API Error Details:', errorJson); 511 | } catch (e) { 512 | console.error('Could not parse error response as JSON'); 513 | } 514 | throw new Error(`HTTP error! status: ${quoteResponse.status} - ${errorText}`); 515 | } 516 | 517 | const quoteData = await quoteResponse.json(); 518 | // console.log('Jupiter API response:', JSON.stringify(quoteData, null, 2)); 519 | 520 | // Verify the output token in the route matches our intended token 521 | if (quoteData.routePlan && quoteData.routePlan.length > 0) { 522 | const lastRoute = quoteData.routePlan[quoteData.routePlan.length - 1]; 523 | if (lastRoute.swapInfo.outputMint !== outputMint) { 524 | console.error(`Route output mint mismatch. Expected: ${outputMint}, Got: ${lastRoute.swapInfo.outputMint}`); 525 | return false; 526 | } 527 | } 528 | 529 | // Check if we have a valid route plan 530 | if (!quoteData || !quoteData.routePlan || quoteData.routePlan.length === 0) { 531 | throw new Error('No valid route plan received'); 532 | } 533 | 534 | // 2. Get serialized transaction 535 | const swapRequestBody = { 536 | quoteResponse: quoteData, 537 | userPublicKey: wallet.publicKey.toString(), 538 | wrapUnwrapSOL: true, 539 | prioritizationFeeLamports: config.PRIORITY_FEE_SOL * 1e9, 540 | asLegacyTransaction: false, 541 | useVersionedTransaction: true, 542 | dynamicComputeUnitLimit: true 543 | }; 544 | 545 | // console.log('Swap request body:', JSON.stringify(swapRequestBody, null, 2)); 546 | 547 | const swapResponse = await fetch('https://quote-api.jup.ag/v6/swap', { 548 | method: 'POST', 549 | headers: { 550 | 'Content-Type': 'application/json' 551 | }, 552 | body: JSON.stringify(swapRequestBody) 553 | }); 554 | 555 | if (!swapResponse.ok) { 556 | const errorText = await swapResponse.text(); 557 | // console.error('Swap API Error Response:', errorText); 558 | try { 559 | const errorJson = JSON.parse(errorText); 560 | // console.error('Swap API Error Details:', errorJson); 561 | } catch (e) { 562 | // console.error('Could not parse error response as JSON'); 563 | } 564 | throw new Error(`Swap API error: ${swapResponse.status} - ${errorText}`); 565 | } 566 | 567 | const swapData = await swapResponse.json(); 568 | // console.log('Swap API response:', JSON.stringify(swapData, null, 2)); 569 | 570 | if (!swapData.swapTransaction) { 571 | throw new Error('No swap transaction received'); 572 | } 573 | 574 | // 3. Deserialize and send the transaction 575 | const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64'); 576 | const transaction = VersionedTransaction.deserialize(swapTransactionBuf); 577 | 578 | // 4. Execute the transaction 579 | console.log('Sending buy transaction...'); 580 | const signature = await customSendAndConfirmTransaction(connection, transaction, wallet); 581 | 582 | // Start checking token balance immediately 583 | let retries = 0; 584 | const maxRetries = 10; 585 | const checkInterval = 3000; // 3 seconds 586 | 587 | async function checkPurchase() { 588 | try { 589 | const tokenAccount = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 590 | mint: new PublicKey(outputMint) 591 | }); 592 | 593 | const tokenBalance = tokenAccount.value.length > 0 594 | ? tokenAccount.value[0].account.data.parsed.info.tokenAmount.uiAmount 595 | : 0; 596 | 597 | if (tokenBalance > 0) { 598 | console.log('Purchase successful!\n'); 599 | 600 | // Add purchase time and amount to token data 601 | activeTokens.set(token.address, { 602 | name: token.name || 'Unknown', 603 | symbol: token.symbol || 'Unknown', 604 | initialPrice: token.info.priceUsd, 605 | initialLiquidity: token.info.liquidity, 606 | address: token.address, 607 | purchaseTime: new Date().toISOString(), 608 | purchaseAmount: tokenBalance 609 | }); 610 | 611 | // Save to file immediately after purchase 612 | saveActiveTokens(); 613 | 614 | console.log('Token added to active positions:', token.symbol); 615 | const solPrice = await getSolPrice(); 616 | const updatedPositionsInfo = await getAllPositionsInfo(connection, wallet, solPrice); 617 | wsClient.updateMonitoringInfo(updatedPositionsInfo); 618 | return true; 619 | } 620 | 621 | if (retries < maxRetries) { 622 | retries++; 623 | setTimeout(checkPurchase, checkInterval); 624 | } else { 625 | console.error('Purchase verification failed: No token balance found after maximum retries'); 626 | return false; 627 | } 628 | } catch (error) { 629 | console.error('Error checking purchase:', error.message); 630 | return false; 631 | } 632 | } 633 | 634 | // Start checking purchase status 635 | checkPurchase(); 636 | return true; 637 | 638 | } catch (error) { 639 | console.error('Error buying token:', error.message); 640 | return false; 641 | } 642 | } 643 | 644 | async function executeSell(connection, wallet, tokenAddress, tokenData, isTriggerSell = false) { 645 | try { 646 | console.log(`\nExecuting sell order...`); 647 | 648 | // Get current token balance 649 | const tokenAccount = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 650 | mint: new PublicKey(tokenAddress) 651 | }); 652 | 653 | if (!tokenAccount.value.length) { 654 | console.error('No token account found for this token'); 655 | return false; 656 | } 657 | 658 | const tokenBalance = tokenAccount.value[0].account.data.parsed.info.tokenAmount.amount; 659 | if (!tokenBalance || tokenBalance === '0') { 660 | console.error('Token balance is zero'); 661 | return false; 662 | } 663 | 664 | // Get current token info for P&L calculation 665 | const tokenInfo = await getDexScreenerInfo(tokenAddress); 666 | if (!tokenInfo) { 667 | console.error('Could not get token info for P&L calculation'); 668 | return false; 669 | } 670 | 671 | // Calculate P&L before executing sell 672 | const profitLoss = ((tokenInfo.priceUsd - tokenData.initialPrice) / tokenData.initialPrice) * 100; 673 | 674 | // Get quote from Jupiter 675 | const quoteResponse = await fetch('https://quote-api.jup.ag/v6/quote?' + new URLSearchParams({ 676 | inputMint: tokenAddress, 677 | outputMint: "So11111111111111111111111111111111111111112", // SOL 678 | amount: tokenBalance, 679 | slippageBps: config.SELL_SLIPPAGE_BPS 680 | })); 681 | 682 | const quoteData = await quoteResponse.json(); 683 | 684 | // Validate output amount (minimum 0.00001 SOL = 10000 lamports) 685 | const minimumOutputAmount = 10000; 686 | if (!quoteData.outAmount || parseInt(quoteData.outAmount) < minimumOutputAmount) { 687 | console.error(`Output amount (${quoteData.outAmount} lamports) is too low to execute sell`); 688 | return false; 689 | } 690 | 691 | // 2. Get serialized transaction 692 | const swapResponse = await fetch('https://quote-api.jup.ag/v6/swap', { 693 | method: 'POST', 694 | headers: { 695 | 'Content-Type': 'application/json' 696 | }, 697 | body: JSON.stringify({ 698 | quoteResponse: quoteData, 699 | userPublicKey: wallet.publicKey.toString(), 700 | wrapUnwrapSOL: true, 701 | prioritizationFeeLamports: 'auto', 702 | dynamicComputeUnitLimit: true 703 | }) 704 | }); 705 | 706 | if (!swapResponse.ok) { 707 | throw new Error(`HTTP error! status: ${swapResponse.status}`); 708 | } 709 | 710 | const swapData = await swapResponse.json(); 711 | 712 | if (!swapData.swapTransaction) { 713 | throw new Error('No swap transaction received'); 714 | } 715 | 716 | // 3. Deserialize and send the transaction 717 | const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64'); 718 | const transaction = VersionedTransaction.deserialize(swapTransactionBuf); 719 | 720 | // 4. Execute the transaction 721 | console.log('Sending sell transaction...'); 722 | const signature = await customSendAndConfirmTransaction(connection, transaction, wallet); 723 | 724 | // After successful sell, save the P&L information 725 | if (isTriggerSell || await checkSaleSuccess(connection, wallet, tokenAddress)) { 726 | console.log(`Sale successful! P&L: ${profitLoss.toFixed(2)}%`); 727 | 728 | // Add to sold positions with P&L info 729 | soldTokensWithPnL.set(tokenAddress, { 730 | symbol: tokenData.symbol, 731 | name: tokenData.name, 732 | profitLoss: profitLoss, 733 | soldAt: new Date().toISOString() 734 | }); 735 | 736 | // Save to file immediately 737 | saveSoldTokensWithPnL(); 738 | 739 | // Remove from active tokens and tracking 740 | activeTokens.delete(tokenAddress); 741 | tokenRemovalCandidates.delete(tokenAddress); 742 | await saveActiveTokens(); 743 | 744 | // Update monitoring info immediately 745 | const solPrice = await getSolPrice(); 746 | const updatedPositionsInfo = await getAllPositionsInfo(connection, wallet, solPrice); 747 | wsClient.updateMonitoringInfo(updatedPositionsInfo); 748 | 749 | return true; 750 | } 751 | 752 | return false; 753 | } catch (error) { 754 | console.error('Error executing sell:', error.message); 755 | return false; 756 | } 757 | } 758 | 759 | async function getAllPositionsInfo(connection, wallet, solPrice) { 760 | const positions = []; 761 | const walletBalance = await connection.getBalance(wallet.publicKey); 762 | const walletBalanceSOL = walletBalance / 1e9; 763 | const walletBalanceUSD = walletBalanceSOL * solPrice; 764 | 765 | console.log(`Active tokens: ${activeTokens.size}`); 766 | 767 | // Create a Map to track which tokens we've processed 768 | const processedTokens = new Map(); 769 | 770 | // First, check all tokens in the wallet 771 | const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 772 | programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') 773 | }); 774 | 775 | console.log(`Found ${tokenAccounts.value.length} token accounts in wallet`); 776 | 777 | // Process all tokens in wallet 778 | for (const account of tokenAccounts.value) { 779 | const tokenAddress = account.account.data.parsed.info.mint; 780 | const balance = account.account.data.parsed.info.tokenAmount.uiAmount; 781 | 782 | // Skip if token was sold before 783 | if (soldTokensWithPnL.has(tokenAddress)) { 784 | continue; 785 | } 786 | 787 | if (balance > 0) { 788 | // Remove from removal candidates if token is found with balance 789 | if (tokenRemovalCandidates.has(tokenAddress)) { 790 | tokenRemovalCandidates.delete(tokenAddress); 791 | } 792 | 793 | // Process token as before... 794 | if (activeTokens.has(tokenAddress)) { 795 | const tokenData = activeTokens.get(tokenAddress); 796 | const tokenInfo = await getDexScreenerInfo(tokenAddress); 797 | 798 | if (tokenInfo) { 799 | const priceChange = ((tokenInfo.priceUsd - tokenData.initialPrice) / tokenData.initialPrice) * 100; 800 | const positionValue = balance * tokenInfo.priceUsd; 801 | 802 | positions.push({ 803 | symbol: tokenData.symbol, 804 | name: tokenData.name, 805 | marketCap: tokenInfo.marketCap, 806 | priceUsd: tokenInfo.priceUsd, 807 | priceChange: priceChange, 808 | balance: balance, 809 | positionValue: positionValue, 810 | purchaseTime: tokenData.purchaseTime 811 | }); 812 | 813 | processedTokens.set(tokenAddress, true); 814 | } 815 | } else { 816 | // Handle new tokens as before... 817 | try { 818 | const tokenInfo = await getDexScreenerInfo(tokenAddress); 819 | const metadata = await getTokenMetadata(tokenAddress); 820 | 821 | if (tokenInfo && metadata) { 822 | activeTokens.set(tokenAddress, { 823 | name: metadata.name, 824 | symbol: metadata.symbol, 825 | initialPrice: tokenInfo.priceUsd, 826 | initialLiquidity: tokenInfo.liquidity, 827 | address: tokenAddress, 828 | purchaseTime: new Date().toISOString(), 829 | purchaseAmount: balance 830 | }); 831 | 832 | positions.push({ 833 | symbol: metadata.symbol, 834 | name: metadata.name, 835 | marketCap: tokenInfo.marketCap, 836 | priceUsd: tokenInfo.priceUsd, 837 | priceChange: 0, 838 | balance: balance, 839 | positionValue: balance * tokenInfo.priceUsd 840 | }); 841 | 842 | processedTokens.set(tokenAddress, true); 843 | saveActiveTokens(); 844 | } 845 | } catch (error) { 846 | console.error(`Error processing unknown token ${tokenAddress}:`, error.message); 847 | } 848 | } 849 | } 850 | } 851 | 852 | // Handle tokens not found in wallet 853 | for (const [address, tokenData] of activeTokens) { 854 | if (!processedTokens.has(address)) { 855 | // If token is not in removal candidates, add it with timestamp 856 | if (!tokenRemovalCandidates.has(address)) { 857 | tokenRemovalCandidates.set(address, { 858 | timestamp: Date.now(), 859 | data: tokenData 860 | }); 861 | } else { 862 | // Check if token has been missing for more than 30 seconds 863 | const removalData = tokenRemovalCandidates.get(address); 864 | if (Date.now() - removalData.timestamp > 30000) { // 30 seconds 865 | console.log(`Removing ${tokenData.symbol} from active tokens - not found in wallet for 30 seconds`); 866 | activeTokens.delete(address); 867 | tokenRemovalCandidates.delete(address); 868 | saveActiveTokens(); 869 | } else { 870 | // Still include the token in positions while waiting 871 | const tokenInfo = await getDexScreenerInfo(address); 872 | if (tokenInfo) { 873 | const priceChange = ((tokenInfo.priceUsd - tokenData.initialPrice) / tokenData.initialPrice) * 100; 874 | positions.push({ 875 | symbol: tokenData.symbol, 876 | name: tokenData.name, 877 | marketCap: tokenInfo.marketCap, 878 | priceUsd: tokenInfo.priceUsd, 879 | priceChange: priceChange, 880 | balance: tokenData.purchaseAmount, 881 | positionValue: tokenData.purchaseAmount * tokenInfo.priceUsd, 882 | purchaseTime: tokenData.purchaseTime 883 | }); 884 | } 885 | } 886 | } 887 | } 888 | } 889 | 890 | // Load and add sold positions to the result 891 | let soldPositions = []; 892 | try { 893 | if (fs.existsSync('sold_positions.json')) { 894 | const soldData = fs.readFileSync('sold_positions.json', 'utf8'); 895 | const soldTokens = JSON.parse(soldData); 896 | soldPositions = soldTokens.map(token => ({ 897 | symbol: token.symbol, 898 | profitLoss: token.profitLoss 899 | })).sort((a, b) => new Date(b.soldAt) - new Date(a.soldAt)); 900 | 901 | console.log(`Loaded ${soldPositions.length} sold positions from file`); 902 | } 903 | } catch (error) { 904 | console.error('Error loading sold positions:', error); 905 | } 906 | 907 | const result = { 908 | positions, 909 | soldPositions, 910 | walletBalanceSOL, 911 | walletBalanceUSD, 912 | lastUpdateTime: new Date().toLocaleTimeString() 913 | }; 914 | 915 | return result; 916 | } 917 | 918 | async function monitorPositions(connection, wallet) { 919 | while (true) { 920 | try { 921 | // Check for new tokens to buy 922 | if (fs.existsSync('selected_token.json')) { 923 | const data = fs.readFileSync('selected_token.json', 'utf8'); 924 | let selectedToken; 925 | 926 | try { 927 | selectedToken = JSON.parse(data); 928 | } catch (error) { 929 | fs.unlinkSync('selected_token.json'); 930 | continue; 931 | } 932 | 933 | if (!selectedToken.address || !selectedToken.name || !selectedToken.symbol) { 934 | fs.unlinkSync('selected_token.json'); 935 | continue; 936 | } 937 | 938 | if (activeTokens.has(selectedToken.address)) { 939 | fs.unlinkSync('selected_token.json'); 940 | continue; 941 | } 942 | 943 | const tokenInfo = await getDexScreenerInfo(selectedToken.address); 944 | if (!tokenInfo || !tokenInfo.liquidity || tokenInfo.liquidity < 1000 || !tokenInfo.priceUsd || tokenInfo.priceUsd <= 0) { 945 | fs.unlinkSync('selected_token.json'); 946 | continue; 947 | } 948 | 949 | const tokenData = { 950 | address: selectedToken.address, 951 | name: selectedToken.name || 'Unknown', 952 | symbol: selectedToken.symbol || 'Unknown', 953 | info: tokenInfo 954 | }; 955 | 956 | const success = await buyToken(connection, wallet, tokenData); 957 | if (success) { 958 | activeTokens.set(selectedToken.address, { 959 | name: selectedToken.name || 'Unknown', 960 | symbol: selectedToken.symbol || 'Unknown', 961 | initialPrice: tokenInfo.priceUsd, 962 | initialLiquidity: tokenInfo.liquidity, 963 | address: selectedToken.address 964 | }); 965 | 966 | console.log('Token added to active positions:', selectedToken.symbol); 967 | const solPrice = await getSolPrice(); 968 | const updatedPositionsInfo = await getAllPositionsInfo(connection, wallet, solPrice); 969 | wsClient.updateMonitoringInfo(updatedPositionsInfo); 970 | fs.unlinkSync('selected_token.json'); 971 | } 972 | } 973 | 974 | // Update positions every 10 seconds 975 | const solPrice = await getSolPrice(); 976 | const allPositionsInfo = await getAllPositionsInfo(connection, wallet, solPrice); 977 | wsClient.updateMonitoringInfo(allPositionsInfo); 978 | 979 | // Check for sell conditions 980 | for (const [address, tokenData] of activeTokens) { 981 | const position = allPositionsInfo.positions.find(p => p.symbol === tokenData.symbol); 982 | if (!position) continue; 983 | 984 | const tokenInfo = await getDexScreenerInfo(address); 985 | if (tokenInfo) { 986 | const liquidityDropPercentage = ((tokenData.initialLiquidity - tokenInfo.liquidity) / tokenData.initialLiquidity) * 100; 987 | if (liquidityDropPercentage > 50) { 988 | console.log(`\n=== Emergency Sell: ${tokenData.symbol} ===`); 989 | console.log(`• Liquidity drop: ${liquidityDropPercentage.toFixed(2)}%`); 990 | const success = await executeSell(connection, wallet, address, tokenData, true); 991 | if (success) { 992 | wsClient.updateMonitoringInfo(await getAllPositionsInfo(connection, wallet, solPrice)); 993 | } 994 | continue; 995 | } 996 | } 997 | 998 | if (position.priceChange <= -config.STOP_LOSS_PERCENTAGE || 999 | position.priceChange >= config.TAKE_PROFIT_PERCENTAGE) { 1000 | console.log(`\n=== Selling ${tokenData.symbol} ===`); 1001 | console.log(`• Price change: ${position.priceChange.toFixed(2)}%`); 1002 | const success = await executeSell(connection, wallet, address, tokenData, true); 1003 | if (success) { 1004 | wsClient.updateMonitoringInfo(await getAllPositionsInfo(connection, wallet, solPrice)); 1005 | } 1006 | } 1007 | } 1008 | 1009 | // Wait 10 seconds before next update 1010 | await new Promise(resolve => setTimeout(resolve, 10000)); 1011 | } catch (error) { 1012 | console.error('Error in monitoring loop:', error.message); 1013 | currentRpcIndex = (currentRpcIndex + 1) % RPC_ENDPOINTS.length; 1014 | connection = await createConnection(); 1015 | await new Promise(resolve => setTimeout(resolve, 1000)); 1016 | } 1017 | } 1018 | } 1019 | 1020 | async function getTokenBalance(connection, tokenMint, owner) { 1021 | try { 1022 | const tokenAccounts = await connection.getParsedTokenAccountsByOwner(owner, { 1023 | mint: new PublicKey(tokenMint) 1024 | }); 1025 | 1026 | if (tokenAccounts.value.length > 0) { 1027 | const balance = tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount; 1028 | return balance > 0 ? balance : 0; 1029 | } 1030 | return 0; 1031 | } catch (error) { 1032 | console.error(`Error getting token balance for ${tokenMint}:`, error); 1033 | return 0; 1034 | } 1035 | } 1036 | 1037 | async function verifyTokensInWallet() { 1038 | try { 1039 | const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 1040 | programId: TOKEN_PROGRAM_ID 1041 | }); 1042 | 1043 | console.log(`Found ${tokenAccounts.value.length} token accounts in wallet`); 1044 | 1045 | // Update token account cache 1046 | tokenAccountCache.clear(); 1047 | for (const { account, pubkey } of tokenAccounts.value) { 1048 | const { mint, tokenAmount } = account.data.parsed.info; 1049 | if (tokenAmount.uiAmount > 0) { 1050 | tokenAccountCache.set(mint, { 1051 | balance: tokenAmount.uiAmount, 1052 | lastSeen: Date.now(), 1053 | accountPubkey: pubkey 1054 | }); 1055 | } 1056 | } 1057 | 1058 | // Check active tokens against cache 1059 | for (const [tokenAddress, tokenData] of activeTokens.entries()) { 1060 | const cachedData = tokenAccountCache.get(tokenAddress); 1061 | 1062 | if (!cachedData) { 1063 | // Only mark for removal if we haven't seen the token for more than 2 minutes 1064 | if (!tokenData.lastSeen || Date.now() - tokenData.lastSeen > 120000) { 1065 | if (!tokenData.isBeingRemoved) { 1066 | console.log(`Token ${tokenData.symbol} (${tokenAddress}) not found in wallet for 2 minutes - marking for removal`); 1067 | tokenData.isBeingRemoved = true; 1068 | tokenData.removalStartTime = Date.now(); 1069 | } else if (Date.now() - tokenData.removalStartTime > 30000) { 1070 | console.log(`Removing ${tokenData.symbol} from active tokens - confirmed absence`); 1071 | activeTokens.delete(tokenAddress); 1072 | await saveActiveTokens(); 1073 | } 1074 | } 1075 | } else { 1076 | // Token is present, update its data 1077 | tokenData.lastSeen = Date.now(); 1078 | tokenData.isBeingRemoved = false; 1079 | tokenData.removalStartTime = null; 1080 | tokenData.balance = cachedData.balance; 1081 | } 1082 | } 1083 | 1084 | // Add new tokens found in wallet but not in activeTokens 1085 | for (const [mint, data] of tokenAccountCache.entries()) { 1086 | if (!activeTokens.has(mint) && data.balance > 0) { 1087 | try { 1088 | const tokenInfo = await getTokenMetadata(mint); 1089 | if (tokenInfo) { 1090 | activeTokens.set(mint, { 1091 | address: mint, 1092 | symbol: tokenInfo.symbol, 1093 | name: tokenInfo.name, 1094 | balance: data.balance, 1095 | lastSeen: Date.now() 1096 | }); 1097 | console.log(`Added new token to active tokens: ${tokenInfo.symbol} (${mint})`); 1098 | await saveActiveTokens(); 1099 | } 1100 | } catch (error) { 1101 | console.error(`Error getting metadata for token ${mint}:`, error); 1102 | } 1103 | } 1104 | } 1105 | 1106 | } catch (error) { 1107 | console.error('Error verifying tokens in wallet:', error); 1108 | } 1109 | } 1110 | 1111 | // Update the monitoring loop 1112 | async function startMonitoring() { 1113 | while (true) { 1114 | try { 1115 | await verifyTokensInWallet(); 1116 | 1117 | // Get positions info and send update 1118 | const positionsInfo = await getAllPositionsInfo(); 1119 | if (wsClient) { 1120 | wsClient.updateMonitoringInfo(positionsInfo); 1121 | } 1122 | 1123 | } catch (error) { 1124 | console.error('Error in monitoring loop:', error); 1125 | } 1126 | 1127 | await new Promise(resolve => setTimeout(resolve, 10000)); 1128 | } 1129 | } 1130 | 1131 | async function main() { 1132 | try { 1133 | let connection = await getWorkingConnection(); 1134 | let privateKeyArray; 1135 | try { 1136 | privateKeyArray = JSON.parse(config.PRIVATE_KEY); 1137 | } catch { 1138 | privateKeyArray = Array.from(bs58.decode(config.PRIVATE_KEY)); 1139 | } 1140 | const wallet = Keypair.fromSecretKey(new Uint8Array(privateKeyArray)); 1141 | 1142 | console.log('Starting trader...'); 1143 | console.log(`Wallet address: ${wallet.publicKey.toString()}`); 1144 | 1145 | // Wrap the monitoring function with RPC retry logic 1146 | while (true) { 1147 | try { 1148 | await withRpcRetry(() => monitorPositions(connection, wallet)); 1149 | } catch (error) { 1150 | console.error('Error in monitoring loop, attempting to reconnect:', error.message); 1151 | connection = await getWorkingConnection(); 1152 | await new Promise(resolve => setTimeout(resolve, 1000)); 1153 | } 1154 | } 1155 | } catch (error) { 1156 | console.error('Error in main:', error.message); 1157 | } 1158 | } 1159 | 1160 | // Update other functions to use withRpcRetry 1161 | const originalGetTokenMetadata = getTokenMetadata; 1162 | getTokenMetadata = async function(mintAddress) { 1163 | return withRpcRetry(() => originalGetTokenMetadata(mintAddress)); 1164 | }; 1165 | 1166 | const originalGetTokenDecimals = getTokenDecimals; 1167 | getTokenDecimals = async function(connection, mintAddress) { 1168 | return withRpcRetry(() => originalGetTokenDecimals(connection, mintAddress)); 1169 | }; 1170 | 1171 | const originalGetSolPrice = getSolPrice; 1172 | getSolPrice = async function() { 1173 | return withRpcRetry(() => originalGetSolPrice()); 1174 | }; 1175 | 1176 | const originalGetDexScreenerInfo = getDexScreenerInfo; 1177 | getDexScreenerInfo = async function(tokenAddress) { 1178 | return withRpcRetry(() => originalGetDexScreenerInfo(tokenAddress)); 1179 | }; 1180 | 1181 | main(); --------------------------------------------------------------------------------