Increment 1 of solv1-rig — a Rust dual-feed Solana latency-measurement rig — is built and green. Its whole job is to produce self-measured shred-vs-Yellowstone-gRPC arrival-delta data, the kill-criterion the copy-trading feasibility left unproven. 15 tasks, subagent-driven on Opus, every task individually reviewed plus a final whole-branch review returning “ship it” (0 critical).
For Agents
This rig is measurement only — no transaction is ever signed or sent. The trustworthiness of the one number it exists to produce (
delta_ms) rests on five load-bearing decisions (see below); do not “simplify” any of them without re-reading why. Source of truth for full detail is the spec + plan (local repo paths in Related) — this note summarizes and links, it does not duplicate them.Key facts: histogram is the percentile authority (raw rows are 1:100 sampled debug data); the Yellowstone leg is venue-classification authority (shred side is a best-effort hint, blind to ALT-routed swaps);
delta_ms = geyser_at − shred_at, positive = shreds arrived earlier. Increment 1 uses a single-consumer mpsc pipeline (not the spec’s broadcast bus — deferred to increment 2). Storage is the Supabasemgmtproject (mkofmdtdldxgmmolxxhc), 8solv1_-prefixed tables, via session-mode pooler :5432.
What Shipped
One crate, one binary (solv1-rig, tokio async), three subcommands:
run— the dual-feed collector (the deliverable).report— SQL percentile readout per venue/hour from the histogram authority, with an n-floor that suppresses under-sampled cells.capture— records ~5 min of live feed frames to a fixture file for the replay tests / shakeout.
15 tasks (scaffold+pinned deps → config → venue classifier → histogram → matcher+stats → migrations → sink → tips → FeedSource trait+ReplayFeed → RPC Fast adapter → Yellowstone adapter → orchestration → e2e → report → Docker/CI). Subagent-driven development on Opus; each task individually reviewed, then a final whole-branch review = “ship it”, 0 critical findings.
Architecture
Feed-agnostic by hard requirement (Jito ToS is revocable — swapping a feed must be a config change, never a code change). Full design is spec §3; the increment-1 realization:
Increment 1 data path (single-consumer mpsc)
graph LR RF["RpcFastShreds<br/><i>stamp Instant pre-decode</i>"] --> BUS{{"mpsc TxSeen<br/>(1 consumer)"}} YS["Yellowstone<br/><b>venue authority</b>"] --> BUS BUS --> M["matcher<br/>cross-feed sig pairing"] M --> H["minutely histograms<br/><b>percentile authority</b>"] M --> RAW["1:100 sampled<br/>raw rows"] H --> SINK["batched sqlx sink<br/>500 rows / 2s, retry×3"] RAW --> SINK TIPS["Jito tip-floor poller<br/><i>minutely</i>"] --> SINK SINK --> PG[("Supabase PG<br/>mgmt · solv1_*")] style RF fill:#264653,stroke:#2a9d8f,color:#fff style YS fill:#3d2020,stroke:#c1666b,color:#fff style H fill:#2d2d2d,stroke:#e9c46a,color:#fff style PG fill:#2d2d2d,stroke:#888,color:#fffCohort-watch + paper-mirror pricer are the increment-2 consumers that don’t exist yet — see Increment-2 Backlog.
Components (all behind the feed::FeedSource trait seam where feed-facing):
FeedSourcetrait —subscribe()yieldsTxSeen { feed, sig, slot, arrived: Instant, venue, fee_payer }.arrivedis stamped at gRPC message receipt before any decode. Increment-1 impls:RpcFastShreds(SubscribeEntries→ bincodeVec<Entry>→VersionedTransactions) andYellowstone(SubscribeRequestFilterTransactions, commitmentProcessed, echoes server pings).JitoDirectand any replacement vendor slot in later by config only.matcher— bounded map keyed by signature; all shred events enter (not venue-pre-filtered — the geyser leg is server-side venue-filtered, so a completed match implies a venue tx and inherits the geyser leg’s venue). 30s TTL; expiries splitunmatched_hinted(alarming) vsunmatched_other(expected bulk); 1M cap with counted eviction; per-feed flush on reconnect so stale first-sightings can’t fabricate deltas.hist/sink/tips/report— minutelyMinuteHist(1ms buckets 0–2000ms + under/overflow, percentile-by-bucket), batched sqlx writer (bounded mpsc → 500-row/2s INSERT batches, 3 retries then drop-with-counter), minutely Jitotip_floorpoller, and the histogram-percentile readout.
Load-Bearing Design Decisions
These are what make the measured delta_ms trustworthy. Ordered by how badly a “simplification” would corrupt the data:
- Pre-decode
Instanttimestamps on BOTH feeds, one monotonic clock, one box.arrivedis stamped at message receipt before any protobuf/bincode decode, on both adapters, on a single machine — so the delta is pure monotonic arithmetic and NTP drift can’t touch it. Adapter asymmetry (Yellowstone’s client lib decodes protobuf before yielding) is ≤1ms, negligible against the 50ms kill threshold. This is the integrity of the entire measurement. - The Yellowstone leg is venue-classification authority — it classifies over static plus ALT-resolved keys (
loaded_writable/loaded_readonly), closing the router-wrapped / ALT blind spot that the shred side structurally has. The shred side computes only a best-effortvenue_hintfrom static keys. Full trap write-up: solana-v0-alt-classification-trap. - The histogram is the percentile authority; raw rows are 1:100 sampled. Percentiles (p50/p90/p99) come from constant-size
solv1_delta_histbuckets, never from raw rows. This is what lets the rig survive the corrected 10⁷–10⁸ matched tx/week volume (an earlier estimate was 10–100× low) — persisting every match would be infeasible on a standard Supabase plan. delta_ms = geyser_at − shred_at; positive = shreds arrived earlier. The sign convention is the whole point (does the shred feed actually lead commodity gRPC?) — it is asserted in the matcher tests.- No
std::sync::MutexGuardheld across anyawait. Learned mid-build while writing the adapters: a non-Sendguard alive across an await point makes the future non-Sendand won’ttokio::spawn. Adapter state is dropped/scoped before await, or an async-aware lock is used.
Two more decisions worth knowing
- Increment 1 is an mpsc pipeline, not the spec’s
broadcastbus. With a single consumer (the matcher) mpsc is correct and simpler; broadcast only earns its keep once cohort/mirror become a second consumer (increment 2).- DB connection is Supavisor session mode
:5432only — config hard-fails on a:6543URL (transaction mode breaks sqlx prepared statements). See supabase-from-gcp-connection-gotchas.
Storage
- Supabase
mgmtproject (refmkofmdtdldxgmmolxxhc), all tables prefixedsolv1_— 8 tables:solv1_runs,solv1_delta_hist,solv1_tx_deltas,solv1_feed_stats,solv1_wallets,solv1_fills,solv1_mirror_decay,solv1_tip_snapshots(the last three seed increment 2). - RLS on / FORCE off / anon-denied. RLS enabled with zero policies (PostgREST roles default-deny); the rig connects as
postgresand writes because it owns the tables — neverFORCE ROW LEVEL SECURITY(that would subject the owner to the empty policy set and deny all writes). Defense-in-depthREVOKE ALL … FROM anon, authenticated. - Applied live via the Supabase MCP and mirrored as
migrations/0001_solv1_init.sqlin-repo. MCP is a dev/ops tool, never on the write path — the rig writes via plainDATABASE_URL. - Full pooler-port / RLS-via-ownership rationale: supabase-from-gcp-connection-gotchas.
Verification
- 21 tests green — matcher (dup sig, TTL hint-split sweep, reconnect flush, cap eviction, delta sign), venue classifier, histogram buckets/percentiles/merge, config
:6543guard, sink batching, plus adapter and tips unit tests. - Pipeline proven END-TO-END against a live throwaway Docker Postgres:
ReplayFeed → matcher → histogram → sink → read-backproduced pumpfun 121ms / pumpswap 155ms histogram rows;run_with_feedsreturned cleanly (proving the T12 shutdown-deadlock fix — tips poller is now aborted on shutdown so the run doesn’t hang);reportrendered the percentile table. - Docker image builds (release binary compiled in-container) and runs fail-fast without secrets (no silent no-op collection when
DATABASE_URL/ tokens are absent).
Status & Blockers
- On git
main, local, unpushed (no remote configured). 15 feature commits + fixups. - Cannot run for real yet — two external dependencies gate live shakeout:
- RPC Fast credentials (owner: levander) — real ShredStream + Yellowstone endpoints into env.
- GCP
europe-west3e2-standard-2VM (deploy via babylon) —e2-standard-2specifically, because a shared-core burstable instance would add CPU-throttle jitter to the arrival-Instantstamps, biasing the exact quantity being measured.
- Everything except live capture works today (the e2e proof ran against Docker Postgres with a
ReplayFeed).
Increment-2 Backlog
Carried forward deliberately (deferred, not missed):
- Cohort watch + paper-mirror decay — the second kill criterion (entry-decay vs modeled copy PnL); needs seeded
solv1_wallets, pool-state tracking, and swap-instruction parsers. - Shutdown-tail
on conflict do update— the final partial (<60s) minute of histogram/stats is currently dropped on shutdown; upsert-merge it. - mpsc → broadcast bus — flip when cohort/mirror become the second consumer of
TxSeen. - Second-shred-feed (Jito direct) guards —
FeedId::is_shred/feed_paircurrently assume a single shred feed; add guards before wiringJitoDirect. reporttimestamp column formatting — cosmetic.
Related
- slv1 — project overview / strategy pipeline
- slv1-copy-trading-feasibility-2026-07-03 — the feasibility study this rig adjudicates
- solana-v0-alt-classification-trap — why the geyser leg is venue authority (decision #2)
- supabase-from-gcp-connection-gotchas — pooler-mode + RLS-via-ownership storage recipe
- rust-otel-logs-signoz-export — the rig’s OTel log path (cross-project root cause)
- yellowstone-grpc-proto-layout-versioning — proto module layout + version gotchas hit building the adapter
- rust-protoc-docker-wkt-proto — the Docker/CI protoc well-known-types gotcha
- Spec (source of truth, detail):
~/coding/slv1/docs/superpowers/specs/2026-07-03-measurement-rig-design.md - Plan (source of truth, task-by-task):
~/coding/slv1/docs/superpowers/plans/2026-07-03-solv1-rig-increment-1.md - LOG · TOPICS