# evmquery — Full Blog Corpus > Full Markdown of every published evmquery blog post, concatenated for LLM ingestion. Source index: https://evmquery.com/llms.txt Site: https://evmquery.com --- # Multi-Wallet ERC-20 Balance Scanning from TypeScript: No ABIs, No RPC, No Viem Source: https://evmquery.com/blog/erc20-balance-scan-rest-api-typescript Published: 2026-04-26 Author: evmquery team Category: guides Scan ERC-20 token balances for many wallets, across Ethereum, Base, and BNB Chain, using TypeScript fetch. No Viem, no ABI files, no RPC provider needed. Reading ERC-20 token balances sounds like a five-minute job. One contract, one wallet, one `balanceOf` call. But the jobs that actually ship to production almost never stay that small. A treasury monitor watching 20 wallets, a yield tracker checking six DeFi positions across three chains, a liquidation bot scanning 500 borrowers on every block. Each of those multiplies the baseline read by a factor that quickly swamps any public RPC endpoint. The standard fix is batching through Multicall3, but it still requires a web3 library, a provider account, and ABI management. There is a shorter path: one HTTP POST, a typed expression, and structured JSON back. No library installs, no provider keys, no ABI files. The evmquery REST API accepts a CEL expression, a contract address map, and optional context variables, then returns a typed result from the live chain. Scanning 50 wallets costs one HTTP call. Switch chains by changing one field. Works from any language with `fetch`; this guide uses plain TypeScript. [Get a free key](https://app.evmquery.com/onboarding?plan=free) to follow along. ## How the request body is structured Every query is a `POST` to `https://api.evmquery.com/api/v1/query` with an `x-api-key` header and this JSON shape: ```json { "chain": "evm_ethereum", "schema": { "contracts": { "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, "context": { "wallet": "sol_address" } }, "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, "expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())" } ``` Three concepts worth internalizing before you write any expressions: - **`schema.contracts`** maps a local name (`usdc`) to a deployed address. ABI resolution is automatic: evmquery fetches the verified source, reconstructs the interface, and exposes every public read method. You never manage an ABI file. - **`schema.context`** declares variable types. `sol_address` tells the engine this variable holds a single Ethereum address. `list` tells it you are passing a list. Declaring the wrong type causes a clear type error at evaluation time rather than a silent wrong result. - **`expression`** is CEL with a Solidity overlay. `formatUnits`, `parseUnits`, `solInt`, and `isZeroAddress` are built-in helpers. Arithmetic, list literals, ternary expressions, and the `map`, `filter`, `all`, and `exists` list macros work as expected. Supported chains: `evm_ethereum`, `evm_base`, `evm_bnb_mainnet`. ## Reading a single ERC-20 balance Start with the simplest case, using only the `fetch` global available in Node 18+: ```ts const API_KEY = process.env.EVMQUERY_API_KEY!; const ENDPOINT = "https://api.evmquery.com/api/v1/query"; async function usdcBalance(wallet: string): Promise { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, context: { wallet: "sol_address" }, }, context: { wallet }, expression: "formatUnits(usdc.balanceOf(wallet), usdc.decimals())", }), }); if (!res.ok) throw new Error(await res.text()); const { result } = await res.json(); return parseFloat(result); } // 5567.40 USDC console.log(await usdcBalance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")); ``` No `npm install`. The `result` field holds the return value; the response also includes the block number, on-chain call count, and credits consumed, which is useful for cost accounting and debugging. This runs on Node 18+ with no additional packages. Deno, Bun, and browser contexts use the same `fetch` call without modification. ## Scanning many wallets with `map` The real leverage comes from the `map` macro. Declare the context variable as `list`, pass an array, and the expression fans out across every address in a single HTTP request: ```ts async function usdcBalances(wallets: string[]): Promise { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, context: { wallets: "list" }, }, context: { wallets }, expression: "wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))", }), }); if (!res.ok) throw new Error(await res.text()); return (await res.json()).result as number[]; } const addresses = [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // 5,567.40 USDC "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503", // 0.01 USDC "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8", // 3.00 USDC ]; console.log(await usdcBalances(addresses)); // [5567.402493, 0.009929, 3] ``` One HTTP call. Three on-chain reads. Internally the engine batches the `balanceOf` calls the same way Multicall3 does — the batching logic is not your problem. Scale this to 100 or 500 wallets; the request shape does not change. One detail that bites people: the type declaration must match the runtime value. Passing an array but declaring `"sol_address"` (not `"list"`) in `schema.context` causes a type error at evaluation time. The fix is always the same. ## Filtering: wallets above a threshold Replace `map` with `filter` to get back only the addresses for which a predicate holds: ```ts const body = { chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, context: { wallets: "list" }, }, context: { wallets: addresses }, // Return only wallets holding more than 100 USDC expression: 'wallets.filter(w, usdc.balanceOf(w) > parseUnits("100", 6))', }; ``` `parseUnits("100", 6)` converts 100 USDC to its raw `uint256` representation (100,000,000). Applied to the three-wallet list above, the result is a single-element array containing only the wallet with 5,567 USDC. This pattern maps directly to [DeFi monitoring workflows](/for/developers): pass a list of 500 Aave borrowers, filter to those with a health factor below 1.05, and act on a far smaller set. One request does the work that used to require a loop and a threshold check in application code. ## Reading two tokens in one expression Declare two contracts in `schema.contracts` and return a list literal from the expression. Both calls execute in the same batched round: ```ts const body = { chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", }, context: { wallet: "sol_address" }, }, context: { wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, expression: "[formatUnits(usdc.balanceOf(wallet), 6), formatUnits(weth.balanceOf(wallet), 18)]", }; // result: [5567.402493, 0.0000001] ``` Two contract calls, one HTTP round trip, one response object. You can extend this to as many tokens as you need by adding entries to `schema.contracts` and expanding the list expression. ## Cross-chain: Ethereum, Base, and BNB Chain Each query targets exactly one chain. To compare the same wallet across chains, fire requests in parallel and correlate in your application: ```ts async function fetchBalance( chain: string, contract: string, wallet: string, decimals: number, ): Promise { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ chain, schema: { contracts: { token: contract }, context: { wallet: "sol_address" }, }, context: { wallet }, expression: `formatUnits(token.balanceOf(wallet), ${decimals})`, }), }); if (!res.ok) throw new Error(await res.text()); return parseFloat((await res.json()).result); } const ETH_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const BNB_USDT = "0x55d398326f99059fF775485246999027B3197955"; // 18 decimals on BNB const wallet = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; const [ethBal, baseBal, bnbBal] = await Promise.all([ fetchBalance("evm_ethereum", ETH_USDC, wallet, 6), fetchBalance("evm_base", BASE_USDC, wallet, 6), fetchBalance("evm_bnb_mainnet", BNB_USDT, wallet, 18), ]); console.log(`Ethereum: ${ethBal.toFixed(2)} USDC`); // 5,567.40 console.log(`Base: ${baseBal.toFixed(2)} USDC`); // 44.48 console.log(`BNB: ${bnbBal.toFixed(2)} USDT`); // 1,190.97 ``` Note that USDC's contract address differs by chain. On BNB Chain the dominant stablecoin is USDT at `0x55d3...b955` with 18 decimals, unlike the 6 decimals used on Ethereum and Base. ## A complete TypeScript balance monitor Here is a self-contained script that reads stablecoin balances for a watchlist across two chains and writes the result to disk. Copy it into a `.ts` file, set `EVMQUERY_API_KEY`, and run with `tsx` or `ts-node`: ```ts const API_KEY = process.env.EVMQUERY_API_KEY!; const ENDPOINT = "https://api.evmquery.com/api/v1/query"; const WATCHLIST = [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503", "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8", ]; async function scanBalances( chain: string, contract: string, wallets: string[], ): Promise { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ chain, schema: { contracts: { token: contract }, context: { wallets: "list" }, }, context: { wallets }, expression: "wallets.map(w, formatUnits(token.balanceOf(w), token.decimals()))", }), }); if (!res.ok) throw new Error(await res.text()); return (await res.json()).result as number[]; } const [ethBalances, baseBalances] = await Promise.all([ scanBalances("evm_ethereum", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", WATCHLIST), scanBalances("evm_base", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", WATCHLIST), ]); const report = WATCHLIST.map((address, i) => ({ address, usdc_ethereum: ethBalances[i], usdc_base: baseBalances[i], })); console.table(report); writeFileSync("balances.json", JSON.stringify(report, null, 2)); ``` Two parallel requests, three wallets each, six on-chain reads total. No ABI files, no provider configuration, no web3 library in `node_modules`. The output is a typed array you can log, push to a database, or forward to a webhook. If you need the same pattern in Python, the [Python REST API guide](/blog/query-evm-contract-data-python/) uses an identical request shape with `requests.post`. ## Credits and rate limits The free tier includes 2,000 credits per month. A `map` across any number of wallets costs `totalCalls + totalRounds` credits. A single batched `balanceOf` scan costs 2 credits (1 call, 1 round), regardless of how many wallets are in the list. The filter examples each consumed 1 credit because the engine folded them into one optimized round. For applications that poll frequently, the credit model rewards batching. Scanning 50 wallets in one request is no more expensive than scanning one. ## Next steps - [Multicall3: batching EVM contract reads with Viem, Ethers, and Wagmi](/blog/multicall3-batching-evm-contract-reads/) — the library-based approach and when it still makes sense - [Query EVM contract data from Python](/blog/query-evm-contract-data-python/) — same REST API, Python syntax - [Monitor DeFi positions and trigger alerts in Python](/blog/blockchain-monitoring-python-evmquery/) — applying this pattern to Aave health factors and liquidation monitoring - [Get a free key and run your first scan](https://app.evmquery.com/onboarding?plan=free) — 2,000 credits included, no card required --- # Blockchain Monitoring in Python: Poll EVM Contract State Without the Boilerplate Source: https://evmquery.com/blog/blockchain-monitoring-python-evmquery Published: 2026-04-25 Author: evmquery team Category: guides How to write a Python script that polls EVM contract state (Aave health factors, ERC-20 balances, multi-wallet checks) using evmquery's REST API. No ABIs, no ABI decoders, no Multicall3 setup. Most Python blockchain monitoring scripts start the same way. You want one number, typically a wallet balance, a health factor, or a pool reserve, on a schedule, with an alert if it crosses a threshold. You end up with 80 lines of RPC plumbing before you touch the alert logic. This guide uses evmquery's REST API instead. Send a named contract address and a CEL expression, get the decoded result back. The multi-wallet `.map()` and `.filter()` macros batch internally. A working Aave health-factor monitor that checks a wallet list every minute comes out to roughly 45 lines of Python. `POST https://api.evmquery.com/api/v1/query` takes a chain, a named contract, and a CEL expression. Python gets a decoded result back: no ABI files, no proxy detection, no Multicall3 setup. The `.filter()` macro turns a multi-wallet at-risk check into a single request. ## Why raw RPC breaks down for polling The standard approach for reading contract state from Python is `web3.py` against a JSON-RPC endpoint. That works for one-off scripts. It fights you when you want a production monitor: - **Multiple wallets, one call.** Multicall3 batching means constructing calldata, encoding ABI selectors, bundling into the `aggregate3` struct, and decoding bytes responses. Each contract type needs its own encoder/decoder pair. - **Proxy contracts.** Aave Pool, Uniswap v4's pool manager, most of DeFi sit behind EIP-1967 proxies. Your ABI needs to be the _implementation's_, which means a second `eth_call` to `implementation()` before you can start. - **Typed results.** `web3.py` decodes a `(uint256, uint256, uint256, uint256, uint256, int256)` tuple correctly, but you still need to know which slot is `healthFactor`, then scale it from `1e18` fixed-point. For a monitor running every 60 seconds against 10 wallets and two chains, "solvable but tedious" compounds fast. The boilerplate dwarfs the logic. ## The evmquery REST API evmquery exposes a single endpoint for contract reads: ```bash POST https://api.evmquery.com/api/v1/query x-api-key: ``` The JSON body has three fields: | Field | Type | Purpose | |---|---|---| | `chain` | string | `evm_ethereum`, `evm_base`, or `evm_bnb_mainnet` | | `schema` | object | `contracts` (name → address map) + optional `context` (typed variable declarations) | | `expression` | string | A CEL expression over the named contracts and context variables | The optional top-level `context` field pairs with `schema.context`: declare types there, pass runtime values here. That's the whole API surface. Grab an API key from the [evmquery dashboard](https://app.evmquery.com/onboarding?plan=free); the free tier gives you 2,000 credits/month. ## Your first contract read ```bash pip install httpx ``` A thin wrapper around the endpoint, then a USDC balance check: ```python import os import httpx API_KEY = os.environ["EVMQUERY_API_KEY"] ENDPOINT = "https://api.evmquery.com/api/v1/query" def execute( chain: str, contracts: dict, expression: str, context_types: dict | None = None, context_values: dict | None = None, ): schema = {"contracts": contracts} if context_types: schema["context"] = context_types body = {"chain": chain, "schema": schema, "expression": expression} if context_values: body["context"] = context_values resp = httpx.post(ENDPOINT, headers={"x-api-key": API_KEY}, json=body, timeout=10) resp.raise_for_status() return resp.json()["result"] # Single-wallet USDC balance balance = execute( chain="evm_ethereum", contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, expression="formatUnits(usdc.balanceOf(wallet), usdc.decimals())", context_types={"wallet": "sol_address"}, context_values={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}, ) print(balance) # → 5567.402493 ``` `formatUnits` scales the raw `uint256` by the token's decimals. `usdc.decimals()` is a second on-chain read; both calls are batched automatically. The result is a plain Python float. ## Multi-wallet balance checks with `.map()` The CEL `.map()` macro applies an expression to every element of a list. evmquery batches all the resulting contract calls via Multicall3 internally, so checking 20 wallets is one HTTP request, not 20. ```python WALLETS = [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5", "0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97", ] balances = execute( chain="evm_ethereum", contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, expression="wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))", context_types={"wallets": "list"}, context_values={"wallets": WALLETS}, ) # → [5567.402493, 1510.424957, 343.649519] ``` The result list aligns with the input list: `balances[i]` is the balance for `WALLETS[i]`. If you've built a Multicall3 wrapper before, this is what it looks like once the plumbing is gone. This same `execute()` helper works as a LangChain tool or an OpenAI function-calling wrapper. Wrap it, give it a docstring, and your AI agent can read any EVM contract state on demand without fetching ABIs or managing RPC connections. ## Aave health factor alerts with `.filter()` Health factor below `1.0` means liquidation. Below `1.2` means you're close. `.filter()` returns only the list elements that match a predicate, so you get back only the wallets that need attention. ```python AAVE_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" def wallets_at_risk(wallets: list[str], threshold: float = 1.2) -> list[str]: """Return addresses whose Aave v3 health factor is below `threshold`.""" threshold_scaled = str(int(threshold * 10**18)) return execute( chain="evm_ethereum", contracts={"aave": AAVE_POOL}, expression=f"wallets.filter(w, aave.getUserAccountData(w).healthFactor < parseUnits('{threshold}', 18))", context_types={"wallets": "list"}, context_values={"wallets": wallets}, ) ``` `getUserAccountData` returns a struct with six fields. The expression drills into `.healthFactor` directly; evmquery resolves the proxy to the Aave Pool implementation and decodes the struct automatically. Here is the complete monitor loop: ```python import time def send_alert(wallets: list[str]) -> None: import httpx httpx.post( os.environ["SLACK_WEBHOOK"], json={"text": f":warning: Aave health factor < 1.2 on Ethereum:\n" + "\n".join(wallets)}, ) def monitor(wallets: list[str], interval_seconds: int = 60) -> None: print(f"Monitoring {len(wallets)} wallet(s) every {interval_seconds}s…") while True: at_risk = wallets_at_risk(wallets) if at_risk: send_alert(at_risk) print(f"Alert sent for {len(at_risk)} wallet(s).") else: print("All positions safe.") time.sleep(interval_seconds) if __name__ == "__main__": monitor(WALLETS) ``` One request per interval, one decoded list, conditional alert. That is the entire monitoring script. ## Cross-chain checks on Ethereum, Base, and BNB Smart Chain evmquery supports Ethereum, Base, and BNB Smart Chain. A user who runs positions on multiple networks needs two or three calls, not one per chain (cross-chain expressions in a single call are not supported yet; each call targets one chain). ```python AAVE_POOLS = { "evm_ethereum": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", "evm_base": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", } def health_factor(wallet: str, chain: str, pool_address: str) -> float: return float(execute( chain=chain, contracts={"aave": pool_address}, expression="formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)", context_types={"wallet": "sol_address"}, context_values={"wallet": wallet}, )) wallet = "0xYourWallet" for chain, pool in AAVE_POOLS.items(): hf = health_factor(wallet, chain, pool) label = chain.replace("evm_", "") print(f"{label}: {hf:.4f}") ``` Two sequential calls, both under 50ms each typically. Fire them concurrently with `asyncio.gather` if you're polling many wallets across all chains and latency matters. If you're building this for [automation workflows](/for/automation), the same expressions that run in Python run in the n8n community node, so a working Python prototype translates directly to a no-code workflow. ## Scheduling the monitor **Cron (simplest, on any Linux host):** ```bash # Run every 5 minutes; API key in environment */5 * * * * EVMQUERY_API_KEY=evq_... SLACK_WEBHOOK=https://... python3 /opt/monitor.py ``` **GitHub Actions (no server required):** ```yaml name: Aave Health Monitor on: schedule: - cron: "*/15 * * * *" # every 15 minutes jobs: monitor: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pip install httpx && python monitor.py env: EVMQUERY_API_KEY: ${{ secrets.EVMQUERY_API_KEY }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} ``` GitHub Actions cron has a minimum granularity of one minute and runs for free within the included quota. The `secrets` store keeps the API key out of the repository. The free tier is 2,000 credits/month. A `.filter()` call over 10 wallets costs 1 credit when no wallet matches (short-circuit) or up to 11 credits when all match. At 15-minute intervals that's roughly 3,000-7,000 credits/month depending on how often positions are at risk; budget for a paid plan if you poll frequently or watch many wallets. ## From Python script to REST call The full flow for any new contract is the same three steps: 1. **Name the contract** in `schema.contracts`, using a short key. 2. **Write the expression**: call methods by name, chain helpers like `formatUnits`, use `.map()` or `.filter()` for lists. 3. **Declare context variables** in `schema.context` if the expression is parameterized, and pass values in the top-level `context` field. For unfamiliar contracts, the `describe_schema` tool in the [evmquery MCP server](/blog/evm-blockchain-mcp-server) lists every callable `view`/`pure` method with parameter types. Run it in Claude Code before you write the expression and you'll know exactly what's available. ## Next steps - Want the same expressions in a no-code workflow? [Read smart contracts in n8n](/blog/read-smart-contracts-in-n8n) covers the community node and three paste-in recipes. - Building for an AI assistant instead of a script? [The MCP server guide](/blog/evm-blockchain-mcp-server) wires evmquery directly into Claude, Cursor, and VS Code. - Comparing this approach to raw Alchemy or QuickNode RPC? [The Moralis / Alchemy / QuickNode comparison](/blog/moralis-alchemy-quicknode-evmquery-comparison) maps out which layer fits which use case. - More details on the query engine and supported chains: [evmquery for developers](/for/developers). --- # Live Onchain Data in LangChain: Build a Custom EVM Blockchain Tool in 30 Lines Source: https://evmquery.com/blog/langchain-evm-blockchain-tool Published: 2026-04-25 Author: evmquery team Category: integrations Define a custom LangChain tool that queries live EVM smart contract data — USDC balances, ETH prices, Aave positions — without ABIs, RPC nodes, or web3.py boilerplate. LangChain's built-in blockchain integrations stop at Etherscan transaction history and NFT metadata, both of which require an Alchemy API key and a specific loader class per data type. For reading arbitrary contract view functions in an agent workflow, none of that helps. You would need to wire in web3.py or a raw RPC provider, write ABI files, handle decimal scaling, and manage connection state. None of that has anything to do with the agent you are actually trying to build. The better path: define one `@tool` that wraps evmquery's REST API. The tool accepts a chain identifier, a named contract map, and a CEL expression. evmquery resolves the ABI automatically, executes the expression on the live chain, and returns a decoded human-readable value. Your LangChain tool stays under 30 lines. `pip install langchain-core langchain-anthropic langgraph requests`, decorate a function with `@tool`, and POST to `https://api.evmquery.com/api/v1/query`. Any LangChain agent can then read live USDC balances, ETH prices from Chainlink, or Aave health factors on Ethereum, Base, or BNB. No ABIs, no RPC node, no web3.py. Free tier: 2,000 credits/month. ## How LangChain tools work LangChain tools are Python functions decorated with `@tool` from `langchain_core.tools`. The decorator reads the docstring and type annotations to generate the tool's name, description, and parameter schema automatically. When a model decides it needs external data, it emits a structured tool call; LangChain validates the arguments, calls the function, and feeds the result back to the model. For blockchain queries this is the right primitive. The model knows it cannot answer "what is my USDC balance right now?" from training data. That number changes every block and was never in any corpus. A properly scoped tool gives the model a path to the real answer without you predicting every possible query in application code. LangChain's `BlockchainDocumentLoader` was designed for a different job: loading NFT metadata into a vector store for retrieval. It requires Alchemy, only supports a fixed set of schemas, and returns documents, not structured values. This tool fills the gap for any contract read at runtime. ## What you'll build A single `evmquery_read` tool that any LangChain agent or chain can invoke. The tool takes a chain, a contract map, a CEL expression, and optional context variables. It calls the evmquery REST API and returns the decoded result and the block number it was read from. If you are building an AI-powered DeFi dashboard, a portfolio monitor, or any agent that needs current contract state, this is the integration surface. If you want to query the chain directly from Claude Desktop or Cursor without writing any code, the [evmquery MCP server](/blog/evm-blockchain-mcp-server/) is the faster path. ## Setup ```bash pip install langchain-core langchain-anthropic langgraph requests ``` ```bash export ANTHROPIC_API_KEY=sk-ant-... export EVMQUERY_API_KEY=eq_... ``` Get a free evmquery key at `https://app.evmquery.com/onboarding?plan=free`. The free tier covers 2,000 credits per month, roughly 1,000 typical contract reads. ## Defining the evmquery tool ```python import os from typing import Optional import requests from langchain_core.tools import tool EVMQUERY_API = "https://api.evmquery.com/api/v1/query" @tool def evmquery_read( chain: str, contracts: dict, expression: str, context: Optional[dict] = None, ) -> str: """Read live data from an EVM smart contract. Use for current token balances, DeFi positions, oracle prices, or any contract view function on Ethereum, Base, or BNB Smart Chain. Supported chains: evm_ethereum, evm_base, evm_bnb_mainnet. Do NOT use for historical data or event logs. Args: chain: Chain identifier — evm_ethereum, evm_base, or evm_bnb_mainnet. contracts: Mapping of short name to 0x contract address. Example: {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"} expression: CEL expression to evaluate. Contract names become variables. Example: "formatUnits(usdc.balanceOf(wallet), usdc.decimals())" context: Optional runtime values for wallet addresses used in the expression. Pass a list of addresses for multi-wallet expressions. Example: {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"} """ def _type(v) -> str: return "list" if isinstance(v, list) else "sol_address" body: dict = { "chain": chain, "schema": {"contracts": contracts}, "expression": expression, } if context: body["schema"]["context"] = {k: _type(v) for k, v in context.items()} body["context"] = context resp = requests.post( EVMQUERY_API, json=body, headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, timeout=10, ) resp.raise_for_status() data = resp.json() return f"{data['result']} (block {data.get('blockNumber', '?')})" ``` Three things worth noting: - **The docstring is the schema.** LangChain reads it to build the description the model sees when deciding whether to call the tool. Specific scope constraints ("Do NOT use for historical data") reduce hallucinated or misrouted calls. - **Type inference for context variables.** evmquery needs to know whether a context value is a single address (`sol_address`) or a list (`list`). The `_type` helper infers this from the Python value so the model does not have to know about evmquery's type system. - **`resp.raise_for_status()`** propagates HTTP errors into LangChain's error handling pipeline. The agent receives the error and can either retry with a reformulated call or surface a clear message to the user. ## Three live recipes All expressions below were validated against the live chain before publication. ### ERC-20 balance check ```python evmquery_read.invoke({ "chain": "evm_ethereum", "contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())", "context": {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}, }) # → "5567.402493 (block 24957481)" ``` `formatUnits` reads `decimals()` from the contract itself, so the scaling is always correct regardless of whether the token uses 6, 8, or 18 decimal places. ### Live ETH/USD price via Chainlink ```python evmquery_read.invoke({ "chain": "evm_ethereum", "contracts": {"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"}, "expression": "formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())", "context": {}, }) # → "2314.77808628 (block 24957477)" ``` No context variables needed. This is a pure contract read with no wallet parameter. The address is the canonical Chainlink ETH/USD aggregator on Ethereum mainnet. ### Multi-wallet balance scan ```python evmquery_read.invoke({ "chain": "evm_ethereum", "contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "expression": "wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))", "context": { "wallets": [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8", "0x40B38765696e3d5d8d9d834D8AaD4bB6e418E489", ] }, }) # → "[5567.402493, 3.0, 10.005273] (block 24957481)" ``` Passing a list to `context` triggers the `list` type automatically. The CEL `map` macro iterates over the list and returns results in order. One RPC round-trip for all three wallets, not three separate tool calls. ## Wiring into a LangChain agent With the tool defined, assembling an agent takes four lines: ```python from langchain_anthropic import ChatAnthropic from langgraph.prebuilt import create_react_agent model = ChatAnthropic(model="claude-sonnet-4-6") agent = create_react_agent( model, tools=[evmquery_read], state_modifier=( "You are a blockchain data assistant. When users ask about token balances, " "DeFi positions, oracle prices, or any current contract state, call " "evmquery_read to fetch live data before answering. Always include the " "block number so the user knows the result is current." ), ) ``` Invoke it with a natural language prompt: ```python response = agent.invoke({ "messages": [ { "role": "user", "content": ( "What is the USDC balance of " "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum?" ), } ] }) print(response["messages"][-1].content) # The wallet holds 5,567.40 USDC as of block 24,957,481. ``` The model sees the tool description, recognises the query requires live data, emits a structured tool call, receives the result, and incorporates it into a natural language reply. You do not wire up the routing logic. The model handles that from the description you provided. Pass additional tools alongside `evmquery_read` in the list. The model will route to the right one based on the descriptions. For example, add a portfolio calculation tool or a notification sender. The chain-read tool and the application-logic tool stay cleanly separated. ## Struct results and Aave health factors evmquery expressions can return structured values, not just scalars. Aave's `getUserAccountData` method returns a six-field struct. You can read the full position in one call or pull a single field: ```python evmquery_read.invoke({ "chain": "evm_ethereum", "contracts": {"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"}, "expression": "formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)", "context": {"wallet": "0x..."}, }) ``` The `healthFactor` field represents how far above the liquidation threshold the position sits. A value above 1.0 is safe; below 1.0 triggers liquidation. For wallets with no active borrow position, evmquery returns the maximum `uint256` value, effectively infinite health indicating no debt. The model will interpret this correctly from context in the system prompt. For wallets with active positions, the returned value is a decimal like `1.43`, which the model can flag as healthy, borderline, or at risk depending on the threshold you define in the prompt. Developers building DeFi tooling for AI agents can find more expression patterns, including multi-wallet balance filters, reserve data reads, and protocol-level aggregations, in the [developer resources](/for/developers). If you are focused on AI agent workflows specifically, the [AI users page](/for/ai-users) covers the REST tool and MCP surfaces side by side. ## LangChain tool vs MCP: picking the right surface | | LangChain tool (this post) | MCP server | |---|---|---| | Language | Python | Any MCP client | | Setup | 30 lines in your agent file | Paste one config block | | Control | Full: schema, error handling, logging | Client manages the conversation | | Multi-tool agents | Yes, composable with other tools | Limited to the MCP surface | | Best for | Production agents, custom backends | Claude Desktop, Cursor, VS Code | If you are building a Python agent with custom business logic, the `@tool` approach gives full control over the schema, error messages, and how results are formatted before the model sees them. If you want to query the chain interactively from your IDE without writing code, the [evmquery MCP server guide](/blog/evm-blockchain-mcp-server/) gets you there in under five minutes. ## Next steps - [Set up the evmquery MCP server in Claude Desktop and Cursor](/blog/evm-blockchain-mcp-server/), no code required - [Add a live EVM tool to the Vercel AI SDK in TypeScript](/blog/vercel-ai-sdk-blockchain-tool/) - [Monitor Aave health factors with a Python polling script](/blog/blockchain-monitoring-python-evmquery/) - [Browse the evmquery REST API docs](https://app.evmquery.com/api/docs) for multi-wallet macros, list filtering, and the full expression reference --- # Live Onchain Data in the OpenAI Agents SDK: Add an EVM Blockchain Tool Source: https://evmquery.com/blog/openai-agents-sdk-evm-blockchain-tool Published: 2026-04-25 Author: evmquery team Category: integrations Wire the OpenAI Agents SDK to live EVM smart contract data with one @function_tool. Reads USDC balances, Chainlink prices, and Aave positions on Ethereum, Base, or BNB. No ABIs needed. The OpenAI Agents SDK turns a Python function into an agent tool with one decorator. The model still arrives blind to the chain: ask it for the current USDC balance of any wallet or the live ETH/USD price and it will pull from training data rather than read the actual value. Onchain state changes every block. Training snapshots are months stale. One `@function_tool` wrapper fixes that. POST a chain identifier, a named contract map, and a CEL expression to evmquery, and the agent gets back a live decoded value from the chain. No ABIs, no RPC node, no web3 boilerplate. The whole tool fits in 25 lines. `pip install openai-agents requests`, decorate a function with `@function_tool`, and POST to `https://api.evmquery.com/api/v1/query`. Your agent reads live USDC balances, ETH prices from Chainlink, and Aave health factors on Ethereum, Base, or BNB Smart Chain. Free tier: 2,000 credits/month. ## The OpenAI Agents SDK in one paragraph OpenAI released the Agents SDK in 2025 as a minimal Python framework for agent loops. An `Agent` holds a model name, a system prompt, and a list of tools. `Runner.run()` drives the loop: call the model, execute any tool calls it requests, feed the results back, repeat until the model produces a final response. There is no hidden orchestration, no graph to define, and no state machine. The loop is transparent and async by default. Tools are registered with the `@function_tool` decorator. It reads your type hints to generate the JSON schema the model receives, and reads your docstring for the tool description. The model uses both when deciding whether and how to invoke the function. A vague docstring produces bad calls. A tight docstring with concrete examples produces correct ones. ## Install and configure ```bash pip install openai-agents requests ``` Two environment variables are required: - `OPENAI_API_KEY`: your OpenAI key - `EVMQUERY_API_KEY`: your evmquery key. The free tier gives 2,000 credits per month with no credit card required. A single contract read costs 2 to 3 credits, so the free tier is enough to prototype comfortably. [Grab a key here](https://app.evmquery.com/onboarding?plan=free). ## The evmquery tool in 25 lines ```python import os import requests from agents import function_tool EVMQUERY_URL = "https://api.evmquery.com/api/v1/query" @function_tool def query_evm( chain: str, contracts: dict[str, str], expression: str, ) -> str: """ Read live state from EVM smart contracts. chain: Chain to query. One of: evm_ethereum, evm_base, evm_bnb_mainnet. contracts: Named contract map. Keys become variable names in the expression; values are the contract's 0x address. expression: CEL expression to evaluate. Use formatUnits() for decimal scaling. Embed wallet addresses with solAddress('0x...'). Examples: formatUnits(usdc.balanceOf(solAddress('0x...')), usdc.decimals()) formatUnits(feed.latestAnswer(), feed.decimals()) """ resp = requests.post( EVMQUERY_URL, headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": chain, "schema": {"contracts": contracts}, "expression": expression, }, timeout=30, ) resp.raise_for_status() return str(resp.json().get("result", resp.text)) ``` Three things worth unpacking. **The docstring is load-bearing.** The model reads it to learn what `chain`, `contracts`, and `expression` mean. The expression examples teach the correct CEL syntax so the model does not have to guess. Without them, the model commonly tries Python-style method calls (`usdc.balanceOf(wallet)` with a bare string) and gets a type error back. With them, it writes correct expressions on the first attempt. **No ABI needed.** evmquery resolves contract ABIs from verified on-chain sources, including Etherscan and Sourcify, and falls back to bytecode signature matching when the source is not available. You name the contract; the resolution is handled server-side. **No proxy headaches.** A large fraction of production contracts sit behind EIP-1967 upgradeable proxies. A naive `eth_call` to a proxy hits the empty fallback. evmquery detects the proxy, resolves the implementation, and decodes against the right ABI automatically. Your expression targets implementation methods directly. ## Wire it into an agent ```python import asyncio from agents import Agent, Runner agent = Agent( name="chain-reader", model="gpt-4o", instructions=( "You have live EVM blockchain access via query_evm. " "Always call the tool when asked about token balances, DeFi positions, " "or on-chain prices. Never answer from training data. " "Key contracts: " "USDC on Ethereum: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. " "Chainlink ETH/USD on Ethereum: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419. " "Aave V3 Pool on Ethereum: 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2." ), tools=[query_evm], ) async def main() -> None: result = await Runner.run( agent, "What is the current ETH price from Chainlink on Ethereum?" ) print(result.final_output) asyncio.run(main()) ``` The `instructions` field does two jobs. It tells the agent when to call the tool (always, not from training data) and it carries the canonical contract addresses so the model does not hallucinate them. Embedding addresses in the system prompt is intentional: letting the model look them up adds a failure mode; hardcoding them removes it. For production use, pull the contract map from a config file or environment variable rather than a hardcoded string. The agent logic stays the same; the instructions string is just a string. ## Three queries that prove it works ### ETH/USD from Chainlink ``` "What is the current ETH price from Chainlink on Ethereum?" ``` The model calls `query_evm` with `chain: evm_ethereum`, `contracts: {"feed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"}`, and `expression: formatUnits(feed.latestAnswer(), feed.decimals())`. At time of writing the return value is `2316.74`. One round trip, two credits. ### USDC balance for a wallet ``` "What is vitalik.eth's USDC balance on Ethereum?" ``` The model writes `formatUnits(usdc.balanceOf(solAddress('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')), usdc.decimals())`. Returns `5567.40`. The `solAddress()` helper wraps the string in the correct on-chain address type; the model learns this from the docstring example. ### Aave V3 health factor ``` "Is the Aave health factor for 0xd8dA…6045 safe on Ethereum?" ``` The model writes `formatUnits(aave.getUserAccountData(solAddress('0xd8dA...6045')).healthFactor, 18)` and calls `query_evm`. For Aave positions with no active borrowing, `healthFactor` returns `2^256 / 1e18`, approximately `1.16e59`. That is not an error. It means no borrowed balance, so no liquidation risk. A value below `1.0` means the position is underwater. Add an explicit note in your `instructions` so the model surfaces this interpretation to the user rather than printing a confusing raw float. ## Multi-wallet scans with the map macro The evmquery expression language includes a `map` macro for list operations. For multi-wallet queries, this means one tool call instead of N sequential calls, and one Multicall3 round trip on the node side. If a user asks for USDC balances across several wallets on Base, the model calls `query_evm` with: ``` chain: evm_base contracts: {"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"} expression: [solAddress('0xWallet1'), solAddress('0xWallet2')].map(w, formatUnits(usdc.balanceOf(w), usdc.decimals())) ``` The result is a JSON array of balances, one per wallet. Credit cost stays flat regardless of wallet count — the reads are batched server-side. Left to its own devices the model defaults to sequential single calls when given a list of wallets. If wallet count matters for cost or latency, prompt the model explicitly to use the list form. Adding "Batch multi-wallet reads using the list.map() expression pattern" to the instructions is enough. ## What the tool cannot do Three hard constraints to communicate before shipping to users. **No writes.** `query_evm` is read-only by design. evmquery never broadcasts transactions on your behalf. For anything that requires signing, add a separate wallet tool with an explicit user confirmation step. **No event history.** The tool reads current state at the block the query lands on. For historical data ("what was this balance six months ago?"), you need an indexer rather than a read layer. The [blockchain indexer guide](/blog/blockchain-indexer-guide-the-graph-vs-query-layer) covers when to use each. **No off-chain data.** NFT floor prices on marketplaces, token prices on CEXes, and any other data not published to an on-chain feed live outside what `query_evm` can reach. Adding "Do not invent off-chain data" to `instructions` ensures the model declines gracefully instead of fabricating a number. ## Next steps - [For developers](/for/developers): the REST API this tool wraps is the same engine behind the evmquery MCP server and n8n node. One query language, every integration surface. - Already using Claude or Cursor? The [EVM blockchain MCP server guide](/blog/evm-blockchain-mcp-server) is a zero-code alternative to wiring up your own tool. - Using LangChain instead of the OpenAI SDK? The [LangChain EVM blockchain tool post](/blog/langchain-evm-blockchain-tool) covers the same pattern with `@tool` and LangGraph. - Prefer no-code automation? The [/for/ai-users](/for/ai-users) page has a checklist for connecting evmquery to Claude Desktop, Cursor, and other clients. --- # Live Onchain Data in the Vercel AI SDK: Add an EVM Blockchain Tool Source: https://evmquery.com/blog/vercel-ai-sdk-blockchain-tool Published: 2026-04-25 Author: evmquery team Category: integrations Define a custom evmquery tool in the Vercel AI SDK and give your AI app live access to USDC balances, Aave positions, and any EVM contract read on Ethereum, Base, or BNB. The Vercel AI SDK makes it straightforward to build AI-powered apps in TypeScript, but the model arrives blind to the chain. Ask it for your current USDC balance and it will either fabricate a number from training data or refuse. That data lives on-chain, changes every block, and was never in any training corpus. Without a live tool, the model can't help. Tool calling fixes this. Define a function, describe its inputs to the model, and the SDK handles routing, calls, and result injection. This post walks through adding a single `evmquery_read` tool to the Vercel AI SDK so any prompt about onchain state returns a live, decoded result from the chain. Install the `ai` package, define a `tool()` that POSTs to `https://api.evmquery.com/api/v1/query`, pass it to `streamText()`, and your chat handler can answer questions about USDC balances, Aave positions, and any EVM contract view function. The free tier gives 2,000 credits/month, no credit card needed. ## How tool calling works in the Vercel AI SDK The AI SDK ships `streamText()` and `generateText()` with a `tools` option. You pass it a record of named tools; each tool has a `description`, a Zod `parameters` schema, and an `execute` function. When the model decides it needs external data to answer a prompt, it emits a tool call with arguments that match your schema. The SDK validates those arguments, calls `execute`, and feeds the result back to the model with no manual parsing or JSON wrangling. The model receives the structured result and incorporates it into its reply. For onchain queries, this is the right primitive. The model already knows when it needs external data (balance checks, price reads, position health) and when it doesn't (explaining how Uniswap V3 works). You do not have to hard-code that decision in your application logic. ## What you'll build A TypeScript handler with one tool: `evmquery_read`. The tool takes a chain identifier, a named contract address map, a CEL expression, and optional context variables. It calls evmquery's REST API and returns the decoded result. The model decides when to invoke it and how to present the answer. The handler works in a Next.js App Router route, an Express endpoint, or a plain Node.js script. There is no framework dependency beyond the `ai` package and a provider adapter. If you are building a product that needs programmatic access to chain data, continue here. If you want to query the chain directly from Claude Desktop or Cursor without writing any code, the [evmquery MCP server](/blog/evm-blockchain-mcp-server/) is the faster path. ## Project setup Start from any TypeScript project. Node 18 or later is required for the native `fetch` API. ```bash npm install ai @ai-sdk/anthropic zod ``` Set two environment variables. The Anthropic adapter is used here; any AI SDK-compatible provider works. ```bash ANTHROPIC_API_KEY=sk-ant-... EVMQUERY_API_KEY=eq_... ``` Get your free evmquery key at `https://app.evmquery.com/onboarding?plan=free`. The free tier covers 2,000 credits per month, which is roughly 1,000 typical contract reads. ## Defining the evmquery tool The evmquery REST API accepts a single POST body: a chain, a named contract map, a CEL expression, and optional typed context variables. The response includes a `result` field with the decoded, human-readable value. Here is the full tool definition: ```ts const EVMQUERY_API = "https://api.evmquery.com/api/v1/query"; export const evmqueryReadTool = tool({ description: "Read live data from an EVM smart contract. Use for current token balances, " + "DeFi positions, pool state, or any contract view function on Ethereum, Base, " + "or BNB Smart Chain. Do not use for historical data or event logs. " + "Supported chains: evm_ethereum, evm_base, evm_bnb_mainnet.", parameters: z.object({ chain: z .enum(["evm_ethereum", "evm_base", "evm_bnb_mainnet"]) .describe("Chain to query"), contracts: z .record(z.string()) .describe( "Named contract addresses. Key is the short name used in the expression, " + "value is the 0x address. Example: { usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' }" ), expression: z .string() .describe( "CEL expression to evaluate. Named contracts become variables. " + "formatUnits(usdc.balanceOf(wallet), usdc.decimals()) returns a human-readable balance." ), context: z .record(z.string()) .optional() .describe( "Runtime values for wallet addresses or other parameters used in the expression. " + "Example: { wallet: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' }" ), }), execute: async ({ chain, contracts, expression, context }) => { const body: Record = { chain, schema: { contracts }, expression, }; if (context && Object.keys(context).length > 0) { const contextTypes = Object.fromEntries( Object.keys(context).map((k) => [k, "sol_address"]) ); body.schema = { contracts, context: contextTypes }; body.context = context; } const res = await fetch(EVMQUERY_API, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.EVMQUERY_API_KEY!, }, body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text(); throw new Error(`evmquery ${res.status}: ${text}`); } const data = await res.json(); return { result: data.result, block: data.blockNumber }; }, }); ``` A few design choices worth noting: - **Context types are fixed to `sol_address`** for simplicity. The model passes wallet addresses; the tool types them correctly without exposing evmquery's type system to the model. - **The description is specific about scope.** Telling the model what the tool does _not_ do (historical data, event logs) prevents misrouting and hallucinated tool calls. - **The expression is passed through verbatim.** The model constructs the CEL expression from the contract name and the method it wants to call. The `describe_schema` endpoint is available if you want to let the model introspect available methods first, useful for unknown or user-supplied contracts. ## Wiring into a streaming chat handler With the tool defined, the chat handler is four lines of logic: ```ts export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: anthropic("claude-sonnet-4-6"), system: "You are a blockchain data assistant. When the user asks about token balances, " + "DeFi positions, pool prices, or any current contract state, call the " + "evmquery_read tool to fetch live data before answering. " + "Always include the block number in your reply so the user knows the result is current.", messages, tools: { evmquery_read: evmqueryReadTool }, maxSteps: 3, }); return result.toDataStreamResponse(); } ``` `maxSteps: 3` allows up to three tool round-trips per response. Most single-contract reads take one step. If the user asks a multi-contract question, the model may chain calls or batch them depending on what the expression supports. Drop the `POST` handler into `app/api/chat/route.ts`. Pair it with the AI SDK's `useChat` hook on the client side and you have a full streaming chat UI with live onchain data in under 50 lines total. ## Two live examples All expressions below were validated against the live chain before publication. **USDC balance on Ethereum** Prompt: *"What is the USDC balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum?"* The model emits this tool call: ```json { "chain": "evm_ethereum", "contracts": { "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, "expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())", "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } } ``` Validated result: **5,567.40 USDC** at block 24,957,162. The `formatUnits` helper reads the contract's own `decimals()` return value, so the scaling is always correct regardless of whether the token uses 6, 8, or 18 decimals. **WETH balance on Base** Prompt: *"Check the WETH balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Base."* ```json { "chain": "evm_base", "contracts": { "weth": "0x4200000000000000000000000000000000000006" }, "expression": "formatUnits(weth.balanceOf(wallet), weth.decimals())", "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } } ``` Validated result: **0.0628 WETH** at block 45,166,293. No ABI file. No RPC endpoint to configure. No decimal scaling to hard-code. The expression handles all of that. ## Struct results and DeFi positions The expression language returns structured values, not just scalars. Aave's `getUserAccountData` method returns a six-field struct: ```json { "chain": "evm_ethereum", "contracts": { "aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" }, "expression": "aave.getUserAccountData(wallet)", "context": { "wallet": "0x..." } } ``` The response includes `totalCollateralBase`, `totalDebtBase`, `availableBorrowsBase`, `currentLiquidationThreshold`, `ltv`, and `healthFactor`, all decoded and returned as a JSON object. The model receives the full struct and can surface the health factor, flag liquidation risk, or compute a collateral ratio without any additional parsing on your side. For wallets with no active Aave position, `healthFactor` returns the maximum `uint256` value, which represents no debt (effectively infinite health). The model will interpret this correctly from the context you provide in the system prompt. ## Extending the tool for integer context The current implementation maps all context variables to `sol_address`. If you need integer parameters, for example checking whether a wallet's balance exceeds a threshold, extend the schema with a `contextTypes` field: ```ts parameters: z.object({ // ... existing fields ... contextTypes: z .record(z.enum(["sol_address", "sol_int", "bool"])) .optional() .describe("Override types for context variables. Default is sol_address."), }), // In execute: const contextTypes = Object.fromEntries( Object.keys(context).map((k) => [ k, params.contextTypes?.[k] ?? "sol_address", ]) ); ``` This gives the model control over how variables are typed when the prompt involves numeric thresholds or boolean flags. ## Developers and AI builders If you are building AI tooling for DeFi or onchain apps, the [developer resources](/for/developers) page covers the evmquery REST API in full, including multi-wallet batch macros, list filtering, and expression examples for common patterns. If you are focused on AI agent workflows specifically, the [AI users page](/for/ai-users) covers both the REST tool approach above and the MCP surface. ## REST tool vs MCP: picking the right surface | | REST tool (this post) | MCP server | |---|---|---| | Use case | Custom apps, backend agents, programmatic access | Claude Desktop, Cursor, VS Code, any MCP client | | Setup | Add tool to your AI SDK handler | Paste one config block into your client | | Control | Full: schema, error handling, logging | Client manages the conversation | | Code required | ~50 lines | Zero | If you are building a product, the REST tool gives you full control over the schema, error messages, and how results are formatted before the model sees them. If you want to query the chain interactively from your IDE today, the [evmquery MCP server guide](/blog/evm-blockchain-mcp-server/) gets you there in under five minutes. ## Next steps - [Set up the evmquery MCP server in Claude Desktop and Cursor](/blog/evm-blockchain-mcp-server/) (no code required) - [Monitor Aave health factors and ERC-20 balances with a Python polling script](/blog/blockchain-monitoring-python-evmquery/) - [Read EVM contract data from Python without ABI files](/blog/query-evm-contract-data-python/) - [Browse the evmquery REST API docs](https://app.evmquery.com/api/docs) for multi-wallet batch macros, list filtering, and expression reference --- # Blockchain Indexers in 2026: The Graph, Goldsky, and When to Skip the Index Entirely Source: https://evmquery.com/blog/blockchain-indexer-guide-the-graph-vs-query-layer Published: 2026-04-24 Author: evmquery team Category: guides When The Graph wins and when a direct contract query is faster. A practical guide to blockchain indexers with TypeScript examples for ERC-20, Uniswap v3, and DeFi. Picking a blockchain indexer is the first architectural decision most Ethereum developers get wrong. The Graph and its alternatives are genuinely excellent tools. They are also the wrong tool for roughly half the reads developers use them for, and that mismatch costs days of setup that should take minutes. The skill is classifying the question before you choose the tool. A blockchain indexer like The Graph processes past events and writes them to a queryable database: you need it for history and aggregations. For current state (balances, positions, pool prices), a direct contract query is faster to ship, cheaper to run, and always up-to-date. Match the tool to the question. ## What a blockchain indexer actually does An indexer is a background process that watches the chain, extracts data from transactions and events as they land, and writes it into a queryable store. Every `Transfer` that moves an ERC-20, every `Swap` that flows through a DEX, every `LiquidationCall` on Aave: if you define a handler for it, the indexer records it. The Graph Protocol is the canonical open standard for this pattern. You write a _subgraph_: a manifest declaring which contracts to watch, a GraphQL schema defining the entities you want, and AssemblyScript mapping functions that transform raw event bytes into entity rows. Deploy it, and The Graph's indexer network starts processing historical blocks and backfilling data. By the time you query, you're hitting a pre-joined database at sub-100ms latency, not the chain directly. The Graph is the dominant decentralized indexer, but it's not the only option in 2026. Goldsky, SubQuery, and Ponder (now under the Monad Foundation) are managed and self-hosted alternatives with varying chain coverage and developer-experience trade-offs. All share the same architectural premise: process events once, query many times. ## Two categories of blockchain read Before choosing a data layer, classify the question: **Historical reads** — "what happened over time" - "All ERC-20 transfers for wallet X in the past 30 days" - "Total swap volume through this pool since launch" - "Every governance vote cast by address Y" - "Which wallets held this token at block 19,000,000" **State reads** — "what is true right now" - "What is wallet X's current USDC balance?" - "What is the current tick in this Uniswap v3 pool?" - "Is this Aave position above the liquidation threshold?" - "What is the total supply of this token right now?" Indexers solve the first category. The second category doesn't need an index at all. The answer is sitting in contract storage, readable with a single `eth_call`. This distinction matters because a lot of developers reach for an indexer out of habit, then spend three days writing subgraph mappings for a question that has a one-line answer on the RPC layer. If the phrase "right now" or "current" appears in the requirement, stop before opening the Graph documentation. ## Reading current state: no indexer needed For state reads, the pattern is direct: name the contract, write an expression, get a typed result back. No ABI files to manage, no subgraph schema, no GraphQL boilerplate. Here are the three most common patterns, each validated against live contracts. ### ERC-20 balance (single wallet) ```text chain: evm_ethereum contracts: { usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 } context: { wallet: sol_address } expression: formatUnits(usdc.balanceOf(wallet), usdc.decimals()) ``` Returns `5567.402493` for a given wallet: the decimal-scaled balance, tagged with block metadata, in one round. No ABI download, no manual decoder, no proxy check. ### ERC-20 balances across a list of wallets ```text chain: evm_ethereum contracts: { usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 } context: { wallets: list } expression: wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals())) ``` Pass an array of addresses in the `wallets` context value. Returns a typed list of balances. Under the hood, all `balanceOf` calls are batched into a single Multicall3 round, so one RPC request covers the whole list. ### Uniswap v3 pool state ```text chain: evm_ethereum contracts: { pool: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 } expression: pool.slot0() ``` Returns the live `slot0` struct: ```json { "sqrtPriceX96": "1647022250302993738808583493622716", "tick": "198852", "observationIndex": "581", "observationCardinality": "723", "feeProtocol": "68", "unlocked": true } ``` `tick` encodes the current price. `sqrtPriceX96` is the raw square-root price the AMM uses internally. Neither piece of information is historical. Both are live contract state. No subgraph required. ### DeFi position health ```text chain: evm_base contracts: { aave: 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 } context: { wallet: sol_address } expression: aave.getUserAccountData(wallet) ``` Returns the full position struct: `healthFactor`, `totalCollateralBase`, `totalDebtBase`, and available borrow capacity. Wire this into a cron script, an n8n node, or a GitHub Action and you have a liquidation monitor without touching an indexer. Aave v3 on Base is an EIP-1967 proxy. A raw `eth_call` to the proxy address would need the implementation ABI to decode the return. evmquery resolves the implementation automatically, so you call `getUserAccountData` by name and get a decoded struct back. ## When you actually need an indexer Indexers are indispensable when the question involves events that have already been processed and are no longer visible in current contract state. **Transfer history.** The `Transfer` event is emitted as tokens move. Once processed, the event lives only in historical logs. The contract itself only stores current balances. "All USDC transfers to my address in the past month" requires something that has indexed those logs. That's The Graph, Goldsky, or your own `eth_getLogs` scraper. **Aggregate metrics.** "Total volume through the USDC/ETH pool in the past 7 days" requires summing `Swap` events across thousands of transactions. The pool contract stores only the current liquidity and price. An indexer accumulates as it goes; a direct call cannot aggregate what was never tracked on-chain. **Ownership snapshots at a past block.** "Who held this NFT at block 19,000,000" is a historical question. The contract today only knows the current owner. An indexer that recorded every `Transfer` event can reconstruct the state at any past block. **Event-driven pipelines.** If your system reacts to on-chain events in near-real-time (liquidation bots, arbitrage watchers, settlement confirmations), an event-streaming indexer or `eth_subscribe` is the right surface. A polling `eth_call` loop is not. The tell: if the phrase "over the past N days/blocks," "all instances where," or "at block N" appears in the requirement, reach for an indexer. If the phrase is "current," "right now," or "latest," start with a direct read. ## The real cost of a subgraph It's worth naming the setup cost explicitly, because it shapes whether the investment makes sense for your situation. A subgraph requires: 1. A `subgraph.yaml` manifest declaring data sources, start blocks, and event handlers. 2. A `schema.graphql` defining the entities and their relationships. 3. AssemblyScript mapping functions that transform each event into entity updates. 4. A deploy step to the decentralized Graph Network (GRT billing) or a managed host. 5. A sync wait: a new subgraph can take hours to days to backfill historical blocks. For a well-defined, stable protocol (a subgraph tracking all Uniswap v3 swaps since deployment, for example), this one-time investment pays off quickly. For "I need the current price of a pool I deployed this morning," it's a week of overhead for a one-line query. There's also an ongoing maintenance tax: contract upgrades that emit new event signatures require subgraph updates and re-syncs. The Graph's hosted service was deprecated in 2026, so teams now run on the decentralized network or a managed alternative like Goldsky or SubQuery, each with its own billing model to account for. ## The decision table | Question shape | Needs indexer? | Right tool | |---|---|---| | Current token balance | No | evmquery / direct `eth_call` | | Current pool price or tick | No | evmquery | | Current DeFi position health | No | evmquery | | Full transfer history | Yes | The Graph / Goldsky | | Token holders at a past block | Yes | The Graph / Goldsky | | Aggregate protocol volume | Yes | The Graph / Goldsky | | Multi-wallet balance snapshot | No | evmquery (Multicall3 batch) | | Event stream (real-time) | Yes | `eth_subscribe` or indexer | | Contract read without ABI | No | evmquery (auto-resolved) | One more rule of thumb: if the data changes with every new block (price, balance, health factor), it's a state read. If it accumulates over time (history, aggregations, event counts), it's an indexer job. ## Using both in the same stack The choice isn't binary. Many production stacks use both layers in parallel: - **Indexer for history** — a Goldsky or The Graph subgraph tracks all protocol events and exposes them via GraphQL. - **Query layer for current state** — evmquery handles live balances, positions, and prices without rebuilding the index every time you add a new contract read. This split matches tool semantics to query semantics. The indexer handles the "what happened" question reliably. The query layer handles the "what is true now" question without any sync delay. For AI agent builders, the split also keeps tool budgets lean: MCP tools that read current state cost 1-3 credits each; tools that query indexed history against a GraphQL API can be expensive depending on data volume. Keeping current-state reads in evmquery and historical reads in a subgraph means each tool does what it was designed for. If you're building something that only needs current state (a monitoring script, a DeFi dashboard, a Claude tool that checks your Aave health factor), skip the indexer entirely. You'll ship in an afternoon instead of a week. ## Next steps - Not sure which contracts to read? The [developer overview](/for/developers) has the full expression language reference and chain list. - Building a multi-wallet balance dashboard? The [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the batching primitive evmquery uses under the hood. - Want to turn any of these reads into an automated alert? The [n8n integration post](/blog/read-smart-contracts-in-n8n) shows how to wire up contract reads to Slack, Discord, or any webhook. No indexer required. --- # Building with the EVM Blockchain MCP Server: Query Smart Contracts from Claude, Cursor and ChatGPT Source: https://evmquery.com/blog/evm-blockchain-mcp-server Published: 2026-04-24 Author: evmquery team Category: integrations How to give Claude, Cursor, and ChatGPT live access to EVM smart contracts using the Model Context Protocol — install, example prompts, and what MCP does (and doesn't) solve. Large language models are fluent in Solidity and in block explorers, but they're blind to the actual chain state unless you wire one up. If you want Claude to tell you your current Aave health factor, which DAOs you've voted in, or what the floor price of a collection is _right now_, the model needs a live feed from the network — not a screenshot, not a paste, not a stale training snapshot. That's what the Model Context Protocol (MCP) is for. And it's why an **EVM blockchain MCP server** is worth installing even if you've never written a line of Solidity. MCP lets AI assistants like Claude, Cursor, and ChatGPT call external tools. An EVM MCP server exposes smart contract reads as tool calls, so the model can fetch live onchain data in the same conversation. The [evmquery MCP server](https://app.evmquery.com/onboarding?plan=free) is a hosted HTTP endpoint at `https://api.evmquery.com/mcp`, ships two tools (`execute_query` and `describe_schema`), auto-resolves ABIs and proxies, and currently supports Ethereum, Base, and BNB Smart Chain. ## What is the Model Context Protocol? MCP is an open standard introduced by Anthropic in late 2024 for connecting AI assistants to external data sources and tools. Where function calling lets you hand a single model a one-off list of tools, MCP treats tools as _servers_ the client can discover, introspect, and call — the same way a language server feeds your IDE. In practical terms: an MCP server is a small program (local or remote) that advertises a set of tools. An MCP client (Claude Desktop, Cursor, Zed, or ChatGPT with MCP enabled) speaks the protocol, reads the tool list, and lets the model decide when to invoke them. The server returns structured data; the model folds that data into its next reply. This matters for blockchain because the chain is _fundamentally_ external state. No amount of training data will tell you what block 21 million looks like. The model needs a live tool. ## What does an EVM MCP server actually do? An EVM MCP server exposes the chain as a small, well-typed toolbox. The shape that scales is _expression-based_: instead of one tool per RPC method, you give the model a way to write a tiny query against a named contract and read the typed result back. The evmquery server is the canonical example, and it ships exactly two tools: - `execute_query` — run a Smart Expression Language (SEL) expression against one or more named contracts on a chain. Returns the typed value plus block metadata and credits consumed. - `describe_schema` — introspect what's callable on a given set of contracts (every `view` / `pure` method, plus the SEL helpers, list macros, and types). The model calls this before writing an expression so it knows what exists. That's it. Two tools, one expression language, every read pattern that fits in a SEL expression. A good MCP design here is narrow on purpose — every extra tool is one more thing for the model to misuse. The catch is in the fine print of what `execute_query` has to do under the hood. Anyone can wrap `eth_call` in an MCP server. The hard parts are: - **ABI resolution.** Claude doesn't know the ABI of the contract you just named. A good server fetches it (from Etherscan, Sourcify, or an embedded catalogue) so the model can call functions by name, not by selector. - **Proxy handling.** Roughly a third of production contracts are behind EIP-1967 proxies. A naive `eth_call` hits the proxy's empty fallback. The server has to resolve the implementation, merge ABIs, and decode against the right one. - **Chain coverage.** "Ethereum" is fine until someone asks about a balance on Base. A single-chain MCP server will send the model in circles. - **Rate limiting and caching.** LLMs retry. A lot. Without caching, a single conversation can burn through an RPC quota in minutes. Servers that skip these end up being cute demos. Servers that handle them end up being the tool you reach for daily. ## Installing the evmquery MCP server The evmquery MCP server is a hosted HTTP endpoint at `https://api.evmquery.com/mcp`. There's no local process to run, no `npx` install, no Docker container — your client connects to the URL and signs in with OIDC. No API key to generate, paste, or rotate for MCP. The fastest path is Claude Code, which has a one-liner: ```bash claude mcp add --scope user --transport http evmquery https://api.evmquery.com/mcp ``` For Claude Desktop, Cursor, VS Code, Windsurf, Zed, and other clients that take a JSON config, the equivalent block is: ```json { "mcpServers": { "evmquery": { "url": "https://api.evmquery.com/mcp" } } } ``` Restart your client. On first use it will open a browser window for OIDC sign-in; after that, the `execute_query` and `describe_schema` tools appear in the tool picker. For ChatGPT's MCP connector, point it at the same URL and sign in with OIDC when prompted. New to evmquery? The free tier gives you 2,000 credits per month, which is more than enough to validate the setup — [grab it here](https://app.evmquery.com/onboarding?plan=free). The HTTP endpoint also accepts `X-API-Key` if your client doesn't speak OIDC or you'd rather hard-pin credentials. Generate a key in the [dashboard](https://app.evmquery.com) and set it in the `headers` block of your client's MCP config. OIDC is the default because it avoids leaking long-lived secrets into client configs. ## Three prompts that prove it works The fastest way to tell if an MCP server is actually useful is to ask questions that _must_ touch the chain. Here are three we use as smoke tests for a reads-shaped MCP like evmquery. ### 1. "What's my Aave v3 health factor on Base?" Good MCP + good model gives you a decoded number and a sentence of context. Underneath, the model picks `execute_query` and runs a SEL expression like: ``` chain: evm_base schema: { aave: 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 } context: { wallet: sol_address = 0xYourWallet… } expression: formatUnits(aave.getUserAccountData(wallet).healthFactor, 18) ``` What the server has to do that a naive wrapper won't: auto-resolve the Aave Pool ABI from verified source (so the model can call `getUserAccountData` by name, not selector), unwrap the EIP-1967 proxy to the implementation, decode the six-tuple return as a typed struct, and let `formatUnits` scale `healthFactor` from its 1e18 fixed-point form into a readable ratio. The model never sees an ABI; it sees `aave.getUserAccountData(wallet).healthFactor` and gets a number back. ### 2. "Is BAYC #7890 still owned by vitalik.eth?" A pure on-chain question — exactly what `execute_query` is for. The model uses `describe_schema` once to confirm `ownerOf` exists on the BAYC contract, then runs: ``` expression: bayc.ownerOf(solInt(7890)) == solAddress("0xd8dA…6045") ``` Returns `true` or `false` in one round, one credit-per-call. The same shape works for "who owns token #N", "is this token minted", or "how many NFTs does this wallet hold" — all single SEL expressions. ### 3. "What's the OpenSea floor right now?" This one is a deliberate failure case to listen for. Floor prices live on a marketplace API, not on-chain — so an honest MCP server says so instead of inventing a tool. evmquery's MCP refuses to make up data: ask it for an off-chain price and it'll tell you it can read contract state, not marketplace orderbooks. That's the right behavior. Pair it with a separate marketplace MCP if you want both. If your MCP server (a) handles question #1 without you pasting ABIs and (b) is honest about question #3, you've found a keeper. ## Why not just write a function-calling tool? A fair question. Why bother with MCP if you can define tools directly in your agent framework? Three reasons, in order of importance: 1. **Portability.** An MCP server works in Claude Desktop _and_ Cursor _and_ Zed _and_ ChatGPT _and_ any future client that speaks the protocol. A custom tool binding only works in the SDK you wrote it against. 2. **Separation.** The server runs in its own process (or remotely). It can maintain caches, hold credentials, resolve ABIs, retry failed RPCs — all without polluting your agent code. Your agent prompt stays short; the tool implementation stays out of your git history. 3. **Discovery.** MCP clients list tools with their schemas. The model sees "read any ERC-20 balance" with typed inputs, not "a function called `callContract` that takes a random JSON blob." That structured surface is what makes MCP tools feel native. Put bluntly: if you're writing a single-purpose script, use function calling. If you want a persistent capability that shows up across every AI tool you use, ship an MCP server. ## Where MCP stops helping MCP is powerful but narrow. A few things it does not solve: - **Signing transactions.** Reading is safe. Writing is a loaded gun. evmquery's MCP server is read-only by design — it will never broadcast on your behalf. For writes, you want an explicit wallet flow, not an ambient AI session. - **Real-time subscriptions.** MCP is request/response. If you need "tell me when this balance changes," you want webhooks or an indexer, not an AI loop polling. - **Multi-hop workflows.** A single MCP call returns a single result. If you're chaining "fetch these 50 contracts, then filter, then alert," you're rebuilding orchestration on top of the model. That works for small tasks; for anything scheduled, reach for [an n8n workflow](/for/automation) instead. The right mental model: MCP is for _conversational_ onchain work. The model asks, the chain answers, the conversation continues. For everything else, there are better primitives. ## Next steps - Read more about the [developer integrations](/for/developers) — the MCP server shares its query engine with the REST API and n8n node. - Building for an agent-heavy workflow? The [/for/ai-users](/for/ai-users) page has recipes for Claude Desktop and Cursor specifically. - Want to batch reads efficiently instead of one-by-one? The [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the batching primitive that evmquery uses under the hood. --- # Moralis vs Alchemy vs QuickNode vs evmquery: Picking the Right API for Smart Contract Reads Source: https://evmquery.com/blog/moralis-alchemy-quicknode-evmquery-comparison Published: 2026-04-24 Author: evmquery team Category: comparisons A fair comparison of the four main ways to read EVM contract data in 2026. What each vendor is best at, where they stop helping, and how to pick without lock-in. If you need to read smart contract data in a product, the hardest part isn't writing the code — it's picking the right layer. Alchemy, QuickNode, Moralis, and evmquery all let you "read data from the blockchain," and they all have pricing pages that make the decision look obvious. Spend a week integrating the wrong one and it stops looking obvious. This post is a fair read on where each of the four wins. We make one of them — spoiler — so the last column has a bias, but we've tried to stay honest about what the others do better. The decision should depend on what problem you actually have, not on which logo is cleanest. Alchemy and QuickNode are **RPC + enhanced data APIs** — best for general-purpose infrastructure and block-data queries. Moralis is a **web3 data API** — best for wallet-shaped questions (transfers, NFTs, token balances across chains). evmquery is a **contract-logic query layer** — best when you need to execute expressions over specific contracts rather than crawl the whole chain. ## The question you should actually ask Before comparing vendors, narrow the question. Most teams pick wrong because they're optimizing for "cheapest per-call" when the real bottleneck is developer time. Pick a layer based on the shape of your read: - **"Give me the raw chain, fast and everywhere."** Alchemy / QuickNode. They run the nodes, they cache responses, they give you a firehose. - **"Give me indexed, pre-joined views of wallets, tokens, and NFTs."** Moralis (or an alternative indexer). They've crawled the chain already; you're querying their database. - **"Give me specific contract state via one expression, with auto-resolved ABIs and proxies."** evmquery. You point at contracts and ask questions; we handle the plumbing. These are different layers. You can use more than one. The wrong choice isn't fatal — it's just a few weeks of glue code you didn't need to write. ## Alchemy **What it is:** A managed RPC provider with a set of "Enhanced APIs" stacked on top — token metadata, NFT ownership, transaction history. **Where it wins:** - Reliable, well-cached RPC on every major EVM chain. Their `eth_call` latency is consistently best-in-class. - The Enhanced APIs cover the most common read patterns (ERC-20 balances across a wallet, NFT metadata, transfer history) without you having to index yourself. - Great observability — the dashboard shows you which methods your app calls most, which lets you size plans sensibly. **Where it stops helping:** - You still write the RPC-layer code. `eth_call`, ABI encoding, proxy resolution, Multicall3 batching — all on you. The Enhanced APIs only cover _their_ happy paths; anything outside (a custom Governor contract, a new DEX) and you're back to raw RPC. - Pricing is CU-based (compute units). A single `getLogs` with a wide block range can silently burn a significant slice of your monthly allowance. **Pick Alchemy when:** You need industrial-grade RPC and you're comfortable writing client-side read logic. The Enhanced APIs are a nice bonus, not the reason you'd pay. ## QuickNode **What it is:** Also a managed RPC provider, with a feature set that's closer to a platform — custom endpoints, QuickNode Functions (serverless on top of RPC), Streams (realtime webhooks), and marketplace add-ons for indexing and analytics. **Where it wins:** - Widest chain coverage of the big four. If you need Blast, Fraxtal, or the latest Arbitrum Orbit chain before anyone else, QuickNode usually has it first. - Streams is a real-deal webhook product — define a filter, get a POST per matched event. Fewer teams need to run their own indexer because of it. - Functions let you deploy small bits of logic next to the RPC, which cuts round-trips for stateful reads. **Where it stops helping:** - Same core constraint as Alchemy: you're writing RPC-flavored code. The abstractions are bigger and more composable, but you're still responsible for ABIs, decoding, and proxy resolution. - The marketplace is a mixed bag. Some add-ons are essential; others are thin and you'll outgrow them quickly. **Pick QuickNode when:** You want RPC plus webhook-driven workflows, or you're on a long tail chain where Alchemy doesn't go yet. ## Moralis **What it is:** A web3 data API. Moralis crawls every major EVM chain ahead of time and exposes wallet-shaped REST endpoints — `/:wallet/tokens`, `/:wallet/nfts`, `/:wallet/history`, plus NFT and token metadata endpoints. **Where it wins:** - If your question is wallet-shaped — "what tokens does this address hold on these five chains?" — Moralis gives you a JSON answer in one HTTP call. Building the same thing on raw RPC means indexing transfer events across every chain. - NFT metadata handling (IPFS resolution, animated URIs, rarity where available) is a real product. Rolling your own is miserable. - Cross-chain by default. One request, multi-chain response. **Where it stops helping:** - If your question isn't wallet-shaped, Moralis has less to offer. Reading a custom Governor's `proposalSnapshot(id)` isn't a standard endpoint; you're back to raw RPC via their node gateway. - Index freshness varies per chain. For trading UIs where the last 30 seconds matter, you'll want a direct RPC read, not an indexed API. - Pricing can surprise you on NFT-heavy apps. A single "get all NFTs" call for a whale wallet is expensive. **Pick Moralis when:** Your product revolves around wallets, NFTs, or transfer history across chains. That's their sweet spot and they're good at it. ## evmquery **What it is:** A contract-logic query layer. You point it at one or more contracts, write an expression (our Smart Expression Language, SEL, looks a lot like JavaScript), and get back typed results. It handles ABI resolution, proxy unwinding, and Multicall3 batching under the hood. Chain is a request-level parameter — pick Ethereum, Base, or BNB Smart Chain per query. **Where it wins:** - When the data you want lives in a specific contract — a Governor, a Vault, a custom AMM, a new protocol that shipped yesterday — you don't wait for us to index anything. You point, you query, you're done. - One expression can read across many contracts on the same chain, with all the independent calls auto-batched into a single Multicall3 round. Compared to writing Multicall3 by hand, the abstraction saves real time. Compared to calling an indexer, you get data that's fresh to the block. - Ships with a REST API, an [n8n node](/blog/read-smart-contracts-in-n8n), and an [MCP server for Claude/Cursor](/blog/evm-blockchain-mcp-server) — same query engine, three clients. You don't have to pick one. **Where it stops helping:** - evmquery is read-oriented. For wallet-shaped indexed history ("all transfers in this address's lifetime"), Moralis is the right tool. - We don't replace a full RPC provider. If your app needs `eth_sendRawTransaction` or `debug_traceTransaction`, you still need Alchemy or QuickNode behind the curtain — and that's fine; a lot of our users run us alongside an RPC they already have. - New protocol? If there's no verified source on Etherscan/Sourcify, we can't auto-resolve the ABI and you'll need to upload it. That takes a minute; still worth flagging. **Pick evmquery when:** You're reading from specific contracts, especially across chains or behind proxies, and you want to spend zero minutes on ABI/decoding plumbing. The MCP and n8n integrations are the most common "oh, that's why" moment. ## Decision matrix | Use case | Best fit | |---|---| | Production dApp serving raw RPC to a wallet | Alchemy or QuickNode | | Wallet page showing tokens + NFTs + history across chains | Moralis | | Dashboard reading 50 positions from 8 protocols on one chain | evmquery | | Webhook when a specific event happens on a contract | QuickNode Streams (or Alchemy Notify) | | AI agent that answers questions about live contract state | evmquery MCP | | n8n / Zapier-style automation over contract reads | evmquery n8n node | | Bulk historical event scan across millions of blocks | Alchemy `getLogs` with careful chunking, or a purpose-built indexer | None of these rows are gospel — you can do most tasks with most tools, it's a question of how much code you write. The matrix reflects what we see teams default to when they stop fighting their stack. ## What about a "free RPC + homegrown layer" option? Fair question. Many teams start with a public RPC (`ethereum.publicnode.com` and friends) plus a folder of utility scripts. That works until one of three things happens: 1. You add a second chain and have to generalize. Now `readContract` needs a chain parameter, an RPC selector, and per-chain ABI handling. 2. You hit a proxy and spend an afternoon debugging why `totalSupply` returned zero. 3. Your public RPC rate-limits you mid-demo. At that point, paying someone — _any_ of the four vendors in this post — buys back your time. The question is just which one maps best to your product shape. ## A note on switching cost Every vendor in this comparison will tell you their client library is the right one to standardize on. We disagree with that framing. Use whichever works today and keep the boundaries thin — a single "read this expression against these addresses" function that you can re-point is worth more than deep integration with any provider's SDK. evmquery is designed for this: our MCP server, REST API, and n8n node all accept the same expression language, so switching between them (or running them side-by-side) is free. Other vendors vary. ## Next steps - If you've decided the right layer is evmquery, the [developer page](/for/developers) has concrete integration examples. - Building for AI agents? The [MCP server post](/blog/evm-blockchain-mcp-server) covers the Claude/Cursor integration in detail. - Want to skip code entirely? The [n8n integration](/blog/read-smart-contracts-in-n8n) has paste-in recipes. --- # Multicall3 in 2026: The Practical Guide to Batching EVM Contract Reads Source: https://evmquery.com/blog/multicall3-batching-evm-contract-reads Published: 2026-04-24 Author: evmquery team Category: guides Multicall3 lets you collapse hundreds of RPC roundtrips into a single call. This guide covers how it works, Viem / Ethers / Wagmi usage, common pitfalls, and when to hand it off. If you've ever written a loop that calls `contract.balanceOf` a hundred times, you've paid the price of the EVM's greatest secret tax: the per-call RPC roundtrip. Public endpoints rate-limit. Private endpoints bill per request. Browsers throttle concurrent fetches. And every single one of those calls is doing exactly the same work — opening a connection, signing a request, parsing a response — to read a field that's already sitting in one SLOAD on the node. **Multicall3** collapses that loop into a single call. It's been the default batching primitive on every major EVM chain since 2021, and it's still the thing people get wrong most often. Multicall3 is a contract deployed at the same address on every EVM chain (`0xcA11bde05977b3631167028862bE2a173976CA11`). You send it a list of `(target, calldata)` pairs in one `eth_call`, and it returns all the results at once. It cuts roundtrips, not gas — and it has three different entry points for different failure semantics. ## The problem it actually solves Reading `N` contracts the naive way costs `N` roundtrips. On a typical consumer connection, that's maybe 30 requests per second. For a DeFi dashboard showing 40 positions, you're staring at a blank screen for over a second before the first number shows up — and that's the happy path, before your public RPC rate-limits you into backoff. Multicall3 turns those `N` requests into 1. The node still has to do `N` SLOADs internally, but your app pays one network roundtrip and one JSON-RPC framing cost. On the same dashboard, you get all 40 numbers back in a single bounce. The gas story is more subtle. Multicall3 is an `eth_call` (off-chain read), so you aren't paying gas — you're asking the node to simulate the reads. Simulation has its own limits (gas caps on public RPCs, usually 100M–250M), but for reads those limits are almost never a problem. ## How Multicall3 works The contract exposes three flavors of the same idea: | Function | Allows reverts? | Returns success flag? | |----------|-----------------|-----------------------| | `aggregate3((target, allowFailure, callData)[])` | Optional per-call | Yes | | `tryAggregate(requireSuccess, (target, callData)[])` | Global flag | Yes | | `aggregate((target, callData)[])` | No — any revert fails the batch | No | You almost always want `aggregate3`. It lets you mark each individual call as "may fail" or "must succeed," and it returns a `(success, returnData)` tuple per call. That means a single contract that reverts doesn't take down your entire batch — which matters, because partial failures are how production data looks. Calldata goes in ABI-encoded. Return data comes out ABI-encoded. Your client library is responsible for encoding and decoding. Which brings us to the tooling. ## Viem (recommended) Viem has first-class Multicall3 support. Pass a list of `readContract` args, get back a list of typed results. ```ts const client = createPublicClient({ chain: mainnet, transport: http(), }); const tokens = [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI ] as const; const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // vitalik.eth const balances = await client.multicall({ contracts: tokens.map((token) => ({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [holder], })), }); // balances[i] = { status: "success", result: 1234n } | { status: "failure", error: ... } ``` A few things Viem is quietly doing for you: - It uses `aggregate3` under the hood so a single reverting call doesn't blow up the rest. - It chunks automatically (`batchSize` option) so you don't hit gas caps on huge batches. - It falls back to individual `eth_call` if the chain has no Multicall3 (rare in 2026, but some L2s still lag). - The return type is a discriminated union per call — you have to narrow on `status` before touching `result`. If you're starting a new codebase, use Viem. The DX is a decade ahead of everything else. ## Ethers v6 Ethers doesn't ship with first-party multicall, but you can write it in 20 lines. The cleanest path is to construct a `Multicall3` contract instance and hand-encode calldata via the target contract's `interface`. ```ts const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"; const multicall3Abi = [ "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) external payable returns ((bool success, bytes returnData)[])", ]; const erc20Abi = ["function balanceOf(address) view returns (uint256)"]; const erc20Iface = new Interface(erc20Abi); const provider = new JsonRpcProvider(process.env.RPC_URL); const multicall = new Contract(MULTICALL3, multicall3Abi, provider); const tokens = [ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xdAC17F958D2ee523a2206206994597C13D831ec7", "0x6B175474E89094C44Da98b954EedeAC495271d0F", ]; const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; const calls = tokens.map((token) => ({ target: token, allowFailure: true, callData: erc20Iface.encodeFunctionData("balanceOf", [holder]), })); const results = await multicall.aggregate3.staticCall(calls); const balances = results.map((r, i) => r.success ? (erc20Iface.decodeFunctionResult("balanceOf", r.returnData)[0] as bigint) : null, ); ``` The gotchas: - Use `.staticCall(...)` — you're reading, not sending a transaction. - `allowFailure: true` is almost always what you want. The default (`false`) makes one revert fail the batch. - ABI decoding returns a `Result` array — the leading `[0]` extracts the single return value. ## Wagmi + React For React apps, `useReadContracts` is the high-level primitive. It uses Viem's `multicall` under the hood. ```tsx export function TokenBalances({ holder }: { holder: `0x${string}` }) { const { data, isLoading } = useReadContracts({ contracts: [ { address: USDC, abi: erc20Abi, functionName: "balanceOf", args: [holder] }, { address: USDT, abi: erc20Abi, functionName: "balanceOf", args: [holder] }, { address: DAI, abi: erc20Abi, functionName: "balanceOf", args: [holder] }, ], query: { refetchInterval: 15_000 }, }); if (isLoading) return ; return (
    {data?.map((r, i) => (
  • {r.status === "success" ? r.result.toString() : "—"}
  • ))}
); } ``` Same API surface as Viem, plus React Query caching and refetch intervals for free. If you're building a dashboard, this is the shortest path to a working one. ## The gotchas that eat hours These are the things that make Multicall3 look "broken" when it's working exactly as specified. ### Reverts look like successes if you forget to check `aggregate3` returns `(bool success, bytes returnData)` per call. If `success` is `false`, `returnData` holds the revert reason (or nothing). Beginners forget to check and happily decode garbage into zero-bigints. Always narrow on `status === "success"` (Viem) or `.success` (direct). ### Calls to nonexistent contracts succeed with empty return data The EVM returns 0x for `eth_call` to a zero-code address. Multicall3 faithfully forwards that 0x. Your ABI decoder then either throws or returns default-zero values. If your token list might contain an EOA by mistake, check `getCode(target)` first or gate on `returnData.length > 0`. ### Proxies don't make your job easier If you're reading `totalSupply` on an EIP-1967 proxy, the proxy's fallback forwards the call to the implementation — that works. But if your "ABI" is the proxy's own ABI (which is usually just `implementation()` and a few admin functions), you'll build calldata for the wrong contract and the implementation will revert. You have to decode against the implementation ABI. Tooling that resolves this automatically (Viem when it has the right ABI; [evmquery](/for/developers) always) saves real time. ### Gas caps on public RPCs A free-tier RPC might cap `eth_call` gas at 50M. A batch of 10,000 balance reads easily exceeds that. Viem's `batchSize` option chunks for you; if you're rolling your own, split into groups of ~500 calls. ### Block consistency across a batch Every call in a Multicall3 batch runs against the same block. That's the whole point — if you want `totalSupply()` and `balanceOf(me)` in the same block, this is how you get it. If you split into multiple calls, you might read across a block boundary and see inconsistent state. ## When to stop hand-rolling multicall Multicall3 is the right primitive when you know the shape of your calls up front. It stops being pleasant in three situations: 1. **You need the implementation ABI, not the proxy ABI.** Every proxy you add means a second call to `implementation()` and a merge step. 2. **You want cross-chain reads in one query.** Multicall3 is per-chain. Reading the same token on Ethereum, Base, and Arbitrum means 3 separate batches and manual fan-in. 3. **You're shipping an LLM tool or an n8n node.** The indirection between "I want a number" and "encode selector, bundle into aggregate3, decode tuple" is exactly the friction you don't want in a prompt. That's the point where a query layer pays for itself. [evmquery](/for/developers) lets you write one expression — `wallets.map(w, token.balanceOf(w))` against a whole list — and it handles Multicall3 batching and proxy resolution for you, per chain. (Cross-chain is still one request per chain — the language has no expression-level chain switch, and that's on purpose.) The free tier gives you 2,000 credits/month; you'll know within an afternoon whether the abstraction helps. ## Next steps - If you're building an AI tool that needs live contract state, the [MCP server guide](/blog/evm-blockchain-mcp-server) walks through the same concepts with a Claude/Cursor target. - Comparing query services? The [Moralis / Alchemy / QuickNode / evmquery post](/blog/moralis-alchemy-quicknode-evmquery-comparison) breaks down when Multicall3 isn't enough. - Shipping automations? [Reading contracts from n8n](/blog/read-smart-contracts-in-n8n) covers the no-code path. --- # Query EVM Contract Data from Python: No ABIs, No RPC Nodes, No web3.py Source: https://evmquery.com/blog/query-evm-contract-data-python Published: 2026-04-24 Author: evmquery team Category: guides How to read live EVM smart contract data from Python using the evmquery REST API — no ABI files, no RPC nodes, no web3.py boilerplate. Five working recipes. 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. `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: ```python 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`, or `evm_bnb_mainnet` - `schema.contracts` — a map of name to contract address; the name becomes a variable in your expression - `schema.context` — typed declarations for any input variables you pass at runtime - `expression` — a CEL expression; the return value is what gets decoded and returned Here is a one-time setup block that covers every example below: ```python 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](https://app.evmquery.com/onboarding?plan=free). 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 ```python 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: ```python 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: ```python 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. 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: ```python 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()`: ```python 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: ```python 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](/for/developers) 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](/blog/evm-blockchain-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](/blog/read-smart-contracts-in-n8n) covers evmquery's native n8n community node. ## Next steps - [Get a free API key](https://app.evmquery.com/onboarding?plan=free) 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](/blog/multicall3-batching-evm-contract-reads) explains the primitive evmquery uses under the hood. - Building AI agents that read onchain state? The [MCP server post](/blog/evm-blockchain-mcp-server) covers the Claude and Cursor setup. - The full expression reference and chain list are on the [developers page](/for/developers). --- # Read Smart Contracts in n8n: Fetching EVM Data in Automation Workflows (No Code) Source: https://evmquery.com/blog/read-smart-contracts-in-n8n Published: 2026-04-24 Author: evmquery team Category: integrations How to read live smart contract data from an n8n workflow. Install the evmquery community node, then ship three paste-in recipes — price alerts, DAO votes, and NFT floor monitors. n8n is the best tool in 2026 for gluing internal workflows together. It handles the nine boring parts of an automation — schedules, credentials, branching, retries, notifications — so you can focus on the one interesting part. Everything except one thing: reading live data from a smart contract. Until recently, anyone who needed that had to fall back on the HTTP Request node, hand-encode an `eth_call`, remember to hex-prefix the function selector, parse a raw bytes response, and then — if they were lucky — pipe the result into a Code node for decoding. Fifteen minutes per recipe. Multiply by every chain. Multiply by every contract. The evmquery community node collapses that into a single node: pick a chain, paste a contract address, write one expression, click execute. Install the `n8n-nodes-evmquery` community node, drop an _evmquery_ node into any workflow, and read from any contract on Ethereum, Base, or BNB Smart Chain. Works in n8n Cloud and self-hosted. Free tier gives you 2,000 credits/month. ## Why read contracts in an automation? The obvious stuff first. If you already run n8n for internal tooling, any of these pay for themselves within a day: - **Treasury alerts.** Ping Slack when your ops wallet drops below a threshold on any chain. - **Position monitors.** Watch your Aave / Morpho / Euler health factor and alert before you get liquidated. - **DAO tracking.** Notify the team when a Governor proposal moves from `Active` to `Succeeded`. - **Market triggers.** React when a token's price or an AMM pool's reserves cross a boundary. - **Compliance snapshots.** Capture vault totals at end-of-day for reporting. None of these need Solidity. They need _reads_, on a schedule, with conditional branching. That's n8n's entire job — plus one extra node. ## Installing the community node n8n supports community nodes natively since v0.187. Two paths: **n8n Cloud / Desktop:** Settings → Community Nodes → Install → paste `n8n-nodes-evmquery` → Install. The node appears in the node picker under "evmquery." **Self-hosted:** `npm install n8n-nodes-evmquery` inside your `.n8n/custom` directory, or add it to your `package.json` and rebuild the container. Restart n8n. Same picker entry. You'll also need credentials. In n8n: Credentials → New → evmquery API → paste your key. Grab one from the [dashboard](https://app.evmquery.com/onboarding?plan=free) — the free tier covers the three recipes below with room to spare. ## Recipe 1: ERC-20 balance alert **Goal:** Post to Slack when your ops wallet drops below 10,000 USDC on Base. The workflow is four nodes: 1. **Schedule Trigger** — every 5 minutes. 2. **evmquery** — Operation: _Execute Query_, read the USDC balance. 3. **IF** — compare result to threshold. 4. **Slack** — send message on the "true" branch. The evmquery node config: ```text Operation: Execute Query Chain: Base Contracts: Token = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 Context: ops : sol_address = 0xYourOpsWalletHere Expression: formatUnits(Token.balanceOf(ops), Token.decimals()) ``` The node returns the decoded value plus block metadata: ```json { "value": 9832.11, "type": "double", "block": 21034992 } ``` Wire `{{ $json.value }}` into the IF node, compare to `10000`, and you have a working treasury alarm. Total build time: 4 minutes. The same shape works for any view function: name the contract, declare your inputs as typed context, and write a single Smart Expression Language line — `Token.balanceOf(ops)` for raw integers, `formatUnits(Token.balanceOf(ops), Token.decimals())` for a human number, or `solAddress("0x…").balance()` for native ETH. ## Recipe 2: DAO proposal state watcher **Goal:** Notify a Discord channel when a Governor proposal transitions from `Active` to `Succeeded`, `Defeated`, or `Queued`. This one needs a tiny bit of state. We'll store the last-seen state in n8n's built-in data store (`n8n-nodes-base.set`) or in a Postgres row if you have one handy. Workflow: 1. **Schedule Trigger** — every minute. 2. **evmquery** — Operation: _Execute Query_, read current proposal state. 3. **Get Previous State** — from your data store. 4. **IF** — compare. 5. **Discord + Update Store** — notify on change and update the stored value. The Governor `state(uint256)` function returns an enum (0..7). The evmquery node returns the numeric value; we map it client-side: ```text Operation: Execute Query Chain: Ethereum Contracts: Governor = 0xYourGovernorHere Context: proposalId : sol_int = 123 Expression: Governor.state(proposalId) ``` In a Code node (or a Set node with an expression): ```js const STATES = [ "Pending", "Active", "Canceled", "Defeated", "Succeeded", "Queued", "Expired", "Executed", ]; return { state: STATES[$json.value] ?? "Unknown" }; ``` The IF node compares `state` to the stored `lastState`. The Discord message only fires on actual transitions. The trick here — and the reason doing this on raw RPC is painful — is that the evmquery node handles the proxy unwinding for Governors that use OpenZeppelin's upgradeable pattern. You don't have to know it's a proxy; you just call `state(uint256)`. ## Recipe 3: NFT floor monitor **Goal:** Slack alert when the OpenSea floor for a collection drops below a target, correlated with onchain totalSupply (so you know the collection hasn't been rug-burned). This one uses one batched contract read, then a market call. Workflow: 1. **Schedule Trigger** — every 15 minutes. 2. **evmquery** — `totalSupply()` and `ownerOf(1)` on the NFT contract, returned as one map. 3. **HTTP Request** — OpenSea floor endpoint. 4. **Merge** — join the two results. 5. **IF** — floor below threshold AND supply unchanged. 6. **Slack** — alert. The reason `totalSupply()` is in the loop: if the collection was migrated, rugged, or had a major mint, the floor price means something different. Your alarm should care. ```text Operation: Execute Query Chain: Ethereum Contracts: Collection = 0xBd3531dA5CF5857e7CfAA92426877b022e612cf8 Expression: { "supply": Collection.totalSupply(), "tokenOneOwner": Collection.ownerOf(solInt(1)) } ``` (Substitute the contract for the collection you actually care about. The two reads are independent, so the engine batches them into one Multicall3 round and returns a typed map.) The evmquery node batches reads to the same chain into a single Multicall3 call under the hood. Two reads or two hundred, it's one request on the wire. You don't have to structure your workflow differently to get the efficiency — it just happens. ## Comparing to the HTTP Request approach The HTTP Request node can do all of this. We've built the exact same recipes both ways, and the line count in the JSON export tells the story: the raw-RPC version of Recipe 1 is 4 nodes including a Code node with 30 lines of ABI encoding. The evmquery version is 4 nodes with no Code node at all. The bigger cost is the next time you want to add a contract. With the HTTP Request approach, you copy the Code node and adjust the selector, the ABI, the decoding. With the evmquery node, you change one field. If you're running three automations, either approach is fine. If you're running thirty, the abstraction pays for itself in about a week. ## Mistakes that trip people up - **Chain names.** The dropdown lists Ethereum, Base, and BNB Smart Chain — those are the three networks the engine currently serves. The corresponding ids (if you ever set the node via an expression) are `evm_ethereum`, `evm_base`, and `evm_bnb_mainnet`. More chains land progressively; check the dashboard for the latest list. - **Decimals.** ERC-20 decimals vary (USDC is 6, WETH is 18). Use `formatUnits(Token.balanceOf(holder), Token.decimals())` to scale to a human number; if you see a result that's ~12 orders of magnitude off, you forgot the `formatUnits` wrap. - **Token IDs.** Bare integer literals are 64-bit `int` in SEL; ERC-721 token ids are `uint256`. Wrap them with `solInt(...)` — `Collection.ownerOf(solInt(7890))`, not `Collection.ownerOf(7890)`. - **Rate limits on the free tier.** 2,000 credits per month is generous for one or two recipes on 5-minute schedules, but a 10-second-interval scanner will burn through it. A single contract read typically costs 1–2 credits, so budget accordingly and watch the dashboard. ## Next steps - The [automation landing page](/for/automation) has a deeper reference for the node, including the full expression language. - Reading the same contracts from code? The [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) shows the primitive that the n8n node is using under the hood. - Automating with AI agents instead of n8n? The [MCP server post](/blog/evm-blockchain-mcp-server) is the Claude/Cursor equivalent.