Skip to main content
Glama
monostate

100ms Raydium Sniper MCP

by monostate
index.js18.2 kB
// Cloud function implementation with pool finding and transaction execution const { Connection, Keypair, PublicKey } = require('@solana/web3.js'); const functions = require('@google-cloud/functions-framework'); const WebSocket = require('ws'); const axios = require('axios'); const bs58 = require('bs58'); const { setTimeout } = require('timers/promises'); // Import local modules const { getOrCreateATA } = require('./test-ata.js'); const { quickBuy } = require('./quick-buy.js'); // Constants const RAYDIUM_PROGRAM_ID = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'; const MIN_AMM_DATA_SIZE = 300; const STATIC_AMM_AUTHORITY = new PublicKey('5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'); // Register HTTP function functions.http('quickBuyFunction', async (req, res) => { // Always respond with 200 OK for health checks and GET requests if (req.method === 'GET') { console.log('Health check received'); return res.status(200).send('OK'); } console.log(`Function started in region: ${process.env.FUNCTION_REGION || 'unknown'}`); const startTime = Date.now(); try { // Validate request - require a token mint if (!req.body || !req.body.tokenMint) { console.log('Missing tokenMint in request'); return res.status(400).json({ success: false, region: process.env.FUNCTION_REGION || 'unknown', error: 'Missing tokenMint parameter' }); } const tokenMint = req.body.tokenMint; const slippageBps = req.body.slippageBps || 100; const amountSol = req.body.amountSol || 0.05; const region = process.env.FUNCTION_REGION || 'unknown'; console.log(`Searching for token ${tokenMint} in region ${region}`); // Initialize Solana connection const connection = new Connection( `https://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`, { commitment: 'processed' } ); // Initialize wallet if available let wallet = null; if (process.env.WALLET_PRIVATE_KEY) { try { wallet = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_PRIVATE_KEY)); console.log(`Wallet initialized: ${wallet.publicKey.toString()}`); } catch (walletError) { console.error('Failed to initialize wallet:', walletError.message); } } // Active WebSocket connections to close at the end const wsConnections = []; // Setup timeout and pool finding promises const graphQLPromise = findPoolViaGraphQL(tokenMint); const wsPromise = findPoolViaWebSocket(tokenMint, wsConnections); const tokenAgePromise = checkTokenAge(tokenMint); // Set timeout for the whole operation const timeoutPromise = new Promise(resolve => { setTimeout(5000, { value: { timeout: true } }).then(resolve); }); // Race between pool finding methods and timeout const result = await Promise.race([ graphQLPromise.then(pool => pool ? { poolInfo: pool, source: 'graphql' } : null), wsPromise.then(pool => pool ? { poolInfo: pool, source: 'websocket' } : null), timeoutPromise ]); // Close any active WebSocket connections wsConnections.forEach(ws => { try { if (ws.readyState === WebSocket.OPEN) { ws.close(); } } catch (e) { // Ignore close errors } }); // Wait for token age check to complete const tokenAge = await tokenAgePromise; console.log(`Token age: ${tokenAge.ageInHours} hours, isNew: ${tokenAge.isNew}`); // If we found a pool and have a wallet, try to execute a swap if (result && result.poolInfo && wallet) { console.log(`Found pool for ${tokenMint} via ${result.source}, proceeding with swap execution`); try { // Get market info const marketInfo = await getSerumMarketInfo( result.poolInfo.market_id || result.poolInfo.marketId, result.poolInfo.market_program_id || result.poolInfo.marketProgramId, connection ); // Combine pool and market info const combinedInfo = combinePoolAndMarketInfo(result.poolInfo, marketInfo); // Execute quick buy const buyResult = await quickBuy(connection, wallet, tokenMint, combinedInfo); // Return result with full info return res.status(200).json({ success: true, region, tokenMint, poolSource: result.source, tokenAge, transaction: buyResult.signature ? { signature: buyResult.signature, ata: buyResult.ata } : null, message: buyResult.signature ? `Successfully executed swap for token ${tokenMint}` : `Found pool but transaction failed: ${buyResult.error}`, executionTime: Date.now() - startTime }); } catch (swapError) { console.error('Error executing swap:', swapError); // Return pool info but with transaction error return res.status(200).json({ success: true, // We still found the pool region, tokenMint, poolSource: result.source, poolInfo: { pubkey: result.poolInfo.pubkey, baseMint: result.poolInfo.base_mint || result.poolInfo.baseMint, quoteMint: result.poolInfo.quote_mint || result.poolInfo.quoteMint }, tokenAge, transactionError: swapError.message, message: `Found pool for token ${tokenMint} via ${result.source} but swap failed`, executionTime: Date.now() - startTime }); } } // If we found a pool but don't have a wallet or swap failed, just return pool info else if (result && result.poolInfo) { return res.status(200).json({ success: true, region, tokenMint, poolSource: result.source, poolInfo: { pubkey: result.poolInfo.pubkey, baseMint: result.poolInfo.base_mint || result.poolInfo.baseMint, quoteMint: result.poolInfo.quote_mint || result.poolInfo.quoteMint }, tokenAge, message: wallet ? `Pool found for token ${tokenMint} via ${result.source}` : `Pool found but wallet not available for swap`, executionTime: Date.now() - startTime }); } // Handle timeout cases else if (result && result.timeout) { if (tokenAge && tokenAge.isNew) { // No pool, but token is new return res.status(200).json({ success: false, region, tokenMint, tokenAge, message: `Token is new (${tokenAge.ageInHours.toFixed(2)} hours old), waiting for liquidity pool to be created`, executionTime: Date.now() - startTime }); } else { // No pool and token is not new - likely not a Raydium pool token return res.status(200).json({ success: false, region, tokenMint, tokenAge, message: 'No Raydium pool found for this token. It may not be a Raydium pool token.', executionTime: Date.now() - startTime }); } } else { // Should not reach here, but just in case return res.status(200).json({ success: false, region, tokenMint, message: 'No pool found but search completed', executionTime: Date.now() - startTime }); } } catch (error) { console.error('Function error:', error); return res.status(200).json({ success: false, region: process.env.FUNCTION_REGION || 'unknown', error: error.message || 'Unknown error', executionTime: Date.now() - startTime }); } }); // GraphQL pool search - make initial call with 2 retries async function findPoolViaGraphQL(tokenMint) { console.log('Starting GraphQL pool search'); const maxRetries = 2; // Initial call + 2 retries = 3 total attempts for (let attempt = 0; attempt <= maxRetries; attempt++) { try { // Shyft API GraphQL endpoint const endpoint = 'https://api.shyft.to/sol/v1/graphql'; // GraphQL query to find Raydium pools const query = ` query GetRaydiumPool { raydium_amm_pool( where: { _or: [ {base_mint: {_eq: "${tokenMint}"}}, {quote_mint: {_eq: "${tokenMint}"}} ] } ) { pubkey open_orders target_orders base_vault quote_vault market_id market_program_id base_mint quote_mint } } `; console.log(`GraphQL attempt ${attempt + 1}/${maxRetries + 1}`); const response = await axios.post(endpoint, { query }, { headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.SHYFT_API_KEY || '' }, timeout: 2000 // 2 second timeout }); const pools = response.data?.data?.raydium_amm_pool; if (pools && pools.length > 0) { console.log(`Found pool via GraphQL on attempt ${attempt + 1}:`, pools[0].pubkey); return pools[0]; } // If no pool found and we have retries left, wait before trying again if (attempt < maxRetries) { await setTimeout(500); // 500ms delay between attempts } } catch (error) { console.error(`GraphQL error on attempt ${attempt + 1}:`, error.message); // If not the last retry, continue to next attempt if (attempt < maxRetries) { await setTimeout(500); } } } console.log('No pools found via GraphQL after retries'); return null; } // WebSocket pool search implementation async function findPoolViaWebSocket(tokenMint, wsConnections) { return new Promise(resolve => { console.log('Starting WebSocket pool search'); // We need lowercase for comparison const targetTokenLower = tokenMint.toLowerCase(); // Create WebSocket connection const wsEndpoint = `wss://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`; const ws = new WebSocket(wsEndpoint); wsConnections.push(ws); // Keep track if we found a match let poolFound = false; // Set timeout to ensure WebSocket doesn't hang indefinitely const wsTimeout = setTimeout(() => { if (!poolFound) { console.log('WebSocket search timeout after 5s'); if (ws.readyState === WebSocket.OPEN) { ws.close(); } resolve(null); } }, 5000); ws.on('open', () => { console.log('WebSocket connected'); // Subscribe to Raydium program const subscriptionId = Math.floor(Math.random() * 1000000); ws.send(JSON.stringify({ jsonrpc: '2.0', id: subscriptionId, method: 'programSubscribe', params: [ RAYDIUM_PROGRAM_ID, { encoding: 'base64', commitment: 'processed' } ] })); console.log('Subscribed to Raydium program'); }); ws.on('message', (data) => { try { const message = JSON.parse(data); // Skip subscription confirmation if (message.result) return; // Process program notifications if (message.method === 'programNotification') { handleProgramNotification(message.params, targetTokenLower, (poolInfo) => { if (!poolFound) { poolFound = true; clearTimeout(wsTimeout); console.log('Found pool via WebSocket:', poolInfo.pubkey); if (ws.readyState === WebSocket.OPEN) { ws.close(); } resolve(poolInfo); } }); } } catch (error) { console.error('WebSocket message parse error:', error.message); } }); ws.on('error', (error) => { console.error('WebSocket error:', error.message); if (!poolFound) { resolve(null); } }); ws.on('close', () => { console.log('WebSocket closed'); clearTimeout(wsTimeout); if (!poolFound) { resolve(null); } }); }); } // Process program notification from WebSocket function handleProgramNotification(params, targetTokenLower, callback) { if (!params?.result?.value?.account?.data?.[0]) return; try { const { pubkey, account } = params.result.value; const accountData = account.data[0]; // Skip accounts that are too small to be AMM accounts if (accountData.length < MIN_AMM_DATA_SIZE) return; // Decode account data // In a real implementation, we would decode the account data properly // using the library functions, but for the cloud function we'll use // this simplified approach // Decode base64 data const buffer = Buffer.from(accountData, 'base64'); // Skip if not enough data if (buffer.length < 300) return; // Extract potential mint addresses (this is simplified - real implementation would use proper decoder) // In real implementation, we would use the decodeAmmMints function // This is just a placeholder - real implementation would properly extract mints const potentialMints = []; for (let i = 0; i < buffer.length - 32; i++) { const slice = buffer.slice(i, i + 32); try { const pubkey = new PublicKey(slice); potentialMints.push(pubkey.toString().toLowerCase()); } catch (e) { // Not a valid pubkey, continue } } // Check if any of the mints match our target const matchingMint = potentialMints.find(mint => mint === targetTokenLower); if (matchingMint) { // In a real implementation, we would decode the full account data // and return proper pool info. For the cloud function, we return // simplified data. callback({ pubkey, baseMint: matchingMint, quoteMint: potentialMints[0] !== matchingMint ? potentialMints[0] : potentialMints[1], source: 'websocket' }); } } catch (error) { console.error('Error processing account data:', error.message); } } // Get market accounts from a Serum market async function getSerumMarketInfo(marketId, marketProgramId, connection) { try { console.log(`Getting market info for ${marketId} using program ${marketProgramId}`); // Get the market account data const marketAccount = await connection.getAccountInfo(new PublicKey(marketId)); if (!marketAccount) { throw new Error('Market account not found'); } // This is a simplified implementation compared to the actual decoder // In a real implementation, we would properly decode the market account data const data = marketAccount.data; // Derive vault signer const seeds = [new PublicKey(marketId).toBuffer()]; let vaultSigner; try { [vaultSigner] = PublicKey.findProgramAddressSync( seeds, new PublicKey(marketProgramId) ); } catch (error) { throw new Error(`Failed to derive vault signer: ${error.message}`); } // Extract accounts from specific buffer positions // This is a simplified version - the real implementation would properly decode these return { serumProgramId: new PublicKey(marketProgramId), serumMarket: new PublicKey(marketId), serumBids: new PublicKey(data.slice(40, 72)), serumAsks: new PublicKey(data.slice(72, 104)), serumEventQueue: new PublicKey(data.slice(104, 136)), serumCoinVault: new PublicKey(data.slice(136, 168)), serumPcVault: new PublicKey(data.slice(168, 200)), serumVaultSigner: vaultSigner }; } catch (error) { throw new Error(`Failed to get Serum market info: ${error.message}`); } } // Combine pool and market info function combinePoolAndMarketInfo(pool, serumInfo) { return { ammMarket: new PublicKey(pool.pubkey), ammAuthority: STATIC_AMM_AUTHORITY, ammOpenOrders: new PublicKey(pool.open_orders || pool.openOrders), ammTargetOrders: new PublicKey(pool.target_orders || pool.targetOrders), poolCoinTokenAccount: new PublicKey(pool.base_vault || pool.baseVault), poolPcTokenAccount: new PublicKey(pool.quote_vault || pool.quoteVault), ...serumInfo }; } // Check token age async function checkTokenAge(tokenMint) { try { // Get token metadata const metadataUrl = `https://api.shyft.to/sol/v1/token/get_metadata?network=mainnet-beta&token_address=${tokenMint}`; const response = await axios.get(metadataUrl, { headers: { 'x-api-key': process.env.SHYFT_API_KEY || '' }, timeout: 2000 // 2 second timeout }); // Get token creation transaction const createTx = response.data?.result?.first_mint_transaction; if (createTx) { // Get transaction details const txUrl = `https://api.shyft.to/sol/v1/transaction/parsed_transaction?network=mainnet-beta&tx_hash=${createTx}`; const txResponse = await axios.get(txUrl, { headers: { 'x-api-key': process.env.SHYFT_API_KEY || '' }, timeout: 2000 // 2 second timeout }); const timestamp = txResponse.data?.result?.timestamp; if (timestamp) { const createTime = new Date(timestamp * 1000); const now = new Date(); const ageInHours = (now - createTime) / (1000 * 60 * 60); return { createTime: createTime.toISOString(), ageInHours, isNew: ageInHours < 24 }; } } return { isNew: false, ageInHours: 999999 }; } catch (error) { console.error('Error checking token age:', error.message); return { isNew: false, ageInHours: 999999 }; } } // For Google Cloud Function healthcheck - explicitly listen on the PORT env var // Parse port as an integer to ensure proper binding const port = parseInt(process.env.PORT) || 8080; console.log(`Listening on port ${port} for health checks`); functions.start({ port });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/monostate/100ms-SPL-Token-Sniper-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server