Skip to main content

Token Indexing

With blocks and transactions stored in the database, you can layer on token indexing. Nethereum.BlockchainStorage.Token.Postgres provides a three-stage pipeline for indexing token activity: capture transfer events, denormalise them into typed records, and aggregate current balances via batched RPC calls. Each stage runs as an independent BackgroundService with its own progress checkpoint, so they can run in parallel and resume after restarts.

Package: Nethereum.BlockchainStorage.Token.Postgres

dotnet add package Nethereum.BlockchainStorage.Token.Postgres

This package focuses on indexing the chain to build a complete picture of all token activity. If you just need to check a wallet's current token balances without indexing, see Token Portfolio in the Data Services section — it uses batched multicall over a known token list and requires no database.


Quick Start

var builder = Host.CreateApplicationBuilder(args);
var connectionString = "Host=localhost;Database=tokens;Username=postgres;Password=secret";

// Stage 1: Capture Transfer event logs
builder.Services.AddTokenLogPostgresProcessing(builder.Configuration, connectionString);

// Stage 2: Denormalise logs into typed token transfer records
builder.Services.AddTokenDenormalizerProcessing(builder.Configuration, connectionString);

// Stage 3: Aggregate current balances via batched RPC
builder.Services.AddTokenBalanceAggregationProcessing(builder.Configuration, connectionString);

var host = builder.Build();
await host.RunAsync();

All three services start automatically as hosted services. Each tracks its own progress independently.


Three-Stage Pipeline

Stage 1 — Capture Transfer Logs

TokenLogPostgresProcessingService scans the chain for Transfer events (ERC-20, ERC-721, ERC-1155) using eth_getLogs in batch ranges. Raw logs are stored in the transactionlogs table (shared schema with the main blockchain processor).

Configuration (TokenLogProcessing section):

{
"TokenLogProcessing": {
"BlockchainUrl": "http://localhost:8545",
"NumberOfBlocksToProcessPerRequest": 1000,
"MinimumNumberOfConfirmations": 0,
"ReorgBuffer": 10,
"ContractAddresses": null
}
}
PropertyDefaultDescription
BlockchainUrlrequiredJSON-RPC endpoint
NumberOfBlocksToProcessPerRequest1000Blocks per eth_getLogs batch
MinimumNumberOfConfirmations0Blocks behind head before processing
ReorgBuffer10Re-check recent blocks for reorgs
ContractAddressesnullFilter to specific tokens (null = all)

Progress is tracked in the tokenblockprogress table.

Stage 2 — Denormalise Transfers

TokenDenormalizerService reads raw logs from the transactionlogs table, matches them against known Transfer event signatures (ERC-20, ERC-721, ERC-1155 TransferSingle, ERC-1155 TransferBatch), and decodes them into typed TokenTransferLog records.

  • ERC-20: Transfer(address from, address to, uint256 value)
  • ERC-721: Transfer(address from, address to, uint256 tokenId)
  • ERC-1155: TransferSingle(address operator, address from, address to, uint256 id, uint256 value)
  • ERC-1155: TransferBatch(address operator, address from, address to, uint256[] ids, uint256[] values) — expanded to individual records

Configuration (TokenDenormalizer section):

{
"TokenDenormalizer": {
"BatchSize": 1000
}
}

Progress is tracked in the denormalizerprogress table via row index.

Stage 3 — Aggregate Balances

TokenBalanceRpcAggregationService processes new transfer records and queries the chain for current balances via batched RPC calls:

  • ERC-20: balanceOf(address) for each affected account
  • ERC-721: balanceOf(address) + ownerOf(tokenId)
  • ERC-1155: balanceOf(address, tokenId)

Calls are batched using MultiQueryBatchRpcHandler with BlockParameter for point-in-time accuracy.

Configuration (TokenBalanceAggregation section):

{
"TokenBalanceAggregation": {
"RpcUrl": "http://localhost:8545",
"BatchSize": 1000
}
}

Progress is tracked in the balanceaggregationprogress table via row index.

Reorg recovery: On startup, the service finds any non-canonical transfer logs and re-queries the affected accounts at the latest block to correct balances.


Database Schema

TableKey ColumnsPurpose
tokentransferlogstransactionhash, logindex, blocknumber, contractaddress, fromaddress, toaddress, amount, tokenid, tokentypeDecoded transfer events
tokenbalancesaddress + contractaddress (unique)Current ERC-20 balances per account
nftinventoryaddress + contractaddress + tokenid (unique)Current NFT ownership with amounts
tokenmetadatacontractaddress (PK)Token name, symbol, decimals, type
tokenblockprogresslastblockprocessedStage 1 checkpoint
denormalizerprogresslastprocessedrowindexStage 2 checkpoint
balanceaggregationprogresslastprocessedrowindexStage 3 checkpoint

The transactionlogs table is shared with the main blockchain storage — the token log processor writes to it, and the denormalizer reads from it. If you're also running the main BlockchainProcessingHostedService, the token log processor can piggyback on logs already captured there.


Supported Token Standards

StandardTransfer EventWhat Gets Indexed
ERC-20Transfer(address,address,uint256)Fungible token transfers and balances
ERC-721Transfer(address,address,uint256)NFT transfers, ownership, and inventory
ERC-1155TransferSingle(address,address,address,uint256,uint256)Multi-token transfers with amounts
ERC-1155TransferBatch(address,address,address,uint256[],uint256[])Batch transfers (expanded to individual records)

All three standards are detected automatically by matching the event topic hash. No configuration needed.


Running Alongside the Blockchain Processor

The token pipeline works independently or alongside the main blockchain processor:

var builder = Host.CreateApplicationBuilder(args);
var connectionString = "Host=localhost;Database=blockchain;Username=postgres;Password=secret";

// Main blockchain indexer (blocks, transactions, logs)
builder.Services.AddPostgresBlockchainProcessor(builder.Configuration, connectionString);
builder.Services.AddPostgresInternalTransactionProcessor();

// Token pipeline (transfers, balances, NFTs)
builder.Services.AddTokenLogPostgresProcessing(builder.Configuration, connectionString);
builder.Services.AddTokenDenormalizerProcessing(builder.Configuration, connectionString);
builder.Services.AddTokenBalanceAggregationProcessing(builder.Configuration, connectionString);

// Explorer UI over the indexed data
builder.Services.AddExplorerServices(builder.Configuration);

var host = builder.Build();
await host.RunAsync();

Repository Interfaces

If you need to query indexed token data programmatically:

InterfaceKey Methods
ITokenTransferLogRepositoryQuery decoded transfer events
ITokenBalanceRepositoryQuery current token balances by address
INFTInventoryRepositoryQuery NFT ownership by address and token ID
ITokenMetadataRepositoryQuery token name, symbol, decimals, type

Register repositories without the full pipeline:

builder.Services.AddTokenPostgresRepositories(connectionString);

Next Steps