Read a token balance, pool reserve, or any other view function without sending a transaction.
This guide reads the USDC balance of the Uniswap V2 USDC/WETH pair at block 18000000 with
eth_call, then pins several reads to one block so their results describe the same chain state.
Prerequisites
- A GraphOps RPC API key.
- Ethereum mainnet endpoint access.
- A contract address, ABI or encoded call data, and a block reference.
Request
The data field is balanceOf(address) encoded with selector 0x70a08231 and the target address
left-padded to 32 bytes.
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_call",
"params": [
{
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"data": "0x70a08231000000000000000000000000b4e16d0168e52d35cacd2c6185b44281ec28c9dc"
},
"0x112a880"
]
}'import { createPublicClient, erc20Abi, http } 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 balance = await client.readContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: erc20Abi,
functionName: "balanceOf",
args: ["0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"],
blockNumber: 18_000_000n,
});
console.log(balance);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 usdc = new ethers.Contract(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
["function balanceOf(address owner) view returns (uint256)"],
provider,
);
const balance = await usdc.balanceOf(
"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
{ blockTag: 18_000_000 },
);
console.log(balance);from web3 import Web3
w3 = Web3(
Web3.HTTPProvider(
"https://rpc.graphops.xyz/v1/evm/1",
request_kwargs={
"headers": {
"Authorization": "Bearer <GRAPHOPS_API_KEY>",
},
},
)
)
usdc = w3.eth.contract(
address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi=[
{
"name": "balanceOf",
"type": "function",
"stateMutability": "view",
"inputs": [{"name": "owner", "type": "address"}],
"outputs": [{"name": "", "type": "uint256"}],
}
],
)
balance = usdc.functions.balanceOf(
"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
).call(block_identifier=18_000_000)
print(balance)Response
{ "jsonrpc": "2.0", "id": 1,Matches the id you sent "result": "0x0000000000000000000000000000000000000000000000000000197016bc99ff"uint256 balance: 27,969,208,490,495 base units, 27,969,208.490495 USDC}USDC has 6 decimals, so divide the decoded integer by 1,000,000. Client libraries such as viem,
ethers, and web3.py decode the bytes for you and return the integer.
Read several values at one block
Separate latest reads can land on different blocks as the chain advances. When results must
agree, such as two pool balances used to compute a price, resolve one block first and pass its
number to every read.
This viem example resolves the finalized block, then reads the pair's USDC and WETH balances at
that block:
import { createPublicClient, erc20Abi, http } 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 pair = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
const block = await client.getBlock({ blockTag: "finalized" });
const [usdc, weth] = await Promise.all(
(
[
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
] as const
).map((address) =>
client.readContract({
address,
abi: erc20Abi,
functionName: "balanceOf",
args: [pair],
blockNumber: block.number,
}),
),
);
console.log({ block: block.number, usdc, weth });Use finalized when the result must not change after a reorganization, safe for user-visible
reads that can trail the head slightly, or a block number you stored earlier to repeat a read.
See Consistency and finality for how the tags differ.
Interpret errors
- A revert is a contract execution result, not a transport failure. The JSON-RPC
errorobject carries the revert data when the contract provides it, and retrying the same call at the same block reverts again. - Retry only when the HTTP status or JSON-RPC error indicates a transport or server failure. See Retries and reorgs.
- A historical block may need archive access on your network. See Archive reads.