Lowest-latency algorithm for computing lifetime SOL PnL of any Solana wallet using only Helius RPC. Entry for Mert's mini Solana dev weekend competition.
A 4-phase adaptive pipeline that computes SOL PnL for any wallet — sparse or busy — without knowing transaction density upfront. It also outputs a full balance curve over time with peak/trough detection.
Results:
| Wallet type | Transactions | Pipeline latency | Naive baseline | Speedup |
|---|---|---|---|---|
| Sparse | ~500 | 2–4s | ~5s | ~1.5x |
| Busy | ~34,000 | ~80s | ~262s | 3.2x |
PnL verified to the lamport against naive sequential baseline across all test wallets.
Before writing the algorithm, we ran experiments to understand the RPC:
-
Experiment 1 (latency curve): Measured single-call latency for
sigs@[100,500,1000]andfull@[10,50,100]across sparse and busy wallets. Found that sig calls are ~150ms regardless of limit, full@100 is ~565ms on busy wallets, and tail ratios go up to 2.9x. -
Experiment 2 (concurrency knee): Fired N parallel sig calls for N=4,8,16,32,64,128. Wall-clock was flat from N=4 to N=32 (~580ms), jumped to 962ms at N=64 and 1744ms at N=128. This told us: cap concurrency at 16.
-
Naive baseline: Sequential paginated
full@100sweep. 33,392 txs took 284.6 seconds. This is the yardstick.
All experiment data is in experiment/bench-results.md.
Fire two getTransactionsForAddress calls in parallel:
sort=ASC, limit=1000— oldest 1000 signaturessort=DESC, limit=1000— newest 1000 signatures
One round trip tells us: the full slot range, density at both ends, and whether the wallet has ≤2000 txs (batches overlap → done). Sparse wallets are fully discovered here — no wasted calls.
Only runs when Phase 1 leaves a middle gap (>2000 txs). Partition the gap into density-adaptive chunks (density × 1.5 safety multiplier — our estimator runs ~30% low). Fire all chunks in parallel. Any chunk returning exactly 1000 sigs = overflow → recursively split in half.
Key insight we discovered: pagination tokens are slot:txIndex — you can fabricate them to start from any position in history. Instead of slow slot-range filtered calls, we:
- Pick 16 evenly-spaced starting points from discovered signatures
- Fabricate pagination tokens for each position
- Run 16 parallel keyset-paginated streams
- Each stream stops when it reaches the next stream's territory
This uses Helius's fast pagination path rather than range-scan queries.
For each transaction chronologically: find the wallet's index in accountKeys, compute post_balance[idx] - pre_balance[idx], accumulate. Builds a full balance curve with peak and trough detection. Uses status: any to include failed transactions (fees still count toward PnL).
The bottleneck on busy wallets isn't the algorithm — it's server response time. Each full@100 response takes ~3 seconds to return regardless of how you request it (slot filter, pagination, batch getTransaction). On a Developer plan (50 req/s), the effective throughput is ~450 transactions/second. For 34k txs, that's a ~76s floor.
Our 3.2x speedup over naive comes from parallelizing discovery and gap fill (Phases 1-2), not from Phase 3. The naive baseline makes 342 sequential calls; we make the same number but 16-wide.
# One-time setup
cp .env.example .env
# paste your HELIUS_API_KEY into .env
# Run the submission (full pipeline)
cargo run --release -p submission -- <wallet_address>
# Dev CLI (experiments, baselines, individual phases)
cd experiment
cargo run --release -- compute <address> # full pipeline with dev output
cargo run --release -- discovery <address> # Phase 1 only
cargo run --release -- baseline <address> # naive sequential sweep
cargo run --release -- profile <address> # classify a walletDev commands use the disk cache at experiment/cache/ by default. Add --no-cache to force live calls.
All derived from Phase 0 experiments, not guesses.
| Constant | Value | Source |
|---|---|---|
MAX_CONCURRENT_REQUESTS |
16 | E2: knee between N=32 and N=64 |
| Phase 1 parallelism | 2 calls | fixed (ASC + DESC) |
| Phase 2 chunk target | 800 sigs | 1000 cap minus 20% headroom |
| Density safety multiplier | 1.5x | Phase 1 estimator runs ~30% low |
| Fabricated token streams | 16 | matches concurrency cap |
computing-sol-algo/
├── submission/ ← the algorithm Mert runs (self-contained)
│ ├── Cargo.toml
│ ├── README.md ← submission-specific docs
│ └── src/
│ ├── main.rs ← CLI: takes address, prints JSON result
│ ├── lib.rs ← module registry
│ ├── rpc.rs ← Helius client (sigs, full, batch, fabricated pagination)
│ ├── types.rs ← JSON-RPC structs
│ ├── config.rs ← tuning constants
│ ├── discovery.rs ← Phase 1
│ ├── gap_fill.rs ← Phase 2
│ ├── fetch.rs ← Phase 3
│ ├── pipeline.rs ← orchestration
│ └── pnl.rs ← Phase 4 + balance curve
│
├── experiment/ ← dev sandbox (not shipped)
│ ├── bench-results.md ← E1 + E2 raw data
│ ├── baselines.json ← ground-truth PnL per wallet
│ ├── correctness-check.md ← pipeline vs baseline verification
│ ├── wallets.txt ← test wallet addresses
│ ├── cache/ ← gitignored record-and-replay
│ └── src/
│ ├── main.rs ← dev CLI with all subcommands
│ ├── rpc.rs ← Helius client + disk cache wrapper
│ ├── cache.rs ← SHA256-keyed disk cache
│ ├── baseline.rs ← naive sequential sweep
│ ├── experiments.rs ← E1 + E2 harnesses
│ └── (shared algorithm files)
│
├── .env.example ← env template
└── mert.png ← contest announcement
Pipeline PnL verified against fresh naive baseline (both live, no cache, run within minutes of each other):
| Wallet | Txs | Baseline PnL | Pipeline PnL | Match |
|---|---|---|---|---|
| 6F7c... (sparse) | 441 | 0 | 0 | exact |
| Fzyeeepi... (sparse) | 722 | 0 | 0 | exact |
| BoKeXuYd... (busy, 34k) | 34,066 | 166,580,562 | 166,580,562 | exact |
Full details in experiment/correctness-check.md.
