├── 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 | 
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 |
254 |
Waiting for script to start...
255 |
256 |
257 |
258 |
259 |
Active positions:
260 |
261 |
262 |
263 | Loading...
264 |
265 |
266 |
267 |
268 |
Sold positions:
269 |
270 |
271 |
272 | Loading...
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 | Balance:
283 |
284 |
285 | Loading...
286 |
287 |
288 |
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 |
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 | Clone the repository: git clone https://github.com/yourusername/project.git
351 | Install dependencies: npm install
352 | Create a .env file in the root directory
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 | Start the server: npm run start
375 | Open your browser and navigate to: http://localhost:3000
376 | Monitor the terminal for trading activity
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();
--------------------------------------------------------------------------------