Okay, so most people jumping into Solana event listeners? They fire up onLogs without a solid RPC connection. Boom. Misses every event. Why? Public endpoints like api.mainnet beta.solana.com throttle you hard after a few seconds. Your callback never even hits.
The right way? Grab a dedicated WebSocket endpoint from the jump. I use QuickNode or Helius - they handle the heavy lifting. Costs like 0.000005 SOL per log pull equivalent, but honestly, free tiers get you far for testing. Set it up first, test with a simple ping. Sound familiar? You've been there.
That's it. No fluff. In my experience, skipping the init screws package.json later. Annoying.
Mainnet? Use "https://api.mainnet beta.solana.com" for HTTP, but flip to wss:// for real time. Devnet's kinder: "https://api.devnet.solana.com". Fees? Negligible, like ~0.000005 SOL per subscription.
Look, here's the skeleton. Replace YOURPROGRAMID with whatever program's logs you're chasing. Like a DEX or NFT drop contract.
import { Connection, PublicKey } from "@solana/web3.js"; const PROGRAMID = new PublicKey("YOURPROGRAMIDHERE");
const URL = "wss://api.mainnet beta.solana.com"; // WebSocket for real time
const connection = new Connection(URL, "confirmed"); console.log("Connected. Listening..");
Run it. See "Connected"? Good. Now the fun part.
So onLogs is your bread and butter. It streams every log from that program ID. Transactions, swaps, whatever. Callback fires instantly.
But here's the deal - logs are raw strings. "Instruction: Swap". You parse 'em. I usually slap in a filter right away.
connection.onLogs(PROGRAM_ID, async (logInfo) => { const logs = logInfo.logs; for (const log of logs) { if (log.includes("Instruction: Swap")) { // Your event name console.log("Swap detected!", log); // Process here } }
}, "confirmed");
Why "confirmed"? Faster than "finalized", but safe enough. "Processed" is quickest, risks dupes though.
Logs ain't JSON. Messy. Build an extract function. Say you're watching token swaps - grab amounts, addresses.
function extractSwapData(logs) { // Hunt for patterns like "Amount: 1000 USDC" const amountMatch = logs.find(l => l.includes("Amount:")); return { amount: amountMatch ? amountMatch.split(":").trim() : "unknown", timestamp: new Date(), raw: logs };
}
Call it in your callback. console.log(extractSwapData(logs)). Boom, structured data. In my experience, regex gets fancy here, but start simple.
Test it. Request an airdrop on devnet: connection.requestAirdrop(ACCOUNT, 1e9). Wait 10 secs. Watch balance jump. That's your listener working live.
What's next? Unsubscribe when done. connection.removeAccountChangeListener(subscriptionId). Clean.
| Listener | What It Does | Best For | Gotchas |
|---|---|---|---|
| onLogs | Program logs | Custom events, DEX trades | Parse manually |
| onAccountChange | Wallet balance shifts | Alerts, bots | Only account data |
| onProgramAccountChange | Program owned accounts | NFT metadata, positions | Filter by data |
| signatureSubscribe | Tx confirmations | Track sends | One off |
Pick based on need. onLogs covers 80% of cases. Pretty much.
Solana blasts blocks every 400ms. Thousands of txs. Your basic listener? Fine for dev. Production? It'll choke.
First fix: WebSockets only. HTTP polling sucks - misses stuff, lags. Use wss:// from QuickNode. Free dev tier handles events/sec.
But websockets flake. Reconnect logic, man.
let subId;
async function subscribeWithRetry() { try { subId = await connection.onLogs(..); } catch (e) { console.log("RPC hiccup. Retry in 5s.."); setTimeout(subscribeWithRetry, 5000); }
}
subscribeWithRetry();
I usually wrap in try catch. Logs errors to file. Saved my bot once during congestion.
Heavy traffic? Ditch raw RPC. Jump to webhooks. Helius does this killer - POST events to your endpoint. No parsing needed. They even decode instructions. Free for 1000 events/day. Scales to 100k addresses per hook. Game changer.
No code on chain side. Latency? Sub-100ms. Beats websockets for reliability.
Okay, 2026 now. Geyser's matured. Validators stream direct to your Kafka or DB. Zero RPC middleman. Latency? 5ms. For HFT, DeFi liqs.
Don't self host unless masochist. Chainstack or Helius Geyser as a Service. ~$50/mo starter. Streams Raydium swaps, Bonk pumps live.
Setup? Docker their plugin. Point to your endpoint. Events flood in: txs, accounts, slots. Filter server side.
Why does this matter? Public RPCs cap at 100 reqs/sec. Geyser? Unlimited. Built for Solana's speed.
Say you're building a swap sniper. Listen Raydium program logs for new pools.
Step 1: onLogs for "initialize pool" instruction.
Step 2: Extract token addresses from log.
Step 3: Check liquidity > 10k USDC.
Step 4: Auto buy if good odds.
Fees kill ya? Bundle txs. ~0.000005 SOL each. Gas wars? Use Jito for priority.
In my experience, add rate limits. Solana's fast - your bot spams, you ban. Sleep 50ms between checks.
Potential issue: Dupes. Same tx hits multiple validators. Track signatures. Set has unique tx sigs.
Discord bot? onAccountChange your wallet. Balance drops >1 SOL? Ping channel.
if (oldBalance - newBalance > 1e9) { // Discord webhook POST
}
Or Dialect for pushies. Mobile alerts on big transfers.
NFT drops? ProgramSubscribe to minter. "Instruction: Mint". Grab metadata, alert collectors.
No events? Check PROGRAM_ID. Wrong? Nada.
Connection drops? Logs "WebSocket error". Retry loop fixes.
Missing txs? Commitment level. "confirmed" over "processed".
Parse fails? Logs format changed. Programs update. Test on devnet first.
High fees? Nah, Solana's cheap. But spam? Prioritize with Jito bundles. 0.01 SOL tip max.
One more: Testnet airdrops only 24 SOL/day. Use multiple wallets.
Building your program? Use Anchor. Easiest events.
#[event]
pub struct MyEvent { pub value: u64,
} program.addEventListener("MyEvent", (event, slot) => { console.log(event.value);
});
Typed. Auto parsed. No regex hell. Remove listener when done.
Can't scan history tho. Events live only during tx. Want past? getSignaturesForAddress + getTransaction. Slow for volume.
Honestly? For history, ETL to your DB via Geyser. Query anytime.
| Method | Cost | Latency | Reliability |
|---|---|---|---|
| Public RPC | Free | 500ms+ | Low |
| QuickNode WS | $9/mo | 50ms | Good |
| Helius Webhooks | Free-$$ | 100ms | High |
| Geyser Service | $50+/mo | 5ms | God tier |
Start free. Scale as needed. I went QuickNode → Helius. Never looked back.
Last tip: Run in PM2. node --cluster. Handles crashes. Logs to file.