Indexing
One process runs one poll loop per chain. Each loop reads the chain head, stores any large transfers it finds, updates the status, then waits. This page describes the loops as they are coded, including what they miss.
#Poll intervals
The wait starts after a poll finishes, so the real cycle is the wait plus the time the RPC calls take. After a failed poll, the wait doubles, up to 60 seconds.
| Chain | Wait after a good poll | Wait after a failure | Indexes transfers |
|---|---|---|---|
| Ethereum | 12 s | 24 s | Curated tokens and native ETH |
| Base | 16 s | 32 s | Curated tokens |
| BNB Chain | 16 s | 32 s | Curated tokens |
| Arbitrum One | 16 s | 32 s | Curated tokens |
| OP Mainnet | 30 s | 60 s | None |
| Polygon PoS | 30 s | 60 s | None |
| Solana | 30 s | 60 s | One sampled block: curated tokens and SOL |
#The EVM poller
- Send a batch of
eth_getBlockByNumber("latest")andeth_gasPrice. On Ethereum, the block includes full transactions. - Update head, block time, lag, transaction count, gas ratio and fees. Add the block to the in-memory list of 25.
- Stop here on OP Mainnet and Polygon.
- On Ethereum, scan the head block's transactions. Store each one whose
valuetimes the ETH price is at least $100,000. Skip contract creations. - Call
eth_getLogsfor ERC-20Transferevents of the curated tokens, from the block after the last scanned block, up to 200 blocks per poll. - For each log, compute
amountandusd. Store it ifusdis at least $100,000. - Move the cursor to the head.
Storage uses a unique uid. A row found twice is stored once, and it reaches rules and the stream once.
#The log scan cursor
The log range starts at the larger of cursor + 1 and head - 2000. It ends at the smaller of head and from + 199. The cursor then moves to the end of the range. A poll reads 200 blocks at most, which keeps each call small enough for public RPC limits. A chain that produces more blocks than that in one poll is caught up over later polls. No block is skipped unless the reader falls more than 2,000 blocks behind. scan_lag_blocks in Status shows the distance. The cursor is held in memory, so a restart begins again at the head.
This table was measured on 2026-11-04 UTC from the last 25 entries of Recent blocks. The log scan was changed after this measurement. All four chains fit inside the 200 block limit.
| Chain | Seconds between reads | Blocks between reads | Result |
|---|---|---|---|
| Ethereum | 13.5 | 1.1 | Inside the limit |
| Base | 17.3 | 8.7 | Inside the limit |
| BNB Chain | 17.3 | 38.5 | Inside the limit |
| Arbitrum One | 16.8 | 57.8 | Inside the limit |
#The Solana poller
- Send a batch of
getSlotwith finalized commitment,getRecentPerformanceSamplesandgetRecentPrioritizationFees. - Update TPS and the 75th percentile priority fee.
- Call
getBlockfor that slot. If the RPC returns an error for it, try up to three older slots. - For each successful transaction in that one block, compute balance changes per owner for the curated Solana tokens, and per account for SOL.
- For each asset, take the largest decrease and the largest increase.
amountis the smaller of the two. Store it ifusdis at least $100,000.
Only one block per poll is read. A separate loop reads recent transactions of 7 public exchange wallets every 120 s with the same logic. Read sampling before you use these rows.
#What gets missed
| Case | Why | Effect |
|---|---|---|
| Tokens outside the curated list | The log filter names curated contracts only | Never stored |
| Transfers below $100,000 | The threshold applies at index time | Never stored |
| Blocks more than 2,000 behind the head | Described above | Lost |
| Blocks passed during a restart | The cursor is held in memory | Lost |
| ETH in a block between two polls | The native scan reads only the head block | Lost. The Ethereum read gap was 2 blocks now and then in the measurement above. |
| ETH moved inside a contract call | The scan reads top-level value only, with no traces | Never stored |
| Native assets on Base, BNB Chain and Arbitrum | Native indexing is on for Ethereum and Solana only | Never stored |
| All of OP Mainnet and Polygon | No index on these chains | Head, fees and lookups only |
| Solana slots that were not sampled | One slot per poll | Most Solana activity is absent |
| Time while the service is down | The cursor lives in memory, and a new start begins at the current head | Lost. No backfill. |
| Time while the price is unknown | A row needs a price. With no price since start, the row is skipped. | Lost |
| Reorganized blocks | Rows are not removed after a reorg, and EVM polls read latest | A row can refer to a block that is no longer canonical |
The Ethereum native scan checks the receipt of each transaction at or above the threshold. A reverted transaction is not stored. If the receipt read fails, the row is not stored either.
ERC-20 rows come from logs, and logs exist only for successful transactions.
#Other timers
| Timer | Interval | Effect |
|---|---|---|
| Price refresh | 60 s | Updates the CoinGecko prices used for valuation |
| Status sample | 60 s, stored in 5 minute buckets | Feeds Status history. The last write in a bucket wins. |
| Prune | 1 hour | Deletes transfers older than 7 days, samples older than 30 days and delivery rows older than 14 days |
| Webhook worker | 3 s | Sends up to 20 due deliveries |
#How to use this data
- Treat the index as a filter of large movements in a short list of tokens. It is not a full copy of any chain.
- Use
sampled,chainand this page's table when you compute totals. - Read
indexed_since_startandlast_okfrom Status to tell whether a quiet period means a quiet chain or a stalled poller.