Build a transfer history, audit trail, or analytics table from months of contract events, and
resume it after any failure without starting over. This guide scans USDC Transfer logs in
bounded block ranges and stores a checkpoint after each range. Older ranges need archive access;
check Supported networks for coverage on your network.
Prerequisites
- A GraphOps RPC API key.
- Archive access for the target chain.
- A durable store for logs and range progress.
- An idempotency key such as
(blockHash, transactionHash, logIndex).
How the backfill runs
- Choose a starting block and a range size. This guide uses 1,920 blocks per range.
- Request one range with
eth_getLogs, filtered by contract address and event topic. - Write the returned logs idempotently, keyed by
(blockHash, transactionHash, logIndex). - Store the range's last block as your checkpoint, then request the next range.
Run one live consumer separately for new blocks, such as a WebSocket subscription, and stop the backfill where the live consumer starts.
Request
Each variant requests the first range, blocks 18,000,000 to 18,001,919. Repeat the same request shape for each following range.
curl "https://rpc.graphops.xyz/v1/evm/1" \
-H "Authorization: Bearer <GRAPHOPS_API_KEY>" \
-H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [
{
"fromBlock": "0x112a880",
"toBlock": "0x112afff",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}
]
}'import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http("https://rpc.graphops.xyz/v1/evm/1", {
fetchOptions: {
headers: {
Authorization: "Bearer <GRAPHOPS_API_KEY>",
},
},
}),
});
const range = {
fromBlock: 18_000_000n,
toBlock: 18_001_919n,
};
const logs = await client.getLogs({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)",
),
fromBlock: range.fromBlock,
toBlock: range.toBlock,
});
console.log({ range, logCount: logs.length });
// Store logs idempotently, then mark the range complete in your durable store.import { ethers } from "ethers";
const request = new ethers.FetchRequest("https://rpc.graphops.xyz/v1/evm/1");
request.setHeader("Authorization", "Bearer <GRAPHOPS_API_KEY>");
const provider = new ethers.JsonRpcProvider(request);
const range = {
fromBlock: 18_000_000,
toBlock: 18_001_919,
};
const logs = await provider.getLogs({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
topics: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
],
fromBlock: range.fromBlock,
toBlock: range.toBlock,
});
console.log({ range, logCount: logs.length });
// Store logs idempotently, then mark the range complete in your durable store.from web3 import Web3
w3 = Web3(
Web3.HTTPProvider(
"https://rpc.graphops.xyz/v1/evm/1",
request_kwargs={
"headers": {
"Authorization": "Bearer <GRAPHOPS_API_KEY>",
},
},
)
)
range_ = {
"fromBlock": 18_000_000,
"toBlock": 18_001_919,
}
logs = w3.eth.get_logs(
{
**range_,
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
],
}
)
print({"range": range_, "log_count": len(logs)})
# Store logs idempotently, then mark the range complete in your durable store.Range sizing
Start with small ranges and widen them only after observing response size and latency. Avoid
unbounded fromBlock to latest scans; a failed broad request repeats all of its work.
| Situation | Action |
|---|---|
| Timeout | Reduce range size. |
| Response too large | Reduce range size or add more selective topics. |
| Sparse logs | Increase range size gradually. |
Rate limit (429) | Lower concurrency and add backoff with jitter. |
Progress tracking
Store the checkpoint in the same transaction as the range's logs, so a restart never skips a range. After the first range, the record looks like this:
{
"network": "ethereum",
"chainId": 1,
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"lastCompletedBlock": 18001919
}On restart, read lastCompletedBlock and request the next range from the block after it.