Every developer who tries to build a Solana sniper bot for Pump.fun or Raydium eventually hits the same frustrating wall.
You write a simple script that listens to new pools, catches a token, buys it, and then enters a loop to check the price for Take-Profit or Stop-Loss targets. It looks great in theory. But in reality, while your bot is sleeping (await sleep(2000)) inside that price-match loop, it is completely blind. It misses dozens of other profitable tokens launching at that exact same second.
In this article, I want to share how I solved this structural bottleneck by implementing isolated asynchronous token tracking and making the bot resilient to aggressive RPC rate-limits (HTTP 429).
The Core Bottleneck: Sequential Execution
Most open-source bots on GitHub suffer from synchronous or sequential logic blocking. If you use a standard for or while loop to track a trade, the execution thread stops there.
If you want to catch 10+ tokens simultaneously and track them all for 60 seconds, you can't just block the main monitoring thread.
The Fix: Non-blocking Fire-and-Forget Promises
Instead of waiting for the transaction and the entire price-tracking routine to finish sequentially, we can offload the tracking of each specific mint to an isolated async context.
When a token is purchased (or simulated), we spin up the price matcher without using await in the main loop, catching any eventual errors down the road:
TypeScript
// Inside the main token monitoring stream:
this.buyToken(mint).then((tokensIn) => {
// Fire and forget: this runs in its own isolated context parallelly!
this.pumpFunPriceMatch(mint, tokensIn).catch((err) => {
logger.error({ mint: mint.toString() }, "Tracking failed", err);
});
}).catch(err => logger.error("Purchase failed", err));
Resiliency: Fighting RPC null States and 429 Errors
When trading on Solana (especially on high-frequency platforms like Pump.fun), public or cheap RPC nodes will constantly throw 429 Too Many Requests or return null when you spam getAccountInfo for a newly created token curve.
If your bot assumes a null response or a network error means the token is dead or invalid, it might panic and drop the tracking, leading to ghost positions or missed exits.
Here is the robust do-while tracking pattern I implemented to combat this:
TypeScript
private async pumpFunPriceMatch(mint: PublicKey, tokensIn: bigint) {
const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
let timesChecked = 0;
const bondingCurve = getBondingCurvePDA(mint);
do {
try {
const info = await this.connection.getAccountInfo(bondingCurve, this.connection.commitment);
// 1. If the node returns null due to lag or rate-limits, WE DO NOT drop the token!
if (!info?.data) {
logger.info({ mint: mint.toString() }, `[RPC-LAG] Node returned empty state, waiting for next interval...`);
await sleep(this.config.priceCheckInterval);
continue; // Skips the increment, saving the check attempt
}
const curve = decodeBondingCurve(info.data);
if (curve.complete) {
logger.info({ mint: mint.toString() }, "[EXIT] Bonding curve graduated to Raydium!");
break;
}
const solOut = computeSolOutForTokens(curve, tokensIn);
// Dynamic TP/SL triggers here...
if (solOut <= stopLossLamports) break;
if (solOut >= takeProfitLamports) break;
// 2. Only increment when we successfully processed a real node state
timesChecked++;
await sleep(this.config.priceCheckInterval);
} catch (e) {
// 3. Network timeout or 429 caught safely
logger.trace({ mint: mint.toString() }, `Price check failed, retrying...`);
await sleep(this.config.priceCheckInterval);
}
} while (timesChecked < timesToCheck);
}
Why this works:
State Isolation: If getAccountInfo fails or catches an exception, the timesChecked++ counter is not incremented. The bot doesn't ложно exit by timeout just because your RPC node lagged for 5 seconds.
Infinite Parallelism: Because this runs asynchronously, 20 different tokens can execute this loop simultaneously.
Clean Monitoring: Adding Token Tags to Logs
When running 10+ positions concurrently, your terminal can quickly turn into unreadable text soup. To maintain full control over what is happening, I modified the logging output to inject a short, 4-character substring of the token's mint address as a unique tracker tag ([${mint.toString().substring(0,4)}]).
Here is how the clean terminal looks when multiple positions are tracked side-by-side:
Plaintext
[07:01:21.102] INFO: [8wvB] Iteration: 1/60 | TP: 1200000 | SL: 900000 | Current: 989938
[07:01:21.540] INFO: [2nJu] Iteration: 1/60 | TP: 1200000 | SL: 900000 | Current: 989935
[07:01:22.105] INFO: [8wvB] Iteration: 2/60 | TP: 1200000 | SL: 900000 | Current: 989942
[07:01:22.545] INFO: [2nJu] Iteration: 2/60 | TP: 1200000 | SL: 900000 | Current: 989935
Now you can instantly spot exactly which token is getting close to its Take-Profit level or which one is stalling.
Next Steps
This architecture works perfectly for a reliable Paper-Trading (simulation) setup to backtest your strategies with zero financial risk using live on-chain data.
To take this to production and beat other MEV bots, the logical next upgrades are:
Moving away from HTTP Polling to Websockets (onAccountChange) or gRPC LaserStream to eliminate manual request limits.
Implementing Jito Bundles to avoid getting frontrun by searchers on Raydium.
The repository and ongoing work can be found here: github.com/rdin777/solana-trading-bot
What are your strategies for managing high-concurrency state tracking in Web3 bots? Let's discuss in the comments below!
https://github.com/rdin777/solana-trading-bot
Top comments (0)