How OpenScan finds your txs without an indexer #168
AugustoL
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
How OpenScan finds your txs without an indexer
The Problem
Every block explorer you've used — Etherscan, Blockscout, Arbiscan — relies on centralized indexers. These are databases that scan every block, extract transaction data, and store it for quick lookups.
When you search for an address, you're not querying the blockchain. You're querying their database.
This works great for speed, but it requires:
OpenScan takes a different approach. We query the blockchain directly through your configured RPC, using a binary search algorithm to find transactions efficiently.
The Key Insight
Two pieces of on-chain state change when an address transacts:
Here's the critical part: you can query these values at any historical block using standard RPC methods:
If the nonce at block 1,000,000 is 5 and at block 2,000,000 is 8, we know 3 outgoing transactions occurred somewhere in that range.
If the balance changed but the nonce didn't, we know the address received ETH (or interacted with a contract).
The Algorithm
Phase 1: Initial State Comparison
First, we fetch the address state at two points:
We now know: 47 outgoing transactions, and some incoming activity.
RPC Calls: 4 (2 parallel calls × 2 blocks)
Phase 2: Binary Search
Instead of scanning 18 million blocks, we binary search for state changes:
We prioritize the right half first (newer blocks) so recent transactions appear in your UI faster.
When we narrow down to adjacent blocks where state changed, we've found a block containing transactions.
RPC Calls per iteration: 2 (nonce + balance in parallel)
Phase 3: Transaction Fetching
When an "important" block is identified:
eth_getBlockByNumber(block, true)from(sent) orto(received)Phase 4: Internal Transaction Detection
If the balance changed but nonce didn't, and no direct transfer was found, we look for internal transactions:
This catches interactions with contracts like Uniswap, Disperse.app, and multisigs.
RPC Call Estimates
Here's what to expect for different address histories on Ethereum mainnet (~20M blocks as of early 2026):
Binary Search Phase
The math: Binary search is O(log n). log₂(20,000,000) ≈ 24 iterations. Each iteration queries one midpoint = 2 RPC calls (nonce + balance in parallel).
Transaction Fetching Phase
For each block with transactions found:
eth_getBlockByNumber)Total Estimates by Transaction Count
Note: Actual numbers vary based on transaction distribution across blocks.
Compared to Naive Approach
Scanning all 20M blocks would require 20 million
eth_getBlockByNumbercalls. Binary search reduces this to 50-250 calls — a reduction of 99.999%.Optimizations We Use
1. Right-First Search
We search newer blocks first. Most users care about recent activity, so this gets results on screen faster.
2. Receipt Batching
Instead of fetching receipts one by one (which triggers rate limits), we batch them in groups of 20 with small delays between batches.
3. Calldata Scanning
Before making expensive receipt calls for internal transaction detection, we scan the transaction input data. This catches many contract interactions for free.
4. Session Caching
Within a single search session, we cache nonce/balance results. If the algorithm queries the same block twice, we don't make redundant RPC calls.
5. Streaming Results
Transactions appear in your UI as they're found, not after the entire search completes.
Current Limitations
We believe in being transparent about trade-offs. Here's what we're still working on:
1. Speed vs Trust Trade-off
This approach is slower than centralized indexers. A full search can take 10-30 seconds for active addresses, while Etherscan returns results instantly.
But those instant results come from trusting their database. Our results come directly from the blockchain through your RPC.
2. Internal Transaction Detection
Detecting transactions where your address is only involved internally (not as
fromorto) is challenging:We detect most cases, but some edge cases slip through.
3. RPC Rate Limits
Public RPCs have rate limits. When searching old addresses with many transactions:
Our solution: OpenScan lets you configure Infura and Alchemy API keys directly in Settings. These providers offer:
Just add your API key once, and OpenScan automatically uses it for all supported networks. You can also configure multiple RPC endpoints in fallback mode for redundancy.
4. Balance Ambiguity
If an address sends and receives ETH in the same block, we detect both transactions, but the algorithm can't distinguish which balance change came from which transaction without fetching all transactions in the block.
5. Memory Usage
All found transactions are held in memory during the search. For addresses with thousands of transactions, this can be significant. Results are cached to localStorage afterward (with 1-week expiry) to avoid re-searching.
Why We Built This
OpenScan exists because we believe in trustless infrastructure.
Every transaction you see in OpenScan was fetched directly from the blockchain through your RPC. No middleman. No database we control. No trust required.
Is it slower? Yes. Is it worth it? We think so.
Try It Yourself
What's Next
We're exploring several improvements:
balanceOf(address)at different blocks, we can track all ERC20 transfers — including internal calls and contract interactions. Unlike ETH where internal transfers are hard to detect, token balance changes are fully visible on-chain.Have ideas? We'd love to hear them in the comments or on GitHub.
Technical Reference
For those who want to dig into the code:
src/services/AddressTransactionSearch.tssrc/services/adapters/EVMAdapter/EVMAdapter.tsThe implementation is ~600 lines of TypeScript. PRs welcome.
The OpenScan Team
Beta Was this translation helpful? Give feedback.
All reactions