SolSniper
Real-time Solana token sniping with multi-tier safety analysis
Problem
New SPL tokens launch on Solana constantly, and the window to enter a legitimate launch before the price moves is measured in the first block or two after detection. Manual trading at that speed is not viable. I wanted an autonomous bot that could subscribe to every meaningful launch surface in real time, run each candidate through a pessimistic safety pipeline, commit capital only when every hard check passed, and manage the exit with a ladder that ended in a 49% slippage emergency sell so no position could get stuck. The non-obvious constraint was that speed and safety were not on a single dial: Tier 1 hard blocks had to run in parallel, Tier 2 scoring had to degrade gracefully on RPC timeouts, and the whole pipeline had to survive a crash mid-buy without leaking capital or leaving a phantom position that the position manager would try to sell forever.
Approach & Architecture
I architected SolSniper as a single Node.js process in TypeScript strict mode, with eight subsystems initialized in dependency order: Config, Detection, Safety, Execution, Position, Persistence, Recovery, Dashboard. RecoveryManager.run() is blocking and reconciles in-flight trades against on-chain wallet state before DetectionManager.start() fires.
Detection runs from three independent sources. A PumpPortalListener extending a ResilientWebSocket (exponential 3s→60s backoff, 30-second heartbeat) streams pump.fun bonding-curve mints. A single onLogs subscription handles both Raydium V4 and PumpSwap pool creations, with a silence-based health check that recreates the subscription after two minutes without events. Every candidate hits a pre-filter that rejects spam keywords and eleven impersonation targets before any RPC call is made.
The safety pipeline runs eight checks across three tiers. Tier 1 hard blocks fire in parallel via Promise.all: mint and freeze authority revoked, Jupiter sell route exists, liquidity present. Tier 2 scoring runs through Promise.allSettled: RugCheck risk score, top-holder concentration (top-1 > 25% or top-10 > 50%), LP-lock, and Metaplex isMutable metadata. Tier 3 queries Helius DAS for creator wallet history with a serial-deployer heuristic that auto-blocklists any wallet with ten or more prior deploys. Tier 2/3 timeouts degrade to score=0, pass=true: pessimistic but non-blocking, so a RugCheck outage cannot globally halt trading.
Execution hinges on a synchronous duplicate-buy guard in TradeStore. Because better-sqlite3 is synchronous, the activeMints Set check, the INSERT of a BUYING row, and activeMints.add(mint) all execute in one call-stack frame with no await. No concurrent token event can slip through. The BUYING row is written before any on-chain broadcast, so RecoveryManager can replay wallet balance on startup to decide whether a half-committed trade becomes MONITORING or FAILED. Every UPDATE trades carries WHERE state = @expectedState for optimistic locking.
The six-step sell-escalation ladder runs STANDARD → HIGH_FEE → JITO_BUNDLE → CHUNKED → PUMPPORTAL → EMERGENCY with per-step timeouts and advances on timeout regardless of the underlying error. EMERGENCY fires at 4900 bps slippage with a 10× priority fee. Before each run, the ladder re-queries on-chain token balance via getParsedTokenAccountsByOwner(wallet, { mint }) rather than trusting the DB value, and partial sells use a COALESCE + delta SQL pattern so accumulated proceeds survive a crash mid-ladder. A typed botEventBus fans fourteen event types to SSE clients backing a Preact SPA.
Tradeoffs
Tier 1 parallel hard blocks pay the full latency of the slowest RPC call but eliminate the class of bug where a degraded check silently approves a rugged token; I chose fail-closed over fast-open for Tier 1 specifically. better-sqlite3 in WAL mode gave me the synchronous semantics that made the duplicate-buy guard possible, at the cost of ruling out Postgres or Redis. That tradeoff was acceptable because the bot is single-process by design. Preact + Signals kept the dashboard bundle smaller than React would have. The six-step sell ladder burns progressively more fee budget and slippage to guarantee exit: strictly worse than a clean STANDARD sell in the good case, strictly better than a stuck position in the bad case.
Outcome
SolSniper runs as one Node.js process with an embedded Fastify 5 HTTP server, better-sqlite3 in WAL mode, and dual-RPC failover (three consecutive failures trip the switchover, ten-second recovery polling). The safety pipeline evaluates eight checks across three tiers with per-API circuit breakers (30-second cooldown after five consecutive failures) on FeeEstimator, checkRugCheck, and checkCreatorHistory. The six-step sell ladder ends at 49% slippage with a 10× priority fee. The botEventBus emits fourteen typed event types over SSE to a Preact dashboard. The Vitest suite covers thirty-nine .test.ts files across safety, execution, sell-ladder, dashboard, monitoring, persistence, position, recovery, and detection, including a Nyquist-validation pattern that asserts idempotency of pause-when-paused and force-sell-when-selling (HTTP 409 on concurrent force-sell).
Learnings
The hardest problem was the balance query itself. An earlier dual-query approach that filtered by programId double-counted Token-2022 balances and produced Jupiter error 6024 when the quote math disagreed with the on-chain balance. The fix was getParsedTokenAccountsByOwner(wallet, { mint }) without a programId filter, which searches both token programs in a single call, and the lesson was that defensive-looking duplicate code paths often hide the real bug. A close second was runtime config: POST /api/config validates shape, then the full TradingConfigSchema, then a validateSemantics() cross-field pass, and rolls back via a structuredClone snapshot on failure. My first version aliased nested references during the patch, so a failed check left the config half-applied. structuredClone was the one-line fix, and the broader lesson was that rollback correctness requires deep isolation, not shallow pointer preservation.