I was watching a token on Solana and kept refreshing DEXScreener like an idiot. Every few minutes, tab switch, refresh, check the price, go back to what I was doing.
After an hour of that, I thought — I’m a developer. Why am I doing this manually?
So I wrote a bot. It took 20 minutes and it’s under 50 lines. Here it is.
The Full Bot
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const POOL_ADDRESS = "YOUR_RAYDIUM_POOL_ADDRESS"; // Raydium AMM pool
const ALERT_ABOVE = 0.00015; // Alert when price goes above this
const ALERT_BELOW = 0.00008; // Alert when price drops below this
const CHECK_INTERVAL = 30_000; // Check every 30 seconds
async function getPoolPrice(poolAddress: string): Promise<number | null> {
const pool = new PublicKey(poolAddress);
const info = await connection.getAccountInfo(pool);
if (!info || !info.data) return null;
const data = Buffer.from(info.data);
// Raydium AMM pool layout — token amounts at these offsets
const tokenA = Number(data.readBigUInt64LE(253));
const tokenB = Number(data.readBigUInt64LE(261));
if (tokenA === 0 || tokenB === 0) return null;
return tokenB / tokenA;
}
async function sendAlert(message: string) {
// Swap this with Telegram, Discord, or whatever you use
console.log(`🔔 ALERT: ${message}`);
// Example: Telegram bot
// await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ chat_id: CHAT_ID, text: message }),
// });
}
let lastAlert = 0;
async function check() {
const price = await getPoolPrice(POOL_ADDRESS);
if (price === null) return;
const now = Date.now();
const cooldown = 5 * 60 * 1000; // Don't spam — 5 min between alerts
if (price > ALERT_ABOVE && now - lastAlert > cooldown) {
await sendAlert(`Price is UP: ${price.toFixed(8)}`);
lastAlert = now;
}
if (price < ALERT_BELOW && now - lastAlert > cooldown) {
await sendAlert(`Price is DOWN: ${price.toFixed(8)}`);
lastAlert = now;
}
console.log(`Price: ${price.toFixed(8)}`);
}
setInterval(check, CHECK_INTERVAL);
check();
That’s it. Under 50 lines of actual logic.
How It Works
The trick is reading the pool account data directly instead of making swap quotes or hitting external APIs. Raydium AMM pools store both token balances on-chain. The price is just tokenB / tokenA.
No API keys. No rate limits. No third-party dependencies. Just one RPC call every 30 seconds.
Finding the Pool Address
This is the part that trips people up. You need the Raydium AMM pool address, not the token mint.
Easiest way to find it:
- Go to Raydium’s pool list or use their API
- Search for your token
- Copy the pool ID
Or programmatically:
async function findPool(tokenMint: string) {
const res = await fetch(
`https://api.raydium.io/v2/ammV3/ammPools`
);
const data = await res.json();
const pool = data.data.find(
(p: any) =>
p.mintA === tokenMint || p.mintB === tokenMint
);
return pool?.id || null;
}
Making It Actually Useful
The basic bot works, but here’s what I added to mine over time:
Percentage-based alerts instead of fixed prices:
let referencePrice: number | null = null;
async function checkWithPercentage() {
const price = await getPoolPrice(POOL_ADDRESS);
if (price === null) return;
if (referencePrice === null) {
referencePrice = price;
console.log(`Reference price set: ${price.toFixed(8)}`);
return;
}
const change = ((price - referencePrice) / referencePrice) * 100;
if (Math.abs(change) > 10) {
await sendAlert(
`Price moved ${change.toFixed(1)}%: ${price.toFixed(8)}`
);
referencePrice = price; // Reset reference after alert
}
}
This is better than fixed thresholds. You set it once and it alerts you on any significant move — up or down.
Adding Telegram Notifications
Console logs are useless when you’re away from your desk. Here’s the Telegram setup:
- Message
@BotFatheron Telegram, create a bot, get the token - Send a message to your bot, then hit
https://api.telegram.org/bot<TOKEN>/getUpdatesto get your chat ID - Uncomment the fetch call in
sendAlert:
async function sendAlert(message: string) {
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text: message,
}),
});
}
Now you get a Telegram ping whenever the price crosses your threshold. I run this on a $5 VPS and it just works.
Why Not Just Use DEXScreener Alerts?
Fair question. DEXScreener has alerts now. But:
- Customization. I can add whatever logic I want. Percentage moves, volume spikes, multiple tokens.
- Speed. My bot reads directly from the chain. No middleman.
- Integration. I pipe alerts into the same Telegram group where my other bots post. One place for everything.
- Learning. Building this taught me how Raydium pools actually store data on-chain. That knowledge compounds.
Watch Out For
A few things that bit me:
- Pool offsets change between AMM versions. The byte offsets I showed work for Raydium AMM v4. If you’re reading a CLMM pool, the layout is completely different.
- RPC rate limits. Every 30 seconds is fine for public RPCs. If you go faster, use a paid endpoint.
- Stale data. Public RPCs sometimes serve slightly outdated state. For trading decisions, use
commitment: "confirmed"at minimum.
Start Here, Build From Here
This bot is intentionally simple. It’s a starting point, not a finished product.
Once you have price data flowing, you can build anything on top — automated buys at certain levels, portfolio trackers, multi-token dashboards. The hard part isn’t the logic. It’s getting clean, fast price data. And now you have that.