Skip to main content
Looking for Contributors! This Skill needs an implementation.Contributing a Skill involves:
  1. Writing a reference implementation with full functionality
  2. Adding comprehensive tests
  3. Writing documentation with usage examples
See the ethers-provider Skill for an example of a complete Skill implementation.Interested? Open an issue or PR at github.com/evmts/voltaire.
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
Build your own event indexer. Fetch, decode, and store blockchain events locally for fast queries without relying on external indexing services.

Why Build Your Own Indexer?

Third-party indexers (The Graph, Dune, etc.) are great but come with trade-offs:
  • Latency — Indexing delay before data is available
  • Cost — Query costs at scale
  • Dependencies — Service outages affect your app
  • Limitations — May not support your specific queries
A local indexer gives you:
  • Instant access — Query your own database
  • Full control — Custom schemas, any query pattern
  • No dependencies — Works offline, no API limits
  • Cost effective — One-time sync, unlimited queries

Planned Implementation

Basic Indexer

const indexer = EventIndexer({
  provider,
  storage: new SQLiteStorage('./events.db'), // or PostgresStorage, etc.
  contracts: [
    {
      address: USDC_ADDRESS,
      abi: ERC20_ABI,
      events: ['Transfer', 'Approval'],
      startBlock: 17000000,
    },
    {
      address: UNISWAP_ROUTER,
      abi: ROUTER_ABI,
      events: ['Swap'],
      startBlock: 17000000,
    }
  ]
});

// Start indexing (runs in background)
await indexer.start();

// Query indexed events
const transfers = await indexer.query({
  event: 'Transfer',
  where: {
    from: myAddress,
    blockNumber: { gte: 18000000 }
  },
  orderBy: 'blockNumber',
  limit: 100
});

Incremental Sync

const indexer = EventIndexer({
  provider,
  storage,
  contracts: [...],
  sync: {
    // Batch size for historical sync
    batchSize: 2000,
    // Confirmations before considering final
    confirmations: 12,
    // Poll interval for new blocks
    pollInterval: 12000,
    // Resume from last indexed block
    resume: true,
  }
});

// Get sync status
const status = indexer.getStatus();
// { indexed: 18500000, latest: 18500100, syncing: true, progress: 0.99 }

Custom Event Handlers

const indexer = EventIndexer({
  provider,
  storage,
  contracts: [{
    address: UNISWAP_POOL,
    abi: POOL_ABI,
    events: ['Swap'],
    // Transform event before storing
    transform: (event) => ({
      ...event,
      priceUSD: calculatePrice(event.args.sqrtPriceX96),
      volumeUSD: calculateVolume(event.args.amount0, event.args.amount1),
    }),
    // Filter which events to index
    filter: (event) => event.args.amount0 > 0n,
  }]
});

Query API

// Simple queries
const events = await indexer.query({
  event: 'Transfer',
  where: { to: myAddress },
  limit: 50
});

// Aggregations
const volume = await indexer.aggregate({
  event: 'Swap',
  field: 'volumeUSD',
  operation: 'sum',
  groupBy: 'day',
  where: { blockNumber: { gte: startBlock } }
});

// Raw SQL (if using SQL storage)
const result = await indexer.raw(`
  SELECT DATE(timestamp) as day, SUM(amount) as volume
  FROM transfers
  WHERE token = ?
  GROUP BY day
`, [USDC_ADDRESS]);

Storage Backends

// SQLite (simple, local)
import { SQLiteStorage } from './storage/sqlite.js';
const storage = new SQLiteStorage('./events.db');

// PostgreSQL (production, scalable)
import { PostgresStorage } from './storage/postgres.js';
const storage = new PostgresStorage('postgres://...');

// In-Memory (testing, temporary)
import { MemoryStorage } from './storage/memory.js';
const storage = new MemoryStorage();

// Custom storage (implement interface)
const storage = {
  async write(events) { ... },
  async query(filter) { ... },
  async getLastBlock() { ... },
};

Use Cases

Token Transfer History

// Index all transfers for a token
const indexer = EventIndexer({
  contracts: [{
    address: TOKEN_ADDRESS,
    abi: ERC20_ABI,
    events: ['Transfer'],
    startBlock: deploymentBlock,
  }]
});

// Query user's transfer history
const history = await indexer.query({
  event: 'Transfer',
  where: {
    $or: [{ from: userAddress }, { to: userAddress }]
  },
  orderBy: { blockNumber: 'desc' }
});

DEX Analytics

// Index Uniswap swaps
const indexer = EventIndexer({
  contracts: [{
    address: POOL_ADDRESS,
    abi: POOL_ABI,
    events: ['Swap'],
    transform: (e) => ({
      ...e,
      price: sqrtPriceX96ToPrice(e.args.sqrtPriceX96),
      volume: calculateVolume(e.args),
    })
  }]
});

// OHLCV candles
const candles = await indexer.aggregate({
  event: 'Swap',
  groupBy: 'hour',
  select: {
    open: { field: 'price', operation: 'first' },
    high: { field: 'price', operation: 'max' },
    low: { field: 'price', operation: 'min' },
    close: { field: 'price', operation: 'last' },
    volume: { field: 'volume', operation: 'sum' },
  }
});

NFT Ownership Tracking

// Index NFT transfers to track ownership
const indexer = EventIndexer({
  contracts: [{
    address: NFT_ADDRESS,
    abi: ERC721_ABI,
    events: ['Transfer'],
  }]
});

// Get current owner of token
const transfers = await indexer.query({
  event: 'Transfer',
  where: { tokenId: 1234 },
  orderBy: { blockNumber: 'desc' },
  limit: 1
});
const currentOwner = transfers[0]?.args.to;