Real-time Solana MEV sandwich detector. Subscribes to Helius's enhanced WebSocket, ring-buffers per-pool swap activity, and identifies front-run / victim / back-run triples within ≤3 slots. Persists to Supabase, exposes an SSE feed and a /stats endpoint.
WS → parser pool → detector (per-pool ring buffer, 30s window)
│
├──→ sqlx writer → Postgres (transactions + sandwich_attempts)
├──→ axum SSE → GET /events
└──→ Telegram (optional)
The WebSocket reader does only I/O. Parsing happens on a separate task with bounded backpressure on the channel — when parsers fall behind, the socket reader yields, never the event loop. This is the whole game during a busy slot.
- Subscribes to Raydium AMM v4 + Orca Whirlpool on Solana mainnet via Helius
transactionSubscribe(with public mainnetlogsSubscribefallback when no Helius key). - IDL-correct pool extraction for both DEXes — walks outer + inner instructions, identifies the program, takes the AMM account at the IDL-specified index. Falls back to a heuristic only if no recognizable swap instruction is found.
- Detection algorithm:
- Walk the per-pool ring buffer for a prior swap by the same signer.
- Confirm a different-signer swap sits between front and back, in the same pool.
slot(back) - slot(front)must be ≤ 3.- Multi-victim brackets emit one row per victim (
unique(front_sig, victim_sig, back_sig)). - Confidence scoring: 50 base + 20 (opposite WSOL signs on attacker) + 10 (victim shares non-WSOL mint, same direction as front) + 10 (positive aggregate profit).
- Profit calc (when Helius enhanced-tx available):
Σ(attacker WSOL Δ across front + back) - Σ(network+priority fees) - Σ(Jito tips). Jito tips are detected by scanning inner instructions for System Program transfers to any of the 8 known Jito tip accounts. - USD profit filled from a Pyth/Hermes SOL/USD poller (30s refresh, lock-free read on the writer hot path).
- Slot-resume marker persisted to
/var/lib/sandwich-rs/last_slotevery 30s — operational visibility on outage gaps. - Idempotent persistence:
ON CONFLICT (signature) DO NOTHINGontransactions,ON CONFLICT (front_sig, victim_sig, back_sig) DO NOTHINGonsandwich_attempts. Restart-safe. - HTTP endpoints on
:8080:GET /healthz— 200 if last WS frame within 60s OR within 30s startup grace, 503 otherwise.GET /events— Server-Sent Events; replays the most recent 50 sandwiches then streams new ones.GET /stats— singleton 24h aggregate (count, profit_sol, profit_usd, unique attackers, unique victim pools, delta vs prior 24h).
cargo run --bin scrape-fixtures fixtures/seed.csv fixtures/known-sandwiches.csv— turns a hand-curated list ofvictim_sig,front_sig,back_sigtriples into a fully-populated backtest CSV by calling HeliusgetTransactionfor each leg.
- Jupiter v6 aggregator coverage (multi-hop routing makes pool identification harder). v1.6.
- Meteora DLMM + standard pool subscriptions. v1.6.
- US-east region migration (Hetzner EU adds ~120ms RTT to Helius). When precision is proven on EU, migrate.
- Auto-scrape ground-truth from Sandwiched.wtf (currently
scrape-fixturesrequires hand-curated victim/front/back triples). v2. - Public dashboard frontend. Built in this repo at
frontend/(Vite + React + TS); operator deploys to Netlify.
cp .env.example .env
# fill HELIUS_API_KEY, SUPABASE_POOLER_URL, SUPABASE_URL, SUPABASE_ANON_KEY
RUST_LOG=sandwich_rs=info cargo run --releaseWithout DB or Helius the agent still runs, falling back to public mainnet logsSubscribe and skipping persistence.
cargo run --release --bin backtest -- fixtures/known-sandwiches.csvReads a CSV of labeled known sandwiches (positives) and decoy patterns (negatives), replays each through the actual detector, and reports precision / recall / F1. Ship gate: precision ≥ 0.70, recall ≥ 0.50, n_positive ≥ 30. Below threshold the binary exits non-zero.
The fixtures/known-sandwiches.csv shipped with this repo has 1 positive + 1 negative as a smoke test. Populate it with real signatures from Sandwiched.wtf before running the gate.
VPS_HOST=77.42.83.22 VPS_PORT=2222 ops/deploy.shIdempotent. First run creates the sandwich system user, installs to /opt/sandwich-rs/, drops a systemd unit at /etc/systemd/system/sandwich-rs.service, seeds /etc/sandwich-rs/env from the example, and tails journalctl -u sandwich-rs -f. Edit the env file with real secrets, then systemctl restart sandwich-rs.
sqlxdirect Postgres to Supabase pooler (port 6543, transaction mode), not PostgREST. Saves a 30–80ms HTTP roundtrip per insert.- Helius
transactionSubscribewhenHELIUS_API_KEYis set, falling back to publiclogsSubscribe. Helius gives parsed inner instructions + pre/post token balances inline; public WS would need an extragetTransactionfollowup per swap, which kills throughput on a busy slot. - Split schema:
transactions(raw observed swaps) andsandwich_attempts(joined triples). Audit trail beats a single flat table. - Bounded mpsc with backpressure on the socket reader. Architectural landmine: parsing in the same task that reads the WebSocket fails the moment Solana has a busy slot. Don't.
Rust 1.85+ · tokio · tokio-tungstenite · axum · sqlx · dashmap · tracing · Supabase Postgres · Hetzner.
A single Raydium subscription emits hundreds of frames per second during peak Solana activity. The hot path is JSON deserialization + cross-pool state lookups + an idempotent batched insert. Rust gives us bounded channels with backpressure semantics that don't lie, zero-copy parsing options when we measure parsing as the bottleneck (simd-json is the next swap), and a memory model where the per-pool ring buffer never surprises us. Equivalent Python or Node implementations spend their cycles in GC.
These are documented because the v1 ship-blocker review caught them before deploy:
-
Backtest fixtures are synthetic. The CSV-driven harness builds 3-swap streams with hand-crafted topology. Of course it hits 100% precision. The ship gate's real role is not validating algorithm correctness against the bench — that's the unit test suite — it's enforcing the ground-truth populating discipline. Until 30+ real Helius
getTransactionblobs from labeled Sandwiched.wtf signatures are infixtures/, the precision number on the README is lying. v1.5: atools/scrape-sandwiched-wtfbinary that fetches and persists real-data fixtures. -
Jito tips are not subtracted from profit. The detector subtracts
meta.fee(network base + priority fee combined) but not Jito tips, which are paid as separate SOL transfers to a Jito tip account. Real attackers tip 0.001–0.01 SOL routinely. v1.5 adds tip-account scanning over the transaction's transfers. -
Pool extraction works for direct Raydium V4 swaps and CPI-via-Jupiter routes. The parser walks both outer and inner instructions to find
programId == Raydium V4and pullsaccounts[1]per the IDL. For aggregator routes through unknown intermediaries, falls back to a non-program-non-signer heuristic. Unrecognized DEXes (Phoenix, Lifinity, etc.) are not yet supported and yieldpool == "raydium-v4-unknown", which the detector ignores. -
No slot-resume on reconnect. WS reader uses exponential backoff (1s → 30s with reset on first frame received) but does not request a starting slot from
lastSlot - N. A 30s outage is silently lost. Defensible for v1 because the detector is event-driven and Helius'stransactionSubscribedoesn't offer slot-resume out of the box, but a v1.5 disk-persisted resume marker is on the roadmap. -
Multi-victim brackets emit one row per victim. Schema unique constraint is
(front_sig, victim_sig, back_sig)for this reason. Aggregating tototal_profit / total_victimsis straightforward in SQL.
This backend went through a multi-pass adversarial review before it was considered ready to deploy:
- Engineering plan reviewed (
/plan-eng-review): scope locked at HOLD, 6 architecture issues resolved, test plan written. - Architecture challenged via outside-voice subagent: 7 ship-blockers identified, 6 fixed (the seventh — synthetic-vs-real backtest — is a v1.5 expansion documented above).
- 12/12 unit tests green: parser handles Helius enhanced-tx and
logsNotificationfallback; detector handles single victim, multi-victim, slot-span limit, different pools, self-sandwich, logs-fallback skip, base-confidence topology, and fee-subtracted profit math. cargo clippy --all-targets -- -D warningsclean.cargo build --releaseclean, lto=thin, single codegen-unit, stripped symbols.
Backend ready to deploy. Operator next steps:
- Get a Helius free API key from dashboard.helius.dev.
- Fill
/etc/sandwich-rs/envon VPS (or.envlocally) with Helius key + Supabase credentials. - Run
ops/deploy.shfrom a dev box that can SSH to the VPS. - Watch
journalctl -u sandwich-rs -ffor the first detected sandwich. - Populate
fixtures/known-sandwiches.csvwith ≥30 real cases from Sandwiched.wtf, then runcargo run --release --bin backtestto drop the real precision number into the README.