Prompt
How do I set up an event indexing API for tracking smart contract events with reliable historical backfill?
Latest observation
To set up an event indexing API for smart contract events with reliable historical backfill, design it as a pipeline with four layers:
- Chain data ingestion
- Event decoding and normalization
- Persistent indexing/storage
- API/query layer
The key to reliability is to treat historical backfill and live sync as the same pipeline, just with different starting blocks and retry logic.
1) Define your indexing target
First decide:
- Which chain(s): Ethereum, L2s, EVM-compatible, etc.
- Which contracts: one or many, static or user-submitted
- Which events: specific event signatures or all logs
- How much history: from deployment block, a given height, or genesis
- Freshness needs: near-real-time vs delayed
For smart contract events, you usually index:
eth_getLogsfor historical and live logs- optionally
eth_subscribe/ websocket log subscriptions for live updates
2) Use logs as your source of truth
For EVM chains, contract events are emitted as logs.
Historical backfill
Use eth_getLogs in block ranges:
- Query by:
addresstopicsfromBlocktoBlock
Live indexing
Use one of:
- WebSocket subscriptions for new logs
- Polling latest blocks and querying logs per block range
Recommendation: even with WebSockets, still run periodic block-range reconciliation to catch missed events.
3) Build a robust backfill strategy
Historical backfill is where most indexers fail. The safest pattern is:
A. Chunk by block ranges
Do not query huge spans at once. Split into manageable ranges:
- e.g. 1,000–10,000 blocks per request depending on chain/provider limits
- adapt chunk size based on response size and latency
B. Persist progress checkpoints
Store:
last_processed_blocklast_finalized_blockor confirmation depth- per-contract / per-task cursor
This lets you resume after failures.
C. Make processing idempotent
Assume the same log may be seen multiple times. Deduplicate using:
chain_idblock_numbertransaction_hashlog_index
Create a unique constraint on these fields.
D. Reorg handling
For live indexing, only consider blocks final after a confirmation window, e.g.:
- Ethereum mainnet: 12–64 confirmations depending on risk tolerance
- L2s: follow chain-specific finality rules
Also support reprocessing recent blocks if a reorg is detected.
4) Event decoding
Logs contain:
topicsdataaddress- metadata like block and tx info
To decode events:
- Load the contract ABI
- Match
topic0against event signature hash - Decode indexed and non-indexed parameters
Store both:
- raw log data
- decoded event fields
That gives you flexibility if ABI decoding changes later.
5) Storage schema
A practical schema:
contracts
chain_idaddressabi_versiondeployment_block
indexed_events
chain_idaddressblock_numberblock_hashtransaction_hashlog_indexevent_signaturedecoded_jsonraw_topicsraw_datatimestampremovedorreorgedflag
Indexes to add:
(chain_id, address, block_number)(chain_id, event_signature)- unique
(chain_id, transaction_hash, log_index)
If query volume is high, use:
- PostgreSQL for metadata and moderate scale
- ClickHouse/BigQuery/Elastic for analytics-heavy workloads
6) API design
Expose endpoints like:
Get events for a contract
GET /v1/events?chain_id=1&address=0x...&from_block=19000000&to_block=19010000
Filter by event type
GET /v1/events?chain_id=1&event=Transfer
Get decoded event by tx hash
GET /v1/events/{transaction_hash}
Backfill status
GET /v1/indexers/{id}/status
Return:
- current cursor
- synced height
- lag
- last error
- reorg status
7) Recommended ingestion architecture
A reliable pattern:
Option A: Worker + queue
- Scheduler enqueues block ranges
- Fetcher workers call
eth_getLogs - Decoder workers normalize logs
- Writer stores in DB
- Checkpoint updater advances cursor only after commit
This is the most fault-tolerant approach.
Option B: Single service
Good for small scale, but less resilient.
8) Backfill algorithm
A simple reliable loop:
- Determine
start_blockfrom deployment or checkpoint - Determine
end_blockfrom latest safe/finalized block - Query logs in a range
- Decode and write logs transactionally
- Update checkpoint only after success
- Repeat with next range
- Retry on transient errors with exponential backoff
Pseudo-flow:
while cursor <= latest_finalized:
range = [cursor, min(cursor + chunk_size - 1, latest_finalized)]
logs = getLogs(range)
write logs idempotently
checkpoint = range.end + 1
If a request fails:
- retry
- reduce chunk size if provider limits are hit
- persist failure state for observability
9) Live sync pattern
For live events:
- Poll or subscribe for latest blocks
- Wait for confirmations
- Index confirmed range
- Periodically reconcile recent blocks
- Detect reorgs by block hash mismatch
A good rule:
- index
latest_finalized, notlatest, unless your app can tolerate reorgs
10) Provider considerations
RPC providers often impose:
- log range limits
- response size limits
- rate limits
- unstable websocket connections
To improve reliability:
- use multiple RPC providers with failover
- retry on 429/5xx
- dynamically shrink block range if response is too large
- prefer archived nodes for deep history
- cache contract ABIs locally
11) Reorg-safe data model
To support reorgs:
- store
block_hash - mark removed logs if provider emits them
- when a canonical block changes, delete or invalidate affected rows
- reprocess affected block ranges
You may maintain:
canonical_blockseventsreorg_events
12) Operational best practices
Add:
- metrics
- indexing lag
- logs processed/sec
- error rate
- retry count
- alerts
- stuck cursor
- growing lag
- RPC failures
- dead-letter queue
- for unprocessable logs
- audit trail
- block range processing history
13) Minimal implementation stack
A practical stack:
- Node.js / TypeScript or Python
ethers.jsorweb3.py- PostgreSQL
- Redis for queue/cursors
- Worker process for backfill/live sync
- FastAPI / Express / NestJS for API
14) Example workflow
Backfill
- Start from contract deployment block
- Query 2,000 blocks at a time
- Decode events
- Insert with unique key on
(tx_hash, log_index) - Update cursor
- Continue until caught up
Live
- Subscribe to new logs or poll every block
- Wait 12 confirmations
- Index final blocks
- Reconcile every N minutes
15) Common pitfalls to avoid
- Querying too many blocks per RPC call
- Not storing raw logs
- Advancing checkpoints before writes succeed
- Ignoring reorgs
- Trusting websocket subscriptions alone
- Not using idempotent inserts
- Not versioning ABIs
- Missing archived node support for old history
16) If you want the simplest reliable architecture
Use:
eth_getLogsfor both backfill and catch-up- block-range chunking
- checkpointing in DB
- idempotent inserts
- confirmation delays
- periodic reconciliation
That gives you a robust indexer without needing overly complex infra.
If you want, I can also provide:
- a reference architecture diagram
- a Postgres schema
- a TypeScript/ethers.js implementation
- a Python/web3.py implementation
- a FastAPI/Express API example
Observation history
Unlock observation history
Create a free Obsurfable account to unlock historical responses, model comparisons, and deeper brand intelligence.
How did Obsurfable measure this prompt?
Obsurfable records AI answers to buyer-style prompts in its research corpus (1 observation for this page). Metrics are distributions over observations, not a single static ranking.
Which AI systems does Obsurfable collect answers from?
OpenAI, ChatGPT, Google, Gemini, Google AI Mode, Anthropic, Claude, Perplexity, Grok, DeepSeek, Mistral, Copilot, and Meta AI.