Python is the first tool most developers reach for when they need to read blockchain data from a script or data pipeline. It is also the tool that makes them reach for something else. The standard path — install web3.py, track down a JSON ABI, figure out which proxy implementation is actually deployed, manually scale USDC’s 6 decimals vs WETH’s 18 — takes an afternoon the first time and still costs ten minutes every new contract.
The evmquery REST API handles ABIs, proxies, and call batching for you. Point it at a contract address, write a typed expression, get decoded JSON back. No local ABI files, no separate RPC account. The blockchain python api call is six lines.
TL;DR
pip install requests, grab a free API key, then POST https://api.evmquery.com/api/v1/query with a chain name, a contract address map, and a typed expression. Decoded result returns as JSON with the block number included.
Why Python blockchain reads get messy
The canonical approach is web3.py. It works, but the per-contract overhead is real:
from web3 import Web3
import json
w3 = Web3(Web3.HTTPProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"))
# You need the full ABI — source it, paste it in, keep it in sync
USDC_ABI = json.load(open("usdc_abi.json"))
usdc = w3.eth.contract(
address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi=USDC_ABI,
)
raw = usdc.functions.balanceOf("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").call()
decimals = usdc.functions.decimals().call()
balance = raw / 10**decimals
To do this you need: an RPC provider account, the full USDC ABI in a file, two separate .call() invocations, and manual decimal math. Multiply by ten contracts across three chains and the maintenance surface grows fast.
If you need to batch reads, you either call each method sequentially or set up Multicall3 yourself. If you hit a proxy, you have to resolve it manually. If the contract is on Base instead of Ethereum, you set up a second Web3 instance.
evmquery handles all of that at the API level.
How the expression language works
evmquery uses SEL — a typed expression language built on Google’s Common Expression Language — to describe reads. You declare contracts by address, write an expression that calls their methods, and evmquery resolves the ABI, batches the calls, and decodes the result.
A query has four fields:
chain— the target network:evm_ethereum,evm_base, orevm_bnb_mainnetschema.contracts— a map of name to contract address; the name becomes a variable in your expressionschema.context— typed declarations for any input variables you pass at runtimeexpression— a CEL expression; the return value is what gets decoded and returned
Here is a one-time setup block that covers every example below:
import requests
API_KEY = "your_api_key_here"
API_URL = "https://api.evmquery.com/api/v1/query"
HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"}
def evmquery(chain: str, schema: dict, expression: str, context: dict | None = None):
payload = {"chain": chain, "schema": schema, "expression": expression}
if context:
payload["context"] = context
resp = requests.post(API_URL, headers=HEADERS, json=payload)
resp.raise_for_status()
return resp.json()
Get a free API key from app.evmquery.com/onboarding. The free tier is 2,000 credits per month — more than enough to run all five recipes below many times over.
Recipe 1: Read an ERC-20 balance
result = evmquery(
chain="evm_ethereum",
schema={
"contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
"context": {"wallet": "sol_address"},
},
expression="formatUnits(usdc.balanceOf(wallet), usdc.decimals())",
context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
)
print(result)
# {"value": 5567.402493, "type": "double", "block": 24953137}
Two things to note.
formatUnits handles decimals automatically. USDC has 6 decimal places. WETH has 18. Some stablecoins differ. Rather than hardcoding a scale factor, the expression calls usdc.decimals() at query time and passes the live result to formatUnits. Both calls are batched into one Multicall3 round — you don’t pay extra for the second call.
wallet is typed as sol_address in schema.context. This tells SEL that the variable holds an EVM address before any network traffic happens. A type mismatch fails at check time with a pointer to the offending token, not silently at the RPC layer.
Recipe 2: Read a Chainlink price feed
Chainlink aggregators are among the most commonly queried contracts on Ethereum. The ETH/USD feed stores answers with 8 decimal places:
result = evmquery(
chain="evm_ethereum",
schema={
"contracts": {
"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"
}
},
expression="formatUnits(eth_usd.latestAnswer(), 8)",
)
print(result["value"]) # e.g. 2314.86
No ABI file needed. The Chainlink aggregator is verified on Etherscan; evmquery resolves the ABI automatically. Swap the address for any other Chainlink feed (BTC/USD, LINK/ETH, etc.) and the expression stays the same.
Recipe 3: Read a multi-token portfolio in one call
Here is where the expression language earns its keep. Returning a map from the expression causes evmquery to batch all reads into a single Multicall3 round:
WALLET = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
result = evmquery(
chain="evm_ethereum",
schema={
"contracts": {
"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
},
"context": {"wallet": "sol_address"},
},
expression="""{
"usdc": formatUnits(usdc.balanceOf(wallet), usdc.decimals()),
"weth": formatUnits(weth.balanceOf(wallet), weth.decimals()),
"dai": formatUnits(dai.balanceOf(wallet), dai.decimals())
}""",
context={"wallet": WALLET},
)
print(result["value"])
# {"usdc": 5567.402493, "weth": 0.0000001, "dai": 0.0}
Six contract calls — three balanceOf and three decimals — collapsed into one HTTP request. In raw web3.py that is either six sequential .call() invocations or Multicall3 setup code you write yourself. Here it is one expression.
Batching is implicit
Any expression referencing multiple contracts on the same chain is auto-batched into a single Multicall3 round. You do not change the structure of your query to get the efficiency — it happens automatically.
Recipe 4: Check a DeFi position
Reading an Aave position is a good test of the cel.bind helper. getUserAccountData returns a struct; cel.bind lets you extract individual fields without calling the contract twice:
result = evmquery(
chain="evm_ethereum",
schema={
"contracts": {
"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
},
"context": {"user": "sol_address"},
},
expression="""cel.bind(pos, aave.getUserAccountData(user), {
"collateral_usd": formatUnits(pos.totalCollateralBase, 8),
"debt_usd": formatUnits(pos.totalDebtBase, 8),
"health_factor": formatUnits(pos.healthFactor, 18)
})""",
context={"user": "0xYourWalletHere"},
)
print(result["value"])
# {
# "collateral_usd": 12430.5,
# "debt_usd": 4820.0,
# "health_factor": 1.847
# }
cel.bind(pos, aave.getUserAccountData(user), ...) evaluates the contract call once and binds the result to pos. The rest of the expression reads named fields from pos. One network round-trip; three decoded numbers.
A health factor below 1.0 triggers liquidation. This is the kind of check you might poll on a cron job, feed into a Slack alert, or pass to an AI agent for interpretation.
Recipe 5: Read native ETH balance
Native ETH balance is not an ERC-20 method — it comes from the network itself. SEL handles it with solAddress(...).balance():
result = evmquery(
chain="evm_ethereum",
schema={"context": {"wallet": "sol_address"}},
expression="formatUnits(wallet.balance(), 18)",
context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
)
print(result["value"]) # ETH balance as a float
No contract address needed. The sol_address type exposes .balance() directly, which resolves to the address’s Wei balance. formatUnits(..., 18) converts to ETH.
How this compares to web3.py
The same USDC balance in web3.py, without error handling or proxy resolution:
from web3 import Web3
import json
w3 = Web3(Web3.HTTPProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"))
usdc = w3.eth.contract(
address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi=json.load(open("usdc_abi.json")),
)
balance = usdc.functions.balanceOf("0xd8dA6BF...").call() / 10 ** usdc.functions.decimals().call()
The evmquery version removes the ABI file, the separate RPC provider account, and the manual decimal scaling. It also handles proxy contracts transparently — if you point it at a proxy, it automatically resolves the implementation ABI.
web3.py makes sense when you are sending transactions, subscribing to events, or need fine control over the RPC layer. For read-only data work — price feeds, portfolio snapshots, position monitors, alert scripts — an expression-based layer removes a significant amount of infrastructure.
The developers overview lists all supported chains, the full SEL standard library, and the authentication options for production deployments.
What about AI agents and automation?
The five recipes above are synchronous one-off reads. Two natural extensions from here:
AI agents. The evmquery MCP server exposes the same expression language as a Model Context Protocol endpoint. Connect it to Claude or Cursor and the model can call execute_query directly from its context window — no Python code required from your side.
Scheduled automations. If you want these reads to trigger Slack alerts or feed into a workflow engine without writing Python, the n8n integration guide covers evmquery’s native n8n community node.
Next steps
- Get a free API key and run the ERC-20 balance recipe against your own wallet.
- Need to batch hundreds of reads efficiently at the protocol level? The Multicall3 guide explains the primitive evmquery uses under the hood.
- Building AI agents that read onchain state? The MCP server post covers the Claude and Cursor setup.
- The full expression reference and chain list are on the developers page.