Real-time Solana slot event stream over WebSocket. Typed events, local leader schedule, Jito detection — no gRPC, no Yellowstone.
Vigil subscribes to slotsUpdatesSubscribe on any standard Solana RPC endpoint and emits typed slot events as they arrive. On top of the raw stream it optionally resolves leader identity using a local ChaCha20 schedule (the same deterministic algorithm all validators run) and flags Jito validators in real time.
The primary use case is as a fallback slot source when gRPC infrastructure is unavailable, or as a lightweight standalone slot monitor.
SlotEvent::FirstShredReceived { slot, timestamp_ms, leader?, is_jito_leader? }
SlotEvent::Completed { slot, timestamp_ms }
SlotEvent::Dead { slot, err, timestamp_ms, leader? }
SlotEvent::OptimisticConfirmation { slot }
SlotEvent::Root { slot }
SlotEvent::Fork { slot, expected_parent, actual_parent }
SlotEvent::LeaderApproaching { pubkey, slot, slots_away } // syntheticleader and is_jito_leader fields are populated only when the respective feature flags are enabled.
| Flag | What it adds |
|---|---|
leader-schedule |
Local ChaCha20 schedule. One getVoteAccounts call per epoch, zero hot-path RPC. Populates leader on FirstShredReceived and Dead. |
jito-detection |
Fetches the Jito validator set from the Kobe API at startup. Populates is_jito_leader on FirstShredReceived. Requires leader-schedule. |
# Cargo.toml
vigil = { path = "../vigil", features = ["leader-schedule", "jito-detection"] }use vigil::{SlotEvent, SlotStream};
let stream = SlotStream::builder()
.endpoint("wss://your-rpc-endpoint")
.rpc_endpoint("https://your-rpc-endpoint") // for leader schedule
.build()
.await?;
while let Some(event) = stream.next().await {
match event {
SlotEvent::FirstShredReceived { slot, leader, is_jito_leader, .. } => {
println!("slot {slot} leader={leader:?} jito={is_jito_leader:?}");
}
SlotEvent::Dead { slot, leader, .. } => {
println!("skip {slot} leader={leader:?}");
}
_ => {}
}
}stream.current_slot().await // latest FirstShredReceived slot
stream.confirmed_slot().await // latest optimistically confirmed slot
stream.root_slot().await // latest rooted slot
stream.estimated_slot_duration_ms().await // 48-sample rolling median
stream.observation_lag_ms().await // validator ts → local receive time
stream.skip_rate().await // dead-slot fraction, last 100 slots
stream.leader_at(slot).await // O(1) schedule lookupOn startup vigil calls getVoteAccounts and getEpochInfo once to bootstrap the current and next epoch schedules. It then runs the same ChaCha20 stake-weighted algorithm the validator binary uses:
- Seed =
epoch.to_le_bytes()(32-byte seed, epoch in the first 8 bytes) - Stakes sorted descending, pubkey descending as tie-breaker
- Leaders assigned in consecutive 4-slot chunks
After bootstrap there are no RPC calls on the hot path — every leader_at(slot) is an in-memory array index. At the epoch boundary vigil promotes the pre-computed next schedule and recomputes the following one in a background task.
Vigil reconnects automatically on disconnect or stale connection (no event for 3 seconds). Backoff starts at 500ms and doubles up to 30s. A 300-slot ring buffer allows replaying missed events after reconnect via .from_slot(slot) on the builder.
cargo run --example smoke --features leader-schedule,jito-detectionStreams mainnet events for 60 seconds and prints a summary including skip rate, median slot duration, observation lag, and Jito market share for the window.