Background
A common problem for EVM developers is the “historical balance problem” - recovering the full mapping of accounts to balances for an ERC-20, ERC-721, or ERC-1155 token. Solidity doesn’t keep track of the keys in a mapping; values are stored in the appropriate storage slot after hashing the key. Therefore, to recover the balances, a typical strategy is to replay all transfer events for that token and compute the rolling sum. Envio HyperSync is an indexer that allows developers to query millions of blockchain events in seconds. In this guide, we’ll use data from HyperSync to reconstruct the historical balances for a token.Prerequisites
- Node.js 18+
- Free HyperSync API key from envio.dev/app/api-tokens
HyperSync Endpoints
| Network | URL |
|---|---|
| Testnet | |
| Mainnet |
Ingredients
This guide surveys how to reconstruct balances for all three of the most popular token standards - ERC-20, ERC-721, and ERC-1155. Each uses some common ingredients, while requiring different logic to assemble the end reuslt.Querying Transfer Events
First, let’s write some code to query all of the Transfer events for our contract, filtering for the appropriate signature. ERC-20 and ERC-721 use theTransfer event below, while ERC-1155 uses the TransferSingle and TransferBatch events:
| Event | Signature |
|---|---|
Transfer(address,address,uint256) | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef |
TransferSingle(address,address,address,uint256,uint256) | 0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62 |
TransferBatch(address,address,address,uint256[],uint256[]) | 0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb |
lib/signatures.ts
lib/hypersync.ts
Field Selection
To optimize our queries, we should only request the fields we actually need. This reduces response size and speeds up queries significantly. Different token standards encode data in different topics: ERC-721 (tokenId in topic3):Parsing Log Data
Now that we have the raw event data, we need to parse it into usable values. Topics are 32-byte hex strings where addresses are left-padded with zeros, occupying the last 20 bytes:lib/parse.ts
Note:parseValuereturns the raw token value as abigint. ERC-20 tokens have adecimalsproperty (typically 18) — divide by10n ** BigInt(decimals)to convert to a human-readable amount.
Pagination
HyperSync returns paginated results to handle large datasets efficiently. We need to continue querying untilnext_block is undefined to get all transfers:
lib/paginate.ts
ERC-721 Balance Snapshot
For ERC-721, we will reconstruct the current ownership state by replaying all Transfer events. For NFTs, the last transfer for each token ID determines the current owner:app/api/snapshot/route.ts
ERC-20 Balance Snapshot
For ERC-20 tokens, we need to sum all transfers to calculate current balances. Unlike NFTs where we track ownership, ERC-20 balances require summing all incoming and outgoing transfers for each address. First, query ERC-20 transfers with the right field selection (topic1 forfrom, topic2 for to, and data for the value):
lib/erc20.ts
lib/erc20.ts
ERC-1155 Balance Snapshot
ERC-1155 tokens work differently from ERC-20 and ERC-721. They storeid and value in the data field rather than topics, requiring more complex parsing:
lib/erc1155.ts
Querying Multiple Event Types
When working with ERC-1155 tokens, we can optimize by querying both TransferSingle and TransferBatch events in a single request:lib/multi-event.ts
topic0:
Get Latest Block
To verify you’ve synced all available data, you can check the current chain height:lib/height.ts
Common Mistakes
1. Forgetting pagination
HyperSync returns partial results. Always checknext_block:
2. Requesting unused fields
Every field adds to response size. Be explicit:3. Using libraries for simple parsing
HyperSync returns raw hex. Native BigInt handles it:4. Not filtering burned tokens
Address0x000...000 means burned:
API Reference
POST /query
Query event logs. Request body:| Field | Type | Description |
|---|---|---|
from_block | number | Starting block (inclusive) |
to_block | number | Ending block (optional) |
logs | array | Log filters |
logs[].address | string[] | Contract addresses to filter |
logs[].topics | string[][] | Topic filters (OR within array, AND across arrays) |
field_selection.log | string[] | Fields to return |
| Field | Type | Description |
|---|---|---|
data | array | Blocks containing matching logs |
data[].logs | array | Matching logs in block |
next_block | number | Next block to query (pagination) |
GET /height
Get current chain height. Response:| Field | Type | Description |
|---|---|---|
height | number | Latest block number |
Summary
Envio HyperSync lets you query any contract’s event history across Monad with a single paginated API — no node to run, no indexer to maintain. The/query endpoint accepts topic and address filters, and /height gives you the current chain tip. Check out the full HyperSync documentation for additional query types and options.
From here, you could extend this pattern to build airdrop eligibility checkers, governance voting power snapshots, or multi-token portfolio trackers.
Next Steps
- Envio docs - Full HyperSync documentation
- API tokens - Get your free API key

