# 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 --- # Free EVM Tools Source: https://evmquery.com/tools In-browser EVM utilities: read contracts, inspect tokens, scan wallets. Each tool is one SEL expression on the evmquery API. - [ERC-1155 Inspector](https://evmquery.com/tools/erc1155-inspector): Inspect any ERC-1155 contract for uri(id), supply, balance, and ERC-165, without ABIs or {id} substitution headaches. - [ERC-20 Token Inspector](https://evmquery.com/tools/erc20-inspector): One look at any ERC-20: name, symbol, decimals, and total supply, formatted with the right precision, using typed reads that skip ABI files. - [Aave Health Factor Checker](https://evmquery.com/tools/aave-health): Read any wallet's Aave health factor across chains in one expression. Proxy-aware, struct-decoded. - [ERC-721 NFT Inspector](https://evmquery.com/tools/erc721-inspector): Read any ERC-721 NFT: name, symbol, tokenURI, and owner, with the metadata JSON resolved from IPFS. No ABIs or wallet connection required. - [Chainlink Price Feed Reader](https://evmquery.com/tools/chainlink-price-feed): Read any Chainlink aggregator: latest answer, decimals, and description, decoded to a human-readable price in one expression. - [Contract Inspector](https://evmquery.com/tools/contract-inspector): Paste any contract address to see its proxy chain, resolved implementation, and full method schema, including which address each call executes on and where its ABI came from. - [Calldata Decoder](https://evmquery.com/tools/calldata-decoder): Decode raw transaction calldata into a function name and typed arguments. Paste the contract address instead of an ABI file: evmquery resolves the ABI for you. - [Proxy Contract Detector](https://evmquery.com/tools/proxy-detector): Paste any contract address to check whether it's a proxy: EIP-1967, UUPS, Beacon, and Diamond (EIP-2535) patterns resolved and rendered as a chain. - [Token Allowance Checker](https://evmquery.com/tools/token-allowance): Check what an ERC-20 spender is approved to move from a wallet: the exact allowance, decimal-formatted, with unlimited approvals flagged. - [Gnosis Safe Inspector](https://evmquery.com/tools/gnosis-safe-inspector): Inspect any Gnosis Safe (Safe{Wallet}) multisig: owners, signature threshold, nonce, and version, resolved through the Safe proxy automatically. - [ENS Resolver](https://evmquery.com/tools/ens-resolver): Resolve any .eth name to its ETH address in two chained onchain reads: the ENS Registry's resolver lookup, then the resolver's address record. Namehash computed entirely server-side, no crypto library required. --- # Compound V3 (Comet): Read Borrow Balance and Liquidation Risk via API Source: https://evmquery.com/blog/compound-v3-comet-liquidation-risk-api Published: 2026-09-07 Author: evmquery team Category: guides Compound V3 (Comet) skips the health-factor field. Read borrow balance, collateral, and liquidation risk with isBorrowCollateralized and isLiquidatable via API. Aave V3 gives you one number: `healthFactor`. Below 1.0, a position is liquidatable; above it, you're safe by however much margin. Compound V3 (Comet) has no equivalent field. There is no `getUserAccountData`, no single `uint256` you can compare against a threshold. Instead Comet exposes two booleans, `isBorrowCollateralized` and `isLiquidatable`, and expects you to compute the actual margin yourself from collateral factors and price feeds if you want more than a yes/no answer. Compound V3 has no `healthFactor`. Read `borrowBalanceOf` and `userCollateral` for the raw position, `isBorrowCollateralized`/`isLiquidatable` for the boolean risk check, and combine `getAssetInfoByAddress`'s collateral factors with `getPrice` if you need an Aave-style ratio instead of a yes/no. ## Why Comet doesn't have a health factor Compound V3's core design change from V2 is that each deployed market, called a Comet instance, borrows exactly one base asset. The Ethereum mainnet deployment alone runs six separate Comet contracts today — USDC, USDS, USDT, WBTC, WETH, and wstETH markets — each with its own address, its own collateral list, and its own risk parameters. Base runs five (USDC, USDbC, USDS, WETH, AERO), Polygon runs two (USDC, USDT). Because a wallet's position lives entirely inside one Comet contract (unlike Aave's single pool aggregating every asset), Compound's Solidity doesn't need to compute a portfolio-wide ratio to answer "can this account be liquidated." It just checks collateral value against the borrowed base asset directly, and exposes that check as a boolean rather than a ratio. That's a reasonable design choice for the protocol's own liquidation bot; it's mildly annoying if you're building a risk dashboard and want a number, not a boolean. ## Reading the raw position: balance and collateral Every Comet market exposes `borrowBalanceOf(account)` for the base-asset debt and `userCollateral(account, asset)` for how much of a given collateral asset the account has deposited. Both need `decimals()` to format correctly — the base asset's decimals for the debt (6 for USDC, 18 for WETH), always 18 for `userCollateral` since Comet stores raw collateral balances in wei regardless of the token. ```python import os import requests COMET_USDC = "0xc3d688B66703497DAA19211EEdff47f25384cdc3" # Compound V3 USDC market, Ethereum WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"comet": {"address": COMET_USDC}}, "context": {"wallet": "sol_address"}, }, "context": {"wallet": "0xYourWalletAddress"}, "expression": ( f'cel.bind(weth, solAddress("{WETH}"), {{' ' "borrowBalanceUSDC": dyn(formatUnits(comet.borrowBalanceOf(wallet), comet.decimals())),' ' "wethCollateral": dyn(formatUnits(comet.userCollateral(wallet, weth).balance, 18)),' ' "isCollateralized": dyn(comet.isBorrowCollateralized(wallet)),' ' "isLiquidatable": dyn(comet.isLiquidatable(wallet))' " })" ), }, timeout=10, ) resp.raise_for_status() print(resp.json()["result"]["value"]) ``` Run live against a wallet holding no Compound position, this returns: ```json { "borrowBalanceUSDC": 0, "wethCollateral": 0, "isCollateralized": true, "isLiquidatable": false } ``` That's the real response, four `eth_call`s batched into one round trip, validated against the live USDC Comet market at block 25923009. A wallet with zero debt is trivially collateralized and never liquidatable — the same "no debt, not an error" case the [Aave health factor guide](/blog/aave-v3-health-factor-explained/) documents, just returned as `true`/`false` here instead of a 59-digit number. ## The two risk checks: isBorrowCollateralized and isLiquidatable These aren't the same check. `isBorrowCollateralized` looks at the account's current borrow against its collateral using the **borrow** collateral factor — the conservative threshold Comet enforces when you try to open or increase a borrow. `isLiquidatable` uses the separate, looser **liquidate** collateral factor — the threshold at which the position actually becomes eligible for liquidation. A position can fail the first check (you couldn't borrow more right now) while still passing the second (you're not liquidatable yet). That gap is Comet's version of Aave's LTV-vs-liquidation-threshold spread. ``` comet.isBorrowCollateralized(wallet) // true if safe to increase borrow further comet.isLiquidatable(wallet) // true if a liquidator can absorb this account now ``` Poll `isLiquidatable` for an alerting system; check `isBorrowCollateralized` before letting a UI offer another borrow. Using either one for both jobs produces false alarms or missed ones. ## Computing an actual risk ratio If a boolean isn't enough — you want to show "this position can absorb a 12% price drop" instead of just "safe" — combine three reads: the collateral factor from `getAssetInfoByAddress`, the collateral's USD price from `getPrice`, and the account's debt. ```python resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": {"contracts": {"comet": {"address": COMET_USDC}}}, "expression": ( f'cel.bind(info, comet.getAssetInfoByAddress(solAddress("{WETH}")), {{' ' "borrowCollateralFactor": dyn(formatUnits(info.borrowCollateralFactor, 18)),' ' "liquidateCollateralFactor": dyn(formatUnits(info.liquidateCollateralFactor, 18)),' ' "wethPriceUsd": dyn(formatUnits(comet.getPrice(info.priceFeed), 8))' " })" ), }, timeout=10, ) ``` Live against the mainnet USDC market, WETH currently carries an 82.5% borrow collateral factor, an 88% liquidate collateral factor, and a Chainlink-fed price around $2,497.54 (block 25923004 — check it live, ETH moves). With round numbers, the math behind Comet's `isLiquidatable` check looks like this: ``` Collateral: 2 WETH deposited, price $2,500 → $5,000 collateral value Threshold: liquidateCollateralFactor 0.88 → $4,400 liquidation threshold Debt: 3,500 USDC borrowed → $3,500 debt value liquidationBuffer = liquidationThresholdValue / debtValue = 4,400 / 3,500 = 1.257 ``` A `liquidationBuffer` above 1.0 means `isLiquidatable` reads `false`; at or below 1.0, it flips to `true`. That's the same shape as Aave's `healthFactor < 1.0`, computed by hand instead of read from a struct field — and it's per-collateral-asset, so a wallet with several collateral types needs one `getAssetInfoByAddress` call per asset, summed before dividing by debt. The formula above assumes the base asset (USDC) holds at $1.00. For USDC that's a safe simplification; for markets like the WETH-base Comet, divide by `getPrice(comet.baseTokenPriceFeed())` in the debt-value line too, not just 1. `basePrice` on the USDC market reads `0.99986181` live, close enough to ignore — don't assume that holds for every base asset. ## Markets differ by chain — and Comet skips BNB Chain entirely Comet is deployed on Ethereum, Base, Arbitrum, Optimism, Polygon, Scroll, Mantle, Linea, Ronin, and Unichain, confirmed against the [`deployments/`](https://github.com/compound-finance/comet/tree/main/deployments) folder in Compound's own contracts repo. Of evmquery's four supported chains, that's Ethereum, Base, and Polygon — **there is no Compound V3 deployment on BNB Chain**. Query one of the addresses below against `evm_bnb_mainnet` and you'll get a contract-resolution failure, not a market with zero activity. | Chain | Market | Comet address | |-------|--------|----------------| | Ethereum | USDC | `0xc3d688B66703497DAA19211EEdff47f25384cdc3` | | Base | USDC | `0xb125E6687d4313864e53df431d5425969c15Eb2F` | | Polygon | USDC | `0xF25212E676D1F7F89Cd72fFEe66158f541246445` | All three resolved live with an identical ABI shape during research for this post — swap the `chain` and `COMET_USDC` values in the examples above and the same expressions run unchanged. Base's USDC market is a different contract from its older USDbC (bridged USDC) market; don't assume one address covers both. ## Watching a list of borrowers in one request Same pattern as any multicall-friendly read: `list.map()` applies one expression to every wallet in a watchlist and returns the results in order, one HTTP round trip instead of one per wallet. ```python WATCHLIST = [ "0xWallet1...", "0xWallet2...", "0xWallet3...", ] resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"comet": {"address": COMET_USDC}}, "context": {"wallets": "list"}, }, "context": {"wallets": WATCHLIST}, "expression": "wallets.map(w, comet.isLiquidatable(w))", }, timeout=10, ) liquidatable_flags = resp.json()["result"]["value"] # list, same order as WATCHLIST ``` Validated live against two real addresses, this returns `[false, false]` — a flat `list` you can zip back against `WATCHLIST` to find which accounts flipped. Swap `isLiquidatable` for `borrowBalanceOf` in the same `.map()` call if you want raw balances instead of the boolean, same one-request pattern. See [Multicall3 batching](/blog/multicall3-batching-evm-contract-reads/) if you're building this outside evmquery and want to understand what the batching is actually doing under the hood. If you're choosing between the two protocols for a monitoring build: Aave gives you a ready-made scalar (`healthFactor`) across its entire multi-asset pool in one call. Compound gives you sharper booleans per market, but only for whichever single Comet contract you query, and requires the manual ratio math above if you want Aave's granularity. Neither is "correct" — Aave optimizes for a simple integration, Comet's isolated markets optimize for containing risk to one base asset at a time. See the [Aave V3 health factor guide](/blog/aave-v3-health-factor-explained/) for the Aave-side equivalent of everything in this post. Building this into a scheduled job rather than a one-off script? [evmquery for developers](/for/developers) covers the API surface end to end; [blockchain monitoring in Python with evmquery](/blog/blockchain-monitoring-python-evmquery/) turns any of the queries above into a polling loop that fires alerts instead of printing to a terminal. ## Next steps - [Aave V3 health factor explained](/blog/aave-v3-health-factor-explained/): the same problem, solved with a single struct field instead of two booleans - [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/): the batching pattern behind every `.map()` example above - [Blockchain monitoring in Python with evmquery](/blog/blockchain-monitoring-python-evmquery/): turn a liquidation check into a polling loop - [evmquery for developers](/for/developers): the full API surface, MCP server, and REST reference --- # Aave V3 Health Factor in Raw viem: What Leaving evmquery Actually Costs Source: https://evmquery.com/blog/aave-v3-health-factor-viem-migration Published: 2026-08-31 Author: evmquery team Category: trust The hand-rolled viem code behind evmquery's Aave V3 health-factor read: full ABI, the uint256 max edge case, and multicall batching, line by line. Our [own post on how evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts) named two guides on this blog that show only the evmquery call, with no hand-rolled equivalent published next to it: the [Aave V3 health-factor guide](/blog/aave-v3-health-factor-explained) and the Uniswap V3 pool-data guide. It said leaving evmquery for either one means writing the raw multicall yourself. This post writes it, for the Aave one, so that claim isn't just a promise. The evmquery expression behind the Aave V3 health-factor guide is 6 lines. The raw viem equivalent, decimals and the `uint256` max edge case handled correctly, is 33 lines for a single wallet and adds a `multicall` block for a wallet list. Both were run live against the same contract on 2026-08-31; the numbers below aren't estimated. - Single-wallet health factor: 6 lines of evmquery CEL vs. 33 lines of viem (ABI fragment, `readContract` call, decimal formatting, `uint256` max guard). - Multi-wallet batching: evmquery's `wallets.map(...)` needs zero extra code; viem needs a second code path built on `client.multicall`, non-optional if you want one round trip instead of one per wallet. - The full 6-field ABI fragment for `getUserAccountData` is 12 lines and, once written, never changes for this function, since Aave hasn't touched the signature since V3 launched. - Both code paths in this post were executed live on 2026-08-31: evmquery against the hosted API, viem against a public Ethereum RPC, same wallet, same contract, three blocks apart. ## The expression this post is replacing The [Aave health-factor guide](/blog/aave-v3-health-factor-explained) reads all six fields of `getUserAccountData` in one evmquery call: ``` cel.bind(d, aave_pool.getUserAccountData(wallet), { "healthFactor": string(formatUnits(d.healthFactor, 18)), "totalCollateralBase": string(formatUnits(d.totalCollateralBase, 8)), "totalDebtBase": string(formatUnits(d.totalDebtBase, 8)), "availableBorrowsBase": string(formatUnits(d.availableBorrowsBase, 8)), "currentLiquidationThreshold": string(formatUnits(d.currentLiquidationThreshold, 4)), "ltv": string(formatUnits(d.ltv, 4)) }) ``` Re-run live for this post, against the same Aave V3 Pool on Ethereum (`0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2`), it still returns exactly what the decimals table in that guide says it should: ```json { "healthFactor": "1.157920892373162e+59", "totalCollateralBase": "0", "totalDebtBase": "0", "availableBorrowsBase": "0", "currentLiquidationThreshold": "0", "ltv": "0" } ``` Block 25,872,779, 1 on-chain call, 1 round, 2 units. The wallet queried has no open Aave position right now, so every base-currency field is zero and `healthFactor` lands on the special `type(uint256).max` case the guide already explains. That's a real, current result, not a canned example, and it's a useful one for this post specifically, because it's the exact case a hand-rolled migration is most likely to get wrong. ## The raw viem equivalent Here's the full replacement: the ABI fragment for `getUserAccountData`, a `readContract` call, and decimal formatting for all six fields. ```ts const client = createPublicClient({ chain: mainnet, transport: http() }); const AAVE_POOL: `0x${string}` = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"; const poolAbi = [ { name: "getUserAccountData", type: "function", stateMutability: "view", inputs: [{ name: "user", type: "address" }], outputs: [ { name: "totalCollateralBase", type: "uint256" }, { name: "totalDebtBase", type: "uint256" }, { name: "availableBorrowsBase", type: "uint256" }, { name: "currentLiquidationThreshold", type: "uint256" }, { name: "ltv", type: "uint256" }, { name: "healthFactor", type: "uint256" }, ], }, ] as const satisfies Abi; const UINT256_MAX = 2n ** 256n - 1n; function formatHealthFactor(raw: bigint): string { if (raw === UINT256_MAX) return "∞ (no active debt)"; return formatUnits(raw, 18); } async function getHealthFactor(wallet: `0x${string}`) { const [ totalCollateralBase, totalDebtBase, availableBorrowsBase, currentLiquidationThreshold, ltv, healthFactor, ] = await client.readContract({ address: AAVE_POOL, abi: poolAbi, functionName: "getUserAccountData", args: [wallet], }); return { totalCollateralBase: formatUnits(totalCollateralBase, 8), totalDebtBase: formatUnits(totalDebtBase, 8), availableBorrowsBase: formatUnits(availableBorrowsBase, 8), currentLiquidationThreshold: formatUnits(currentLiquidationThreshold, 4), ltv: formatUnits(ltv, 4), healthFactor: formatHealthFactor(healthFactor), }; } ``` 33 lines, 29 non-blank, count them in the block above. Run against the same wallet, against Ethereum mainnet, live for this post: ``` { totalCollateralBase: '0', totalDebtBase: '0', availableBorrowsBase: '0', currentLiquidationThreshold: '0', ltv: '0', healthFactor: '∞ (no active debt)' } ``` Block 25,872,795, three blocks after the evmquery call above (the two ran a few seconds apart against different infrastructure, not simultaneously; neither side is favored by the gap). No proxy resolution step shows up in this code, and that's not an oversight: `getUserAccountData` is called against the proxy's own address either way, evmquery's or viem's, because the EVM's `delegatecall` handles the indirection at the protocol level. What the resolution step buys you, on evmquery's side, isn't correctness here; it's not having to already know Aave's Pool ABI in the first place. That is copy-pasted from Aave's own public `IPool.sol` interface above, the same thing `describe_schema` would have handed back if you'd asked evmquery for it instead. ## The trap: `uint256` max, not a sentinel you'll guess The line that makes or breaks this migration is `formatHealthFactor`'s guard against `2n ** 256n - 1n`. Skip it, and `formatUnits(healthFactor, 18)` on a zero-debt wallet returns the string `"115792089237316195423570985008687907853269984665640564039457584007913129639935"` formatted at 18 decimals, a 59-digit number, rendered straight to whatever UI reads this function's output. Nothing throws. Nothing looks obviously wrong in a type checker or a unit test built around a "normal" wallet. It just quietly ships a 59-digit health factor to production the first time a real user with an open, undrawn credit line loads the page. evmquery doesn't remove this trap by magic, either; the [Aave guide](/blog/aave-v3-health-factor-explained) documents the exact same `type(uint256).max` behavior and tells you to compare against a sane ceiling before rendering. What migrating off evmquery changes is where that knowledge has to live: inside evmquery, in the sense that reading the guide once is enough to write `formatHealthFactor` correctly forever after; in raw viem, in the sense that every fresh integration with this contract has to know to write that guard, or find this guide, before it ships. ## Batching a wallet list The [Aave guide](/blog/aave-v3-health-factor-explained)'s second example scans a watchlist in one round trip with CEL's `map` macro. The viem equivalent is `client.multicall`, and it's a second code path, not a parameter change: ```ts async function getHealthFactors(wallets: readonly `0x${string}`[]) { const results = await client.multicall({ contracts: wallets.map((wallet) => ({ address: AAVE_POOL, abi: poolAbi, functionName: "getUserAccountData", args: [wallet], })), allowFailure: false, }); return results.map(([, , , , , healthFactor]: (typeof results)[number]) => formatHealthFactor(healthFactor), ); } ``` Run live against the same wallet plus a second address, both currently without an open Aave position: ``` [ '∞ (no active debt)', '∞ (no active debt)' ] ``` `client.multicall` defaults to the canonical Multicall3 deployment on any chain that has one, Ethereum included, so this is one on-chain round trip regardless of list length, same as evmquery's `.map()`. The evmquery side of this comparison needed one extra word, `list` instead of `sol_address`, in a type declaration. The viem side needed an entirely separate function, because `readContract` and `multicall` are different client methods with different return shapes, and the tuple destructuring has to happen inside a `.map()` callback instead of being inline. ## What this actually proves Both code blocks above are complete and were run against live infrastructure while writing this post, not sketched from memory. That's the same standard the [evmquery vs. raw viem benchmark](/blog/evmquery-vs-viem-benchmark) holds itself to, and the comparison lands in a similar place: for a well-documented function on a contract this widely integrated, the code you'd write by hand isn't exotic. It's 33 lines instead of 6, plus a second function for batching, and every line of it is public information: Aave's own interface file, the standard `type(uint256).max` sentinel pattern, and viem's own multicall action. What's genuinely different, and worth being honest about since this is a trust-pool post making a portability claim: this comparison was cheap to write because the [Aave guide](/blog/aave-v3-health-factor-explained) had already done the hard part, working out which of the six fields uses which of three decimal scales, and documenting the `uint256` max case, before this post ever touched viem. That knowledge came from reading Aave's Solidity source and testing against a live zero-debt wallet, not from evmquery's resolver. A less-documented contract, one without a blog post like that guide already sitting on the internet, would cost you that research too, on top of the 33 lines above. That's the part evmquery is actually selling: not the six lines of CEL, but not needing to have written the guide yourself first. If you're weighing this trade-off for your own [developer](/for/developers) stack, the honest version is: for a read this well-trodden, the exit cost is real but small. For a contract nobody's written the decimals table for yet, the exit cost is whatever it takes to write that table. ## Next steps - [Aave V3 health factor explained](/blog/aave-v3-health-factor-explained): the decimals table and `uint256` max explanation this post's viem code depends on. - [How evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts): the post that named this gap and covers portability in general. - [evmquery vs. raw viem benchmark](/blog/evmquery-vs-viem-benchmark): the same hand-rolled-vs-evmquery comparison, run against three differently-proxied contracts instead of one. - [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads): the batching mechanics behind both `client.multicall` above and evmquery's own round-trip collapsing. --- # ERC-1155 URI and Metadata: The {id} Substitution, the JSON Schema, and Total Supply Explained Source: https://evmquery.com/blog/erc1155-uri-metadata-guide Published: 2026-08-29 Author: evmquery team Category: reference How ERC-1155's uri() function, the {id} hex substitution rule, the metadata JSON schema, and totalSupply(uint256) actually work, with a worked example and the correct ABI source. Call `uri(1)` on an ERC-1155 contract and you might get back a fully resolved link, or you might get back a template containing the literal string `{id}` that your code is expected to replace. Both are correct behavior under the standard. That ambiguity, plus a total supply function that half of ERC-1155 contracts simply don't have, is why "read an ERC-1155 token" trips up more developers than the equivalent ERC-721 call. - `uri(uint256 id)` is one function that serves every token ID on the contract. ERC-721's `tokenURI(uint256)` returns a distinct value per call; ERC-1155 usually returns the same templated string for every ID and expects the client to substitute in the ID. - The `{id}` placeholder, when present, must be replaced with the token ID in lowercase hexadecimal, zero-padded to 64 characters, with no `0x` prefix. - `totalSupply(uint256)` is not part of the ERC-1155 standard interface. It comes from OpenZeppelin's optional `ERC1155Supply` extension, so calling it on a contract that doesn't inherit that extension reverts, not returns zero. - The core ERC-1155 methods share the same function selectors on every conforming contract, so one generic ERC-1155 ABI decodes `uri()`, `balanceOf()`, and the rest anywhere. Extension methods like `totalSupply()` need their own ABI fragment. ERC-1155's `uri()` is a single function shared by every token ID; when its return value contains `{id}`, replace it with the lowercase hex token ID padded to 64 characters. `totalSupply(uint256)` is an optional OpenZeppelin extension, not part of the base standard, so plenty of contracts don't have it. ## Why one `uri()` function serves every token ID ERC-721 gives every token its own `tokenURI(uint256 tokenId)` call, and the contract is expected to return an already-resolved, individual URL for that ID. ERC-1155 doesn't work that way. The [EIP-1155 spec](https://eips.ethereum.org/EIPS/eip-1155) defines exactly one function, `uri(uint256 id)`, that has to answer for every token ID the contract will ever mint, which for a game-item or trading-card contract can be thousands or millions of distinct IDs. Storing a separate, unique string per ID on-chain for that many tokens would be expensive to write and awkward to update. So the standard allows (and most implementations use) one of two shortcuts: - Return the exact same string for every ID, and let the metadata JSON itself carry per-ID differences, or - Return a URI template containing the literal substring `{id}`, and let the client fill in the specific token ID before fetching it. Neither approach is "more correct" than the other; both are valid ERC-1155 contracts. A contract can also skip the template entirely and just concatenate the raw ID into the string server-side inside `uri()`, which is what plenty of real deployments do. As a concrete example, the demo collection on evmquery's [ERC-1155 Inspector](/tools/erc1155-inspector) returns `ipfs://QmdEWNzkWQhvJp6AMs5iMkZ3xX3idxTp6Ai2mKwYCFWaSs/1` for token ID 1: the raw decimal ID appended directly, no `{id}` placeholder, no substitution needed. Your code has to handle both cases, because you can't know which one a given contract chose without calling `uri()` and checking. ## The `{id}` substitution rule, worked When `{id}` does show up in the returned string, the spec is exact about the replacement: hexadecimal, lowercase, no `0x` prefix, left-padded with zeroes to exactly 64 characters (32 bytes). This is the rule most implementations get subtly wrong, usually by forgetting the padding or leaving the `0x` on. Take token ID `1`. Converted to hex that's just `1`, one character. Padded to 64 characters it becomes: ``` before: 1 after: 0000000000000000000000000000000000000000000000000000000000000001 ``` So a template of `https://token-cdn-domain/{id}.json` resolves to `https://token-cdn-domain/0000000000000000000000000000000000000000000000000000000000000001.json` for token ID 1. The EIP text itself uses a less trivial example worth checking your own implementation against: token ID `314592` (`0x4cce0` in hex) against the same template resolves to: ``` https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json ``` Both examples are exactly 64 hex characters after the domain and before `.json`. If your substitution code produces a shorter string, or a string with `0x` still attached, or mixed-case hex, the resulting URL is wrong and will most likely 404 against a gateway that expects the padded form. Off-chain metadata (name, image, description) is never enforced by the contract itself. `uri()` only tells you where to look; nothing on-chain guarantees the JSON at that address actually matches the schema in the next section, or that it exists at all. ## The ERC-1155 Metadata URI JSON Schema Whatever the resolved URL points to is expected to conform to the schema the EIP defines, called the "ERC-1155 Metadata URI JSON Schema." It's a documentation-level contract, not something enforced on-chain, and every field in it is optional: | Field | Type | Meaning | |---|---|---| | `name` | string | Identifies the asset the token represents. | | `decimals` | integer | Number of decimal places to display, for tokens meant to be shown as fractional amounts (a fungible in-game currency, for example). Defaults to 0, meaning the token displays as a whole number. | | `description` | string | Human-readable description of the asset. | | `image` | string (URI) | Points to a resource with MIME type `image/*`, ideally 1:1 aspect ratio, that represents the asset. | | `properties` | object | Arbitrary key/value pairs. Values may be strings, numbers, objects, or arrays; the schema doesn't constrain what goes in here. | That's the full set. There's no `attributes` array standardized the way marketplaces later converged on for ERC-721 collections; anything beyond `name`, `decimals`, `description`, `image`, and `properties` is a de facto convention some marketplace adopted, not part of EIP-1155 itself. If you're building a reader that has to work across arbitrary ERC-1155 collections, treat every field as optionally absent and don't assume a marketplace-specific extension will be there. ## Reading total supply (and why plenty of contracts don't have it) This is the most common surprise for developers coming from ERC-721, where enumeration and supply tracking are common add-ons. ERC-1155's mandatory interface is limited to `safeTransferFrom`, `safeBatchTransferFrom`, `balanceOf`, `balanceOfBatch`, `setApprovalForAll`, and `isApprovedForAll`, plus the `uri()` read covered above. There is no `totalSupply` in that list. It was never part of the standard. `totalSupply(uint256 id)` exists because OpenZeppelin ships it as an optional extension, `ERC1155Supply`, that a project's contract has to explicitly inherit. The extension adds `totalSupply(uint256 id)` (how many of that specific ID have been minted, net of burns) and `exists(uint256 id)` (whether that ID has ever been minted). A contract that doesn't inherit `ERC1155Supply` simply doesn't have this function at all: the call doesn't revert with "zero supply," it reverts because there's no matching function selector on the deployed bytecode. If you're scripting reads across a batch of collections, expect a meaningful fraction of them to fail on `totalSupply` specifically, independent of whether the contract or the token ID is otherwise valid. Practically, that means: check whether a contract implements the extension before you build a query that depends on it, and don't treat a revert on `totalSupply(id)` as evidence the token doesn't exist. It might exist fine; the contract just never opted into supply tracking. ## Where the ABI for these functions comes from Because `uri()`, `balanceOf()`, `safeTransferFrom()`, and the rest of the core interface are defined by the standard itself, their function selectors are identical on every conforming ERC-1155 contract. That means a single, generic ERC-1155 ABI JSON, whether you copy it from the EIP text, pull it from OpenZeppelin's compiled build artifacts, or import it from a package like viem's built-in ABI helpers, decodes those core methods against any ERC-1155 contract regardless of who deployed it or what else the contract does. The optional extension methods break that guarantee. `totalSupply(uint256)` isn't part of the fixed interface, so it isn't in a generic ERC-1155 ABI; you either need the `ERC1155Supply` extension's own ABI fragment, or you need to confirm the specific contract's verified source before assuming the function is there. evmquery skips the manual part of this entirely. Point it at a contract address and it pulls the verified source for that exact deployment (resolving through an EIP-1967 proxy first if there is one), reconstructs the real interface, and exposes `uri()` and `totalSupply()` as callable methods only when the deployed contract actually has them. There's no static ABI file to keep in sync and no guessing whether a given collection implements the supply extension. ```json { "uri": "token.uri(id)", "totalSupply": "string(formatUnits(token.totalSupply(id), 0))" } ``` That expression, run through the [ERC-1155 Inspector](/tools/erc1155-inspector), returns both fields (or just the URI, if the contract skips supply tracking) in one Multicall3 round trip. The same query is available from [any language your stack already uses](/for/developers) through the REST API, no proxy resolution or ABI management required on your end. ## Next steps - [ERC-1155 Inspector](/tools/erc1155-inspector): run `uri(id)` and `totalSupply(id)` against a live contract and see the `{id}` substitution applied automatically. - [Fixing "could not decode result data (value="0x")" in Ethers and Viem](/blog/decode-result-data-0x-error): what actually happens when you call a function, like `totalSupply`, that the deployed contract doesn't implement. - [evmquery for developers](/for/developers): the full integration story for REST, MCP, and n8n. - [Multicall3 batching for EVM contract reads](/blog/multicall3-batching-evm-contract-reads): why `uri()` and `totalSupply()` above cost one round trip instead of two. --- # ENS Reverse Lookup: Turning Addresses Into Names Without an Indexer Source: https://evmquery.com/blog/ens-reverse-lookup-api Published: 2026-08-24 Author: evmquery team Category: guides Resolve ENS primary names from wallet addresses with one REST call — using the Universal Resolver, which verifies forward resolution for you. Live, tested examples. An ENS reverse lookup is the difference between a dashboard that shows `0xd8dA6BF2…6045` and one that shows `vitalik.eth`. Every wallet UI does it, most block explorers do it, and almost every team that tries to build it themselves either bolts on a vendor's proprietary endpoint or discovers — usually in production — that they wired up the naive version and it can be spoofed. The correct way is a single contract read, and it has been a single contract read since ENS shipped the Universal Resolver. Call `reverse(address, 60)` on the ENS Universal Resolver at `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe` on Ethereum mainnet. It returns the address's primary name and — critically — internally verifies that the name forward-resolves back to that address, so you don't have to do the round-trip check yourself. Anything that skips that verification is spoofable. ## Why reverse lookups are a security question, not a convenience Forward resolution — name to address — is trustworthy by construction. The owner of `vitalik.eth` controls what it points at, and if they point it somewhere wrong, that's their problem. Reverse resolution runs the other way, and there the trust model inverts. Reverse records live in a registry where **any address can set its own reverse record to any string**. Nothing stops an attacker from pointing their address's reverse record at `vitalik.eth`. If your UI reads that record and renders it as a label, you have just built a phishing surface: the attacker's address displays under someone else's identity. The fix is the round-trip. After reading the name from the reverse record, resolve that name forward and confirm it comes back to the address you started with. If it doesn't, discard the name and show the raw address. This is not optional, and it is exactly the step hand-rolled implementations skip. ## The Universal Resolver does the round-trip for you ENS deployed the Universal Resolver to collapse that two-step dance into one call. It lives at the same address on Ethereum mainnet and testnets: ```text 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe ``` The [ENS documentation](https://docs.ens.domains/resolvers/universal/) is explicit about the guarantee: `reverse` internally checks that the name forward-resolves to the address you're looking up, so your implementation doesn't need to do any additional checks. Resolving its ABI live gives the shape you care about: ```text reverse(lookupAddress: bytes, coinType: uint256) -> (primary: string, resolver: address, reverseResolver: address) ``` `coinType` is `60` for Ethereum mainnet — the SLIP-44 coin type for ETH. (More on other chains below; the short version is that it's a trap.) Note that `lookupAddress` is `bytes`, not `address`. That's deliberate — the Universal Resolver is chain-agnostic and accepts raw address bytes of any length. It also means that when you declare the parameter for a query, you declare it as `bytes`. ## One request, one name Here is the whole thing as a `curl` against evmquery's REST API. No SDK, no ABI file, no node connection: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: $EVMQUERY_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "ur": { "address": "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe" } }, "context": { "addr": "bytes" } }, "context": { "addr": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, "expression": "ur.reverseWithGateways(addr, 60, []).primary" }' ``` ```json { "result": { "value": "vitalik.eth", "type": "string" }, "units": { "consumed": 1 } } ``` The ABI is resolved automatically from the verified source — you passed an address and a method name, not an artifact. The two-argument `reverse(address, coinType)` and the three-argument `reverseWithGateways(address, coinType, gateways)` do the same job, but the overloaded return struct on `reverse` decodes ambiguously — its first field loses its name, and reading `.primary` off it fails. Passing an empty gateway list to `reverseWithGateways` gives you a cleanly named `(primary, resolver, reverseResolver)` struct. Use the explicit form. ## Reading a name that isn't there Two non-obvious behaviours, both worth handling before you ship. **An address with no primary name returns an empty string, not a revert.** The USDC contract has never set a reverse record: ```json { "result": { "value": "", "type": "string" } } ``` Branch on `name !== ""`. Don't wrap the call in a try/catch and assume failure means "no name" — a genuine failure and a genuine absence look completely different, and conflating them hides real errors. **An unregistered name resolves to the zero address, not a revert either.** Query the ENS registry for a name nobody owns and you get `0x0000…0000` back. Same rule: check the value, don't catch the exception. ## Batching: label a whole table in one round trip The single-lookup case is the boring one. The case that actually costs you money is a transactions table with 200 addresses in it, where the naive implementation fires 200 RPC calls on every render. evmquery's expression language has a `map` macro, so the batch is still one HTTP request: ```json { "chain": "evm_ethereum", "schema": { "contracts": { "ur": { "address": "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe" } }, "context": { "addrs": "list" } }, "context": { "addrs": [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5", "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7" ] }, "expression": "addrs.map(a, ur.reverseWithGateways(a, 60, []).primary)" } ``` Returned live, in order: ```json ["vitalik.eth", "", "nick.eth", "wallet.ensdao.eth"] ``` The empty slot is USDC — a contract, no reverse record. The results come back positionally, so you can zip them straight back onto your input list. Note the context type is `list`, not `bytes`. Declaring the singular type while passing an array is a type error at evaluation time, and it's the single most common mistake with parameterised list queries. Wrapped up for a frontend, that's a labeling helper in about thirty lines: ```ts const EVMQUERY_URL = "https://api.evmquery.com/api/v1/query"; const UNIVERSAL_RESOLVER = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe"; interface QueryResponse { result: { value: T; type: string }; units: { consumed: number }; } async function evmquery(body: Record): Promise { const res = await fetch(EVMQUERY_URL, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.EVMQUERY_API_KEY!, }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`evmquery ${res.status}: ${await res.text()}`); const json = (await res.json()) as QueryResponse; return json.result.value; } /** Resolve ENS primary names for a batch of addresses. Unnamed addresses are omitted. */ export async function primaryNames( addresses: string[], ): Promise> { const names = await evmquery({ chain: "evm_ethereum", schema: { contracts: { ur: { address: UNIVERSAL_RESOLVER } }, context: { addrs: "list" }, }, context: { addrs: addresses }, expression: "addrs.map(a, ur.reverseWithGateways(a, 60, []).primary)", }); return new Map( addresses .map((address, i): [string, string] => [address, names[i] ?? ""]) .filter(([, name]) => name !== ""), ); } ``` Run against the four addresses above, that returns a two-entry `Map` — `vitalik.eth` and `nick.eth` — with the unnamed addresses filtered out, ready to fall back to a truncated hex label in the UI. ## Going the other way: names, addresses, and text records Forward resolution is where the profile data lives — avatar, Twitter handle, GitHub username, website. It's a two-hop read: ask the ENS registry which resolver owns the name, then ask that resolver for the records. evmquery ships `sel.namehash()` as a built-in, so you never have to precompute the node hash out-of-band: ```json { "chain": "evm_ethereum", "schema": { "contracts": { "registry": { "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" } }, "context": { "name": "string" } }, "context": { "name": "vitalik.eth" }, "expression": "registry.resolver(sel.namehash(name))" } ``` ```json { "result": { "value": "0x231b0ee14048e9dccd1d247744d114a4eb5e8e63" } } ``` With the resolver known, one more query pulls the address and any text records you want, in a single round trip: ```json { "expression": "cel.bind(node, sel.namehash(name), { \"address\": dyn(resolver.addr(node)), \"avatar\": dyn(resolver.text(node, \"avatar\")), \"url\": dyn(resolver.text(node, \"url\")), \"twitter\": dyn(resolver.text(node, \"com.twitter\")) })" } ``` Live result for `vitalik.eth`: ```json { "address": { "value": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" }, "avatar": "https://euc.li/vitalik.eth", "url": "https://vitalik.ca", "twitter": "VitalikButerin" } ``` Two things in that expression earn their keep. `cel.bind` computes the namehash once and reuses it across all four reads instead of recomputing it per call. And `dyn()` wraps each value because a map literal otherwise infers a single value type from its first entry and rejects the rest — mixing an `address` with `string` records without it fails with `Map value uses wrong type`. ## The resolver trap: there is no single "the" Public Resolver Here is the mistake that will cost you an afternoon. Plenty of guides hardcode the ENS Public Resolver address and call `addr()` on it directly, skipping the registry hop. It works — right up until it silently doesn't. Ask the registry which resolver four well-known names actually use: ```json ["vitalik.eth", "nick.eth", "ens.eth", "wallet.ensdao.eth"] ``` ```json [ "0x231b0ee14048e9dccd1d247744d114a4eb5e8e63", "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41", "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41", "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41" ] ``` Two different resolvers across four names — and neither is wrong. ENS has shipped several Public Resolver revisions, names point at whichever one they were configured with, and custom resolvers are a supported feature. Hardcode one and query `nick.eth` against it and you get: ```json { "address": { "value": "0x0000000000000000000000000000000000000000" }, "avatar": "", "github": "" } ``` No error. No revert. Just a zero address and empty strings that look exactly like "this name has no records set" — when in reality you asked the wrong contract. Against the resolver the registry actually names, the same query returns the real data: ```json { "address": { "value": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5" }, "avatar": "https://euc.li/nick.eth", "github": "arachnid", "url": "https://ens.domains/" } ``` That address, `0xb8c2…67d5`, is the same one the reverse lookup earlier resolved to `nick.eth` — the round-trip closes. **Always read `registry.resolver(namehash(name))` first**, or guard the hardcoded path explicitly: ```text registry.resolver(node) == solAddress("0x231b...E63") ``` If you'd rather not think about any of this, that's the argument for the Universal Resolver: it walks the registry, finds the correct resolver, and verifies the round-trip in one call. The registry-plus-resolver path is what you reach for when you want text records, which the Universal Resolver's `reverse` doesn't return. ## What doesn't work: L2 primary names and offchain names Being straight about the limits. [ENSIP-19](https://docs.ens.domains/ensip/19/) lets an address hold a different primary name per chain, keyed by a coin type derived from the chain ID (`chainId ^ 0x80000000` — Base's 8453 becomes `2147492101`). Passing that coin type to `reverseWithGateways` with an empty gateway list **reverts**, because L2 reverse records are served over CCIP-Read: the contract intentionally throws an `OffchainLookup` error that the caller is expected to catch, fetch from an HTTP gateway, and resubmit. evmquery executes onchain reads. It does not follow CCIP-Read offchain callbacks, so ENSIP-19 L2 primary names and offchain/wildcard names — the `.cb.id`-style names that resolve through a gateway rather than a contract — are out of scope here. Coin type `60` (mainnet) and coin type `0x80000000` (the default EVM record) resolve fine; both are plain onchain reads. For the L2-specific records, use a CCIP-Read-aware client like viem's `getEnsName` with the appropriate `coinType`. Worth noting this affects a small minority of names in practice. Mainnet primary names, the ones that cover the overwhelming bulk of what a dashboard needs to label, are a straight contract read. Queries that pass a `bytes` argument to a contract method — which every ENS call does, since node hashes and lookup addresses are both `bytes` — return `meta: null` rather than a `blockNumber`/`totalCalls` block, and bill a flat one unit. The `map` macro does the same. Explicit list literals over plain value-typed methods return the full metadata. If you need a block number pinned alongside an ENS read, fetch it in a separate query. ## Where this fits If you're building agent tooling, address labeling is one of the highest-leverage reads there is: an LLM handed `vitalik.eth` reasons about it far better than one handed forty hex characters, and the same single call works from an [AI agent framework](/for/ai-users) as from a React component. For [developers](/for/developers) wiring this into an existing app, the practical win is that a table of 200 addresses becomes one request instead of 200, without standing up an indexer or paying for a proprietary name-resolution endpoint. The broader pattern — resolve the ABI automatically, batch dependent reads into one expression, skip the artifact management entirely — is the same one behind [reading ERC-20 balances across a wallet list](/blog/erc20-balance-scan-rest-api-typescript) and [batching contract reads with Multicall3](/blog/multicall3-batching-evm-contract-reads). ## Next steps - [Scan ERC-20 balances across many wallets in one call](/blog/erc20-balance-scan-rest-api-typescript) — the same `map` macro, applied to token balances. - [How evmquery resolves contract ABIs](/blog/how-evmquery-resolves-contracts) — why the Universal Resolver query above needed no ABI file. - [Multicall3 batching, explained](/blog/multicall3-batching-evm-contract-reads) — what's happening underneath a batched expression. - [evmquery for developers](/for/developers) — REST, MCP, and n8n surfaces in one place. --- # Permit2 Allowances: The Second Approval Layer Most Tools Never Check Source: https://evmquery.com/blog/permit2-allowance-check-api Published: 2026-08-21 Author: evmquery team Category: guides How to read Permit2's two-layer approval system with a REST API — the token-level wrapper approval, and the per-app sub-allowance that actually decides what a router can move. Ask most developers what `token.allowance(owner, spender)` returns and they'll get it right. Ask what a wallet has actually approved for Uniswap, and most tooling gets it wrong, because since 2022 the answer usually isn't a single ERC-20 approval — it's two. Permit2 sits between the token and the app, and it has its own allowance mapping with its own expiration. Read only the ERC-20 side and you'll see a wallet holding an unlimited approval that, in practice, lets nothing move. Permit2 (`0x000000000022D473030F116dDEE9F6B43aC78BA3`, identical address on Ethereum, Base, Polygon, and BNB Chain) adds a second allowance layer on top of ERC-20's. The wrapper approval — `token.allowance(owner, PERMIT2)` — is usually unlimited and granted once. The real, per-app permission is `permit2.allowance(owner, token, spender)`, which returns an amount, an expiration timestamp, and a nonce. Audit that second call, not the first. ## Why Permit2 exists Before Permit2, every dApp you used needed its own ERC-20 approval: one `approve()` transaction per token, per spender, before the first swap or deposit would work. Permit2, [deployed by Uniswap Labs in 2022](https://blog.uniswap.org/permit2-integration-guide) and now integrated by most of the DeFi routers your users touch — Uniswap's Universal Router, 1inch, 0x, Matcha — collapses that into a single approval. Approve Permit2 once per token, and every Permit2-integrated app can request time-boxed, revocable spend permission from Permit2 itself, off-chain, via a signature instead of a transaction. That's the pitch. The part that trips up anyone reading allowances programmatically is that this design means "how much can this token move" is now a two-step question, not a one-step lookup. ## The two-layer allowance model | Layer | Call | What it means | | ---------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1. Wrapper | `token.allowance(owner, PERMIT2_ADDRESS)` | How much of the token the owner has approved Permit2 itself to pull. Usually the max `uint256` — apps prompt for this once, up front, so the user never has to approve again. | | 2. Sub-allowance | `permit2.allowance(owner, token, spender)` | How much a _specific_ app (the `spender`) is currently permitted to pull, and until when. This is the permission that actually lets a swap or deposit execute. | A wallet can hold an unlimited layer-1 approval and a zero layer-2 sub-allowance for every app that exists — meaning nothing can currently move, despite the alarming-looking `115792089237316195423570985008687907853269984665640564039457584007913129639935` sitting in the wrapper approval. Conversely, revoking the layer-1 approval kills every layer-2 permission at once, which is why revocation tools default to touching layer 1. If you're auditing exposure rather than nuking it, layer 2 is the number that matters. Permit2's ABI, resolved live against the verified contract: ``` DOMAIN_SEPARATOR() -> bytes allowance(owner: address, token: address, spender: address) -> (amount: uint160, expiration: uint48, nonce: uint48) nonceBitmap(owner: address, wordPos: uint256) -> uint256 ``` `nonceBitmap` belongs to a different Permit2 flow — single-use, off-chain `SignatureTransfer` permits — and never shows up in `allowance()`. This guide covers the `AllowanceTransfer` flow, the one with a persistent, queryable, revocable sub-allowance. ## Reading the layer-1 wrapper approval Permit2 is deployed at the same address via deterministic `CREATE2` on every evmquery chain, so the same query works across Ethereum, Base, Polygon, and BNB Chain with nothing but the chain identifier changed. Here's the wrapper approval for USDC and DAI on a real Ethereum wallet: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "usdc": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, "dai": { "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F" } }, "context": { "wallet": "sol_address", "permit2": "sol_address" } }, "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, "expression": "{\"usdc\": usdc.allowance(wallet, permit2), \"dai\": dai.allowance(wallet, permit2)}" }' | python3 -m json.tool ``` Run live against vitalik.eth's main wallet, this returns: ```json { "result": { "value": { "usdc": { "value": "0" }, "dai": { "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935" } }, "type": "map" }, "meta": { "blockNumber": "25803904", "totalCalls": 2, "totalRounds": 1 } } ``` Zero USDC approved to Permit2 — this wallet has never gone through a Permit2-integrated flow with USDC. DAI, on the other hand, shows the max `uint256`: an unlimited wrapper approval, consistent with the `>= 2^160 - 1`-style threshold that flags an approval as "unlimited" rather than a specific amount (the same convention evmquery's own [Token Allowance Checker](/tools/token-allowance) uses). Every wallet that has ever swapped through a Permit2-integrated router has an unlimited layer-1 approval for whatever token it swapped. That's the intended UX — it's what lets the *next* swap skip the approval transaction. It says nothing about which apps currently hold spend permission. For that, you need layer 2. ## Reading the layer-2 sub-allowance This is the call that answers "can this specific app move my tokens right now." `permit2.allowance(owner, token, spender)` returns a struct — `amount`, `expiration`, `nonce` — not a bare integer: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "permit2": { "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3" } }, "context": { "wallet": "sol_address", "dai": "sol_address", "spender": "sol_address" } }, "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "spender": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" }, "expression": "permit2.allowance(wallet, dai, spender)" }' | python3 -m json.tool ``` Spender here is one of Uniswap's Universal Router deployments on Ethereum — Uniswap has shipped several Universal Router versions over time, so treat this as an illustrative spender, not a permanently canonical address; resolve the current one from your own integration or the Uniswap deployments page before using it in production. Against vitalik.eth's wallet, the result is: ```json { "result": { "value": { "amount": "0", "expiration": "0", "nonce": "0" }, "type": "SEL_Struct_permit2_allowance" }, "meta": { "blockNumber": "25803905", "totalCalls": 1, "totalRounds": 1 } } ``` Put the two results side by side and the picture is complete: an unlimited wrapper approval on DAI, and a zero sub-allowance to this particular router. Nothing this router can currently pull from that DAI balance without a fresh signature — the wrapper approval alone tells you nothing about that. This is also exactly why a bare `token.allowance()` check is the wrong tool for auditing Permit2-era approval risk: the number that actually gates a transfer lives in a different contract, keyed by a third argument the ERC-20 standard doesn't have. ## Batching a multi-token audit in one call The same wallet, the same spender, three tokens — one Multicall3 round trip instead of three requests: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "permit2": { "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3" } }, "context": { "wallet": "sol_address", "usdc": "sol_address", "weth": "sol_address", "dai": "sol_address", "spender": "sol_address" } }, "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "spender": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" }, "expression": "{\"usdc\": permit2.allowance(wallet, usdc, spender), \"weth\": permit2.allowance(wallet, weth, spender), \"dai\": permit2.allowance(wallet, dai, spender)}" }' | python3 -m json.tool ``` ```json { "meta": { "blockNumber": "25803905", "totalCalls": 3, "totalRounds": 1 }, "units": { "consumed": 4 } } ``` Three `eth_call`s, one round trip. For a wallet-risk dashboard or an agent that needs to answer "what could actually move from this address right now," this is the shape to reach for: fix the owner and spender, vary the token list, and read the whole exposure surface in one request. See [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/) for the batching mechanics underneath this. ## Checking expiration yourself `allowance()` is a plain storage read — it returns whatever was last written, whether or not that grant has since lapsed. The Permit2 interface documents `expiration` as "a timestamp at which a spender's token allowances become invalid," and that check happens in the transfer path, not in the view function. A nonzero `amount` with an `expiration` in the past is a stale, no-longer-usable grant that `allowance()` will still happily report as if it were live. If you're building anything that decides "is this spend permission currently active" — a risk score, a revocation prompt, an agent's go/no-go check — don't stop at `amount > 0`. Compare the returned `expiration` (a Unix timestamp) against the current time. Permit2's default UI grants typically expire in 30 days; a nonzero amount past its expiration is not a live permission, even though the contract will keep returning it until someone overwrites or explicitly revokes it. ## Next steps - [Token Allowance Checker](/tools/token-allowance): a free, no-signup tool for the layer-1 ERC-20 side of this — paste a token, owner, and spender to see the raw approval - [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/): the batching mechanics behind the multi-token audit above - [ERC-4626 Vault Share Price](/blog/erc4626-vault-share-price/): another two-call DeFi read where the obvious first guess turns out to be the wrong number - [evmquery for developers](/for/developers): what else the API can read besides approvals --- # ERC-4626 Vault Share Price: convertToAssets Across Any Vault, No SDK Source: https://evmquery.com/blog/erc4626-vault-share-price Published: 2026-08-17 Author: evmquery team Category: guides Read any ERC-4626 vault share price in a single REST call using convertToAssets — the vault-vs-asset decimals trap, and batching several vaults together. Every yield vault, whether it's Yearn, Morpho, or a savings wrapper like sDAI, answers the same question: how much of the underlying asset is one share worth right now. Before ERC-4626, answering that meant a different SDK, a different ABI, and a different rounding convention per protocol. After ERC-4626, it's one method signature, `convertToAssets(shares)`, implemented identically on every conforming vault. The catch nobody's ABI file warns you about: that method returns a number scaled to the vault's underlying asset, not to the vault's own `decimals()` — and those two are frequently different. `convertToAssets(shares)` is the ERC-4626 method that returns a vault's share price. Format the result with the underlying asset's `decimals()`, not the vault's own `decimals()` — they diverge on vaults like Morpho's MetaMorpho line, which pad share decimals with a virtual offset to defend against inflation attacks. ## What ERC-4626 actually standardizes ERC-4626 is an extension of ERC-20: an ERC-4626 vault is itself an ERC-20 token, and its balance represents a claim on a pool of some other asset. What the standard adds on top of plain ERC-20 is five view methods every conforming vault must implement identically: | Method | Returns | Meaning | |--------|---------|---------| | `asset()` | `address` | The single underlying ERC-20 token the vault holds | | `totalAssets()` | `uint256` | Total underlying assets under management | | `totalSupply()` | `uint256` | Total shares outstanding (inherited from ERC-20) | | `convertToShares(assets)` | `uint256` | How many shares a given amount of assets is worth | | `convertToAssets(shares)` | `uint256` | How many assets a given amount of shares is worth | The full spec (deposit/mint/withdraw/redeem and their preview variants) is worth reading at [eips.ethereum.org/EIPS/eip-4626](https://eips.ethereum.org/EIPS/eip-4626) if you're integrating write paths. For a read-only price feed, the five methods above are the entire surface area — and because every conforming vault implements them the same way, the exact same expression works against Yearn's vaults, Morpho's MetaMorpho vaults, and MakerDAO's sDAI without touching anything but the contract address. ## Reading a vault's share price with convertToAssets sDAI, MakerDAO's Dai Savings Rate wrapper, is the simplest possible ERC-4626 example: one share converts to slightly more than one DAI, and the ratio grows as savings interest accrues. ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "sdai": { "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA" } } }, "expression": "cel.bind(oneShare, parseUnits(\"1\", sdai.decimals()), formatUnits(sdai.convertToAssets(oneShare), sdai.decimals()))" }' | python3 -m json.tool ``` Run live against the current mainnet deployment, this returns: ```json { "result": { "value": 1.179379598357122, "type": "double" }, "meta": { "blockNumber": "25772359" } } ``` One sDAI share is worth roughly 1.1794 DAI as of block 25772359. `cel.bind` computes `parseUnits("1", sdai.decimals())` once and reuses it, so the call resolves to a single `eth_call` rather than a round trip per intermediate value. This particular expression is deceptively easy to get right, because sDAI and DAI both use 18 decimals — format by either one and you get the same answer. That coincidence is exactly what breaks on the next vault. ## The decimals trap: vault decimals vs. asset decimals Swap in a USDC vault and the identical expression, formatted the identical way, produces a number that's off by a factor of a trillion. Here's Re7 Labs' curated USDC vault on Morpho, on Base: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_base", "schema": { "contracts": { "vault": { "address": "0x12AFDeFb2237a5963e7BAb3e2D46ad0eee70406e" } } }, "expression": "vault.decimals()" }' ``` That returns `18` — but the vault holds USDC, a 6-decimal token. Format `convertToAssets()`'s raw return value with `formatUnits(value, 18)` (the vault's own decimals) and one share prices out at `0.000000000000205186` — off by 10^12, because you scaled a 6-decimal-denominated integer as if it were 18-decimal. Format it correctly, with the *asset's* decimals, and it's `0.205186` USDC per share: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_base", "schema": { "contracts": { "vault": { "address": "0x12AFDeFb2237a5963e7BAb3e2D46ad0eee70406e" } } }, "expression": "cel.bind(oneShare, parseUnits(\"1\", vault.decimals()), formatUnits(vault.convertToAssets(oneShare), 6))" }' | python3 -m json.tool ``` ```json { "result": { "value": 0.205186, "type": "double" }, "meta": { "blockNumber": "50075482" } } ``` This isn't a bug in the vault, it's a deliberate defense. MetaMorpho vaults (Morpho's ERC-4626 wrapper) expose a `DECIMALS_OFFSET()` method — `12` on this vault — and pad `decimals()` to `underlying_decimals + offset` (`6 + 12 = 18`) using virtual shares. That padding makes early-depositor inflation attacks (donating assets to a near-empty vault to skew the share price before the next depositor's rounding) prohibitively expensive, at the cost of making `vault.decimals()` useless as a formatting hint. sDAI happens to use 18 decimals for both the vault and DAI, so the two numbers looked interchangeable. They aren't, in general. Never assume a vault's `decimals()` matches its underlying asset's. Call `asset()` once, resolve that address's own `decimals()`, and format `convertToAssets()`'s result with the asset's decimal count — every time, for every vault, regardless of what the vault's own `decimals()` returns. ## Batching multiple vault share prices in one request Because every ERC-4626 vault exposes the same method signatures, comparing share prices across vaults on the same chain is one Multicall3 round trip, not one request per vault. Here's sDAI next to Steakhouse USDC, a flagship MetaMorpho vault, both on Ethereum: ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "sdai": { "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA" }, "steakusdc": { "address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB" } } }, "expression": "{\"sdai_per_dai\": formatUnits(sdai.convertToAssets(parseUnits(\"1\", sdai.decimals())), sdai.decimals()), \"steakusdc_per_usdc\": formatUnits(steakusdc.convertToAssets(parseUnits(\"1\", steakusdc.decimals())), 6)}" }' | python3 -m json.tool ``` ```json { "result": { "value": { "sdai_per_dai": 1.1793796039320332, "steakusdc_per_usdc": 1.136558 }, "type": "map" }, "meta": { "blockNumber": "25772360" } } ``` Both vaults resolve in one HTTP round trip: 2 `eth_call`s batched through Multicall3, 1 execution round, 3 units consumed. See [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/) for the batching mechanics underneath this. Note the underlying-asset decimals still have to be supplied per vault (`sdai.decimals()` happens to work for sDAI, `6` is hardcoded for USDC) — batching doesn't remove the decimals trap from the previous section, it just lets you pay for both vaults' reads in a single request instead of two. ## A CEL gotcha: mixed-type list literals The natural first instinct is to pull `convertToAssets`, `totalAssets`, and `asset()` in one list expression. That fails: ```text [vault.convertToAssets(oneShare), vault.totalAssets(), vault.asset()] → error: List elements must have the same type, expected type 'sol_int' but found 'sol_address' ``` CEL list literals require every element to share a type, and `asset()` returns a `sol_address` while the others return `sol_int`. Two fixes: split `asset()` into its own call (it only needs fetching once per vault anyway, not per query), or use a CEL map literal with `{ "key": value, ... }` syntax like the batching example above — map literals don't have the homogeneous-type restriction that list literals do, since each field is typed independently by its key. If you want to explore an unfamiliar vault's full method set before writing an expression against it, evmquery's [Contract Inspector](/tools/contract-inspector) resolves any address's ABI and lists every callable method, including whether it's a standard ERC-4626 vault or something with a nonstandard extension bolted on. ## Next steps - [Contract Inspector](/tools/contract-inspector): paste any vault address and see its full resolved method schema before writing a query - [Aave V3 Health Factor Explained](/blog/aave-v3-health-factor-explained/): another DeFi read where mismatched decimal scales are the recurring bug - [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/): the batching mechanics behind the multi-vault example above - [evmquery for developers](/for/developers): what else the API can read besides vault prices --- # Wallet Balance Change Alerts in n8n: Telegram Notifications With Zero Code Source: https://evmquery.com/blog/wallet-balance-change-alerts-n8n Published: 2026-08-13 Author: evmquery team Category: integrations Get a Telegram alert the moment a wallet's USDT or USDC balance changes, using n8n's evmquery trigger node. No Schedule Trigger, no IF node, no state store. The usual way to build "notify me when X changes" in n8n is a Schedule Trigger, a node to store the last-seen value, an IF node to diff the current value against it, and a habit of remembering to update the stored value on every run. Miss that last step once and your alerts either never fire or fire on every poll. The evmquery community node ships a dedicated trigger — `evmQueryTrigger` — that does the diffing for you. Point it at a wallet and a token, and it only wakes up your workflow when the value actually changed. The evmquery trigger node polls a contract read on a schedule and only fires when the value differs from the last poll. Wire it straight to Telegram (or Slack, or Discord) and skip the Schedule Trigger, the state store, and the IF node entirely. ## Why watch a wallet balance A few illustrative cases, not case studies — the pattern generalizes to whatever balance you care about: - **Treasury or ops alerts.** Know the moment a payout wallet receives or loses funds, without polling a block explorer. - **Personal wallet peace of mind.** Get pinged the instant a wallet you hold keys for moves, which is a cheap tripwire against a compromised key. - **DAO multisig changes.** Catch a multisig's stablecoin balance shifting before a proposal executes, so the team isn't surprised by a transaction they didn't expect. None of these need custom infrastructure. They need a poll, a diff, and a message — which is exactly what the trigger node is built for. ## Install the community node Same node as our [Execute Query walkthrough](/blog/read-smart-contracts-in-n8n), just used in trigger mode instead of action mode. **n8n Cloud / Desktop:** Settings → Community Nodes → Install → paste `n8n-nodes-evmquery` → Install. Both the action node and the trigger node appear 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. Credentials are shared between the action and trigger nodes: Credentials → New → evmquery API → paste your key. Grab one from the [dashboard](https://app.evmquery.com/onboarding?plan=free) — the free tier's 700 units/hour cap comfortably covers hourly polling of a handful of wallets. ## Build the trigger workflow The whole workflow is two nodes: the evmquery trigger and a Telegram node. We'll use vitalik.eth's public address (`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`) as the worked example, watching its USDT and USDC balance on Ethereum. Drop an **evmquery Trigger** node into a new workflow and configure it: ```text Node type: evmQueryTrigger Chain: Ethereum Contracts: usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7 usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 Context: wallet : sol_address = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 Expression: { "usdt": formatUnits(usdt.balanceOf(wallet), usdt.decimals()), "usdc": formatUnits(usdc.balanceOf(wallet), usdc.decimals()) } emitOn: change Poll: Every hour ``` Note the `type` field on the context entry — `sol_address` — the same typed-context convention as the REST API and the MCP tools. This isn't optional decoration; the node needs it to know how to encode the value into the call. The `formatUnits(x.balanceOf(wallet), x.decimals())` pattern avoids hardcoding a decimals literal — both USDT and USDC use 6 decimals on Ethereum mainnet today, but calling `.decimals()` means the expression still works if that ever changes, or if you copy the pattern to a token that uses 18. Wire a **Telegram** node off the trigger's output. The message text references the fields the trigger node emits directly: ```text New balance detected for {{ $json.value.usdt }} USDT / {{ $json.value.usdc }} USDC Previous: {{ $json.previousValue.usdt }} USDT / {{ $json.previousValue.usdc }} USDC Block: {{ $json.blockNumber }} ``` That's the entire workflow. No Schedule Trigger — the trigger node owns its own polling schedule. No IF node — `emitOn: change` only lets the workflow run when something actually moved. No data-store node — the diff state lives inside the trigger node itself. ## How the diff actually works This is the part that's different from the [Execute Query approach](/blog/read-smart-contracts-in-n8n), where you build the diff yourself with a Schedule Trigger, a stored "last state," and an IF node. The trigger node's output shape on a real fire is: ```json { "value": { "usdt": 290.27, "usdc": 37.19 }, "previousValue": { "usdt": 305.0, "usdc": 37.19 }, "blockNumber": 21034992, "type": "object" } ``` `emitOn` has two modes. `change` (the default) fires only when `value` differs from the last stored poll — this is what you want for an alert. `everyPoll` fires on every scheduled poll regardless of whether anything moved, which is closer to a heartbeat than an alert. If your workflow doesn't fire and you expected it to, check which mode is selected before assuming something's broken — someone testing with `change` selected but expecting `everyPoll` behavior will spend a while chasing a phantom bug. The one behavior that trips up almost everyone testing this for the first time: **the first successful poll after you activate the workflow never fires an event.** It silently seeds the node's stored state so there's something to diff against on the next poll. Activate the workflow, wait for the first poll, see nothing happen — that's correct, not broken. The alert fires starting from the second poll, whenever the value has actually changed since the first one. Manually clicking "Fetch Test Event" in the n8n editor behaves differently from a real scheduled poll: it always returns the current value with `previousValue: null`, and it does not touch the node's stored state. It's the right way to confirm your expression and contracts are configured correctly before you activate the workflow, but it won't tell you anything about the change-detection behavior itself — for that you need to actually activate the workflow and let two real polls run. The `pollTimes` / "every hour" schedule UI you configure the trigger with is n8n core's standard polling-schedule component, shared by every polling trigger in n8n (Airtable, Google Sheets, etc.) — not something evmquery built. What's evmquery-specific is the contract read, the typed context, and the `value`/`previousValue`/`blockNumber` diff payload. ## Generalizing it: watch anything, not just balances The trigger node doesn't know or care that the expression happens to compute a token balance. Swap the expression and it becomes a trigger for anything a CEL/SEL expression can compute: **Aave health factor.** Same trigger, same `emitOn: change`, different expression — fire when a position's health factor crosses into risky territory: ```text Expression: aave_pool.getUserAccountData(wallet).healthFactor ``` `getUserAccountData` returns a struct; dot-accessing `.healthFactor` off the result works the same way in the trigger node as it does in the REST API and MCP surfaces. A position with no active debt returns an enormous number (`2^256 / 1e18`, roughly `1.16e59`) rather than an error — that's "infinite health," not a bug, and worth filtering out in a downstream IF if you only care about at-risk positions. **DAO proposal state.** The same enum-mapping trick from the [Execute Query post's DAO proposal recipe](/blog/read-smart-contracts-in-n8n) applies here, except the trigger node's `change` mode replaces the manual "store last state, diff, update store" plumbing entirely: ```text Expression: Governor.state(proposalId) ``` Map the returned integer to `Pending` / `Active` / `Succeeded` / etc. in a downstream Code node if you want a readable label in the Telegram message — the trigger node itself only cares whether the raw value changed. **NFT supply or floor proxies.** `totalSupply()` on an NFT contract makes a clean trigger for "a mint just happened": ```text Expression: Collection.totalSupply() ``` Fire on change, and you've got a mint-watcher with no polling logic of your own to write. In every case the shape is identical: pick a chain, name your contracts, write one expression, set `emitOn`, set a poll interval. The trigger node doesn't distinguish between "balance" and "any other view function" — that distinction only exists in your expression. ## Watching more than one wallet Two ways to do it, and which one you pick depends on whether the wallets are on the same chain. **Same chain, multiple context entries:** add a second `sol_address` context entry and reference both wallets in the expression: ```text Context: wallet1 : sol_address = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 wallet2 : sol_address = 0xYourSecondWalletHere Expression: { "wallet1_usdc": formatUnits(usdc.balanceOf(wallet1), usdc.decimals()), "wallet2_usdc": formatUnits(usdc.balanceOf(wallet2), usdc.decimals()) } ``` The engine batches both calls into a single Multicall3 round on the wire, so adding a second wallet doesn't cost you a second round trip. **Different chains, multiple trigger nodes:** Multicall3 batching is per-chain, so a wallet on Base and a wallet on Ethereum can't share one call regardless of how you structure the expression. Drop in a second evmquery trigger node pointed at the other chain. Both can feed the same downstream Telegram node. ## Mistakes that trip people up - **Expecting `everyPoll` behavior with `change` selected.** If you want a heartbeat message every poll, you need `emitOn: everyPoll`. With `change` selected (the default), silence between polls means nothing moved — that's the node working as intended, not a stuck workflow. - **Forgetting `formatUnits` and decimals.** A raw `balanceOf` call returns the smallest unit, not a human number. If a balance reads about a million times too large, you skipped `formatUnits(x.balanceOf(wallet), x.decimals())`. - **Expecting the first poll to fire.** It won't — it seeds the stored state silently. Give the workflow two poll cycles before assuming it's broken. - **Tight polling intervals across many wallets.** Each query costs a handful of units — the worked example above costs 5 units per poll, trivial against the 700 units/hour free-tier cap for hourly polling. But a 10-second interval scanning a large wallet list can add up fast; pace your poll frequency to the number of wallets you're actually watching, the same caveat the [Execute Query post](/blog/read-smart-contracts-in-n8n) makes about rate limits. ## Next steps - Need the manual diffing approach instead — Schedule Trigger, IF node, and full control over the state store? See [Read Smart Contracts in n8n](/blog/read-smart-contracts-in-n8n), which covers the node's Execute Query action rather than this trigger. - The [automation landing page](/for/automation) has the full expression language reference and more n8n patterns. - Ready to wire this into your own stack? [Get a free API key](https://app.evmquery.com/onboarding?plan=free) — the free tier covers hourly polling of several wallets with room to spare. --- # Codex.io vs evmquery: Enriched Market Data vs Contract-Logic Reads for AI Agents Source: https://evmquery.com/blog/codex-io-vs-evmquery Published: 2026-08-10 Author: evmquery team Category: comparisons Codex.io indexes token prices, trades, and wallets across 80+ chains for AI agents. evmquery reads contract logic live. A fair comparison, and a naming warning. First, a naming note, because it matters more than usual here: this post is about **Codex (codex.io)**, the enriched blockchain market-data API built by Codex Data Inc. — not OpenAI's coding agent of the same name, and not the unrelated "Codex" blockchain network tracked on chain explorers. Ask any of the big assistants which API an AI agent should use for onchain data, and Codex.io's MCP integration comes up alongside Bitquery and Blockscout — it's a real, well-built product, and it deserves the traffic. It just isn't the same kind of tool as an RPC-first query layer, and the gap is worth being precise about. Codex.io is an enriched market-data API: token prices, charts, trades, holders, and wallet stats, aggregated from 80+ chains and delivered over GraphQL, WebSockets, or a keyless pay-per-request model. evmquery is a contract-logic query layer: you name any contract address and write one expression, and evmquery resolves the ABI, unwinds proxies, and returns a typed value read fresh at the current block. If your agent asks "what is this token worth," use Codex. If it asks "what does this specific contract say right now," Codex has no dataset for it — the contract was never traded, and its own logic was never indexed. ## What Codex.io actually exposes Codex runs a GraphQL API over an enormous indexed dataset — Codex states coverage of 70 million+ tokens and 700 million+ wallets across 80+ networks, with prices refreshed roughly every second. On top of the GraphQL layer sit three agent-facing surfaces: a **Docs MCP server** that lets an AI coding tool read Codex's documentation directly, **Codex Skills** (installed with `npx skills add Codex-Data/skills -g --yes`) that map plain-language intent to the right GraphQL query, and **MPP** (Monetized Per-Request) — a keyless, pay-per-request auth model built on the x402 standard, billed at a stated $0.001 per call, that lets an agent query without provisioning an API key up front. The query surface is unambiguously market-data shaped. `token` returns metadata for a single contract; `filterTokens` screens and ranks tokens by more than 100 on-chain signals; `getTokenPrices` returns a liquidity-weighted USD price for up to 25 tokens per call; `holders`, `balances`, and `filterWallets` cover who owns what; `gettokenevents` and `tokenTopTraders` cover trading history. Every one of these queries reads from Codex's own index — nothing in the schema issues a fresh call against a contract's own code. ## What Codex does well Worth naming plainly, because a fair comparison starts by conceding the other side's real strengths. - **Chain breadth an order of magnitude wider than evmquery's.** 80+ networks, EVM and non-EVM alike — Solana sits next to Ethereum, Base, and BNB Chain in the same schema. evmquery covers four. - **Sub-second price data at scale.** Codex's own positioning claims 1-second data availability against 5 seconds for Birdeye and 10 for CoinGecko — vendor-stated numbers, not something we benchmarked, but the order of magnitude is plausible for a purpose-built indexing pipeline. - **Production customers who'd know if it didn't hold up.** Coinbase, Uniswap, Rainbow, Farcaster, and TradingView are named users, which is a reasonable proxy for "this index is reliable at scale." - **Launchpad and long-tail token coverage.** Pump.fun-style launches get indexed near-immediately, which matters a lot for anything trading-adjacent and not at all for a fixed set of established DeFi contracts. - **A genuinely agent-native access model.** Codex Skills plus MPP means an agent can go from zero to a priced query without a human provisioning credentials first — a real piece of infrastructure, not a marketing claim. - **Prediction markets and specialized feeds.** Coverage extends to prediction-market data and Virtuals-style AI-agent tokens, categories most contract-read tools never touch. None of that is a footnote. If your agent's job is "what's this token worth," "who holds it," or "what traded in the last hour," Codex is the right tool and this post ends here. ## Where the friction shows up for contract-logic reads Codex's schema names the shape of what it covers: tokens, prices, trades, wallets, holders. That's an enormous amount of what people want from onchain data. It has no query for what a contract's own code computes right now, because that's not a dataset — it's a live call. Take an ERC-4626 vault. Savings DAI (sDAI) wraps DAI deposited into MakerDAO's Dai Savings Rate, and the number an integration actually needs — how much DAI one sDAI share redeems for — isn't a market price. It's `convertToAssets(shares)`, a function the vault computes from its own internal accounting, independent of whether anyone has ever traded sDAI on a DEX. `getTokenPrices` would return whatever the last trade implies, if there's enough liquidity to trade against at all; it has no way to return the contract's own redemption rate, because that number was never a trade. This is the general shape of the gap, not a one-off: a lending pool's health factor, a Governor's live proposal state, an oracle's staleness check — all of these are functions a contract exposes, not events an indexer captured. Codex indexes what happened on-chain. It has no facility for asking a contract a new question. ## What evmquery does differently evmquery is a contract-logic query layer. You name a contract address and write one expression in SEL (our CEL-based expression language); ABI resolution, proxy unwinding, and Multicall3 batching all happen server-side before you see a typed result. No pre-indexing, no dataset to wait on — if the contract is deployed, it's queryable. ```ts const query = { chain: "evm_ethereum", schema: { contracts: { sdai: { address: "0x83F20F44975D03b1b09e64809B757c47f942BEeA" } }, }, expression: "[sdai.convertToAssets(1000000000000000000), sdai.totalAssets(), sdai.totalSupply()]", }; const res = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey }, body: JSON.stringify(query), }); ``` - Running the query above against evmquery's live API returned all three values in one round: 3 on-chain calls, 1 Multicall3 round, at Ethereum block 25,722,113, for 4 units. - `convertToAssets(1e18)` returned `1179098427782668209` — one sDAI share currently redeems for about 1.1791 DAI, a number that comes from the vault's own accounting, not from any trade. - `totalAssets` and `totalSupply` returned `171234146270582559076080886` and `145224641332610181750485170` — roughly 171.2M DAI backing 145.2M sDAI shares at that block. - `describe_schema` resolved the sDAI ABI automatically, surfacing 27 callable methods including `convertToAssets`, `previewRedeem`, and `maxWithdraw` — no ABI lookup, no contract-verification step. The same engine backs evmquery's [MCP server](/blog/evm-blockchain-mcp-server), so an agent asks the identical question through a typed tool call instead of a REST body. The [AI agent integration overview](/for/ai-users) covers wiring it into Claude, Cursor, or another MCP client. ## A concrete side-by-side | Question an agent might ask | Codex.io | evmquery | | ------------------------------------------------------ | ------------------------------------------------ | -------------------------------------------------------------- | | Current USD price of a token | `getTokenPrices`, liquidity-weighted, sub-second | Not supported; no price oracle unless the contract exposes one | | Top holders of an ERC-20 | `holders` query, one call | Not supported; use an indexer | | Trending tokens by 100+ on-chain signals | `filterTokens`, one call | Not supported; not a screening tool | | Redemption rate of an ERC-4626 vault | No dataset for it; not a trade | One expression, typed result at the current block | | Health factor of a wallet in a lending pool | Not indexed; derived contract state | One expression, proxy resolved automatically | | Five fields across three differently-proxied protocols | Not applicable | One expression, one Multicall3 round | | A vault that deployed this morning | No price data until it trades | Queryable immediately | | Non-EVM chains (Solana, and 70+ others) | Covered | Not supported; Ethereum, Base, BNB Chain, Polygon only | The top three rows are Codex's outright — they're the reason it has Coinbase and Uniswap as customers. The middle three are the reason this post exists. The last row is the honest ceiling on evmquery's scope. ## Where Codex is still the better fit Being straight about the other direction matters as much as the pitch above. Reach for Codex, not evmquery, when: - **Your question is about price or trading history.** Current price, historical OHLC, top traders, holder distribution. Contract reads answer "what is true now, according to the contract itself" — never "what did the market do." - **You need chains beyond Ethereum, Base, BNB Smart Chain, and Polygon.** Codex's 80+ networks, including Solana and other non-EVM chains, dwarf evmquery's four. - **You want token discovery, not a known address.** `filterTokens` screens tens of millions of tokens by signal. evmquery requires you to already know which contract you're asking about. - **You're building anything trading-adjacent.** Screeners, portfolio trackers, launch monitors — Codex's freshness and breadth are the product; a contract-read layer would make you reconstruct an index by hand. - **You want a keyless, agent-native payment model.** MPP's pay-per-request flow means an agent can query without a human provisioning an API key first. evmquery's free tier still needs a key, even if signup is instant and card-free. ## Can you run both Yes, and for a lot of agent stacks that's the right answer. Both speak MCP, so an MCP-aware client can hold both servers at once: Codex answers "what's this worth and who's trading it," evmquery answers "what does this contract say right now," and the model routes between them based on the question it's actually asked. The failure mode is an agent forced to fake one with the other — grinding a market-data API for a value only a contract's own logic can produce, or trying to reconstruct a live price feed from point-in-time contract reads. Both produce answers. Neither produces correct ones reliably. ## Next steps - [Bitquery vs evmquery](/blog/bitquery-vs-evmquery) makes the same argument against a different indexed-analytics platform — the shape of the gap is nearly identical. - [Blockscout alternative for contract reads](/blog/blockscout-alternative) covers the explorer-API version of this same layer mismatch. - [The evmquery MCP server](/blog/evm-blockchain-mcp-server) walks through connecting the contract-read side to Claude, Cursor, or VS Code. - [AI agent integrations](/for/ai-users) for the overview before picking a client. --- # How to Decode Ethereum Calldata Without an ABI File Source: https://evmquery.com/blog/decode-calldata-without-abi-file Published: 2026-08-10 Author: evmquery team Category: guides You have a contract address and a raw 0x calldata blob, no ABI file. Here's how to decode the function call and its arguments anyway, with a worked example. You're looking at a transaction's input data: a `0x`-prefixed hex blob a few hundred characters long, sent to a contract address you may or may not recognize. You need to know what function it calls and what arguments it passes. What you don't have is the contract's ABI, and every calldata decoder you've tried so far wants you to paste one in before it will do anything. Calldata alone can't tell you what function it calls, its 4-byte selector is a one-way hash. You either need the ABI already, or you need to resolve it from the contract address first. evmquery's [Calldata Decoder](/tools/calldata-decoder) does the resolution automatically, verified source, then code-reuse and signature matching, following proxies, and decodes against whatever it finds. Paste an address and calldata, get a typed function call back. ## The problem: an address and some bytes, nothing else This situation is more common than it looks like it should be: - You're watching a mempool feed and see a transaction headed for a contract you haven't integrated with. - A multisig queue shows a target address and a hex blob waiting for signatures. - A transaction reverted, and before you chase the revert reason you want to confirm the call you built is the call you meant to build. - The contract is unverified on the block explorer, so the explorer's own decoder shows `Method ID: 0x...` and stops there. - The address is a proxy, and the ABI you'd need lives on an implementation contract you haven't looked up yet. In every one of these, you have the two inputs a decoder needs least, an address and some bytes, and not the one it actually wants: a matching ABI. ## Why you can't decode a selector back into a function Calldata has exactly two parts. The first four bytes are the function selector, the leading four bytes of the `keccak256` hash of the function's canonical signature. `transfer(address,uint256)` hashes down to `0xa9059cbb`. Everything after those four bytes is the ABI-encoded argument list, packed into 32-byte words. The second part is mechanical to decode once you know the types involved; any ABI library handles it. The first part is the actual obstacle. A hash only goes one way. You cannot take `0xa9059cbb` and derive `transfer(address,uint256)` from it. You either already know the signature, or you look the selector up somewhere and hope the answer is right for this specific contract. That "hope" is where things go wrong. Signature databases like 4byte and OpenChain are crowd-sourced indexes of selector-to-signature mappings, and a 4-byte selector is short enough that unrelated functions collide on the same one. A lookup can hand you a plausible-looking signature that isn't what the contract in front of you actually implements, decode the arguments against it anyway, and give you numbers that look fine and are wrong. ## Why "paste your ABI" tools don't solve the actual problem Most calldata decoders on the market, [bia.is](https://bia.is/tools/abi-decoder/)'s ABI decoder and solarity.dev's decoder among them, are built around a text area where you paste the contract's ABI JSON first. Once you have it, decoding the calldata against it is the easy, solved half of the job. Finding the right ABI for an unverified contract, a proxy, or one of forty near-identical deployments of the same protocol is the hard half, and it's the half these tools hand back to you. If you already have the ABI sitting in a file, that's fine, any of those tools will decode against it correctly. The situation this post is about is the one where you don't: you have an address and some bytes, and getting from there to an ABI is itself the work. ## Resolving the ABI from the address instead The fix is to invert the order: resolve the ABI from the contract address first, then use the resolved method set as the only candidates a selector match is checked against. That's a materially different search space. Instead of "every signature anyone has ever uploaded to a public database," the candidates become "the methods this specific contract actually exposes," which removes most of the collision risk a bare 4byte lookup carries. This is the same resolution [Contract Inspector](/tools/contract-inspector) uses, and it's covered in full in [how evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts): verified source first (Sourcify, Etherscan), then code-reuse matching against known bytecode, IPFS metadata, known interfaces, and signature databases, following EIP-1967, UUPS, Beacon, EIP-1167, EIP-2535 diamond, Gnosis Safe, and EIP-7702 proxy patterns to the implementation that actually holds the logic. A proxy's own bytecode is usually a thin forwarder with almost nothing decodable in it; the real ABI lives on whatever it delegates to. - The candidate set for a selector match is "methods this contract exposes," not "every signature ever indexed," so overloads and cross-contract selector collisions resolve to the right function instead of a plausible-sounding wrong one. - Proxy addresses decode against the implementation's ABI automatically. You never manually read an EIP-1967 storage slot to find it. - Each resolved method reports where its ABI came from (verified source vs. a signature database), so you can tell a high-confidence decode from a best-effort one. ## Worked example: decoding a USDC balance check Here's what that looks like end to end, using [evmquery's Calldata Decoder](/tools/calldata-decoder) against USDC on Ethereum (`0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) and this calldata, a balance check against a Binance hot wallet: ``` 0x70a0823100000000000000000000000028c6c06298d514db089934071355e5743bf21d60 ``` Three fields go into the tool: the contract address, the chain (Ethereum), and the calldata above. No ABI field exists to fill in, because the tool doesn't need one from you. The result panel decodes it as: - **Function**: `balanceOf(address)` - **Selector**: `0x70a08231` - **Executes at**: `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` (USDC's proxy resolves to its implementation before matching runs) - **Source**: verified source - **Arguments**: `account = 0x28C6c06298d514Db089934071355E5743bf21d60` That single argument is the address whose balance the call reads, in this case a known Binance hot wallet. Nothing about the lookup is specific to the tool, it's what any ABI decoder does once it has the right types, the only step this replaces is finding those types in the first place. ## Build it yourself The tool's result panel is a thin layer over two steps you can run directly: resolve the ABI over REST, then decode locally with an ABI library. The example below repeats the USDC balance check from above. ### Resolve the ABI ```bash curl -X POST https://api.evmquery.com/api/v1/query/describe \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "target": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } }, "include": ["resolution"] }' ``` The response lists every method evmquery resolved for that address, each carrying a real ABI fragment (`name`, `inputs`, `outputs`, `stateMutability`) and, for proxies, the `executesAt` address its logic actually runs on. ### Decode against it ```ts const CALLDATA = "0x70a0823100000000000000000000000028c6c06298d514db089934071355e5743bf21d60"; const resp = await fetch("https://api.evmquery.com/api/v1/query/describe", { method: "POST", headers: { "x-api-key": process.env.EVMQUERY_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ chain: "evm_ethereum", schema: { contracts: { target: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }, }, include: ["resolution"], }), }); const { contracts } = await resp.json(); const selector = CALLDATA.slice(0, 10); const match = contracts[0].methods.find( (m) => toFunctionSelector(m.abi) === selector, ); if (!match) throw new Error(`no resolved method with selector ${selector}`); const args = decodeAbiParameters(match.abi.inputs, `0x${CALLDATA.slice(10)}`); console.log(match.abi.name, args); // balanceOf [ '0x28C6c06298d514Db089934071355E5743bf21d60' ] ``` `toFunctionSelector` computes each resolved method's selector locally; the one that matches the calldata's first four bytes is the function that was called. Everything after that is a normal `decodeAbiParameters` call against that method's `inputs`, no different from decoding against a hand-written ABI, except the ABI itself came from the address instead of a file you had to go find. The [Calldata Decoder page](/tools/calldata-decoder) has the equivalent Python version using `eth_abi` and `eth_utils` if that's your stack. ## Related failure mode: when the output won't decode Everything above is about calldata, the input side of a call. The mirror-image problem shows up on the output side: you call a function and get back `could not decode result data (value="0x")` instead of a value. That's a different failure with different causes (unresolved proxies, wrong chain, a function that doesn't exist on the deployed contract), and [fixing "could not decode result data (0x)" in Ethers and Viem](/blog/decode-result-data-0x-error) walks through all four of them. If you're debugging a transaction end to end, expect to need both: this post for what was sent, that one for what came back. ## Next steps - Try the [Calldata Decoder](/tools/calldata-decoder) on your own transaction, or the [Contract Inspector](/tools/contract-inspector) if a selector doesn't match and you want to see the full resolved method list. - [How evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts) covers the ABI resolution and proxy unwinding behind both tools in more depth. - [Fixing "could not decode result data (0x)"](/blog/decode-result-data-0x-error) for the output-side version of this problem. - [evmquery for developers](/for/developers) for the full integration surface beyond calldata decoding. --- # Which Major Contracts Are Upgradeable Proxies: We Resolved 44 of Them Source: https://evmquery.com/blog/which-contracts-are-upgradeable-proxies Published: 2026-08-09 Updated: 2026-08-12 Author: evmquery team Category: trust 11 of the 44 most-used contracts on Ethereum and Base can swap their code without the address changing. The split is not random: upgradeability clusters where custody clusters. The address you integrated against is not necessarily the code that runs. On many of the contracts you depend on, the bytecode at the address is a thin forwarder — a proxy contract — and the real logic lives at an implementation address the owner can replace with a single admin transaction. We wanted to know how widespread that actually is among the contracts people build on every day, so we resolved 44 of them and counted. 11 of 44 are delegatecall proxies and 33 are not. Upgradeability tracks almost perfectly with whether a contract holds other people's money: both USDC deployments, stETH, Aave V3, Compound V3, EigenLayer's DelegationManager and 5 of 7 bridge contracts can be upgraded. USDT, DAI, WETH and every Uniswap router through V4 cannot. ## What we ran One `describe` call per address against the evmquery API with resolution annotations turned on. This is the same call the [proxy detector](/tools/proxy-detector) makes, and it returns the dispatch route: which pattern matched and which address the call ends up executing at. If you want the mechanics of that resolution step by step, we wrote them up separately in [how evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts). ```bash curl -X POST https://api.evmquery.com/api/v1/query/describe \ -H "content-type: application/json" \ -H "x-api-key: $EVMQUERY_API_KEY" \ -d '{ "chain": "evm_ethereum", "include": ["resolution"], "schema": { "contracts": { "c": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } } }' ``` For USDC that comes back as: ```json { "status": "verified", "route": [ { "kind": "zeppelinos", "to": "0x43506849D7C04F9138D1A2050bbF3A0c054402dd" } ] } ``` An empty `route` means the address runs its own code. A non-empty one means it forwards, and each hop names the pattern and the destination. - 44 contracts plus 3 externally owned accounts, on Ethereum and Base. Sample is hand-picked for usage, not random: major tokens, DeFi protocols, bridges, NFT infrastructure and core infra. - Labels are machine-verified: contracts with a known ticker were confirmed with a live `symbol()` read, so a mistyped address cannot produce a wrongly labelled row. ## The 11 that can change | Contract | Chain | Pattern | Currently executes at | | ---------------------------- | -------- | --------------------- | -------------------------------------------- | | USDC (Circle) | Ethereum | legacy OpenZeppelin | `0x43506849D7C04F9138D1A2050bbF3A0c054402dd` | | USDC native | Base | legacy OpenZeppelin | `0x2Ce6311ddAE708829bc0784C967b7d77D19FD779` | | stETH (Lido) | Ethereum | implementation getter | `0x028271E30a695c0527A0C50cA30603feD004cDb0` | | Aave V3 Pool | Ethereum | EIP-1967 | `0x728a138A4823392C2EFA55e028d434F526fE03CF` | | Compound V3 USDC Comet | Ethereum | EIP-1967 | `0x83D491269720CE925f92C6bF9F66B7a0779A293a` | | Arbitrum Delayed Inbox | Ethereum | EIP-1967 | `0x7C058ad1D0Ee415f7e7f30e62DB1BCf568470a10` | | Optimism L1StandardBridge | Ethereum | EIP-1967 | `0xB37a11AadF167B2F0b8dD85372De4bC66CD4A891` | | Base L1StandardBridge | Ethereum | EIP-1967 | `0x61525EaaCDdB97D9184aFc205827E6A4fd0Bf62A` | | Polygon PoS RootChainManager | Ethereum | implementation getter | `0xF0235dCa8fb0D3999685724dCBB9DD00c5d62DFa` | | Wormhole Core Bridge | Ethereum | EIP-1967 | `0x3c3d457f1522D3540AB3325Aa5f1864E34cBA9D0` | | EigenLayer DelegationManager | Ethereum | EIP-1967 | `0xE7022a128Acd4C6cad7aFf6FA874D61f984BcE75` | Pattern distribution: EIP-1967 seven times, legacy OpenZeppelin twice, implementation getter twice. EIP-1967 is the storage-slot standard behind what's usually called a transparent proxy; the two legacy OpenZeppelin entries (both USDC deployments) predate that standard by a few years. No diamonds (EIP-2535), no minimal proxies (EIP-1167), and no UUPS (EIP-1822) in this set, though all three exist widely elsewhere. Every route was a single hop; nothing in this sample chained two proxies. The word "currently" in that last column is the entire point. Those addresses were true at the moment we read them and carry no guarantee beyond it. ## Upgradeability clusters where custody clusters Sort the 11 by what they do and the pattern is hard to miss. Both USDC deployments, stETH, the Aave lending pool, the Compound market, EigenLayer's delegation manager, and five of the seven bridge contracts we checked. Every one of them either issues an asset or holds a pile of somebody else's. Now the other side. These 33 cannot be upgraded at all: - **Tokens (13):** USDT, DAI, WETH, WBTC, LINK, UNI, CRV, ENS, SHIB, PEPE, MATIC/POL, wstETH, rETH - **DeFi (10):** Uniswap V2 Router, V3 Factory, SwapRouter02, V4 PoolManager, Permit2, Curve 3pool, Morpho Blue, 1inch AggregationRouterV5, 0x Exchange Proxy, Convex Booster - **Infra (5):** Chainlink ETH/USD feed, ENS Registry, Multicall3, Beacon Deposit Contract, Safe Singleton 1.3.0 - **NFT (3):** BAYC, CryptoPunks, Seaport 1.5 - **Bridge (2):** LayerZero EndpointV2, Circle CCTP TokenMessenger The two biggest stablecoins made opposite calls on the same question. USDC sits behind an upgradeable proxy on both chains we checked. USDT, deployed in 2017, cannot be upgraded at all. One issuer kept an exit hatch, the other welded the door shut. The same split runs through DeFi. Uniswap ossifies deliberately, every router and factory through V4 immutable. Aave and Compound keep the ability to patch a live lending market. Neither is wrong, they are different answers to the same tradeoff between fixing bugs and being predictable. Our read: the more a contract looks like a bank, the more likely someone kept the keys. The more it looks like plumbing, the more likely it was welded shut on purpose. ## The name on the contract tells you nothing Chainlink's ETH/USD feed is literally named `EACAggregatorProxy`. It resolves with an empty route. It is not a delegatecall proxy: it forwards reads to the aggregator behind it by ordinary external call, and its own ABI is complete. The aggregator it points to can be repointed, which is a real thing to know about oracles, but no code swap happens underneath your feet at that address. The 0x Exchange Proxy is the same story from the other direction: "proxy" in the name, empty route in the resolution. If you have ever grepped a contract name for "proxy" to decide whether to worry, that check is wrong in both directions. Resolve the address instead. ## EOAs now carry code too Since Pectra shipped EIP-7702, "is this address a contract?" stopped being a yes-or-no question. An externally owned account can point at code that runs in its context. We checked three well-known EOAs: | EOA | Delegation | | ------------- | -------------------------------------------- | | vitalik.eth | `0x5A7FC11397E9a8AD41BF10bf13F22B0a63f96f6d` | | Titan Builder | `0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B` | | beaverbuild | none, plain EOA | Two of three delegate. Resolution reports these as `eip7702-delegation` hops, the same mechanism as a contract proxy route, so code that assumes "EOA means no code" is now wrong in a way that will not announce itself. ## What this means if you integrate An address is a stable identifier for a location, not for behaviour. For the 11 above, the code behind the address you pinned can be replaced without the address changing, without a new deployment for you to notice, and without anyone holding the asset signing anything. Three things worth doing: 1. **Resolve before you trust.** Check what an address actually dispatches to before you write it into a config. Naming, block explorer labels and token lists are all claims; the dispatch route is a fact. 2. **Record the implementation, not just the address.** If you pin dependencies anywhere, pin what it resolved to and when. 3. **Watch the ones that can move.** An implementation change on a contract you depend on is a silent event by design. Nothing emits "your dependency changed" to you. You can run the same check on any address, no API key needed, with the [proxy detector](/tools/proxy-detector). --- # Querying Polygon Contracts with evmquery: Chain ID, Proxy Resolution, and a Live Example Source: https://evmquery.com/blog/querying-polygon-contracts-evmquery Published: 2026-08-08 Author: evmquery team Category: reference evmquery now reads Polygon (evm_polygon, chain ID 137) contracts through REST, MCP, and n8n — same ABI resolution and proxy unwinding as Ethereum, Base, and BNB Chain. Polygon is the fourth chain evmquery reads, joining Ethereum, Base, and BNB Smart Chain. The addition doesn't change how you write queries — it changes one field. Swap `chain` to `evm_polygon` and every schema, expression, and CEL helper you already use keeps working, including the proxy-unwinding logic that turns Aave's minimal proxy contract into a full, callable ABI. - Chain identifier `evm_polygon`, chain ID `137` — this is Polygon PoS mainnet, not Polygon zkEVM (chain ID `1101`), which evmquery does not support. - Same ABI resolution pipeline as every other supported chain: verified source first, then known interfaces, selector databases, and bytecode recovery, following EIP-1967 and other proxy standards automatically. - Available on every surface — REST (`schema.chain: "evm_polygon"`), MCP, and the n8n node — with no separate signup or API key. - Verified live: Aave V3's Pool contract on Polygon (`0x794a61358D6845594F94dc1DB02A252b5b4814aD`) resolves through an EIP-1967 proxy to its implementation and exposes the full `getUserAccountData`, `getReserveData`, and `getReservesList` surface. - Free tier limits (60 units/min, 700 units/hour) apply per chain the same way they do on Ethereum, Base, and BNB Smart Chain. evmquery reads Polygon PoS (`evm_polygon`, chain ID 137) through the same REST, MCP, and n8n surfaces used for Ethereum, Base, and BNB Smart Chain, with identical ABI resolution and EIP-1967 proxy unwinding. Every example below is validated live against Aave V3's Pool contract on Polygon. ## Querying Polygon via REST The only thing that changes from an Ethereum query is the `chain` field. Here's a live read of Aave V3's reserve list on Polygon: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_polygon", "schema": { "contracts": { "aave_pool": { "address": "0x794a61358D6845594F94dc1DB02A252b5b4814aD" } } }, "expression": "aave_pool.getReservesList()" }' ``` This is a real response, captured against the live API: ```json { "result": { "value": [ "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" ], "type": "list" }, "meta": { "blockNumber": 91651660 } } ``` (The full list has 21 reserves; the excerpt above is enough to make the next section's point.) No auth flow, no separate Polygon API key, no different request shape — the `chain` field is the entire integration surface. The same holds for MCP. If you already have evmquery wired into Claude, Cursor, or another MCP client, Polygon shows up as another value for the `chain` parameter on `execute_query` and `describe_schema` — no reconnection, no new tool registration, no separate credentials. If your integration is a client that lets an agent pick the chain per request, Polygon support means one more valid answer to "which chain," not a new code path. ## Same ABI resolution, same proxy unwinding Aave doesn't deploy its Pool contract as a plain, single-address contract. It sits behind an [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) transparent proxy, which is the standard pattern for upgradeable DeFi protocols: the address you call (`0x794a...4aD`) holds no logic of its own, it just delegates every call to an implementation contract that can be swapped out later without changing the address integrators depend on. Calling `getUserAccountData` directly against the proxy's own bytecode would resolve nothing useful — the proxy's real ABI is just `implementation()` and a couple of admin functions. evmquery resolves through the proxy automatically: it reads the EIP-1967 implementation slot, fetches the verified source for the implementation contract from Sourcify, and exposes every one of its methods as if they lived at the proxy address. This is the same resolution pipeline evmquery runs on Ethereum, Base, and BNB Smart Chain — nothing Polygon-specific was needed to make it work, which is the point of testing a new chain against a contract this indirected instead of a flat ERC-20. You can see the resolution yourself with `describe_schema`, which annotates each method with where it actually reads from: ```json { "chain": "evm_polygon", "schema": { "contracts": { "aave_pool": "0x794a61358D6845594F94dc1DB02A252b5b4814aD" } }, "include": ["contracts", "resolution"] } ``` The response shows `dispatches via eip1967 to 0x6030dB989D47cD74FC17bB6F4FcD3A8B29FEe57e` on the contract, and every method (`getUserAccountData`, `getReserveData`, `getReservesList`, `getConfiguration`, and 25 others) annotated with `sourcify, reads from 0x6030d...` — the implementation address, not the proxy. ## Worked example: reserve rates on Polygon The reserve list above includes two addresses that look similar but aren't: `0x2791bca1...84174` is bridged USDC (`USDC.e`, wrapped from Ethereum via the Polygon PoS bridge), and `0x3c499c54...5c3359` is native, Circle-issued USDC. They're different assets with different liquidity — a common gotcha if you're pattern-matching by symbol instead of address. This query reads the live supply rate for native USDC: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_polygon", "schema": { "contracts": { "aave_pool": { "address": "0x794a61358D6845594F94dc1DB02A252b5b4814aD" } }, "context": { "asset": "sol_address" } }, "context": { "asset": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" }, "expression": "formatUnits(aave_pool.getReserveData(asset).currentLiquidityRate, 27)" }' ``` ```json { "result": { "value": 0.028524493958061267, "type": "double" }, "meta": { "blockNumber": 91651683 } } ``` Aave stores interest rates in "ray" units — fixed-point with 27 decimals — so `formatUnits(rate, 27)` converts the raw `currentLiquidityRate` into an annualized fraction. `0.0285` is roughly a 2.85% supply APR for native USDC at the block this was captured. That number moves with utilization, so treat it as a snapshot, not a constant to hardcode. ## Worked example: a wallet's account data `getUserAccountData(user)` is the method most integrations reach for — it returns a struct with the fields you need to render a position: total collateral, total debt, and health factor, all denominated in Aave's base currency (USD, 8 decimals). Here's a live call against Polygon: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_polygon", "schema": { "contracts": { "aave_pool": { "address": "0x794a61358D6845594F94dc1DB02A252b5b4814aD" } }, "context": { "user": "sol_address" } }, "context": { "user": "0x000000000000000000000000000000000000dEaD" }, "expression": "cel.bind(d, aave_pool.getUserAccountData(user), { \"totalCollateralBase\": formatUnits(d.totalCollateralBase, 8), \"totalDebtBase\": formatUnits(d.totalDebtBase, 8), \"healthFactor\": formatUnits(d.healthFactor, 18) })" }' ``` ```json { "result": { "value": { "totalCollateralBase": 6.88907725, "totalDebtBase": 0, "healthFactor": 1.157920892373162e+59 }, "type": "map" } } ``` This is a real address with real collateral and zero debt — and the `healthFactor` value is the edge case worth knowing before you build a monitor on top of it. `2^256 / 1e18 ≈ 1.16e59` is what `healthFactor` returns when there's no active debt to divide by; it represents infinite health, not a bug or an overflow. The [Aave V3 health factor guide](/blog/aave-v3-health-factor-explained) covers the full math and liquidation thresholds if you're building a monitor that needs to handle this case correctly instead of alerting on a number that looks like garbage. ## Where Polygon fits in the multi-chain picture evmquery's chain parameter is per-request, not global — an expression written for `evm_polygon` doesn't automatically also run on `evm_ethereum`. If you need the same read across chains (say, a Multicall3 balance scan on both Ethereum and Polygon), that's still one request per chain, run in parallel by your client, with results fanned in on your side. There's no expression-level chain switch, and that's deliberate: mixing state from two different chains inside one CEL expression would blur "this number came from block X on chain Y" in a way that's easy to get wrong silently. What Polygon support does change is the floor for "can I build this at all." A wallet dashboard, a liquidation monitor, or an agent tool that only reads Ethereum, Base, and BNB Smart Chain now has a real gap for the Polygon side of a user's portfolio — Aave, Uniswap, and QuickSwap all have meaningful TVL there, and a lending or DEX position on Polygon is invisible to a tool that stops at three chains. Adding a fourth chain to an existing integration is usually a config change, not a rewrite: the same contract wrapper that builds a `schema.contracts` object for Ethereum builds the identical shape for Polygon, with a different `chain` string and different addresses. If you're wiring evmquery into an agent or backend service, [evmquery for developers](/for/developers) covers the REST and MCP integration patterns this post's examples are built on. ## What's actually different about Polygon in practice The query language and resolution pipeline don't change chain to chain, but three things are worth knowing before you ship: - **Native gas token is POL**, the 2024 rebrand of MATIC. It has zero effect on read-only queries — evmquery's REST and MCP surfaces never touch gas or send transactions — but it matters if the rest of your stack also writes to Polygon. - **Bridged vs. native assets carry different addresses.** The USDC.e / native USDC split above is the most common instance; the same pattern shows up for other bridged tokens. Don't assume a symbol you recognize maps to the address you expect — resolve by address, always. - **Block times run faster than Ethereum**, so if you're polling on an interval, a fixed 12-second Ethereum-tuned poll loop will lag behind Polygon's actual block cadence. Read `meta.blockNumber` from the response and adjust your polling interval to the chain instead of hardcoding one value for every chain you query. None of these are evmquery quirks — they're Polygon-the-network facts that any integration has to account for, and they're the reason the worked examples above call `getReserveData` and `getUserAccountData` directly against real Polygon state instead of assuming Ethereum's numbers transfer over. ## Next steps - [Multicall3 batching guide](/blog/multicall3-batching-evm-contract-reads): batch these same reads (Polygon's Multicall3 deployment is at the identical `0xcA11bde...` address) into one round trip instead of one call per method. - [Aave V3 health factor explained](/blog/aave-v3-health-factor-explained): the full math behind `getUserAccountData`, including liquidation thresholds and the max-uint256 edge case shown above. - [Chainlink price feed addresses](/blog/chainlink-price-feed-addresses): a reference for the other contract type most dashboards need alongside lending data. - [evmquery for developers](/for/developers): REST and MCP integration patterns for wiring evmquery into an agent or backend service. --- # Bitquery vs evmquery: GraphQL Analytics vs Contract-Logic Reads for AI Agents Source: https://evmquery.com/blog/bitquery-vs-evmquery Published: 2026-08-06 Author: evmquery team Category: comparisons Bitquery indexes DEX trades, holders, and money flow across chains. evmquery reads contract logic live. A fair look at which one your AI agent actually needs. Ask any of the big assistants which API an AI agent should use to read onchain data, and Bitquery's MCP server comes up fast. It deserves to: it's a hosted MCP endpoint over a very large indexed trading dataset, and it answers "what happened" questions across a lot of chains without you writing a line of GraphQL. What it doesn't do — by design, and clearly documented — is call an arbitrary contract method for you. That's a different question, and it's the one this post is about. Bitquery is an indexing and analytics product: DEX trades, holders, balances, transfers, and money flow, pre-indexed and exposed over GraphQL and a hosted MCP server. evmquery is a contract-logic query layer: you point at any contract address, write one expression, and get a typed value read fresh at the current block. If your agent asks "what has been traded," use Bitquery. If it asks "what does this contract say right now," neither an index nor a GraphQL schema will have a column for it. ## What Bitquery's MCP server actually exposes Bitquery runs a hosted MCP server at `mcp.bitquery.io`. Auth is OAuth 2.1 — a browser window opens once, and per Bitquery's docs "the client caches a refresh token for about 30 days and renews it quietly in the background." There's also a less-secure fallback where you append an access token to the server URL as a `token` query parameter. It works with Claude, Cursor, VS Code, Windsurf, and any other MCP-aware client. The product page names six tools: `dex_trades`, `token_holders`, `balances`, `transfers`, `money_flow`, and `nft_trades`. The docs are vaguer, describing "a small set of tools" the model uses "to figure out what's in the dataset and pull rows." Both descriptions point at the same architecture, and it's worth being precise about it, because "plain English" is doing a lot of work in the marketing copy. The plain-English part is not a natural-language-to-GraphQL compiler. Bitquery's own framing is that "a plain-English prompt maps to one tool call with typed params; no hallucinated schema, no GraphQL to hand-write." In other words: the model picks one of a handful of tools and fills in typed parameters like `network`, `protocol`, `pair`, `orderBy`, and `limit`. The GraphQL layer sits underneath as the human-facing interface for the same datasets; the MCP layer is a curated tool surface over the same index. That's a sensible design — it's the reason the MCP server doesn't hallucinate schemas — but it also fixes the ceiling. Your agent can ask anything the six tools cover, and nothing they don't. Bitquery is explicit that this is read-only in the strong sense: "Access is read-only. The agent can't write, delete, or modify anything, even if you ask it to." Good. It also means there is no escape hatch to an arbitrary `eth_call`. ## What Bitquery does well Worth naming plainly, because a fair comparison starts by conceding the other side's real strengths. - **One schema across very different chains.** The Cross-Chain API covers EVM and non-EVM networks — Solana, Tron, and Bitcoin sit next to Ethereum, Base, and BNB Chain in the same GraphQL surface. Bitquery's claim is that "the query you write for Ethereum runs on Solana, BNB or Base by changing a single word," and you can combine chains in a single request using GraphQL aliasing. Nothing in the contract-read world comes close to that breadth. - **Trading analytics that are genuinely hard to build.** Outlier-filtered DEX trades, pre-built OHLC candles at 1-minute through daily intervals, market cap and FDV, token holder distributions, money-flow tracing for AML and forensics work. Every one of these is months of indexing work you'd otherwise own. - **Historical depth.** These are aggregations over history. An agent asking "which wallets accumulated this token last week" is asking a question that only an index can answer; no live contract call reconstructs it. - **Streaming.** WebSocket subscriptions, Kafka, and gRPC delivery exist alongside the request/response API, plus warehouse exports to S3, Snowflake, and BigQuery. Bitquery's published latency figures are under 300ms for gRPC and Kafka and roughly 1 second for WebSocket — vendor-stated numbers, not something we benchmarked. - **The MCP server is a real product, not a wrapper.** Typed tools with a bounded surface are the right way to expose a large dataset to a model. It's the same reasoning behind evmquery's own MCP server. ## Where the friction shows up for contract-logic reads The six MCP tools describe a shape: trades, transfers, balances, holders, flow, NFT trades. Those cover an enormous amount of what people want from onchain data. They cover none of the following: 1. **A protocol Bitquery hasn't indexed.** A vault that launched yesterday, a niche governance contract, a custom AMM with non-standard accounting. There is no `dex_trades` row for a protocol nobody has integrated yet, and the answer to "when will it be indexed" is a roadmap question, not an API question. 2. **A derived value the contract computes.** "What is one share of this vault worth right now" is not a balance, a transfer, or a trade. It's a function the contract exposes, and only the contract knows the answer. Same for a lending pool's health factor, a Governor's proposal snapshot, or an oracle's staleness check. 3. **Freshness at the current block.** Indexes are near-real-time, which is fine for analytics and not fine for anything where the last block matters. A contract read returns the value at the block it executed against, with the block number attached. Bitquery does have a Smart Contract Calls API, and it's easy to misread. It indexes calls that *happened* — the function invoked, input and output parameters, gas, opcodes — as historical records inside transactions. That's a log of past invocations, not a facility for issuing a new one. It answers "who called `swap` on this contract last week," not "what does `convertToAssets` return right now." None of this is a knock on Bitquery. It's an indexing product, and indexing products index things that exist. It's just where the work lands once your agent's question stops being analytics-shaped. ## What evmquery does differently evmquery is a contract-logic query layer. You name a contract address and write one expression in SEL (our CEL-based expression language); ABI resolution, proxy unwinding, and Multicall3 batching all happen server-side before you see a typed result. No pre-indexing, no waiting for coverage — if the contract is deployed, it's queryable. Lido's stETH is a good demonstration, because it's exactly the shape described above. The token address is an Aragon `AppProxyUpgradeable` proxy, and the number most integrations actually want — how much ETH one share is worth — is a function on the implementation, not a balance, a transfer, or a trade. ```ts const query = { chain: "evm_ethereum", schema: { contracts: { steth: { address: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" } }, }, expression: "[steth.getPooledEthByShares(1000000000000000000), steth.getTotalPooledEther(), steth.getTotalShares()]", }; const res = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey }, body: JSON.stringify(query), }); ``` - Running the query above against evmquery's live API returned all three values in one round: 3 onchain calls, 1 Multicall3 round, at Ethereum block 25,695,159, in 320ms. - The address passed in is Lido's stETH proxy. evmquery resolved the implementation ABI from that address alone — 54 callable methods including `getPooledEthByShares`, which the proxy contract itself does not declare. - Raw values returned: `1240996597167330934` (one stETH share, 18 decimals, worth about 1.241 ETH), `9430621807911590802579101` total pooled ether, and `7599232608242199802102056` total shares. - No ABI file, no proxy detection, no Multicall3 wiring, no prior indexing of Lido by evmquery. The contract is the index. The same engine sits behind evmquery's [MCP server](/blog/evm-blockchain-mcp-server), so an agent asks the identical question through a typed tool call rather than a REST body. The [AI agent integration overview](/for/ai-users) covers wiring it into Claude, Cursor, or another MCP client. ## A concrete side-by-side | Question an agent might ask | Bitquery | evmquery | |---|---|---| | Top tokens by 24h DEX volume on Solana | `dex_trades` tool, one call | Not supported; not an EVM contract read | | Holder distribution for an ERC-20 | `token_holders` tool, one call | Not supported; use an indexer | | Money flow between two wallets over 30 days | `money_flow` tool, one call | Not supported; no historical aggregation | | Current share price of an ERC-4626 vault | No tool for it; not an indexed dataset | One expression, typed result at the current block | | Health factor of a wallet in a lending pool | Not a dataset; derived contract state | One expression, proxy resolved automatically | | Five fields across three differently-proxied protocols | Not applicable | One expression, one Multicall3 round | | A protocol that deployed this morning | Wait for indexing | Queryable immediately | | Non-EVM chains (Solana, Tron, Bitcoin) | Covered | Not supported; Ethereum, Base, BNB Chain only | The top three rows are Bitquery's outright. The middle three are the reason this post exists. The last row is the honest ceiling on evmquery's scope. ## Where Bitquery is still the better fit Being straight about the other direction matters as much as the pitch above. Reach for Bitquery, not evmquery, when: - **Your question is about history or aggregation.** Volume, holder counts, flow between addresses, OHLC candles. Contract reads answer "what is true now," never "what happened over the last month." - **You need non-EVM chains.** evmquery covers Ethereum, Base, BNB Smart Chain, and Polygon. Solana, Tron, and Bitcoin are outside its model entirely, and Bitquery treats them as first-class. - **You need streaming.** evmquery has no push mechanism — every read is a request you initiate. Bitquery ships WebSocket, Kafka, and gRPC delivery. - **You want warehouse exports.** S3, Snowflake, BigQuery, and Azure destinations are a real product with no evmquery equivalent. - **Trading analytics is the product.** If you're building a DEX screener or a market-data dashboard, you want a trading index. That's the whole job. One nuance worth flagging, because it circulates in slightly garbled form: Bitquery's pricing page says "every dataset is queryable on every self-service plan — only the rolling history window differs by chain." That's a claim about *datasets*, not about chains. The self-service plans (Personal, Pro, Scale) cover nine core chains; the 40+ figure applies to Enterprise. The product page and the MCP docs also list that ninth chain differently, so check the current list at signup rather than trusting a secondhand summary — including this one. On cost, the two products meter differently. Bitquery's entry plan is Personal at $49/mo month-to-month ($39 billed annually) and is explicitly a "personal-use license no commercial use"; commercial use starts at Pro. There's a 7-day trial with 1,000 API points and 100 MCP credits, and real-time calls spend 5 points each. evmquery's free tier is $0/mo with no card and no commercial-use restriction, metered in units per minute and per hour. Different shapes, different products — worth pricing against your actual query pattern rather than a headline number. ## Can you run both Yes, and for a lot of agent stacks that's the right answer. An MCP-aware client can hold both servers at once: Bitquery answers the analytics questions, evmquery answers the contract-state questions, and the model routes between them based on the question it's given. There's no shared client library to standardize on and no lock-in either direction, because both surfaces are plain MCP. The failure mode we see is agents forced to fake one with the other — either grinding an analytics API for a value only a contract knows, or trying to reconstruct a month of history from point-in-time reads. Both produce answers. Neither produces correct ones reliably. ## Next steps - [Alchemy alternative for contract reads](/blog/alchemy-alternative) makes the same argument against an RPC-first provider rather than an indexer. - [Moralis vs Alchemy vs QuickNode vs evmquery](/blog/moralis-alchemy-quicknode-evmquery-comparison) is the broader four-way comparison across the read-API landscape. - [The evmquery MCP server](/blog/evm-blockchain-mcp-server) covers connecting the contract-read side to Claude, Cursor, or any MCP client. - [Building for AI agents](/for/ai-users) walks through the agent-facing surface end to end. --- # Blockscout Alternative for AI Agent Contract Reads: Where evmquery Fits (and Where Blockscout Still Wins) Source: https://evmquery.com/blog/blockscout-alternative Published: 2026-08-06 Author: evmquery team Category: comparisons Blockscout's MCP server gives agents explorer data across a huge chain list. Here's where that stops short for contract-logic reads, and where evmquery removes the ABI and proxy work instead. Ask any of the big assistants which API an AI agent should use to read on-chain data, and Blockscout's MCP server comes back near the top. That's earned: it's a real MCP server, hosted for free at `mcp.blockscout.com`, backed by an explorer that runs on more chains than anyone else's. If you're evaluating a Blockscout alternative for agent contract reads, the useful question isn't "which one is better" — it's which layer your read actually lives on. Blockscout's MCP server is explorer data for agents: addresses, transactions, tokens, NFTs, decoded calls, and labels across an enormous chain list. It's excellent at that. It gets slower when the answer requires reading a specific contract's own logic, because the agent has to resolve the proxy, fetch the right ABI, and issue one `read_contract` call per field. evmquery collapses that into one typed expression resolved server-side — on Ethereum, Base, BNB Smart Chain, and Polygon, which is the trade. ## What "Blockscout alternative" usually means Two different searches share the phrase. The first is "I want a self-hosted block explorer that isn't Blockscout" — this post has nothing for you; Blockscout is the reference implementation of that category and there's no honest case against it. The second is the one that brought most people here: an agent needs live contract state, Blockscout's MCP server was the obvious first stop, and something about the tool loop feels heavier than the question deserves. That's a layer mismatch, not a quality problem, and it's worth walking through precisely. ## What Blockscout actually does well Worth naming plainly, because a fair comparison starts by conceding the other side's real strengths. - **Chain breadth nobody else has.** Blockscout counts more than 3,000 chains running its explorer software, though they're upfront that the number includes ephemeral and experimental testnets. The Pro API that the MCP server now reads from launched July 1, 2026 with one key, one base URL, and coverage across 100+ chains via both REST and JSON-RPC. - **A genuinely good MCP surface.** Sixteen tools, including `get_address_info`, `get_transaction_info`, `get_tokens_by_address`, `get_token_transfers_by_address`, `nft_tokens_by_address`, `lookup_token_by_symbol`, `get_contract_abi`, `read_contract`, and a `direct_api_call` escape hatch for any endpoint the typed tools don't cover. - **Decoded, labelled output.** Blockscout's own framing is that their APIs return "well-structured JSON responses with decoded data, token metadata, and human-readable labels," and that a model handed a decoded function name instead of raw hex writes better explanations. That's true, and it's the single biggest reason their MCP server does well in agent evaluations. - **Context discipline.** The server truncates oversized fields, paginates with opaque cursors, and slices responses rather than dumping raw explorer JSON into a context window. Details that only matter if you've actually run an agent over an API before. - **You can run it yourself.** The server ships as source on GitHub with published Docker images. It's under Blockscout's own licence rather than a standard OSI one, so read it before you build a product on top, but self-hosting is a supported path and the code is right there. - **Free to start.** The hosted MCP endpoint is public, and the Pro API's free tier is 100K credits/day at 5 RPS with no card. None of that is a footnote. If your agent's job is "explain this transaction," "what does this wallet hold," or "what happened on this chain last week," Blockscout is the right answer and this post ends here. ## Where the friction shows up for contract-logic reads Blockscout is an explorer API. Explorers index what already happened — blocks, transactions, transfers, balances, verified sources. Reading what a contract *says right now*, through its own logic, is a different operation, and Blockscout exposes it through exactly one tool: ```text read_contract(chain_id, address, abi, function_name, args='[]', block='latest') ``` Note the third parameter. The agent supplies the ABI — specifically, per Blockscout's own tool description, "the JSON ABI for the specific function being called," which it's told to obtain from `get_contract_abi` first. So a single field costs at least two tool calls, and each tool call is a separate model turn. Then proxies land on top. Reading the Aave V3 Pool on Ethereum (`0x87870Bca...`), `/api/v2/smart-contracts/` returns exactly what you'd expect from an explorer: the contract is verified, it's named `InitializableImmutableAdminUpgradeabilityProxy`, `proxy_type` is `eip1967`, and the implementation is listed. But the `abi` field is the proxy's own five functions — `admin`, `implementation`, `initialize`, `upgradeTo`, `upgradeToAndCall`. No `getUserAccountData`, no `getReserveData`. USDC behaves the same way: `FiatTokenProxy`, `proxy_type` of `eip1967_oz`, and a five-function ABI with no `totalSupply` in it. The proxy information is available — `get_address_info` surfaces proxy type and implementation addresses, and Blockscout documents that clearly. It just isn't carried by the tool that hands the agent an ABI, so the agent has to know to go look for it. Get that wrong and `read_contract` fails against a function the contract obviously has, which is a confusing failure for a model to recover from. Stack it up for one modest question — the USDC supply rate on Aave plus USDC's own supply and decimals: 1. Session init (`__unlock_blockchain_analysis__`) 2. `get_address_info` on the Pool to discover the proxy target 3. `get_contract_abi` on the implementation 4. `read_contract` for `getReserveData` 5. `get_address_info` on USDC 6. `get_contract_abi` on the USDC implementation 7. `read_contract` for `totalSupply` 8. `read_contract` for `decimals` Eight turns, each one a model round trip with tokens and latency attached. An agent that already knows the ABIs can skip a few. An agent meeting the contract for the first time cannot. ## What evmquery does differently evmquery is a contract-logic query layer rather than an explorer API. You name the contracts, write one expression in SEL (our CEL-based expression language), and ABI resolution, proxy unwinding, and Multicall3 batching all happen server-side before you see a typed result. Same engine behind the REST API and the [MCP server](/blog/evm-blockchain-mcp-server), so an agent gets the identical behaviour. The same question as above, in one request: ```ts const query = { chain: "evm_ethereum", schema: { contracts: { pool: { address: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" }, usdc: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, }, context: { usdcAddress: "sol_address" }, }, context: { usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, expression: "[pool.getReserveData(usdcAddress).currentLiquidityRate, usdc.totalSupply(), usdc.decimals()]", }; const res = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey }, body: JSON.stringify(query), }); ``` - The query above ran against evmquery's live API and returned all three values in one HTTP request: 3 on-chain calls, 1 Multicall3 round, at Ethereum block 25,695,172, costing 4 units. - Both contracts in that query are proxies of different types, and both were unwound automatically: the Aave V3 Pool dispatches via `eip1967` to `0x728a138A4823392C2EFA55e028d434F526fE03CF`, USDC via the legacy `zeppelinos` pattern to `0x43506849D7C04F9138D1A2050bbF3A0c054402dd`. - Fetching `/api/v2/smart-contracts/` for that same Aave Pool address returns a 5-function proxy ABI (`admin`, `implementation`, `initialize`, `upgradeTo`, `upgradeToAndCall`); the implementation address returns 69 functions including `getReserveData`. Explorer APIs report the proxy relationship, they don't collapse it for you. - The returned values decode to a 3.398% USDC supply rate (Aave stores it as a ray, 27 decimals) against roughly 49.22 billion USDC in circulation at that block. No ABI lookup, no proxy check, no per-field round trip. That's the whole difference: not better data than an explorer, less agent reasoning between the address and the answer. ## A concrete side-by-side | Question | Blockscout MCP | evmquery | |---|---|---| | What tokens does this address hold on Base? | One `get_tokens_by_address` call, with market data | Not supported; that's indexed wallet data | | Explain what this transaction did | `get_transaction_info` with decoded params and labels | Not supported; evmquery reads state, not history | | What's this contract's ABI? | `get_contract_abi`, returns the proxy's ABI as stored | Implicit; never surfaced because you never need it | | One field from a proxied contract | `get_address_info` → `get_contract_abi` → `read_contract` | One expression | | Five fields across three differently-proxied contracts | Proxy resolution once per contract, then one `read_contract` per field | One expression, one Multicall3 round | | The same read on an Arbitrum Orbit chain | Supported if the chain has a Blockscout instance | Not supported; Ethereum, Base, BNB Smart Chain, and Polygon only | Rows one, two, and six are Blockscout's outright. Row three is a definitional difference rather than a win for either side. Rows four and five are the reason a team ends up searching for a Blockscout alternative: the read is real, the data is public, and the cost is entirely in resolution bookkeeping the agent shouldn't be doing in its context window. ## Where Blockscout is still the better fit Reach for Blockscout, not evmquery, when: - **You need chains beyond Ethereum, Base, BNB Smart Chain, and Polygon.** This is the big one. evmquery covers a handful of chains; Blockscout's Pro API covers 100+ and its explorer software runs on thousands. If you're on an Orbit chain, an OP Stack rollup, or a testnet, this isn't a live comparison. - **Your question is historical.** Transaction lists, transfer history, block contents, "what changed since Tuesday" — that's indexed explorer data. evmquery answers questions about state at a block; it has no history endpoints and won't grow them. - **Your question is wallet- or NFT-shaped.** Holdings with market data, NFT ownership, ENS resolution, token search by symbol. Blockscout has crawled all of it; evmquery would make you enumerate contracts by hand. - **You want decoded transactions and address labels.** Turning a hex calldata blob into `swap(address,uint256,uint256)` with a labelled counterparty is exactly what an explorer is for, and it makes agent explanations noticeably better. - **You need to self-host or audit the stack.** Blockscout's MCP server and explorer are both published as source with Docker images. evmquery is a hosted service. - **You're browsing rather than querying.** If the agent doesn't yet know which contract matters, explorer search beats an expression language that requires you to name addresses up front. ## Can you run both Most agent stacks should. The two servers answer different question shapes and there's no client-library lock-in on either side to make the pairing awkward: both speak MCP, both are one entry in an `mcpServers` config, and an agent with both connected picks per question without any glue from you. A shape we see often: Blockscout for discovery and history — find the contract, read the transaction, label the counterparty — and evmquery for the live state read once the address is known and the answer has to be typed, current, and cheap enough to poll. The [AI agent integration overview](/for/ai-users) covers wiring evmquery into Claude, Cursor, or any other MCP client alongside whatever else you already have connected. ## Next steps - [Alchemy alternative for contract reads](/blog/alchemy-alternative) makes the same argument against an RPC provider rather than an explorer, if that's the layer you're actually on. - [Moralis vs Alchemy vs QuickNode vs evmquery](/blog/moralis-alchemy-quicknode-evmquery-comparison) is the broader four-way view of where each layer starts and stops. - [The evmquery MCP server](/blog/evm-blockchain-mcp-server) walks through connecting the query engine to Claude, Cursor, or VS Code in a couple of minutes. - [AI agent integrations](/for/ai-users) if you want the overview before picking a client. --- # Aave V3 Health Factor Explained: getUserAccountData, Decimals, and the Infinite Health Case Source: https://evmquery.com/blog/aave-v3-health-factor-explained Published: 2026-08-05 Author: evmquery team Category: guides Decode Aave V3's getUserAccountData correctly: six struct fields, three decimal scales, and why a zero-debt wallet returns a healthFactor near 1.16e59. Aave V3's `getUserAccountData(user)` looks like a single, simple read. It returns six `uint256` values in one call, no follow-up requests needed. The catch: those six values are not scaled the same way. Format all of them with the same decimals and you either display garbage or trigger a false liquidation alert the first time you hit a wallet with no debt. Aave V3's `getUserAccountData` returns three fields in 8-decimal base currency, two in 4-decimal ratio form, and `healthFactor` in an 18-decimal wad. A wallet with zero debt returns `healthFactor` as `2^256 / 1e18` (~1.16e59), not an error, not zero, not `null`. ## The six fields getUserAccountData returns Every Aave V3 Pool contract exposes this view function, and it is the single call behind every health-factor dashboard, liquidation bot, and risk widget built on the protocol: ```solidity function getUserAccountData(address user) external view returns ( uint256 totalCollateralBase, uint256 totalDebtBase, uint256 availableBorrowsBase, uint256 currentLiquidationThreshold, uint256 ltv, uint256 healthFactor ); ``` | Field | Meaning | Decimals | |-------|---------|----------| | `totalCollateralBase` | Total deposited collateral, valued in the market's base currency | 8 | | `totalDebtBase` | Total borrowed debt, same base currency | 8 | | `availableBorrowsBase` | How much more the wallet could borrow before hitting its LTV limit | 8 | | `currentLiquidationThreshold` | Weighted average liquidation threshold across the wallet's collateral | 4 | | `ltv` | Weighted average max loan-to-value across the wallet's collateral | 4 | | `healthFactor` | Safety margin: `(collateral * liquidationThreshold) / debt` | 18 | Format every field with `formatUnits(value, 18)` and `totalCollateralBase` reads as a number ten billion times too small. This is the single most common mistake in DIY Aave integrations, and it is entirely a decimals problem, not a data problem. ## Three decimal scales in one struct Aave V3 mixes three different fixed-point conventions in one return value: 1. **8-decimal base currency** (`totalCollateralBase`, `totalDebtBase`, `availableBorrowsBase`). The "base currency" is whatever the market's price oracle denominates prices in, USD on Ethereum, Base, BNB Chain, and Polygon today, at the same 8 decimals Chainlink USD price feeds use. This is a deliberate choice: Aave's oracle already returns prices at 8 decimals, so the accounting layer inherits that scale instead of converting to 18-decimal wei. 2. **4-decimal ratios** (`currentLiquidationThreshold`, `ltv`). These are basis-point-style percentages: a raw value of `7800` means 78.00%. Divide by `10000`, or use `formatUnits(value, 4)` to get `0.78`. 3. **18-decimal wad** (`healthFactor`). This is the one familiar convention, the same scale as an ERC-20 token with 18 decimals. `formatUnits(value, 18)` gives you the number you actually compare against 1.0. To make the scaling concrete, here is a worked example with round numbers (illustrative, not a live wallet): ``` Raw totalCollateralBase = 1_500_000_000_000 (uint256) formatUnits(1_500_000_000_000, 8) = 15,000.00 // $15,000 of collateral Raw currentLiquidationThreshold = 8000 (uint256) formatUnits(8000, 4) = 0.80 // 80% liquidation threshold Raw healthFactor = 1_920_000_000_000_000_000 (uint256) formatUnits(1_920_000_000_000_000_000, 18) = 1.92 // safely above 1.0 ``` Aave V2's equivalent function returned `totalCollateralETH` and `availableBorrowsETH`, denominated in ETH at 18 decimals, because V2 markets priced everything against ETH. Aave V3 generalized this to a per-market "base currency" and switched those three fields to 8 decimals to match the oracle's own price precision. Code ported from a V2 integration that blindly reuses 18 decimals for collateral and debt will under-report every wallet by a factor of 10^10. `healthFactor` itself has stayed an 18-decimal wad across both versions. A health factor of `1.92` means the position could absorb roughly a 48% drop in collateral value (relative to debt) before crossing 1.0 and becoming eligible for liquidation. Anything under `1.0` is liquidatable right now; the `1.0`–`1.5` band is the zone worth polling closely. ## The infinite health factor: zero debt, not zero risk The edge case every integration eventually hits: a wallet with collateral deposited but no active borrows. Aave's Solidity code computes `healthFactor` as `(collateral * threshold) / debt`, and division by a zero `debt` would normally revert. Aave avoids that by special-casing it: when `totalDebtBase` is zero, `healthFactor` returns `type(uint256).max`, the largest possible `uint256`, which is `2^256 - 1`. Formatted at 18 decimals, that constant becomes `2^256 / 1e18`, approximately **1.16 × 10^59**. Here is that exact case, queried live against the Aave V3 Pool on Ethereum mainnet: ```json { "healthFactor": "1.157920892373162e+59", "totalCollateralBase": "11379.62417936", "totalDebtBase": "0", "availableBorrowsBase": "8534.71813452", "currentLiquidationThreshold": "0.78", "ltv": "0.75" } ``` That result is real, read from the live Aave V3 Pool contract (`0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2` on Ethereum) at block 25690712, for a wallet holding open collateral with no borrows against it. Nothing reverted, nothing errored. The 59-digit `healthFactor` is Aave's honest answer to "how close is this position to liquidation," when the answer is "there is no debt to liquidate against." Don't render the raw 59-digit number to users, and don't treat it as an overflow bug. Compare against a sane ceiling (anything ~1e30 or higher is effectively infinite for display purposes) and show an infinity symbol or "no active debt" message instead. Comparing `healthFactor < 1.5` still works correctly without any special-casing, since 1.16e59 is trivially above any real threshold. ## Query it with evmquery evmquery exposes Aave's Solidity struct fields through dot notation, so a single expression reads whichever fields you need without a separate ABI import or six manual calls: ``` aave_pool.getUserAccountData(wallet).healthFactor ``` That expression, run against a wallet with an open zero-debt position, returns the same `1.157920892373162e+59` value shown above, live-validated on the current Ethereum mainnet deployment. To pull all six fields (scaled correctly) in one round trip, bind the struct once with `cel.bind` and format each field with its own decimal count: ```python import os import requests AAVE_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" # Aave V3 Pool, Ethereum resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"aave_pool": {"address": AAVE_POOL}}, "context": {"wallet": "sol_address"}, }, "context": {"wallet": "0xYourWalletAddress"}, "expression": ( 'cel.bind(d, aave_pool.getUserAccountData(wallet), {' ' "healthFactor": string(formatUnits(d.healthFactor, 18)),' ' "totalCollateralBase": string(formatUnits(d.totalCollateralBase, 8)),' ' "totalDebtBase": string(formatUnits(d.totalDebtBase, 8)),' ' "availableBorrowsBase": string(formatUnits(d.availableBorrowsBase, 8)),' ' "currentLiquidationThreshold": string(formatUnits(d.currentLiquidationThreshold, 4)),' ' "ltv": string(formatUnits(d.ltv, 4))' " })" ), }, timeout=10, ) resp.raise_for_status() print(resp.json()["result"]["value"]) ``` `cel.bind(d, aave_pool.getUserAccountData(wallet), {...})` calls the contract once and reuses the returned struct for every field, instead of six separate `eth_call`s. Each field gets `string(formatUnits(...))` with its own decimal count, since CEL map literals require every value to share a type. Swap `AAVE_POOL` for `0xA238DD80C259a72e81d7e4664a9801593F98d1c5` on Base to run the identical query against Aave V3's Base deployment. If you'd rather not write the request by hand, evmquery's [Aave health factor checker](/tools/aave-health) runs this exact expression against any wallet and any of the three supported chains, and already handles the infinite-health display case described above. ### Watch a whole list of wallets in one request A liquidation bot rarely cares about a single wallet. The CEL `map` macro applies the same expression across a list of addresses in one HTTP round trip, instead of one request per wallet: ```python import os import requests AAVE_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" # Aave V3 Pool, Ethereum WATCHLIST = [ "0xWallet1...", "0xWallet2...", "0xWallet3...", ] resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"aave_pool": {"address": AAVE_POOL}}, "context": {"wallets": "list"}, }, "context": {"wallets": WATCHLIST}, "expression": "wallets.map(w, formatUnits(aave_pool.getUserAccountData(w).healthFactor, 18))", }, timeout=10, ) resp.raise_for_status() factors = resp.json()["result"]["value"] # one healthFactor per wallet, same order as WATCHLIST ``` This is a live-validated pattern: run against two real addresses, it returns a `list` with one `healthFactor` per input wallet, in order, including the ~1.16e59 constant for any zero-debt wallet in the list, so cap or filter it before it distorts a lowest-health-factor calculation. The type declaration matters here, `"list"`, not `"sol_address"`, since the runtime value is an array. Getting that singular/plural distinction wrong produces a type error at query time rather than at build time. If you're wiring this into a scheduled job rather than a one-off script, see [evmquery for automation](/for/automation) for running the same expression on a timer instead of babysitting a cron job by hand. ## Next steps - [Aave health factor checker](/tools/aave-health): the free tool that runs this exact query against any wallet - [Blockchain monitoring in Python with evmquery](/blog/blockchain-monitoring-python-evmquery/): turn this query into a polling loop that fires alerts - [Multicall3: batch EVM contract reads](/blog/multicall3-batching-evm-contract-reads/): batching patterns for reading multiple wallets' health factors in one request - [evmquery's free tools](/tools): more DeFi-focused reads built the same way --- # evmquery in Any AI Agent Framework: The Complete Integration Matrix Source: https://evmquery.com/blog/agent-framework-integrations Published: 2026-08-05 Author: evmquery team Category: integrations One-page reference for wiring evmquery's onchain reads into 21 AI agent frameworks, from LangChain to Semantic Kernel. Install command and tool pattern per framework. Every agent framework runs into the same wall eventually: a user asks about a live USDC balance, a Chainlink price, or an Aave health factor, and none of that was in the training data. The fix is the same no matter which framework you're in, one tool function that POSTs a chain, a contract map, and a CEL expression to evmquery, and gets back a decoded, typed value. However you build agents, evmquery is a tool call away. - **MCP endpoint:** `https://api.evmquery.com/mcp` — connect natively from any MCP-aware framework or client (Claude Agent SDK, Claude Desktop, Cursor, Windsurf) with no REST wrapper code. - **REST endpoint:** `POST https://api.evmquery.com/api/v1/query` — the surface every framework below wires a tool function around. - **Auth header:** `x-api-key: YOUR_KEY` on every REST request. Get a free key at [app.evmquery.com/onboarding](https://app.evmquery.com/onboarding?plan=free); the free tier has no monthly cap. This page is the single reference for all 21 frameworks we've wired evmquery into. The 6 most-searched get a full deep-dive post, with three live recipes and copy-pasteable agent code. The other 15 get the shortest useful version here: the install command and the tool-registration pattern below. No per-framework walkthroughs on this page, that's what the flagship posts are for. ## The matrix Install command per framework: | Framework | Install | | --- | --- | | Agno | `pip install agno anthropic requests` | | AutoGen / AG2 | `pip install ag2 requests` | | Azure AI Foundry | `pip install azure-ai-projects azure-ai-agents azure-identity requests` | | BeeAI | `pip install beeai-framework requests` | | Claude Agent SDK | `pip install claude-agent-sdk` (or `npm install @anthropic-ai/claude-agent-sdk`) | | CrewAI | `pip install crewai requests` | | DSPy | `pip install dspy requests` | | ElizaOS | `elizaos create --type plugin plugin-evmquery` | | Genkit | `npm install genkit @genkit-ai/google-genai zod` | | Google ADK | `pip install google-adk requests python-dotenv` | | Haystack | `pip install haystack-ai anthropic-haystack requests` | | LangChain | `pip install langchain-core langchain-anthropic langgraph requests` | | LangGraph | `pip install langgraph langchain-core langchain-anthropic requests` | | LlamaIndex | `pip install llama-index-core llama-index-llms-anthropic requests` | | Mastra | `npm install @mastra/core zod` | | OpenAI Agents SDK | `pip install openai-agents requests` | | Pydantic AI | `pip install pydantic-ai requests` | | Semantic Kernel | `pip install semantic-kernel requests` | | smolagents | `pip install smolagents requests` | | Spring AI | `org.springframework.ai:spring-ai-starter-model-anthropic` (Maven/Gradle) | | Vercel AI SDK | `npm install ai @ai-sdk/anthropic zod` | {/* Once evmquery/examples ships on GitHub, swap the "Full guide" links for the 15 non-flagship frameworks below from their official docs to the corresponding folder in that repo. */} Tool-registration pattern and full guide, same order: - **Agno** — Plain function passed to `Agent(tools=[fn])`, no decorator needed. [Agno docs](https://docs.agno.com) - **AutoGen / AG2** — `autogen.register_function(fn, caller=assistant, executor=user_proxy, ...)`. [AG2 docs](https://docs.ag2.ai) - **Azure AI Foundry** — `FunctionTool(functions={fn})`. [Azure AI Foundry docs](https://learn.microsoft.com/azure/ai-foundry/) - **BeeAI** — `@tool` decorator from `beeai_framework.tools`. [BeeAI framework docs](https://framework.beeai.dev) - **Claude Agent SDK** — Native MCP, `mcp_servers` config plus `allowed_tools`, no wrapper function. [Claude Agent SDK docs](https://docs.claude.com/en/api/agent-sdk/overview) - **CrewAI** — Subclass `BaseTool` (or `@tool` for quick one-offs). [CrewAI: EVM blockchain tool](/blog/crewai-evm-blockchain-tool/) - **DSPy** — Plain function passed to `dspy.ReAct(signature, tools=[fn])`. [DSPy docs](https://dspy.ai) - **ElizaOS** — Custom `Action` object registered on a plugin. [ElizaOS docs](https://eliza.how/docs/intro) - **Genkit** — `ai.defineTool(meta, fn)` with a Zod input/output schema. [Genkit docs](https://genkit.dev) - **Google ADK** — Plain function passed to `Agent(tools=[...])`. [Google ADK docs](https://google.github.io/adk-docs/) - **Haystack** — `@tool` decorator from `haystack.tools`, `Annotated` param descriptions. [Haystack docs](https://haystack.deepset.ai/overview/intro) - **LangChain** — `@tool` decorator from `langchain_core.tools`. [LangChain: EVM blockchain tool](/blog/langchain-evm-blockchain-tool/) - **LangGraph** — `@tool` decorator plus `create_react_agent` from `langgraph.prebuilt`. [LangGraph: EVM blockchain tool](/blog/langgraph-evm-blockchain-tool/) - **LlamaIndex** — `FunctionTool.from_defaults(fn)`. [LlamaIndex docs](https://docs.llamaindex.ai) - **Mastra** — `createTool({ ... })` with a Zod schema. [Mastra docs](https://mastra.ai/docs) - **OpenAI Agents SDK** — `@function_tool` decorator. [OpenAI Agents SDK: EVM blockchain tool](/blog/openai-agents-sdk-evm-blockchain-tool/) - **Pydantic AI** — `@agent.tool_plain` decorator. [Pydantic AI: EVM blockchain tool](/blog/pydantic-ai-evm-blockchain-tool/) - **Semantic Kernel** — `@kernel_function` method on a plugin class. [Semantic Kernel docs](https://learn.microsoft.com/semantic-kernel/overview/) - **smolagents** — Subclass `Tool`, implement `forward()`. [smolagents docs](https://huggingface.co/docs/smolagents) - **Spring AI** — `@Tool`-annotated service method. [Spring AI docs](https://docs.spring.io/spring-ai/reference/) - **Vercel AI SDK** — `tool({ ... })` passed to `streamText()`. [Vercel AI SDK: EVM blockchain tool](/blog/vercel-ai-sdk-blockchain-tool/) Every framework hits the same REST endpoint with the same request shape underneath: a `chain` identifier, a `schema.contracts` map of short names to `{ "address": "0x..." }`, and a CEL `expression` that names those contracts as variables. What differs is only how each framework wraps that call so its model can invoke it. ## Next steps - [evmquery for AI agent builders](/for/ai-users) — the full integration surface for agent-facing onchain reads, MCP and REST side by side - [LangChain: build a custom EVM blockchain tool](/blog/langchain-evm-blockchain-tool/) - [LangGraph: build an EVM blockchain tool](/blog/langgraph-evm-blockchain-tool/) - [CrewAI: build an EVM blockchain tool](/blog/crewai-evm-blockchain-tool/) - [Vercel AI SDK: add a live EVM blockchain tool](/blog/vercel-ai-sdk-blockchain-tool/) - [OpenAI Agents SDK: wire in an EVM blockchain tool](/blog/openai-agents-sdk-evm-blockchain-tool/) - [Pydantic AI: build an EVM blockchain tool](/blog/pydantic-ai-evm-blockchain-tool/) --- # Alchemy Alternative for Contract Reads: Where evmquery Fits (and Where Alchemy Still Wins) Source: https://evmquery.com/blog/alchemy-alternative Published: 2026-08-05 Author: evmquery team Category: comparisons Searching for an Alchemy alternative? A fair, detailed look at what Alchemy does best, where custom contract reads still cost you time, and where evmquery removes that work instead. If you're reading this, you've probably already got an Alchemy account, or you're about to open one, and you're wondering whether it's the right layer for what you're actually building. "Alchemy alternative" is a search people run for a few different reasons: compute unit costs on a growing app, a need for a chain Alchemy doesn't cover, or, the reason this post exists, a read that Alchemy's own APIs don't reach directly. That last case is the one worth walking through in detail. Alchemy is excellent managed RPC infrastructure with real, useful data APIs on top. It stops being the fastest path the moment your read is a custom contract's own logic rather than a standard token balance or NFT lookup. That's the gap evmquery is built for. If you need webhooks, mempool visibility, or write infrastructure, stay on Alchemy; those are outside evmquery's scope entirely. ## What "Alchemy alternative" usually means This site already has a [broader four-way comparison](/blog/moralis-alchemy-quicknode-evmquery-comparison) covering Alchemy, QuickNode, Moralis, and evmquery at a summary level. This post narrows the lens to just Alchemy, and specifically to the question a lot of "alchemy alternative" searches are actually asking: not "who else runs RPC nodes" but "who else can get me a typed answer out of a specific contract without me writing the ABI and proxy plumbing myself." Those are genuinely different questions. If you need a drop-in RPC replacement with the same breadth of chains and the same Enhanced APIs, this post won't tell you to leave Alchemy. If your actual bottleneck is the code you write between "I have an address" and "I have a typed value," keep reading. ## What Alchemy actually does well Alchemy is a managed node provider first, with a genuinely useful set of products layered on top. Worth naming plainly, because a fair comparison starts by conceding the other side's real strengths: - **Broad chain coverage.** Alchemy documents support for 70+ chains, spanning every major EVM network and several non-EVM ones. If your product needs to be everywhere, that breadth is hard to match. - **Data APIs for the common shapes.** Token balances, NFT ownership and metadata, transfer history, and price data all have dedicated, indexed endpoints. You're not writing `eth_call` loops for any of these. - **Webhooks.** Define a filter (address activity, mined transactions, dropped transactions) and get a push notification instead of polling. This is a real product, not a thin wrapper, and it covers a use case evmquery does not attempt. - **Mempool visibility.** WebSocket subscriptions to pending transactions exist and are documented, letting you react before a transaction is mined, something a request/response query layer like evmquery has no equivalent for. - **Write-side infrastructure.** Account abstraction tooling (a bundler, a gas manager for sponsoring fees) sits alongside the read APIs, and standard write methods (`eth_sendRawTransaction` and friends) run through the same RPC endpoint you're already paying for. - **Observability.** The dashboard breaks down compute unit usage per method, which makes it straightforward to see what's actually expensive in your integration. None of that is a footnote. It's the reason Alchemy is a default choice for teams building anything that touches wallets, NFTs, or write transactions at scale. ## Where the friction shows up for contract-logic reads The Enhanced/Data APIs cover the shapes Alchemy chose to index ahead of time: tokens, NFTs, transfers, prices. The moment your read is a specific contract's own logic (a lending pool's health-factor calculation, a DEX's reserve state, a DAO's proposal snapshot), none of that is a standard endpoint. You're back to raw RPC, and three things land on you every time: 1. **ABI sourcing.** You need the contract's ABI, and if it's a new or less common protocol, that means digging through Etherscan, the project's GitHub, or an npm package that ships the interface. 2. **Proxy detection.** Many of the production contracts you'll integrate against sit behind a proxy, an EIP-1967 transparent proxy, a beacon proxy, or an older pattern like the legacy zeppelinOS proxy USDC itself still uses. The address you're given usually isn't where the logic lives, and nothing about the address alone tells you that. Get this wrong and you'll spend an afternoon debugging why a call returned `0x` or a zero value instead of the number you expected. 3. **Batching.** If you want more than one field back in one round trip, you're writing Multicall3 calls by hand, or reaching for a library helper that still requires you to have already solved problems 1 and 2 for every contract involved. None of this is a knock on Alchemy specifically. QuickNode has the same shape of gap for the same reason: both are RPC-first products, and RPC doesn't know what a proxy is or where an ABI lives. It's just where the work actually sits once you're past the standard token and NFT shapes. ## What evmquery does differently evmquery is a contract-logic query layer, not an RPC provider. You name a contract address and write one expression (SEL, our CEL-based expression language); the ABI resolution, proxy unwinding, and Multicall3 batching all happen server-side before you see a typed result. USDC on Ethereum is a convenient real example, because it's exactly the kind of contract described above: it sits behind a legacy zeppelinOS proxy, not the more common EIP-1967 pattern, which is easy to miss if you're checking for proxies by pattern-matching a known slot. ```ts const query = { chain: "evm_ethereum", schema: { contracts: { usdc: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }, }, expression: "[usdc.totalSupply(), usdc.decimals()]", }; const res = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey }, body: JSON.stringify(query), }); ``` - Running the query above against evmquery's live API resolved USDC's proxy automatically and returned `totalSupply()` and `decimals()` in one round: 2 on-chain calls, 1 round, at Ethereum block 25,691,094. - USDC's own resolution reports `dispatches via zeppelinos`, the legacy proxy pattern, confirming it's not the newer EIP-1967 style a proxy-detection script might be written to expect. - The raw values returned were `49219031970982486` (totalSupply, 6 decimals) and `6` (decimals), roughly 49.2 billion circulating USDC at that block. No ABI import, no proxy check, no Multicall3 wiring. That's the whole difference: not faster RPC, less code between the address and the answer. ## A concrete side-by-side Put next to Alchemy's own token balance endpoint, the contrast is really about scope, not quality: | Question | Alchemy | evmquery | |---|---|---| | Standard ERC-20 balance for a wallet | One call to the Token API | One expression, same result | | NFT ownership and metadata | One call to the NFT API | Not evmquery's job; use an indexer | | A custom Governor's `proposalSnapshot(id)` | Raw `eth_call`, your ABI, your proxy check | One expression, ABI and proxy resolved automatically | | A dashboard reading five fields across three differently-proxied protocols | Raw `eth_call` x5 or hand-written Multicall3 | One expression, one Multicall3 round | | Real-time notification when a contract emits an event | Webhooks | Not supported; poll or use Alchemy/QuickNode alongside | The first row is a tie. The second and last rows are Alchemy's to win outright. The middle two are the actual reason a team ends up searching for an Alchemy alternative in the first place: the read is real, the data is public, and the work is entirely in ABI and proxy bookkeeping that a standard indexed endpoint was never going to cover. ## Where Alchemy is still the better fit Being honest about the other direction matters as much as the pitch above. Reach for Alchemy, not evmquery, when: - **You need webhooks.** evmquery has no push mechanism at all; every read is a request you initiate. If "notify me when X happens on-chain" is the actual requirement, Alchemy's webhook product (or QuickNode's equivalent) is the right tool, not a workaround. - **You need mempool or pending-transaction visibility.** evmquery only ever answers questions about confirmed, on-chain state at a given block. Anything upstream of that, seeing a transaction before it's mined, is outside what a query layer like this can do. - **You need to write to the chain.** evmquery is read-only by design. `eth_sendRawTransaction`, account abstraction, gas sponsorship, all of that lives on the RPC side, and an RPC provider like Alchemy running alongside evmquery covers exactly this gap. - **Your reads are wallet- or NFT-shaped, not contract-shaped.** A wallet's token holdings, its NFT collection, its transfer history: that's indexed data Alchemy has already crawled. Querying a specific contract's own state one expression at a time doesn't buy you anything there. - **You need chains evmquery doesn't support.** evmquery currently covers Ethereum, Base, BNB Smart Chain, and Polygon. Alchemy's chain list is far wider; if you're building for a chain outside those four, this isn't a live question. ## Can you run both Keeping an RPC provider in the stack alongside evmquery is expected, not a failure of either product. A common shape looks like Alchemy (or QuickNode) handling RPC, webhooks, and any write transactions, while evmquery handles the specific contract-logic reads that would otherwise mean writing and maintaining ABI and proxy code by hand. Neither vendor asks you to standardize on its client library exclusively, and the boundaries are thin enough that switching either piece later doesn't require rewriting the other. If you're building for an AI agent rather than your own backend code, the same resolution runs behind evmquery's MCP server instead of the REST endpoint shown above; the [AI agent integration overview](/for/ai-users) covers connecting it to Claude, Cursor, or another MCP-aware client directly. ## Next steps - [Moralis vs Alchemy vs QuickNode vs evmquery](/blog/moralis-alchemy-quicknode-evmquery-comparison) is the broader four-way comparison this post narrows down from. - [evmquery vs. raw viem benchmark](/blog/evmquery-vs-viem-benchmark) puts real line counts and round-trip numbers on the same ABI and proxy work described here. - [How evmquery resolves a contract read](/blog/how-evmquery-resolves-contracts) walks through the ABI resolution and proxy unwinding pipeline in more depth. - [Pricing](/pricing) if you want to run the exact query in this post against your own contracts. --- # Chainlink Price Feed Addresses: Ethereum, Base, and BNB Chain Reference Source: https://evmquery.com/blog/chainlink-price-feed-addresses Published: 2026-08-05 Author: evmquery team Category: reference Chainlink price feed contract addresses for ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain, with decimals, heartbeat intervals, and a working REST example. Every Chainlink price feed is a contract you can read directly, no SDK required, but the addresses live in a registry that changes per chain and per pair. Get one wrong and you're reading a stale feed, a deprecated aggregator, or the wrong asset entirely. This page is a checked reference for the pairs developers ask for most: ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain. - `latestRoundData()` returns five fields: `roundId`, `answer`, `startedAt`, `updatedAt`, and `answeredInRound`. `answer` is the raw price; the other four exist so you can validate freshness. - Decimals are not always 8. USD-quoted pairs on the chains below all use 8, but some crypto-quoted pairs elsewhere use 18. Call `decimals()` and scale with it, don't hardcode `1e8`. - Staleness must be checked against `updatedAt`, never assumed. Compare `updatedAt` to the current time and to the feed's documented heartbeat before trusting the value. - Every address below was verified two ways: cross-checked against Chainlink's own [reference data directory](https://reference-data-directory.vercel.app) and confirmed live on-chain by calling `decimals()` and `description()` against a public RPC for each chain. Chainlink price feed addresses differ per chain and per pair, and some duplicated entries in Chainlink's own registry point at internal streams variants you shouldn't use publicly. The tables below list the canonical proxy address for ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain, each independently verified on-chain. ## Why the wrong address is an easy mistake Chainlink publishes far more than one contract per pair. A single pair like ETH/USD typically has a primary proxy address plus one or two internal "shared SVR" variants used for Chainlink's own low-latency data streams infrastructure, not intended for public `latestRoundData()` reads. All of them show up under the same `"ETH / USD"` label in Chainlink's reference feed listing, and only one is the address that [data.chain.link](https://data.chain.link) documents and that Chainlink's own [reference data directory](https://reference-data-directory.vercel.app) files under the plain, unsuffixed feed path. Pull the wrong one from a stale blog post, a forum answer, or a hallucinated LLM response, and you either get a revert, a feed that silently stopped updating months ago, or a decimals mismatch that scales your price 10 billion times too small. The addresses below are the canonical proxy addresses, the same ones the [Chainlink price feed reader tool](/tools/chainlink-price-feed) on this site uses. ## Ethereum | Pair | Address | Decimals | Heartbeat | |---|---|---|---| | ETH / USD | [`0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419`](https://etherscan.io/address/0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419) | 8 | 3600s (1h) | | BTC / USD | [`0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c`](https://etherscan.io/address/0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c) | 8 | 3600s (1h) | | USDC / USD | [`0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6`](https://etherscan.io/address/0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6) | 8 | 82800s (23h) | ## Base | Pair | Address | Decimals | Heartbeat | |---|---|---|---| | ETH / USD | [`0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70`](https://basescan.org/address/0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70) | 8 | 1200s (20min) | | BTC / USD | [`0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F`](https://basescan.org/address/0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F) | 8 | 1200s (20min) | | USDC / USD | [`0x7e860098F58bBFC8648a4311b374B1D669a2bc6B`](https://basescan.org/address/0x7e860098F58bBFC8648a4311b374B1D669a2bc6B) | 8 | 86400s (24h) | ## BNB Smart Chain | Pair | Address | Decimals | Heartbeat | |---|---|---|---| | ETH / USD | [`0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e`](https://bscscan.com/address/0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e) | 8 | 60s | | BTC / USD | [`0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf`](https://bscscan.com/address/0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf) | 8 | 60s | | USDC / USD | [`0x51597f405303C4377E36123cBc172b13269EA163`](https://bscscan.com/address/0x51597f405303C4377E36123cBc172b13269EA163) | 8 | 900s (15min) | Heartbeat is the maximum interval Chainlink's node operators commit to between updates, even with no price movement. It's the upper bound on how stale a "latest" answer can be; a feed can also update sooner if the price moves past its deviation threshold. BNB Smart Chain's feeds above update roughly every minute; Ethereum's USDC/USD feed only commits to once every 23 hours, which is normal for a stablecoin pair that rarely moves. Chainlink occasionally deprecates or migrates feeds. Before hardcoding any address into production code, confirm it against [data.chain.link](https://data.chain.link) for the pair and chain you need, the same check this reference relied on. ## Checking staleness yourself `latestRoundData()` gives you everything needed to decide whether an answer is trustworthy, but it doesn't decide that for you. The pattern: 1. Read `answer` and `updatedAt` together, in the same call. 2. Compare `updatedAt` to your current time. If the gap exceeds the feed's heartbeat by a wide margin, the feed is stuck, not just running a little behind schedule. 3. Reject or flag the read if step 2 fails, rather than silently using a stale price. ```ts const AGGREGATOR_ABI = [ { name: "latestRoundData", type: "function", stateMutability: "view", inputs: [], outputs: [ { name: "roundId", type: "uint80" }, { name: "answer", type: "int256" }, { name: "startedAt", type: "uint256" }, { name: "updatedAt", type: "uint256" }, { name: "answeredInRound", type: "uint80" }, ], }, ] as const; const ETH_USD = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"; const HEARTBEAT_SECONDS = 3600; const client = createPublicClient({ chain: mainnet, transport: http() }); const { answer, updatedAt } = await client.readContract({ address: ETH_USD, abi: AGGREGATOR_ABI, functionName: "latestRoundData", }); const ageSeconds = Math.floor(Date.now() / 1000) - Number(updatedAt); if (ageSeconds > HEARTBEAT_SECONDS * 2) { throw new Error(`ETH/USD feed is stale: last updated ${ageSeconds}s ago`); } ``` Doubling the heartbeat before treating a feed as stale gives room for normal network variance without masking a genuinely stuck oracle. ## Reading a feed with evmquery The same read collapses into one REST call with evmquery, scaling `answer` by `decimals()` server-side instead of leaving that math to the client: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "feed": { "address": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" } } }, "expression": "cel.bind(d, feed.decimals(), cel.bind(r, feed.latestRoundData(), { \"description\": feed.description(), \"price\": string(formatUnits(r.answer, d)), \"roundId\": string(formatUnits(r.roundId, 0)), \"startedAt\": string(formatUnits(r.startedAt, 0)), \"updatedAt\": string(formatUnits(r.updatedAt, 0)), \"answeredInRound\": string(formatUnits(r.answeredInRound, 0)) }))" }' ``` This is the same expression shape validated live against the ETH/USD feed above; it resolves to a map with the pair description, the human-readable price, and all four round-integrity fields as strings, in one round trip and four billed units total: one for the CEL evaluation plus three calls for `decimals()`, `latestRoundData()`, and `description()`. The [Chainlink price feed reader tool](/tools/chainlink-price-feed) on this site runs a simpler version of this expression (just `description`, scaled `answer`, and `decimals`) if you want to try a feed before writing any code. If you're pulling more than one pair at a time, for example a treasury dashboard reading ETH/USD, BTC/USD, and USDC/USD in the same request, each additional feed just adds another entry to `schema.contracts` and another key to the expression's output map. The [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the batching mechanics if you're building this by hand instead of through evmquery. ## When you'd automate this A one-off price check rarely needs staleness handling this careful, but a scheduled job does. If you're building a price alert, a liquidation monitor, or a treasury dashboard that polls Chainlink on a timer, see [evmquery for automation](/for/automation) for wiring the same expression into a scheduled workflow instead of a script you have to babysit. ## Next steps - [Chainlink price feed reader tool](/tools/chainlink-price-feed): try any of the addresses above against a live query, no code required. - [Multicall3 batching guide](/blog/multicall3-batching-evm-contract-reads): read multiple feeds, or a feed alongside other contract state, in a single call. - [evmquery for automation](/for/automation): wire a feed read into a scheduled monitor or alert. --- # Using Chainlink Price Feeds Correctly: Staleness, Sequencer Uptime, and Decimals Source: https://evmquery.com/blog/chainlink-price-feeds-correctly Published: 2026-08-05 Author: evmquery team Category: guides The three checks a Chainlink price feed integration needs to be correct: staleness against the heartbeat, Base sequencer uptime, and decimals normalization without hardcoding 8. Reading a Chainlink price feed is one contract call. Reading it *correctly* means three checks most integrations skip: is the answer stale, is the underlying sequencer even up (if you're on an L2), and did you scale the result by the right number of decimals. Skip any one of these and the failure mode is silent, a price that's hours old, a feed reporting a healthy market on a sequencer that's been down for ten minutes, or a value that's off by a factor of a billion. Compare `updatedAt` to the feed's published heartbeat (not an arbitrary number) before trusting an answer. On Base, also check the L2 sequencer uptime feed and enforce Chainlink's recommended one-hour grace period after it comes back up. Always call `decimals()` instead of assuming 8. This is a technique post, not an address lookup. For verified contract addresses, decimals, and heartbeats for ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain, see the [Chainlink price feed address reference](/blog/chainlink-price-feed-addresses). This post covers the three checks you run against whichever address you pull from there. ## Check 1: staleness against the heartbeat, not a guess Every Chainlink feed publishes a heartbeat, the maximum interval its node operators commit to between updates even if the price hasn't moved. `latestRoundData()` returns `updatedAt`, the Unix timestamp of the last update, alongside the answer itself. The correct check compares the two: ```ts const ageSeconds = Math.floor(Date.now() / 1000) - Number(updatedAt); if (ageSeconds > heartbeatSeconds * 2) { throw new Error(`feed is stale: last updated ${ageSeconds}s ago`); } ``` Two details matter here. First, the heartbeat is per feed, not a global constant, Ethereum's USDC/USD feed commits to 23 hours while Base's ETH/USD feed commits to 20 minutes, both correct for their respective pairs (a stablecoin barely moves; ETH does). Hardcoding one heartbeat value across every feed you read means treating a genuinely stale ETH/USD price as fresh, or flagging a perfectly normal USDC/USD update as stale. Second, doubling the heartbeat before rejecting gives room for normal network variance (a node running a few minutes behind schedule) without masking a feed that's actually stuck. A feed that updates every 60 seconds in practice can still stop updating entirely if its node operators lose consensus, an oracle contract gets paused, or the wrong proxy address ends up in your config (see the [address reference](/blog/chainlink-price-feed-addresses) for how easy that mistake is). The staleness check is what catches all three failure modes with one comparison, regardless of cause. ## Check 2: sequencer uptime, and why it only applies on Base here Chainlink publishes a separate class of feed for L2 networks: the L2 Sequencer Uptime Status Feed. The problem it solves is specific to rollups. An L2's sequencer orders and submits transactions; if it goes down, the chain doesn't necessarily halt, but new price updates can stop reaching the L2 while old contract state (including a stale Chainlink answer) keeps reading as if nothing's wrong. A staleness check alone can miss this if the sequencer goes down and comes back up faster than the feed's heartbeat would otherwise flag. Of evmquery's four supported chains (Ethereum, Base, BNB Smart Chain, and Polygon), only Base is an L2 with a sequencer, so this check only applies there. Ethereum, BNB Smart Chain, and Polygon are all independent L1s or sidechains with their own validator sets, none of them has a sequencer to check. - Base's Chainlink sequencer uptime feed lives at [`0xBCF85224fc0756B9Fa45aA7892530B47e10b6433`](https://basescan.org/address/0xBCF85224fc0756B9Fa45aA7892530B47e10b6433), a verified `EACAggregatorProxy` confirmed live on-chain: `description()` returns `"L2 Sequencer Uptime Status Feed"`. - It exposes the same `latestRoundData()` shape as a price feed, but `answer` means status, not price: `0` means the sequencer is up, `1` means it's down. - `startedAt` is the timestamp the *current status* began, not the last price update. Chainlink's own reference pattern recommends a one-hour grace period after `startedAt` before trusting reads that happened right as the sequencer recovered. - Chainlink documents this feed for several L2 rollups (Arbitrum, Optimism, Base, and others), not for L1 networks or non-rollup chains like BNB Smart Chain. The check, adapted from Chainlink's own reference pattern: ```ts const GRACE_PERIOD_SECONDS = 3600; // Chainlink's recommended grace period const { answer: sequencerStatus, startedAt } = await client.readContract({ address: "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433", // Base sequencer uptime feed abi: AGGREGATOR_ABI, functionName: "latestRoundData", }); if (sequencerStatus !== 0n) { throw new Error("sequencer is down"); } const timeSinceUp = Math.floor(Date.now() / 1000) - Number(startedAt); if (timeSinceUp <= GRACE_PERIOD_SECONDS) { throw new Error(`sequencer recovered too recently: ${timeSinceUp}s ago, grace period is ${GRACE_PERIOD_SECONDS}s`); } ``` This is a real, live-validated result querying the address above through evmquery: at query time `answer` was `0` (sequencer up) with a `startedAt` well outside the grace period, meaning the price feeds shown in the [address reference](/blog/chainlink-price-feed-addresses) were safe to trust at that moment. That won't always be true, which is the entire point of running the check on every read rather than assuming it once and moving on. The sequencer check is a second `latestRoundData()` read against a fixed, well-known address, not a new dependency or a different data source. On Ethereum, BNB Smart Chain, and Polygon, skip it entirely, there's no sequencer feed to check, and the staleness check on the price feed itself is sufficient. ## Check 3: decimals, called not assumed Every USD-quoted example in the [address reference](/blog/chainlink-price-feed-addresses) happens to use 8 decimals, and it's tempting to bake `1e8` into a helper function and move on. That assumption breaks the moment you read a crypto-quoted pair (several use 18 decimals) or a feed on a chain you haven't tested yet. The fix costs one more call: ```ts const decimals = await client.readContract({ address: feedAddress, abi: AGGREGATOR_ABI, functionName: "decimals", }); const price = Number(answer) / 10 ** decimals; ``` `decimals()` is cheap, cacheable per feed address (it doesn't change), and removes an entire class of "price is off by a power of ten" bugs. Treat a hardcoded decimals constant the same way you'd treat a hardcoded feed address, a shortcut that works until it doesn't, at which point it fails silently instead of loudly. ## Putting the three checks together A production read combines all three in the order they matter: sequencer up (Base only), then price fresh, then price scaled correctly. ```ts const AGGREGATOR_ABI = [ { name: "latestRoundData", type: "function", stateMutability: "view", inputs: [], outputs: [ { name: "roundId", type: "uint80" }, { name: "answer", type: "int256" }, { name: "startedAt", type: "uint256" }, { name: "updatedAt", type: "uint256" }, { name: "answeredInRound", type: "uint80" }, ], }, { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "uint8" }], }, ] as const; const SEQUENCER_FEED = "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433"; const ETH_USD_BASE = "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70"; const HEARTBEAT_SECONDS = 1200; // Base ETH/USD, per the address reference const GRACE_PERIOD_SECONDS = 3600; const client = createPublicClient({ chain: base, transport: http() }); async function readEthUsdOnBase() { const sequencer = await client.readContract({ address: SEQUENCER_FEED, abi: AGGREGATOR_ABI, functionName: "latestRoundData", }); if (sequencer.answer !== 0n) throw new Error("sequencer is down"); const timeSinceUp = Math.floor(Date.now() / 1000) - Number(sequencer.startedAt); if (timeSinceUp <= GRACE_PERIOD_SECONDS) throw new Error("sequencer still in grace period"); const [round, decimals] = await Promise.all([ client.readContract({ address: ETH_USD_BASE, abi: AGGREGATOR_ABI, functionName: "latestRoundData" }), client.readContract({ address: ETH_USD_BASE, abi: AGGREGATOR_ABI, functionName: "decimals" }), ]); const ageSeconds = Math.floor(Date.now() / 1000) - Number(round.updatedAt); if (ageSeconds > HEARTBEAT_SECONDS * 2) throw new Error(`feed is stale: ${ageSeconds}s`); return Number(round.answer) / 10 ** decimals; } ``` That's four separate calls (two `latestRoundData()`, one `decimals()`, plus the implicit RPC round trips) for what reads as a single price. This is exactly the kind of read evmquery collapses server-side: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_base", "schema": { "contracts": { "seq": { "address": "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433" }, "feed": { "address": "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70" } } }, "expression": "cel.bind(s, seq.latestRoundData(), cel.bind(r, feed.latestRoundData(), cel.bind(d, feed.decimals(), { \"sequencerUp\": string(s.answer), \"sequencerStartedAt\": string(formatUnits(s.startedAt, 0)), \"price\": string(formatUnits(r.answer, d)), \"updatedAt\": string(formatUnits(r.updatedAt, 0)) })))" }' ``` `cel.bind` nests the sequencer read, the price read, and the decimals lookup into one round trip and one billed request; the staleness and grace-period arithmetic still happens in your own code against the returned timestamps, evmquery reads the raw fields correctly scaled, it doesn't make the trust decision for you. The [Chainlink price feed reader tool](/tools/chainlink-price-feed) runs the price half of this expression (`description`, scaled `answer`, `decimals`) against any of the addresses in the [reference table](/blog/chainlink-price-feed-addresses) if you want to see the shape of the response before wiring up the full check. If you're running this on a schedule rather than a one-off script, for a price alert, a liquidation monitor, or a treasury dashboard, see [evmquery for automation](/for/automation) for wiring the same three-part check into a polling job instead of re-running it by hand. ## Next steps - [Chainlink price feed address reference](/blog/chainlink-price-feed-addresses): verified addresses, decimals, and heartbeats for the pairs used in the examples above. - [Chainlink price feed reader tool](/tools/chainlink-price-feed): try a feed read live, no code required. - [Aave V3 health factor explained](/blog/aave-v3-health-factor-explained): another case where a single call returns values at multiple, easy-to-confuse decimal scales. - [evmquery for automation](/for/automation): schedule the staleness and sequencer checks instead of running them by hand. --- # Fixing "could not decode result data (value="0x")" in Ethers and Viem Source: https://evmquery.com/blog/decode-result-data-0x-error Published: 2026-08-05 Author: evmquery team Category: guides Ethers and viem throw could not decode result data (value=0x) for four real reasons, from unresolved proxies to wrong chains. Diagnose each one and fix it fast. You call a contract method with ethers or viem, and instead of a number or an address you get a wall of text ending in `could not decode result data (value="0x")`. The stack trace points at your own code, but the actual fault is a few hops upstream: the node handed back zero bytes, and the ABI decoder had nothing to decode. Four situations cause this, and they are not equally likely. `value="0x"` means the RPC node returned empty data instead of your function's return value. In order of frequency: you're reading a proxy through the wrong address or ABI, the function doesn't exist on the deployed contract, the contract is paused or its bytecode is gone, or you're on the wrong chain or a lagging RPC endpoint. Check `eth_getCode` first, then the EIP-1967 implementation slot. ## Why the decoder has nothing to decode `eth_call` is a request/response round trip: your calldata goes in, raw bytes come back. Ethers and viem's job is to take those bytes and slice them into the types your ABI says the function returns, an address here, a `uint256` there. When the node returns literally zero bytes, `0x`, there is nothing to slice. The library doesn't throw a revert error (which usually carries a reason string or a custom error selector), it throws a decode error, because from its point of view the call "succeeded" and simply produced no data. That distinction matters for debugging. This error means the JSON-RPC call itself didn't fail, so retrying it with the same address, same ABI, and same call won't help. Something about what you're calling, or where you're calling it, is off. ## Cause 1: you're reading a proxy that hasn't been resolved Most production EVM contracts you'll integrate against today sit behind an upgradeable proxy, usually the [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) standard slot layout. The proxy holds no business logic itself; it `delegatecall`s into a separate implementation contract and forwards whatever comes back. That only works correctly if your code knows, for a given address, whether it's a proxy and which implementation it currently points at, and gets that answer before you build your `Contract` instance or ABI binding. This is the single most common cause of the error, because it's easy to get half right: point at the wrong one of the two addresses (the raw implementation contract instead of the proxy, most often), and any function that reads real state comes back empty. The implementation contract's own storage is typically blank because the actual state lives in the proxy's storage slots, and some teams additionally guard implementations against direct calls (via `_disableInitializers()` in the constructor), which can make reads against the implementation address fail outright. To diagnose it, confirm the address has code, then read the EIP-1967 implementation slot yourself: ```ts const client = createPublicClient({ chain: mainnet, transport: http() }); // bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; const code = await client.getCode({ address: targetAddress }); if (!code || code === "0x") throw new Error("no contract deployed at this address"); const raw = await client.getStorageAt({ address: targetAddress, slot: EIP1967_IMPLEMENTATION_SLOT, }); // the address occupies the last 20 bytes (40 hex chars) of the 32-byte slot const implementation = raw && raw !== `0x${"0".repeat(64)}` ? `0x${raw.slice(-40)}` : null; ``` The equivalent in ethers v6 uses `provider.getStorage(address, position)` (the current name for what was `getStorageAt` in v5): ```ts const raw = await provider.getStorage(targetAddress, EIP1967_IMPLEMENTATION_SLOT); const implementation = ethers.getAddress(`0x${raw.slice(-40)}`); ``` If `raw` comes back all zeros, the address isn't an EIP-1967 proxy (or hasn't been initialized), which tells you your implementation ABI is being tested against the wrong bytecode entirely. Once you have the real implementation address, use it only to fetch the correct ABI, keep calling through the proxy's address. ## Cause 2: the function doesn't exist on this contract This is a plain wrong-ABI problem: an ERC-20 ABI applied to a contract that isn't a token, or an ABI pulled from a different version of the same protocol whose function signatures moved on. When the function selector in your calldata doesn't match anything the deployed bytecode understands, and the contract has no fallback that handles it gracefully, the call reverts. Depending on the RPC provider, that revert can surface without a reason string, and some providers flatten "reverted with no data" into the same empty response as "succeeded with no data." Ethers and viem can't tell those two apart from the raw bytes alone, so you get the same decode error either way. To diagnose it, first rule out cause 1 or 3 with `eth_getCode`, then call a function you're certain is correct for that contract, `name()` or `symbol()` on anything claiming to be a token, as a sanity probe. If the probe also comes back empty, look at causes 3 and 4 instead. If the probe works but your target function doesn't, re-fetch the verified source for the exact address you're calling. Don't reuse an ABI from a sibling contract in the same protocol family; upgrades and per-market deployments drift. ## Cause 3: the contract is paused, or its bytecode is gone Two distinct failures share this bucket. The first is a pause flag: many protocols guard state-changing functions, and occasionally read functions too, behind a `require(!paused)` check. From the decoder's point of view, that revert looks identical to cause 2, so check the contract's own `paused()` getter if it exposes one. The second is more permanent: if a contract executes `SELFDESTRUCT`, `eth_getCode` for that address returns `0x` from that point forward, and every call to it, regardless of ABI, comes back empty. [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780), active since the March 2024 Dencun upgrade, narrowed this considerably: `SELFDESTRUCT` now only actually removes a contract's code when it's called in the same transaction that deployed it. Contracts destroyed before Dencun stay destroyed under the old rules, which is exactly what happened in 2017 when a user accidentally triggered self-destruct on a shared Parity multisig library contract, permanently bricking every wallet that delegated its logic to that address. Diagnosis here is the simplest of the four: `eth_getCode` at the exact address and block you're calling. If it's `0x`, no ABI decodes a result, because there's no contract left to execute your call. ## Cause 4: wrong chain, or a lagging RPC endpoint Address collisions across chains are routine, deterministic deployers (CREATE2 factories, vanity addresses) put the same address on Ethereum, Base, and BNB Chain on purpose, and copy-pasting an address from the wrong network's block explorer is an easy mistake. A call to a real, well-formed address on the wrong chain just hits an empty account: no code deployed there, and the node returns `0x` without complaint, because from the EVM's perspective that's a correct answer. A sneakier variant of the same failure shows up on public, load-balanced RPC endpoints. Your requests get routed across many backend nodes, and if the specific node answering a given request is still syncing or serving a stale snapshot, it can report empty state for a contract that objectively exists elsewhere on the same chain. Confirm the chain ID matches what you expect (`eth_chainId`, or your client's own `chain.id`), and cross-check the address on that chain's block explorer before touching your own code again. ## How evmquery skips the proxy problem entirely Working through cause 1 by hand, fetch bytecode, read a storage slot, extract an address, fetch a second ABI, is exactly the bookkeeping evmquery's contract resolution removes. `describe_schema` and `execute_query` resolve the correct implementation ABI against a target address server-side, following EIP-1967 and diamond proxies, before your query ever runs. Here's Aave V3's Pool contract on Ethereum, a proxy in production, queried live while writing this post: ``` describe_schema({ chain: "evm_ethereum", schema: { contracts: { aave_pool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } } }) → aave_pool (0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2) resolution: verified dispatches via eip1967 to 0x728a138A4823392C2EFA55e028d434F526fE03CF getReservesCount() -> sol_int -- sourcify, reads from 0x728a...E03CF ... ``` evmquery reports the resolution up front (`dispatches via eip1967 to ...`), and every method it lists is already bound to the implementation contract. Running a real query against the proxy address returns real data, no manual unwinding required: ``` execute_query({ chain: "evm_ethereum", schema: { contracts: { aave_pool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } }, expression: "aave_pool.getReservesCount()" }) → Result: 67 (sol_int) Block: 25690598 | Calls: 1 | Rounds: 1 ``` Over REST, the same query looks like this. The shape below follows the documented request/response format in evmquery's API reference; the query itself (contract, chain, and expression) is the exact one validated live above. ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "aave_pool": { "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } } }, "expression": "aave_pool.getReservesCount()" }' | python3 -m json.tool # { # "result": { "value": 67, "type": "sol_int" }, # "meta": { "blockNumber": ... } # } ``` If you're building the kind of internal tooling or automation that needs to read contract state without carrying proxy-resolution logic yourself, the [developer-focused overview of evmquery](/for/developers) covers how this resolution fits into a larger integration. ## Next steps - If you're batching several reads once your ABI resolution is correct, the [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the same proxy-ABI mismatch pitfall in a batched-call context. - Debugging this class of error outside a JS stack? [Reading EVM contract data from Python](/blog/query-evm-contract-data-python) walks through the same resolution problem with web3.py. - See [pricing](/pricing) for evmquery's free tier if you want to validate a query before wiring it into your own code. --- # ERC-8004 Explained: Query Onchain AI Agent Identity and Reputation with a REST Call Source: https://evmquery.com/blog/erc-8004-agent-identity-reputation-registry Published: 2026-08-05 Author: evmquery team Category: guides ERC-8004 gives AI agents a portable onchain identity and reputation record. Query the live Identity and Reputation registries with evmquery — no ABI, no SDK. An AI agent that transacts with a stranger has no way to check who it is dealing with. There's no LinkedIn for agents, no credit bureau, no way to ask "has this thing done real work before, or was it minted an hour ago to scam the next counterparty." ERC-8004 is Ethereum's answer: three lightweight onchain registries — Identity, Reputation, Validation — that any agent, human, or contract can query without an API key or a bilateral agreement. The registries have been live on Ethereum, Base, and BNB Chain since January 29, 2026, and adoption has been fast: over 45,000 agents registered in the first month, past 200,000 within the quarter. This guide shows you how to read both registries with a single REST call, and — because we tested this live against mainnet while writing it — what the data actually looks like once you go beyond the headline registration count. The Identity and Reputation registries sit at the same address on every chain they're deployed to: `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` (Identity) and `0x8004BAa17C55a88189AE136b182e5fdA19dE9b63` (Reputation). Read an agent's registration file and feedback score with one evmquery POST request — no ABI import, no SDK. ## What ERC-8004 actually defines ERC-8004 ("Trustless Agents") is a Standards Track EIP, still formally in **Draft** status as of this writing — the contracts are live and handling real registrations, but the spec itself hasn't been finalized, and the Validation Registry in particular is expected to change. It was proposed in August 2025 by authors from MetaMask, the Ethereum Foundation, Google, and Coinbase, which is unusual enough to be its own signal: four organizations that don't often co-author a spec agreed this gap was worth closing. Three registries, each deployed once per chain: | Registry | Built on | What it stores | |----------|----------|-----------------| | **Identity** | ERC-721 with URIStorage | A token per agent (`agentId`), pointing to an offchain registration file with the agent's name, capabilities, and service endpoints (A2A, MCP, web) | | **Reputation** | Custom interface | Bounded feedback attestations submitted by addresses that interacted with the agent | | **Validation** | Custom interface | Independent verification requests and results — still under active revision, not something to build production logic against yet | The key design choice: registration is permissionless and cheap (sub-$1 gas on L2s), and the registries store pointers and small numeric signals, not the actual agent logic or the full feedback text. Everything heavy lives offchain. That keeps onchain identity practical to adopt at scale, but it also means a registration by itself proves almost nothing — more on that below. ## Reading an agent's identity Skip the ABI. Skip `ethers.Contract`. evmquery resolves the ABI for you and returns a decoded value from a single POST request. ```python import os import requests resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": { "identity": {"address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"} } }, "expression": "identity.tokenURI(solInt('3000'))", }, timeout=10, ) resp.raise_for_status() print(resp.json()["result"]["value"]) # https://ag0.xyz ``` `tokenURI` is the agent's registration file — the ERC-8004 spec calls it `agentURI`. It resolves to a JSON document declaring the agent's name, its A2A or MCP service endpoints, and its supported trust models. Pull the agent's control wallet the same way: ```python "expression": "identity.getAgentWallet(solInt('3000'))", # 0xa7dcc4a4b123631a71e5b04c3b0d76941077cea2 ``` If you parameterize `agentId` via `schema.context` instead of hardcoding it in the expression, declare it as `"sol_int"` and pass the runtime value as a plain number (`3000`), not a string (`"3000"`). A quoted numeric string in the `context` object fails validation — `solInt('3000')` works inline in the expression itself because `solInt()` explicitly parses a string, but the top-level `context` payload expects a native number for `sol_int` and `list` types. ## Reading reputation feedback The Reputation Registry aggregates feedback per agent, filtered by which client addresses you trust to count. That filtering is intentional — the spec doesn't return a single global average, because a global average is trivial to manipulate. ```python resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": { "reputation": {"address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63"} }, "context": {"clients": "list"}, }, "context": {"clients": ["0x9ce7082814bda389f3ba548bdf2626006279569c"]}, "expression": "reputation.getSummary(solInt('3000'), clients, '', '')", }, timeout=10, ) data = resp.json()["result"]["value"] print(data) # {"count": "0", "summaryValue": "0", "summaryValueDecimals": "0"} ``` `getSummary` takes an `agentId`, a list of client addresses to include, and two optional tag filters (feedback can be tagged by category, e.g. `security-audit` vs. `content-policy`). Pass an empty tag string to match all. Passing `clients: []` to filter by "everyone" doesn't work — the registry contract reverts with `clientAddresses required`. There's no built-in "unfiltered" mode. Resolve the client list first with `getClients(agentId)`, which returns every address that's ever left feedback for that agent, then pass that list into `getSummary`. ```python "expression": "reputation.getClients(solInt('3000'))", # [] ``` ## The gap between "registered" and "real" Here's what running these queries against a batch of real `agentId`s turns up: most of them come back empty. We queried `tokenURI` for agent IDs 1 through 5, then a spread up to 2,000 — every single one returned an empty string. Agent 3000 was the first one in our sample with an actual registration file set. That's not a bug in the query. It matches what the first academic study of ERC-8004 found after crawling Ethereum, BNB Smart Chain, and Base through mid-May 2026: only **3%, 4%, and 15%** of registrations on those three chains, respectively, expose a valid registration file with at least one live service endpoint. The rest are placeholders — minted, never filled in, sitting there as an `agentId` with no agent behind it. A high total-agents number is a vanity metric until you've filtered for agents with a real registration file and non-Sybil feedback. The same study found that after removing coordinated Sybil reviewers, 15.5% to 89.4% of "rated" agents across the three chains were left with zero valid feedback. Check `tokenURI` returns a non-empty value and `getClients` returns more than one independent address before you treat an agent's reputation as a signal. If you're building agent discovery or a hiring/escrow flow on top of ERC-8004, the practical takeaway is: treat the Identity Registry as production-ready for lookups, and treat an unverified reputation score as exactly that — unverified — until you've checked both that a registration file exists and that the feedback behind it isn't a handful of addresses reviewing each other. ### A minimal trust check before you hire an agent Start with the registration file and the reviewer count in one call: ```python resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": { "identity": {"address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"}, "reputation": {"address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63"}, }, "context": {"agentId": "sol_int"}, }, "context": {"agentId": 3000}, "expression": "reputation.getClients(agentId).size()", }, timeout=10, ) reviewer_count = resp.json()["result"]["value"] # 0 for agent 3000 ``` `getSummary` reverts with `clientAddresses required` if the client list you pass in is empty — including when that list came from `getClients` and just happens to be empty because nobody has reviewed the agent yet. Check `reviewer_count > 0` before calling `getSummary`; don't assume a fresh agent has a summary to fetch. A non-empty `tokenURI` and `reviewer_count > 1` (not just one self-interested reviewer) are the floor, not proof of trustworthiness. Fetch the JSON at the registration URI and check the declared service endpoints actually respond before you send an agent anything that costs money — the registry tells you an agent claims to exist, not that the claim holds up. ## Same registries, across chains The Identity and Reputation registries deploy at the identical address on every chain in the ERC-8004 network — a deterministic vanity deployment, hence the `0x8004...` prefix on both. Swap the `chain` field and nothing else changes: | Chain | `chain` value | |-------|---------------| | Ethereum | `evm_ethereum` | | Base | `evm_base` | | BNB Chain | `evm_bnb_mainnet` | | Polygon | `evm_polygon` | ```python for chain in ["evm_ethereum", "evm_base", "evm_bnb_mainnet", "evm_polygon"]: resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": chain, "schema": {"contracts": {"identity": {"address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"}}}, "expression": "identity.name()", }, timeout=10, ) print(chain, resp.json()["result"]["value"]) # evm_ethereum AgentIdentity # evm_base AgentIdentity # evm_bnb_mainnet AgentIdentity # evm_polygon AgentIdentity ``` Feedback submission requires `msg.sender` to be on the same chain as the registry, and the spec has no cross-chain aggregation path. An agent's spotless reputation on Base reads as zero on Ethereum — querying the same `agentId` on a different chain gets you that chain's registration, not a merged record. If you're building cross-chain agent discovery, you need to query each chain separately and decide how to weight them yourself. If you're building the agent side of this rather than the query side — an agent that needs to read live onchain state as part of its own reasoning loop — the [agent framework integration matrix](/blog/agent-framework-integrations/) covers wiring evmquery in via MCP or a tool call so the agent can check a counterparty's ERC-8004 record before transacting with it. See [evmquery for AI agent builders](/for/ai-users) for the broader integration surface. ## Next steps - [evmquery in any AI agent framework](/blog/agent-framework-integrations/) — MCP and REST tool patterns for 21 frameworks, including native MCP for Claude Agent SDK - [ERC-20 Balance Scanner in TypeScript](/blog/erc20-balance-scan-rest-api-typescript/) — REST API patterns for multi-address, parameterized queries - [Uniswap V3 Pool Data via REST API](/blog/uniswap-v3-pool-data-rest-api/) — another struct-returning contract read walked through end to end - [evmquery for AI agent builders](/for/ai-users) — the full integration surface for agent-facing onchain reads --- # Why Your eth_call Loop Hits 429s (and How Multicall3 Fixes It) Source: https://evmquery.com/blog/eth-call-rate-limit-429-multicall3 Published: 2026-08-05 Author: evmquery team Category: guides A loop of eth_call calls works fine in dev, then throws 429 in production. Here's the real free-tier RPC limit behind it, and how Multicall3 fixes it. Your scanner loops over fifty wallets, calls `balanceOf` on each one, and runs clean on your machine. Ship it, point it at a public RPC endpoint in production, and somewhere around the twentieth request in that same loop you start getting `429 Too Many Requests` back instead of a balance. The code isn't wrong. The loop is the problem, and it's a problem with a fixed, measurable ceiling. An `eth_call` loop makes N network round trips for N reads, and free-tier RPC endpoints throttle round trips, not the size of the read. Alchemy's published free tier caps you at 500 compute units per second, and a single `eth_call` costs 26 of those, so a straight loop tops out around 19 calls a second before you're rate-limited. Multicall3 collapses the same N reads into one round trip; an evmquery expression does the same in a single line. ## Why the loop hits a wall Every `eth_call` your code makes is a full JSON-RPC round trip: open (or reuse) a connection, serialize a request, wait for the node to execute the call and reply, deserialize the response. That overhead is roughly constant whether you're reading a `bool` or a `uint256`. Read fifty contracts one at a time and you pay that overhead fifty times, even though the underlying reads are trivial for the node to answer. Rate limits on public and free-tier RPC endpoints are built around exactly this cost. They don't care how cheap your read is; they cap how many requests (or, more precisely, how much computed "weight") you can push through per second. A loop that reads one field per call is the worst possible shape for that kind of limit, because it turns N cheap reads into N billable, rate-limited round trips instead of one. ## What the free-tier limit actually is It's worth naming a real number instead of guessing. [Alchemy's published free tier](https://www.alchemy.com/support/free-tier-details) allows up to 500 compute units per second (CUPS) application-wide, with 30 million compute units included per month. Alchemy's own [compute unit cost table](https://www.alchemy.com/docs/reference/compute-unit-costs) lists `eth_call` at 26 compute units per request. Do the division and the ceiling is concrete: `500 / 26 ≈ 19` `eth_call` requests per second before you hit the throughput cap, and that's before accounting for the `decimals()` or `symbol()` calls most balance-reading code also makes per token. A wallet-balance scanner that reads two fields per token runs out of headroom at around ten tokens a second, not twenty. The 500 CUPS figure is a steady-state average, not a hard wall on every single second. Alchemy's own throughput docs describe a token-bucket limiter: a free-tier account can burst up to 5,000 CUs over any 10-second window, which is the same 500 CUPS average smoothed over a slightly longer clock. It buys you a little slack for a spiky page load, not a way around the underlying cap. Alchemy publishes exact figures, which is why it's the example here, but the shape of the constraint is the same across the RPC market: most free-tier plans cap you in the low hundreds of requests or compute units per second, with tighter burst limits on top of that. If you're on a different provider, look up its published throughput limit before you assume your loop has headroom. Assuming it doesn't is the safer default. ## The loop, concretely Here's the pattern that hits the wall. A sequential loop over ten tokens for one wallet: ```ts const client = createPublicClient({ chain: mainnet, transport: http() }); const balances = []; for (const token of tokens) { const balance = await client.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [holder], }); balances.push(balance); } ``` Ten tokens, ten round trips, done in sequence. Switching to `Promise.all` doesn't fix the underlying problem: ```ts const balances = await Promise.all( tokens.map((token) => client.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [holder] }), ), ); ``` This still sends ten separate `eth_call` requests, it just sends them concurrently instead of one after another. The RPC provider's rate limiter counts them exactly the same way, so at a large enough token list you hit the same 429, faster and in a denser burst. Concurrency changes where the requests queue, not how many of them there are. The next instinct is usually to wrap the loop in retry-with-backoff and call the problem handled. That masks the symptom instead of fixing it: the loop still issues N requests, the rate limiter still counts N requests, and now a chunk of those requests fail on the first attempt and re-queue behind whatever is still in flight. Backoff is the right tool for a genuinely transient network blip. It's the wrong tool for a loop that was always going to exceed a fixed, known throughput cap. ## Multicall3: same reads, one round trip [Multicall3](https://github.com/mds1/multicall3) is a contract deployed at the same address (`0xcA11bde05977b3631167028862bE2a173976CA11`) on every major EVM chain. Instead of sending N separate `eth_call` requests, you ABI-encode N `(target, calldata)` pairs, send them to Multicall3 in a single `eth_call`, and it returns all N results at once. The node still runs N reads internally, but your code and the rate limiter only see one request. Viem's built-in `multicall` uses it automatically: ```ts const balances = await client.multicall({ contracts: tokens.map((token) => ({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [holder], })), }); ``` Same ten reads, one round trip, one line against the rate limiter instead of ten. For the full mechanics, including `aggregate3` vs `tryAggregate`, gas caps on large batches, and the proxy pitfalls that trip people up when they first adopt it, see the [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads). ## The evmquery equivalent: one expression, one request evmquery batches through Multicall3 automatically, so the same reads become a single CEL expression instead of a client-side `multicall` call. This is the actual query, validated live against Ethereum mainnet while writing this post: ``` execute_query({ chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7", dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F" }, context: { wallet: "sol_address" } }, context: { wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, expression: "[formatUnits(usdc.balanceOf(wallet), usdc.decimals()), formatUnits(usdt.balanceOf(wallet), usdt.decimals()), formatUnits(dai.balanceOf(wallet), dai.decimals())]" }) → Result: [37.192124, 290.268219, 4.572078273323113] (list) Block: 25690665 | Calls: 6 | Rounds: 1 | Units: 7 ``` Six underlying calls (`balanceOf` and `decimals()` for each of three tokens) resolve in one round trip, `Rounds: 1` in the response metadata confirms it. Add more tokens to the list and the call count grows; the request count doesn't. Over REST, the same query looks like this. The request shape follows the documented format in evmquery's API reference; the contracts, wallet, and expression are the exact ones validated live above. ```bash curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "usdc": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, "usdt": { "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, "dai": { "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F" } }, "context": { "wallet": "sol_address" } }, "context": { "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" }, "expression": "[formatUnits(usdc.balanceOf(wallet), usdc.decimals()), formatUnits(usdt.balanceOf(wallet), usdt.decimals()), formatUnits(dai.balanceOf(wallet), dai.decimals())]" }' | python3 -m json.tool # { # "result": { "value": [37.192124, 290.268219, 4.572078273323113], "type": "list" }, # "meta": { "blockNumber": 25690665, ... } # } ``` ## When you outgrow a fixed list of reads A hardcoded list of three tokens is easy to batch by hand, in Multicall3 or in evmquery. The scanner from the top of this post reads fifty wallets, and the next one after that reads five hundred, and hand-rolling a `Call3[]` array that scales with a dynamic wallet list is more bookkeeping than most teams want to own. evmquery's `list.map` macro handles that shape directly, batching an arbitrary-length wallet list through Multicall3 in one query without any change to how you call it. If you're building the kind of internal tooling or agent-facing surface that needs this at scale, the [developer-focused overview of evmquery](/for/developers) covers how batching, proxy resolution, and chunking fit together as one integration surface instead of separate problems to solve. ## Next steps - For the deep dive on `aggregate3`, gas caps, and proxy handling, read the [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads). - Building the wallet scanner itself? [Multi-wallet ERC-20 balance scanning from TypeScript](/blog/erc20-balance-scan-rest-api-typescript) covers the REST pattern for a dynamic wallet list. - Comparing RPC and query providers on rate limits and pricing? [Moralis vs Alchemy vs QuickNode vs evmquery](/blog/moralis-alchemy-quicknode-evmquery-comparison) breaks down where each one fits. - See the [developer overview](/for/developers) for how evmquery fits into a larger integration beyond a single query. --- # evmquery vs. Raw viem: What a Falsifiable Benchmark Actually Shows Source: https://evmquery.com/blog/evmquery-vs-viem-benchmark Published: 2026-08-05 Author: evmquery team Category: trust A falsifiable evmquery vs. raw viem comparison: real line counts, real round-trip counts, and an honest list of what evmquery still does not remove today. Every query-layer vendor's landing page says some version of "saves you hours of RPC plumbing." Almost none of them show the plumbing they claim to save you from, and fewer still show a number a reader could reproduce. This post does both: real, complete code for reading five fields across three differently-proxied contracts by hand, the same read as one evmquery expression, and an honest accounting of what actually shrinks, what doesn't, and what evmquery still doesn't do for you. Reading 5 numeric fields across 3 proxy-wrapped Ethereum contracts takes 38 lines of hand-written viem (already using viem's own `multicall` action, the honest best case) versus 22 lines of an evmquery expression, for the same 1 network round trip. The real difference is ABI discovery and knowing which addresses are proxies at all, not a network trick: skip `multicall` and write the naive per-call version instead, and raw viem costs 5 round trips, not 1. - Scenario: read `totalSupply()` and `decimals()` on USDC, `getReservesCount()` and `MAX_NUMBER_RESERVES()` on Aave V3's Pool, and `totalSupply()` on a beacon-proxied ERC-721 avatar contract, 5 fields across 3 Ethereum mainnet contracts, in one request. - Each of the 3 contracts sits behind a different proxy pattern: USDC through a legacy zOS/OpenZeppelin proxy, Aave's Pool through an EIP-1967 transparent proxy, and the ERC-721 avatar contract through an EIP-1967 beacon proxy. - The evmquery side was executed live against evmquery's API while writing this post, on 2026-08-05, at Ethereum block 25,690,819: 5 on-chain calls, 1 execution round, 6 units consumed. - The viem code is real and complete, checked against each contract's genuine implementation ABI, but was not executed against a live RPC in this writing session. No latency number is claimed for either side; see the methodology callout below. ## The scenario, in full Three real, independently verifiable Ethereum mainnet contracts, chosen because each uses a different proxy pattern and a reader can look up every address below directly on Etherscan: | Contract | Address | Proxy pattern | Fields read | |---|---|---|---| | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | Legacy zOS/OpenZeppelin proxy | `totalSupply()`, `decimals()` | | Aave V3 Pool | `0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2` | EIP-1967 transparent proxy | `getReservesCount()`, `MAX_NUMBER_RESERVES()` | | ERC-721 avatar contract | `0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299` | EIP-1967 beacon proxy | `totalSupply()` | None of these are contrived. USDC is the most-integrated ERC-20 on Ethereum. Aave V3's Pool is the entry point for every lending read on the protocol. The third contract is a real, deployed ERC-721 collection behind a beacon, the pattern where many proxies share one upgrade point through an intermediate beacon contract. Reading five numbers across three contracts like this, a mixed dashboard read, not a single-token balance check, is closer to what a real integration looks like than a synthetic benchmark contract built to make one side look good. ## Raw viem, written properly This is the honest baseline: not a naive strawman, but the *correct* way to do this in viem, using its own `multicall` action so it batches into one round trip. Getting here still requires knowing, ahead of time, that all three addresses are proxies, so you go looking for the implementation's ABI instead of the one Etherscan shows for the proxy address itself. None of that comes from the address alone; you have to source the correct implementation ABI per contract before this compiles. ```ts const client = createPublicClient({ chain: mainnet, transport: http() }); const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const AAVE_POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"; const AVATAR = "0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299"; // Each address below is a proxy, but viem calls it directly either way: // the EVM delegates internally regardless of pattern. What's required is // the *implementation*'s ABI, not the proxy's, since USDC, Aave's Pool, and // the ERC-721 avatar contract each forward to a different contract than the one // being called. const erc20Abi = [ { name: "totalSupply", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }, { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] }, ] as const satisfies Abi; const aavePoolAbi = [ { name: "getReservesCount", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }, { name: "MAX_NUMBER_RESERVES", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint16" }] }, ] as const satisfies Abi; const erc721EnumerableAbi = [ { name: "totalSupply", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }, ] as const satisfies Abi; const [usdcSupply, usdcDecimals, reservesCount, maxReserves, avatarSupply] = await client.multicall({ contracts: [ { address: USDC, abi: erc20Abi, functionName: "totalSupply" }, { address: USDC, abi: erc20Abi, functionName: "decimals" }, { address: AAVE_POOL, abi: aavePoolAbi, functionName: "getReservesCount" }, { address: AAVE_POOL, abi: aavePoolAbi, functionName: "MAX_NUMBER_RESERVES" }, { address: AVATAR, abi: erc721EnumerableAbi, functionName: "totalSupply" }, ], allowFailure: false, }); ``` That's 38 lines total, 32 of them non-blank, count them yourself in the block above. The `getReservesCount`/`MAX_NUMBER_RESERVES` return types come from Aave's public `IPool.sol` interface; the ERC-20 and ERC-721 Enumerable fragments are the standard interfaces. None of that ABI research shows up in the line count, only the code that survives it. ## The same read as one evmquery expression ```ts const apiKey = process.env.EVMQUERY_API_KEY!; const query = { chain: "evm_ethereum", schema: { contracts: { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", aave_pool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", avatar: "0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299", }, }, expression: "[usdc.totalSupply(), usdc.decimals(), aave_pool.getReservesCount(), aave_pool.MAX_NUMBER_RESERVES(), avatar.totalSupply()]", }; const res = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey }, body: JSON.stringify(query), }); const { result } = await res.json(); // result.value = ["49414349671864702", "6", "67", "128", "3"] ``` 22 lines total, 20 non-blank. No ABI import, no proxy identification, no per-contract fragment. evmquery resolved all three proxies server-side and reported it back when asked (`describe_schema` shows `dispatches via zeppelinos`, `dispatches via eip1967`, and `dispatches via eip1967-beacon to ... then beacon-implementation to ...` for the three contracts respectively), and the query above returned exactly the values in the comment, live, at block 25,690,819. ## Round trips: naive vs. multicall vs. evmquery The line-count comparison above already uses viem's own batching. If you skip `multicall` and write the version most tutorials show first, five separate `await client.readContract(...)` calls, that's five separate `eth_call` round trips over the network, regardless of which library issues them: | Approach | Round trips | ABI/proxy research required | |---|---|---| | Naive viem (5 separate `readContract` calls) | 5 | Same as the multicall version below | | viem + `multicall` action | 1 | Full: confirm each address is a proxy, source the correct implementation ABI, per contract | | evmquery | 1 | None: point at the address, name the method | One distinction the table above flattens: viem's "1" is one round trip from your own process to your RPC endpoint. evmquery's "1" is one HTTPS request from your process to evmquery, behind which sit the same 5 on-chain calls in 1 Multicall3 round executed server-side (the `Calls: 5, Rounds: 1` metadata shown earlier). Both are "1" from the calling code's point of view; evmquery's version just moves the node hop behind an API instead of making it directly. On round trips alone, correctly-written viem and evmquery tie at 1. That's not a knock on viem, Multicall3 is a public contract and viem's `multicall` action uses it well. The gap this post can actually measure is upstream of the network call: the ABI and proxy research that has to happen before either version compiles. Line counts and round-trip counts are things you can verify yourself by counting the code blocks above and reading Multicall3's `aggregate3` semantics; neither is simulated or rounded for effect. The evmquery side ran live against evmquery's API on 2026-08-05 at Ethereum block 25,690,819 (5 calls, 1 round, shown in the key facts above). The viem code is real and complete, cross-checked against Aave's public `IPool.sol` interface and the standard ERC-20/ERC-721 Enumerable ABIs, but it was not executed against a live RPC endpoint in this writing session. Neither side of this post cites a timed latency number: we don't have a way to gather enough samples against production RPC infrastructure from this writing environment to produce a figure worth trusting, and a single anecdotal run wouldn't describe your network path anyway. For a real number, look at the `performance.latencyMs` field evmquery's own API returns on every request, server-measured, for your query, not a canned one. ## What evmquery does not remove A benchmark that only lists wins is marketing, not proof. Here's what stays exactly as much work as it was before: - **Limited chain coverage.** Ethereum, Base, BNB Smart Chain, and Polygon are supported; Arbitrum, Optimism, and everything else are not. A cross-chain version of this same read is still one Multicall3 batch per chain, not one batch total, on evmquery or on raw viem. - **Read-only.** Nothing here replaces viem or ethers plus a signer for the write half of an app. `eth_sendRawTransaction` and everything downstream of it is still yours to wire up. - **ABI resolution can fail.** If a contract has no verified source on Etherscan or Sourcify and doesn't match a known interface or selector database, evmquery can't invent an ABI any more than a human can; you upload one yourself. - **SEL has its own sharp edges.** Building the exact query above, a single list literal mixing string and numeric return types threw `List elements must have the same type` until every field in it was normalized to `sol_int`. Lists and maps in evmquery's expression language are type-uniform; a heterogeneous read needs separate queries, not one list literal. - **No published latency or uptime numbers yet.** evmquery's own measured p50 latency and uptime figures are still pending internally. Until they're measured and published, the honest position is the same one this post takes with its own numbers: don't take a vendor's word for a figure it hasn't published either. If the read in question is coming from an agent instead of your own code, Claude or Cursor asking a live contract-state question mid-conversation, the same resolution runs behind the MCP server instead of the REST endpoint shown above; the [AI agent integration overview](/for/ai-users) covers that surface specifically. ## Next steps - [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the batching mechanics viem's `multicall` action is built on, including the proxy pitfalls that show up once you're batching. - [Fixing "could not decode result data (0x)"](/blog/decode-result-data-0x-error) walks through the same zOS/EIP-1967 proxy identification problem from a debugging angle. - [Moralis vs Alchemy vs QuickNode vs evmquery](/blog/moralis-alchemy-quicknode-evmquery-comparison) compares evmquery against the other layers a team might reach for instead of raw RPC. - [Pricing](/pricing) if you want to run this exact query against your own contracts. --- # How evmquery Resolves a Contract Read: ABI, Proxy, and Multicall3 Source: https://evmquery.com/blog/how-evmquery-resolves-contracts Published: 2026-08-05 Author: evmquery team Category: trust A live walkthrough of what evmquery does between an address and a typed result: ABI resolution, proxy unwinding, and a Multicall3 batch, plus an honest look at portability. You write `aave_pool.getReservesCount()` and name an address. A moment later a typed value comes back. In between, evmquery has to figure out what that address actually is, whether it forwards to somewhere else, and how to fold your read into as few on-chain round trips as possible, all without you telling it any of that. This post walks through exactly what happens in that gap, with a real address, a real proxy, and a real response, then addresses the question every hosted layer owes an honest answer to: what happens if you need to stop using it. A contract read against evmquery does three things server-side before it does one thing on-chain: resolve the ABI, follow however many proxy hops separate the address you gave it from the contract that actually holds the logic, and fold every independent read into one Multicall3 round per chain. The response is plain JSON either way, and `describe_schema` will hand you the resolved address and ABI for any contract you've queried, which is what you'd need to hand-write the same call in viem or ethers if you ever left. - evmquery's privacy policy states it does **not** retain the full decoded result payload of a query beyond what's needed to deliver the response; what it logs is a timestamp, the chain and contract address targeted, and a hash of the expression, not the expression text itself, kept at full fidelity for 30 days before aggregation or deletion. - Contract resolution recognizes ten distinct dispatch kinds server-side (per the `ProxyKind` type in evmquery's own codebase): EIP-1167 minimal proxy clones, EIP-1967 transparent proxies, EIP-1967 beacon proxies and the separate beacon-implementation hop that follows them, EIP-1822 UUPS, the legacy zeppelinOS/OpenZeppelin proxy, Gnosis Safe, EIP-2535 diamonds, EIP-7702 delegation, and plain getter-based indirection. - Every response is plain JSON over a REST endpoint (`result.value`, `result.type`, `meta.blockNumber`), readable with `fetch` or `requests` and nothing else. An optional typed `@evmquery/sdk` package exists for convenience, but it isn't required to parse a response. ## What "resolving a contract" actually means An address by itself tells you nothing about what functions it exposes. Most production contracts you'll actually integrate against sit behind a proxy, so the address you're given isn't even where the logic lives; it's a thin forwarder. Resolving a read against that address means answering three questions before a single `eth_call` goes out: what ABI does this address respond to, is it a proxy and if so what does it forward to, and which of the reads you asked for can be grouped into one on-chain round trip instead of several. evmquery answers all three server-side. The rest of this post runs that pipeline against two real, differently-proxied Ethereum contracts, live, while writing this post. ## Step one: a one-hop unwind Aave V3's Pool contract on Ethereum is the entry point for every lending read on the protocol, and it sits behind a standard EIP-1967 transparent proxy. Asking evmquery to describe it, with resolution included, shows the hop before any read happens. The response below follows the documented `_extension.resolution` schema exactly (`route` is an ordered list of `{ kind, to }` hops); it's trimmed to one method for length, but the resolution, the address, and the method name are the same ones verified live while writing this post: ```bash curl -X POST https://api.evmquery.com/api/v1/query/describe \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "aave_pool": { "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } } }, "include": ["resolution"] }' # { # "contracts": [{ # "name": "aave_pool", # "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", # "_extension": { "resolution": { # "status": "verified", # "route": [{ "kind": "eip1967", "to": "0x728a138A4823392C2EFA55e028d434F526fE03CF" }] # }}, # "methods": [ # { "name": "getReservesCount", "_extension": { "resolution": { # "executesAt": "0x728a138A4823392C2EFA55e028d434F526fE03CF", "source": "sourcify" } } } # # every other method on this contract resolves to the same implementation address # ] # }] # } ``` One hop, one implementation address, and every method already bound to it. Running the actual read confirms it, live, at the time of writing, over the REST envelope evmquery documents (`result.value`, `result.type`, `meta.blockNumber`, `meta.totalCalls`, `meta.totalRounds`): ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "aave_pool": { "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" } } }, "expression": "[aave_pool.getReservesCount(), aave_pool.MAX_NUMBER_RESERVES()]" }' # { # "result": { "value": ["67", "128"], "type": "list" }, # "meta": { "blockNumber": 25690921, "totalCalls": 2, "totalRounds": 1 } # } ``` Two fields, one contract, one proxy hop resolved automatically, one round, verified live at block 25,690,921. ## Step two: a two-hop unwind Not every proxy resolves in one hop. This second contract, a beacon-proxied ERC-721 on Ethereum, sits behind an EIP-1967 beacon proxy, which is itself an extra layer of indirection: the proxy points at a beacon contract, and the beacon points at the actual implementation. The same `POST /query/describe` call reports both hops in order, in its `route` array: ```bash curl -X POST https://api.evmquery.com/api/v1/query/describe \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "avatar": { "address": "0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299" } } }, "include": ["resolution"] }' # { # "contracts": [{ # "name": "avatar", # "address": "0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299", # "_extension": { "resolution": { # "status": "verified", # "route": [ # { "kind": "eip1967-beacon", "to": "0x415eaCC51dc77E97C6bebb3296d5FFB84cCe5d8F" }, # { "kind": "beacon-implementation", "to": "0x4C9feE9218DCC2d11374dD9ca80669fF9D58d0eD" } # ] # }}, # "methods": [ # { "name": "totalSupply", "_extension": { "resolution": { # "executesAt": "0x4C9feE9218DCC2d11374dD9ca80669fF9D58d0eD", "source": "sourcify" } } } # # every other method on this contract resolves the same two hops down # ] # }] # } ``` The beacon indirection (`eip1967-beacon`) and the hop it points through to reach the implementation (`beacon-implementation`) are reported as two separate steps, not collapsed into one. A caller working this out by hand would need to read the proxy's beacon slot, then read the beacon contract's own implementation getter, before it could even look up the right ABI. ## Step three: one Multicall3 round across both The point of resolving each address individually is to make it possible to batch all of them into one request. Asking for a field from each of the two contracts above, which sit behind two different proxy patterns with two different hop counts, still executes as one on-chain round trip: ```bash curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "aave_pool": { "address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" }, "avatar": { "address": "0x8712238c3CCE66f7207e60BdaBF615D9A9C3d299" } } }, "expression": "[aave_pool.getReservesCount(), avatar.totalSupply()]" }' # { # "result": { "value": ["67", "3"], "type": "list" }, # "meta": { "blockNumber": 25690926, "totalCalls": 2, "totalRounds": 1 } # } ``` Address in, ABI resolved twice over two different proxy shapes, one field read from each, one Multicall3 round out, at block 25,690,926. That's the full pipeline this post set out to show, and every number above is a live server response, not a canned example. (The MCP tool used to verify this response also reports units consumed per query, 3 units for this one; that figure is a metering detail of the tool, not a field in the REST envelope shown above.) ## What evmquery keeps, and what it doesn't The KeyFacts block above summarizes evmquery's own privacy policy on this: for each query, it logs a timestamp, the chain and contract address targeted, a hash of the expression rather than the expression text, the result status, and a short-lived debugging trace, and it explicitly does not retain the full decoded result payload beyond delivering the response to you. Usage logs at full fidelity are kept 30 days, after which they're aggregated or deleted. If the addresses in your expression are wallet addresses rather than protocol contracts, evmquery's [Data Processing Addendum](/legal/dpa) treats you as the controller of that data and evmquery as the processor, since a wallet address can be personal data depending on what else it's tied to. None of this is a claim that no data is logged; it's a claim about which parts are, sourced directly from the published policy rather than assumed. ## If evmquery disappeared tomorrow The honest version of this question starts with the wire format: every response is plain JSON over a REST endpoint. `result.value`, `result.type`, `meta.blockNumber`. Nothing about that shape requires evmquery's client, evmquery's language, or evmquery's account to read. A `fetch` call and `JSON.parse` is the whole client. An optional typed SDK (`@evmquery/sdk`) exists, but you were never required to use it. What doesn't exist is a single "export my integration" button. What does exist, for any contract you've ever pointed evmquery at, is exactly what this post just ran: `describe_schema` with `include: ["resolution"]`, which hands back the resolved implementation address, the hop chain that got there, and the ABI signature for every method, live, for the cost of a request. That's the same information a hand-rolled viem or ethers migration would need to hardcode instead of asking evmquery's resolver to find it. Pull that output down for the contracts your integration depends on, and you have what evmquery's server-side resolution found, without evmquery in the loop. Where this blog is honestly uneven: some posts here, like the [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads), the [evmquery vs. raw viem benchmark](/blog/evmquery-vs-viem-benchmark), and the ["0x" debugging post](/blog/decode-result-data-0x-error), show the equivalent hand-written viem or ethers code next to the evmquery expression, so migrating off is closer to deleting a dependency than rewriting logic. Others, like the Aave health-factor guide or the Uniswap V3 pool-data guide, show only the evmquery call, with no hand-rolled equivalent published alongside it. If your integration leans on one of those, leaving means writing the raw multicall yourself, using the resolved address and ABI above as the starting point, not copying code this blog already wrote for you. What genuinely doesn't move if you leave: the chain state itself was never evmquery's to hold onto. It's public. There's no dataset behind an account wall, no proprietary export format, nothing evmquery keeps that you'd need permission to take with you. The only thing that goes away is the resolution and batching work. evmquery's own [developer-focused overview](/for/developers) names exactly this trade-off in its FAQ: you can still wire viem and an RPC key yourself, free and available whether or not evmquery exists; evmquery's pitch is only that it does that wiring for you. ## Next steps - [Multicall3 guide](/blog/multicall3-batching-evm-contract-reads) covers the batching mechanics behind the "one round trip" claim in this post in more depth. - [evmquery vs. raw viem benchmark](/blog/evmquery-vs-viem-benchmark) puts a line-count and round-trip number on the same resolution work shown here. - [Fixing "could not decode result data (0x)"](/blog/decode-result-data-0x-error) walks through what happens when this same proxy resolution isn't done, from a debugging angle. - [Pricing](/pricing) if you want to run either query in this post against your own contracts. --- # Uniswap V3 Pool Data Without the SDK: Price, Liquidity, and Fees in One REST Call Source: https://evmquery.com/blog/uniswap-v3-pool-data-rest-api Published: 2026-06-05 Author: evmquery team Category: guides Read sqrtPriceX96, tick, liquidity, and fee tier from any Uniswap V3 pool with a single POST request. No SDK, no ABI files, no RPC provider required. Reading one value from a Uniswap V3 pool requires the `@uniswap/v3-sdk`, a web3 provider, an ABI import, and four constructor calls before you see a number. That is 300 KB of SDK, an RPC subscription, and about twenty lines of setup — to call `slot0()` on a contract that is sitting there, entirely readable. evmquery reduces that to a single POST request. You send a pool address and a CEL expression; you get a decoded value back in milliseconds. No ABI, no provider, no SDK. POST `{"chain": "evm_ethereum", "schema": {"contracts": {"pool": {"address": "0x88e..."}}}, "expression": "pool.slot0().sqrtPriceX96"}` to `https://api.evmquery.com/api/v1/query` with your API key. Convert the returned integer to a human price in four lines of Python. Free tier: no monthly cap. ## What Uniswap V3 actually stores Every Uniswap V3 pool is a single contract with a handful of public getters. The ones you usually care about: | Method | Returns | Meaning | |--------|---------|---------| | `slot0()` | struct | `sqrtPriceX96`, current `tick`, protocol and LP fees | | `liquidity()` | `uint128` | Active in-range liquidity at the current tick | | `fee()` | `uint24` | Pool fee in hundredths of a basis point (500 = 0.05%) | | `token0()` / `token1()` | `address` | The two tokens, sorted by address (lower = token0) | `sqrtPriceX96` is the canonical price representation in Uniswap V3. It encodes `sqrt(token1/token0) * 2^96` as a 160-bit integer. Everything downstream — price charts, position managers, liquidation monitors — derives from this one field. The classic SDK approach instantiates a `Pool` object that fetches `slot0`, `liquidity`, and both token contracts, then exposes derived properties like `token0Price`. For a script that just needs the current ETH price or a pool's liquidity depth, that is significant overhead. ## Setup Two things to get started: 1. A free evmquery API key from [app.evmquery.com/onboarding?plan=free](https://app.evmquery.com/onboarding?plan=free). 2. `requests` in Python (or native `fetch` in Node). No additional packages, no ABI files, no RPC credentials. ```bash pip install requests # that's it ``` The REST endpoint: ```http POST https://api.evmquery.com/api/v1/query x-api-key: YOUR_KEY Content-Type: application/json ``` ## Read sqrtPriceX96 in one call The USDC/WETH 0.05% pool on Ethereum mainnet lives at `0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640`. It is the deepest single pool for the ETH/USD rate on-chain. ```python import os import requests resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": { "pool": {"address": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"} } }, "expression": "pool.slot0().sqrtPriceX96", }, timeout=10, ) resp.raise_for_status() data = resp.json() print(data["result"]["value"]) # e.g. "1904617700832983991655699751327701" print(data["meta"]["blockNumber"]) # block at which this was read ``` The dot notation `pool.slot0().sqrtPriceX96` accesses the `sqrtPriceX96` field of the struct returned by `slot0()`. evmquery resolves the ABI, executes the call, and returns the decoded integer as a string. Any method that returns a Solidity struct exposes its fields via dot notation in CEL expressions. `pool.slot0().tick` and `pool.slot0().sqrtPriceX96` are both valid on the same `slot0` return value. ## Interpreting sqrtPriceX96: price math `sqrtPriceX96` is not a price you can display directly. It encodes `sqrt(token1_units / token0_units) * 2^96`. To recover the human-readable ETH/USD price: 1. Divide by `2^96` to get the normalized square root. 2. Square it to get the raw price ratio (WETH raw units per USDC raw unit). 3. Adjust for the decimal difference between the two tokens (USDC has 6 decimals, WETH has 18). 4. Invert to express price as USD per ETH. ```python from decimal import Decimal, getcontext getcontext().prec = 50 # enough precision for Q96 math def sqrtpricex96_to_eth_usd( sqrt_price_x96: str, token0_decimals: int = 6, # USDC token1_decimals: int = 18, # WETH ) -> Decimal: Q96 = Decimal(2**96) sqrt_price = Decimal(sqrt_price_x96) / Q96 price_raw = sqrt_price * sqrt_price # WETH units per USDC unit # Adjust for token decimals price_weth_per_usdc = price_raw * Decimal(10**token0_decimals) / Decimal(10**token1_decimals) # Invert: USD per ETH return Decimal(1) / price_weth_per_usdc ``` Running this against the live data returns approximately `$1,730` per ETH, consistent with the current market price. Uniswap V3 sorts tokens by address. For the USDC/WETH pool, USDC (0xA0b8...) has a lower address than WETH (0xC02a...), so USDC is token0 and WETH is token1. Swap the decimal arguments if you are working with a pool where the higher-value token is token0 (e.g. a WETH/USDT pool where WETH sorts lower). ## Pull all metrics in one call CEL list literals let you batch multiple contract calls into a single API request. One round trip, four values: ```python import os import requests from decimal import Decimal, getcontext getcontext().prec = 50 EVMQUERY_API = "https://api.evmquery.com/api/v1/query" POOL = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" # USDC/WETH 0.05% resp = requests.post( EVMQUERY_API, headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"pool": {"address": POOL}} }, "expression": "[pool.slot0().sqrtPriceX96, pool.slot0().tick, pool.liquidity(), pool.fee()]", }, timeout=10, ) resp.raise_for_status() data = resp.json() sqrt_price_x96, tick, liquidity, fee = data["result"]["value"] block = data["meta"]["blockNumber"] # Price conversion Q96 = Decimal(2**96) sqrt_price = Decimal(sqrt_price_x96) / Q96 price_weth_per_usdc = (sqrt_price**2) * Decimal(10**6) / Decimal(10**18) eth_price_usd = Decimal(1) / price_weth_per_usdc print(f"Block: {block}") print(f"ETH price: ${float(eth_price_usd):.2f}") print(f"Tick: {tick}") print(f"Liquidity: {int(liquidity):,}") print(f"Fee tier: {int(fee) / 10_000:.2f}%") ``` Sample output: ``` Block: 25248715 ETH price: $1730.39 Tick: 201774 Liquidity: 4,396,230,040,359,746,608 Fee tier: 0.05% ``` The `meta.totalCalls` field in the response will show `3` (slot0 is counted twice, once for sqrtPriceX96 and once for tick), but it executes in a single HTTP round trip via on-chain batching. For developers building TypeScript services, the same request works identically with `fetch`: ```ts const resp = await fetch("https://api.evmquery.com/api/v1/query", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.EVMQUERY_API_KEY!, }, body: JSON.stringify({ chain: "evm_ethereum", schema: { contracts: { pool: { address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } }, }, expression: "[pool.slot0().sqrtPriceX96, pool.slot0().tick, pool.liquidity(), pool.fee()]", }), }); const { result, meta } = await resp.json(); const [sqrtPriceX96, tick, liquidity, fee] = result.value; // sqrtPriceX96 is a large integer string; parse with BigInt for precision const Q96 = 2n ** 96n; const sqrtBig = BigInt(sqrtPriceX96); // price_raw = sqrtBig^2 / Q96^2 (WETH units per USDC unit) // eth_price_usd numerator = Q96^2 * 10^12, denominator = sqrtBig^2 const ethPriceUsd = Number((Q96 * Q96 * 10n ** 12n) / (sqrtBig * sqrtBig)); console.log(`ETH price: $${ethPriceUsd.toFixed(2)} at block ${meta.blockNumber}`); console.log(`Fee tier: ${Number(fee) / 10_000}%`); ``` The BigInt path (`2n ** 96n`) avoids floating-point precision loss on large `sqrtPriceX96` values. This matters for pools with extreme price ratios. If you are building production pipelines that regularly scan EVM contract state, the [evmquery TypeScript REST API guide](/blog/erc20-balance-scan-rest-api-typescript/) covers retry logic, pagination, and cross-chain queries. ## Discover a pool address from its token pair You do not need to look up pool addresses manually. The Uniswap V3 Factory exposes `getPool(token0, token1, fee)` and evmquery can call it with parameterized context variables: ```python import os import requests resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": { "factory": {"address": "0x1F98431c8aD98523631AE4a59f267346ea31F984"} }, "context": { "token0": "sol_address", "token1": "sol_address", "fee": "sol_int", }, }, "context": { "token0": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC "token1": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH "fee": 500, # 0.05% }, "expression": "factory.getPool(token0, token1, fee)", }, timeout=10, ) resp.raise_for_status() pool_address = resp.json()["result"]["value"] print(pool_address) # 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 ``` `schema.context` declares the type of each variable (`sol_address`, `sol_int`); the `context` object carries the runtime values. You can swap token addresses and fee tier (500, 3000, or 10000) to look up any pool at runtime — no hardcoded addresses in your code. USDC/WETH has active pools at three fee tiers: 500 (0.05%), 3000 (0.3%), and 10000 (1%). The 0.05% pool typically carries 10-20x more liquidity. You can look up all three addresses with three Factory calls, then query the deepest one based on `liquidity()`. ## Supported chains The same query structure works across all three supported chains. Swap the `chain` field and the pool address: | Chain | `chain` value | Uniswap V3 Factory | |-------|--------------|-------------------| | Ethereum | `evm_ethereum` | `0x1F98431c8aD98523631AE4a59f267346ea31F984` | | Base | `evm_base` | `0x33128a8fC17869897dcE68Ed026d694621f6FDfD` | | BNB Chain | `evm_bnb_mainnet` | `0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7` | The expression and schema structure are identical across chains, which makes cross-chain price comparison straightforward. See the [free tools](/tools) for more DeFi-focused examples. ## Comparing fee tiers: one request per pool To compare the same pair across fee tiers, run two queries with different pool addresses and compare the `liquidity()` return values. Alternatively, [Multicall3-style batching via evmquery list expressions](/blog/multicall3-batching-evm-contract-reads/) lets you pack both reads into a single request if you know both pool addresses in advance. ## Next steps - [ERC-20 Balance Scanner in TypeScript](/blog/erc20-balance-scan-rest-api-typescript/) — REST API patterns for multi-address, multi-chain balance reads - [Multicall3: Batch EVM Contract Reads](/blog/multicall3-batching-evm-contract-reads/) — when to reach for Multicall3 vs the evmquery list expression - [Blockchain Monitoring in Python with evmquery](/blog/blockchain-monitoring-python-evmquery/) — poll pool state on an interval and trigger alerts - [evmquery's free tools](/tools) — try the ones built for DeFi reads --- # Live On-Chain Data in LangGraph: Build an EVM Blockchain Tool in 40 Lines Source: https://evmquery.com/blog/langgraph-evm-blockchain-tool Published: 2026-05-23 Author: evmquery team Category: integrations Wire evmquery into a LangGraph ReAct agent to query live EVM contract data — token supplies, oracle prices, DeFi positions — with full state persistence across tool calls. No ABIs, no RPC node. LangGraph ships from the same team as LangChain, but it is a different product. LangChain gives you prompt templates and `@tool`-decorated functions. LangGraph gives you a stateful execution graph: your agent can call tools in sequence, reason over the intermediate results, and loop back until the question is fully answered. For multi-step on-chain analysis — check an ETH price, then evaluate a DeFi position against it, then produce a risk estimate — the state persistence matters. Earlier results stay in scope. The model can refer back to them without you serializing anything. This post shows how to wire evmquery into a LangGraph ReAct agent. One `@tool` function, `create_react_agent` from `langgraph.prebuilt`, and two environment variables. The agent can then answer any question about live EVM contract state from natural language. `pip install langgraph langchain-core langchain-anthropic requests`, define one `@tool` that POSTs to `https://api.evmquery.com/api/v1/query`, pass it to `create_react_agent`. Your LangGraph agent reads live USDC supplies, ETH prices, staking ratios, or any contract view function on Ethereum, Base, or BNB — no ABIs, no RPC node, no web3.py. Free tier: no monthly cap. ## LangGraph vs LangChain: the real difference Both frameworks share the `@tool` decorator from `langchain_core.tools`. The split is in how the agent loop works. In a standard LangChain `AgentExecutor`, tool calls happen inside a linear loop managed by the executor. The loop is implicit — you configure it by passing tools and a prompt, and the executor runs until the model stops calling tools. You have limited visibility into intermediate states and limited ability to add branching logic. LangGraph makes the loop explicit. An agent is a compiled `StateGraph`: nodes are Python functions (model calls, tool calls, routing logic), edges connect them, and a `MessagesState` object flows through the graph on every step. `create_react_agent` is a convenience factory that builds this graph for you — a model node, a `ToolNode`, and a conditional edge that loops back to the model after each tool call. The practical gain for blockchain queries: the full message history — including every tool result — lives in the graph state and is automatically re-attached to the next model invocation. When you ask "compare the ETH price to the stETH/ETH ratio and tell me if staking looks attractive," the agent calls the price tool, then the stETH ratio tool, then the model has both results in context to reason about together. No manual serialization. ## What you'll build A single `evmquery_read` tool that any LangGraph agent can invoke. The tool takes a chain identifier, a named contract map, a CEL expression, and optional context variables. It calls the evmquery REST API, which resolves the ABI, executes the expression against the live chain, and returns a decoded value with the block number it was read at. The agent in this post is built with `create_react_agent` — the right starting point for 90% of use cases. The final section shows when to reach for the full `StateGraph` API instead. If you are building for developers who prefer querying the chain from a prompt without any code at all, the [evmquery MCP server](/blog/evm-blockchain-mcp-server/) is a faster path. ## Setup ```bash pip install langgraph langchain-core langchain-anthropic 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 has no monthly cap — a rate limit keeps things fair, not a quota. ## Defining the evmquery tool ```python import os import requests from typing import Optional 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. chain: evm_ethereum | evm_base | evm_bnb_mainnet contracts: alias -> 0x address, e.g. {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"} expression: CEL expression using the aliases, e.g. "formatUnits(usdc.totalSupply(), usdc.decimals())" context: optional runtime variables, e.g. {"wallet": "0x..."} for balance queries """ payload: dict = { "chain": chain, "schema": { "contracts": {k: {"address": v} for k, v in contracts.items()}, }, "expression": expression, } if context: payload["schema"]["context"] = {k: "sol_address" for k in context} payload["context"] = context resp = requests.post( EVMQUERY_API, json=payload, headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, timeout=15, ) resp.raise_for_status() data = resp.json() return f"{data['result']['value']} (block {data['meta']['blockNumber']})" ``` A few details worth noting: **Contract entries must be objects.** The API requires `{"address": "0x..."}` — not a bare string. The dict comprehension on line 22 handles the conversion so callers can pass plain address strings and the tool takes care of wrapping. **`schema.context` declares types; `context` carries values.** When a query is parameterized by a wallet address, you declare `"sol_address"` in `schema.context` and put the actual `0x` address in `context`. The tool handles both keys whenever `context` is provided. **CEL expressions do the math.** `formatUnits(usdc.totalSupply(), usdc.decimals())` calls two on-chain view functions and scales the result — all in a single API round-trip. evmquery resolves the ABI automatically from the contract address. ## Wiring into LangGraph ```python from langchain_anthropic import ChatAnthropic from langgraph.prebuilt import create_react_agent model = ChatAnthropic(model="claude-sonnet-4-6", temperature=0) agent = create_react_agent(model=model, tools=[evmquery_read]) ``` That is the complete agent setup. `create_react_agent` compiles a `StateGraph` with a model node, a `ToolNode` wrapping your tool list, and the ReAct routing logic. The compiled graph exposes `.invoke()` and `.stream()` methods. To run a query: ```python result = agent.invoke({ "messages": [("user", "What is the current ETH price in USD?")] }) print(result["messages"][-1].content) ``` The agent calls `evmquery_read` with the Chainlink ETH/USD feed, gets the raw price back, and formats a natural-language answer. No prompt engineering required on your end — the model reads the tool's docstring and constructs the correct arguments. ## Example queries The following examples use contracts that were live-validated against the evmquery REST API before publishing. **ETH/USD from Chainlink:** ```python result = agent.invoke({ "messages": [("user", ( "What is the current ETH price in USD? " "Use the Chainlink ETH/USD feed at 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 on Ethereum." ))] }) # → "The current ETH price is $2,068.90 (as of block 25155514)." ``` **USDC circulating supply:** ```python result = agent.invoke({ "messages": [("user", ( "What is the total USDC in circulation on Ethereum right now? " "USDC contract: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ))] }) # → "There are approximately 52.75 billion USDC in circulation on Ethereum (block 25155513)." ``` **wstETH/stETH exchange rate:** ```python result = agent.invoke({ "messages": [("user", ( "What is the current wstETH to stETH conversion rate? " "wstETH contract: 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 on Ethereum. " "Call stEthPerToken() and format with 18 decimals." ))] }) # → "1 wstETH currently equals approximately 1.235 stETH (block 25155516)." ``` **Multi-step cross-chain query** — this is where LangGraph's state graph shines. A single `invoke` call can drive multiple tool calls, with the model reasoning across all results before producing a final answer: ```python result = agent.invoke({ "messages": [("user", ( "Compare USDC total supply on Ethereum vs Base. " "Ethereum USDC: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. " "Base USDC: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. " "Express the Base supply as a percentage of the Ethereum supply." ))] }) # The agent calls evmquery_read twice — once per chain — then computes the ratio. # → "Ethereum holds ~52.75B USDC; Base holds ~4.22B, about 8% of the Ethereum total." ``` The agent issues two separate tool calls and the graph state accumulates both results before the model synthesizes the answer. This is the workflow where explicit state management outperforms a simpler chain. To check a specific wallet's token balance, pass `context={"wallet": "0x..."}` and use `balanceOf(wallet)` in the expression: `"formatUnits(usdc.balanceOf(wallet), usdc.decimals())"`. The tool declares `wallet` as a `sol_address` type in the schema automatically. ## Beyond create_react_agent: the StateGraph API `create_react_agent` covers the majority of use cases. Reach for the raw `StateGraph` API when you need custom state fields beyond messages — for example, tracking accumulated query results, enforcing a maximum number of tool calls, or building a supervisor that routes between multiple sub-agents. ```python import operator from typing import Literal from typing_extensions import TypedDict, Annotated from langchain_core.messages import AnyMessage from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode class AgentState(TypedDict): messages: Annotated[list[AnyMessage], operator.add] tool_node = ToolNode([evmquery_read]) model_with_tools = model.bind_tools([evmquery_read]) def call_model(state: AgentState) -> dict: return {"messages": [model_with_tools.invoke(state["messages"])]} def should_continue(state: AgentState) -> Literal["tools", "__end__"]: last = state["messages"][-1] return "tools" if last.tool_calls else "__end__" graph = StateGraph(AgentState) graph.add_node("agent", call_model) graph.add_node("tools", tool_node) graph.add_edge(START, "agent") graph.add_conditional_edges("agent", should_continue) graph.add_edge("tools", "agent") app = graph.compile() result = app.invoke({"messages": [("user", "What is the current ETH price?")]}) print(result["messages"][-1].content) ``` The graph is identical to what `create_react_agent` builds internally — but now you can add nodes, inject custom state fields, or hook in LangSmith tracing at the edge level. Extend `AgentState` with additional fields and update them inside `call_model` or a custom node to track whatever you need across the loop. For developers building AI-powered DeFi applications, the [evmquery REST API](/for/ai-users) is the data layer — LangGraph provides the orchestration. Any agent framework that supports `langchain_core` tools works the same way. The evmquery MCP surface (`https://api.evmquery.com/mcp`) is also available for Claude Desktop, Cursor, and VS Code integrations without any code. ## Next steps - [LangChain EVM blockchain tool](/blog/langchain-evm-blockchain-tool/) — the same `@tool` pattern without the graph runtime, for simpler single-step agents - [evmquery MCP server](/blog/evm-blockchain-mcp-server/) — connect directly to Claude Desktop or Cursor without writing any Python - [Blockchain monitoring in Python](/blog/blockchain-monitoring-python-evmquery/) — schedule periodic contract reads with the REST API directly - [Query EVM contract data from Python](/blog/query-evm-contract-data-python/) — REST API fundamentals if you are building without a framework --- # Live Onchain Data in Pydantic AI: Build an EVM Blockchain Tool in 30 Lines Source: https://evmquery.com/blog/pydantic-ai-evm-blockchain-tool Published: 2026-04-29 Author: evmquery team Category: integrations Define a typed Pydantic AI tool that reads live EVM contract data — USDC balances, ETH prices, Aave positions — without ABIs, RPC nodes, or web3.py boilerplate. Pydantic AI's tool system is built around type safety and structured schemas — exactly what you want when reading onchain data, where a misplaced decimal or wrong address type produces silently wrong answers rather than errors. The problem is that the framework ships no blockchain tooling. Connecting to a smart contract means reaching for web3.py, managing an RPC provider, loading ABI files, and handling decimal scaling by hand. None of that has anything to do with the agent you are trying to build. The faster path: define one `@agent.tool_plain` that calls evmquery's REST API. evmquery resolves ABIs automatically, executes a typed expression against the live chain, and returns a decoded human-readable value. The tool stays under 30 lines; the agent can query any contract view function on Ethereum, Base, or BNB Chain from a plain English prompt. `pip install pydantic-ai requests`, define an `@agent.tool_plain`, and POST to `https://api.evmquery.com/api/v1/query`. Your Pydantic AI 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: no monthly cap. ## Why Pydantic AI for agent tooling Pydantic AI builds on the same validation philosophy as Pydantic itself: your data should be typed, validated, and structured from the edge inward. Tools are Python functions with type-annotated parameters; the framework generates the tool schema from those annotations and validates every model-generated argument before your function executes. For blockchain agents this matters. A model that hallucinates a malformed address or passes a string where a number is expected should fail fast with a clear error, not silently produce a wrong balance. Pydantic AI's `tool_plain` decorator does that automatically — it reads parameter types, builds a JSON schema the model must conform to, and validates the call before your HTTP request is ever made. There is also structured output. If you want the agent to return a typed object — say, a `DeFiPosition` model with `usdc_balance`, `eth_price_usd`, and `health_factor` fields — you set `result_type=DeFiPosition` on the agent and the model is forced to produce output that conforms to that shape. That is useful for building downstream pipelines that expect consistent data, not free-form text. ## What you'll build A single `evmquery_read` tool that any Pydantic AI agent can invoke to read live EVM contract state. The tool takes a chain identifier, a short-name-to-address contract map, a CEL expression, and optional context variables. It calls the evmquery REST API and returns the decoded result and block number. If you would rather query the chain interactively from Claude Desktop or Cursor without writing code, the [evmquery MCP server](/blog/evm-blockchain-mcp-server/) is the faster path. This tool is for Python agents with custom business logic — portfolio monitors, DeFi alerting systems, structured data pipelines. ## Setup ```bash pip install pydantic-ai requests ``` ```bash export ANTHROPIC_API_KEY=sk-ant-... export EVMQUERY_API_KEY=your_evmquery_key ``` Get a free evmquery key at `https://app.evmquery.com/onboarding?plan=free`. The free tier has no monthly cap — a rate limit keeps things fair, not a quota. ## Defining the Pydantic AI evmquery tool ```python import os from typing import Any import requests from pydantic_ai import Agent EVMQUERY_API = "https://api.evmquery.com/api/v1/query" agent = Agent( "anthropic:claude-sonnet-4-6", system_prompt=( "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 on-chain data. Always include the block number " "so the user knows the result is current." ), ) @agent.tool_plain def evmquery_read( chain: str, contracts: dict[str, str], expression: str, context: dict[str, Any] | None = 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, event logs, or off-chain prices. 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 in the expression. Pass a list of addresses for multi-wallet expressions. """ def _type(v: Any) -> str: return "list" if isinstance(v, list) else "sol_address" body: dict[str, Any] = { "chain": chain, "schema": { "contracts": {k: {"address": v} for k, v in contracts.items()}, }, "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']['value']} (block {data['meta']['blockNumber']})" ``` A few things worth noting: - **`@agent.tool_plain` vs `@agent.tool`.** `tool_plain` is for tools that don't need access to the run context — no dependency injection, no run metadata. It is the right choice here because everything the tool needs (the API key) comes from the environment. Use `@agent.tool` with a `RunContext[Deps]` parameter when you want to pass the key through the agent's dependency injection system instead. - **Type annotations drive the schema.** Pydantic AI reads the annotations on `contracts`, `expression`, and `context` to build the JSON schema the model must conform to. Wrong types are rejected before your function runs. - **Contract address wrapping.** evmquery's REST API expects each contract entry as `{"address": "0x..."}`, not a bare string. The dict comprehension on line 36 handles that conversion, so callers can pass the simpler `{"usdc": "0xA0b..."}` form. - **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 automatically. ## Three live recipes All expressions below were validated against the live chain before publication. ### ERC-20 balance check ```python result = agent.run_sync( "What is the USDC balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum?" ) print(result.data) # The wallet holds 5,567.40 USDC as of block 24,983,270. ``` Underneath, the model calls `evmquery_read` with: ```python { "chain": "evm_ethereum", "contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())", "context": {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}, } # tool returns → "5567.402493 (block 24983270)" ``` `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 result = agent.run_sync("What is the current ETH price in USD?") print(result.data) # ETH is trading at $2,314.77 as of block 24,983,271. ``` The model calls: ```python { "chain": "evm_ethereum", "contracts": {"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"}, "expression": "formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())", } # tool returns → "2314.77 (block 24983271)" ``` No context variables needed — this is a pure contract read. The address is the canonical Chainlink ETH/USD aggregator on Ethereum mainnet. The feed updates every block; there is no caching layer between the model and the live price. ### Multi-wallet balance scan ```python result = agent.run_sync( "Check USDC balances for these three wallets on Ethereum: " "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, " "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8, " "0x40B38765696e3d5d8d9d834D8AaD4bB6e418E489" ) print(result.data) # USDC balances (block 24,983,272): # 0xd8dA…6045 → 5,567.40 USDC # 0xBE0e…33E8 → 3.00 USDC # 0x40B3…3E8 → 10.01 USDC ``` The model passes the list of wallets in `context`: ```python { "chain": "evm_ethereum", "contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "expression": "wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))", "context": { "wallets": [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8", "0x40B38765696e3d5d8d9d834D8AaD4bB6e418E489", ] }, } # tool returns → "[5567.402493, 3.0, 10.005273] (block 24983272)" ``` The `_type` helper detects a Python list and declares the context type as `list`. The CEL `map` macro iterates over all three wallets in one RPC round-trip — not three separate tool calls. ## Structured results: reading a DeFi position Pydantic AI's `result_type` parameter constrains the agent to return a typed object. This is useful when the output feeds into downstream code rather than a chat interface. ```python from pydantic import BaseModel from pydantic_ai import Agent class DeFiSnapshot(BaseModel): usdc_balance: float eth_price_usd: float block_number: str snapshot_agent = Agent( "anthropic:claude-sonnet-4-6", result_type=DeFiSnapshot, system_prompt=( "Fetch live data for the wallet provided and return a structured snapshot. " "Use evmquery_read for all on-chain reads." ), ) # Register the same tool on this agent snapshot_agent.tool_plain(evmquery_read) result = snapshot_agent.run_sync( "Snapshot wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum." ) print(result.data) # DeFiSnapshot(usdc_balance=5567.402493, eth_price_usd=2314.77, block_number='24983271') print(result.data.usdc_balance) # 5567.402493 ``` The model makes two tool calls — one for the USDC balance and one for the ETH price — then assembles the result into the `DeFiSnapshot` shape. If it tries to return a field that doesn't match the type annotation, Pydantic AI rejects the response and asks the model to try again. For wallets with no active borrow position, Aave's `healthFactor` returns the maximum `uint256` value divided by 1e18 — roughly `1.16e59`. This represents infinite health (no debt), not an error. Document this threshold in your system prompt so the model describes it correctly rather than reporting a confusing large number. ```python # Aave health factor read result = agent.run_sync( "What is the Aave v3 health factor for 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 " "on Ethereum?" ) # Tool call: # chain: evm_ethereum # contracts: {"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"} # expression: "formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)" # context: {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"} # → "1.1579208923731619e+59 (block 24983271)" # Model response: "This wallet has no active borrow position — health factor is effectively infinite." ``` Developers building DeFi tooling and monitoring pipelines can find more expression patterns, including reserve data reads and protocol-level aggregations, in the [developer resources](/). If you are focused on AI-native workflows, the [AI users page](/for/ai-users) covers the REST tool and MCP surfaces side by side. ## Pydantic AI tool vs MCP: picking the right surface | | Pydantic AI tool (this post) | MCP server | |---|---|---| | Language | Python | Any MCP client | | Setup | ~30 lines in your agent file | Paste one JSON config block | | Type safety | Full — Pydantic validates every call | Protocol-level only | | Structured output | Yes — `result_type` enforces shape | Not natively | | Multi-tool agents | Yes, composable with other tools | Limited to MCP surface | | Best for | Production agents, typed pipelines | Claude Desktop, Cursor, VS Code | If you are building a production Python agent with custom business logic, the `@agent.tool_plain` approach gives full control over the schema, type validation, error handling, and result formatting. If you want to query the chain interactively from your AI 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 LangChain in Python](/blog/langchain-evm-blockchain-tool/) — same REST integration, different framework - [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 CEL expression reference --- # Live Onchain Data in CrewAI: Build a Custom EVM Blockchain Tool Source: https://evmquery.com/blog/crewai-evm-blockchain-tool Published: 2026-04-28 Author: evmquery team Category: integrations Wire a custom evmquery tool into CrewAI and give your AI crew live access to EVM contract state — USDC balances, ETH prices, and Aave health factors — without ABIs or RPC nodes. CrewAI ships tools for web search, file I/O, and code execution — but nothing for reading live EVM contract state. Ask a CrewAI agent for the current USDC balance of a wallet and it will either hallucinate a number from training data or refuse the task. That balance changes every block and was never in any corpus. Fixing this takes one custom tool. Define a `BaseTool` subclass that POSTs to the evmquery REST API, assign it to whichever agent in the crew needs chain access, and any prompt about current token balances, oracle prices, or DeFi positions returns a live, decoded answer. `pip install crewai requests`, subclass `BaseTool`, POST to `https://api.evmquery.com/api/v1/query`, and any CrewAI agent can read USDC balances, Chainlink prices, or Aave health factors on Ethereum, Base, or BNB. No ABIs, no RPC node. Free tier: no monthly cap. ## How CrewAI tools work CrewAI tools extend `BaseTool` from `crewai.tools`. Each tool declares a `name`, a `description` the model reads when deciding whether to call it, and an `args_schema` — a Pydantic model that defines the input fields and their descriptions. The `_run` method contains the implementation. When a CrewAI agent decides it needs external data, it emits a structured tool call matching the schema. The framework validates the arguments, routes the call to `_run`, and injects the return value back into the agent's reasoning chain. You define the shape; the crew handles the routing. For onchain queries this is the right design. The agent knows it cannot answer "what is the current Aave health factor?" from training data. A properly described tool gives it a deterministic path to the real answer without you hard-coding routing logic in application code. CrewAI also supports a simpler `@tool` function decorator for quick one-offs. This post uses `BaseTool` because it provides a typed `args_schema`, which produces better model instructions and makes the tool easier to unit-test in isolation. ## What you'll build A single `EvmqueryReadTool` that any agent in a crew can invoke. The tool accepts a chain, a named contract map, a CEL expression, and optional context variables. It calls the evmquery REST API and returns the decoded value and block number. The same tool works in a single-agent workflow and in a multi-agent crew where, for example, a blockchain analyst agent fetches data and a portfolio reporter agent formats the findings for an end user. If you want to query the chain interactively from Claude Desktop or Cursor without writing code, the [evmquery MCP server](/blog/evm-blockchain-mcp-server/) is the faster path. This post is for production Python agents. ## Setup ```bash pip install crewai requests ``` Set two environment variables: ```bash export ANTHROPIC_API_KEY=sk-ant-... export EVMQUERY_API_KEY=evmq_... ``` Get a free evmquery key at `https://app.evmquery.com/onboarding?plan=free`. The free tier has no monthly cap — a rate limit keeps things fair, not a quota. ## Defining the evmquery tool ```python import os from typing import Optional, Type import requests from crewai.tools import BaseTool from pydantic import BaseModel, Field EVMQUERY_API = "https://api.evmquery.com/api/v1/query" class EvmqueryReadInput(BaseModel): chain: str = Field( description="Chain to query. One of: evm_ethereum, evm_base, evm_bnb_mainnet." ) contracts: dict[str, str] = Field( description=( "Named contract addresses. Key is the short name used in the expression, " "value is the 0x contract address. " "Example: {'usdc': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'}" ) ) expression: str = Field( description=( "CEL expression to evaluate. Named contracts become variables. " "Example: 'formatUnits(usdc.balanceOf(wallet), usdc.decimals())'" ) ) context: Optional[dict[str, str | list[str]]] = Field( default=None, description=( "Runtime values for wallet addresses or other parameters used in the expression. " "Pass a list of addresses for multi-wallet expressions. " "Example: {'wallet': '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'}" ), ) def _sel_type(v: str | list) -> str: return "list" if isinstance(v, list) else "sol_address" class EvmqueryReadTool(BaseTool): name: str = "evmquery_read" description: 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. Do NOT use for historical data or event logs." ) args_schema: Type[BaseModel] = EvmqueryReadInput def _run( self, chain: str, contracts: dict[str, str], expression: str, context: Optional[dict] = None, ) -> str: body: dict = { "chain": chain, "schema": { "contracts": {k: {"address": v} for k, v in contracts.items()}, }, "expression": expression, } if context: body["schema"]["context"] = {k: _sel_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() result = data["result"]["value"] block = (data.get("meta") or {}).get("blockNumber", "?") return f"{result} (block {block})" ``` Three design choices worth noting: - **Field descriptions are the model's schema.** Pydantic `Field(description=...)` strings are what the model reads when deciding how to call the tool. Specific scope constraints — "Do NOT use for historical data" — reduce misrouted calls. - **Contracts expand to `{"address": "..."}` objects.** The evmquery REST API expects contract entries as objects, not plain strings. The `_run` method handles this conversion so the agent passes simple name-to-address dicts and never sees the wire format. - **`_sel_type` infers context variable types.** evmquery's type system distinguishes single addresses (`sol_address`) from lists (`list`). The helper infers the correct declaration from the Python value — the model does not need to know about evmquery's type system. ## Three live recipes All expressions below were validated against the live chain before publication. ### ERC-20 balance check ```python tool = EvmqueryReadTool() result = tool._run( chain="evm_ethereum", contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, expression="formatUnits(usdc.balanceOf(wallet), usdc.decimals())", context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}, ) # → "5567.402493 (block 24976057)" ``` `formatUnits` reads `decimals()` directly from the contract, so scaling is always correct regardless of whether the token uses 6, 8, or 18 decimal places. ### Live ETH/USD price via Chainlink ```python result = tool._run( chain="evm_ethereum", contracts={"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"}, expression="formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())", ) # → "2287.04 (block 24976057)" ``` 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. ### Aave v3 health factor ```python result = tool._run( chain="evm_ethereum", contracts={"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"}, expression="formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)", context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}, ) # → "1.157920892373162e+59 (block 24976059)" ``` A health factor above 1.0 means the position is safe; below 1.0 triggers liquidation. The large exponential result here represents a wallet with no active Aave debt — it is the maximum `uint256` value scaled by 1e18, evmquery's way of encoding infinite health. Add a note in your agent's system prompt so it interprets this correctly rather than reporting an error. ## Wiring into a CrewAI agent With the tool class defined, assign it to an agent that needs chain access: ```python from crewai import Agent, Task, Crew, LLM llm = LLM(model="claude-sonnet-4-6") blockchain_analyst = Agent( role="Blockchain Data Analyst", goal=( "Fetch accurate, live onchain data to answer user questions about " "token balances and DeFi positions." ), backstory=( "You are a seasoned DeFi analyst with deep knowledge of EVM smart contracts. " "You always verify data by reading directly from the chain before drawing conclusions." ), tools=[EvmqueryReadTool()], llm=llm, verbose=True, ) task = Task( description=( "Check the USDC balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum " "and the current ETH/USD price from Chainlink. Report both values with block numbers." ), expected_output="A brief report with the USDC balance and ETH price, including block numbers.", agent=blockchain_analyst, ) crew = Crew(agents=[blockchain_analyst], tasks=[task]) result = crew.kickoff() print(result) ``` The agent sees `evmquery_read` in its toolbox, recognises the task requires live data, and calls the tool twice — once for the USDC balance and once for the ETH price. The block numbers in each result confirm the data is current, not cached. ## Multi-agent workflow: analyst and reporter CrewAI's strength is composing multiple specialised agents. A blockchain analyst fetches raw data; a portfolio reporter formats it for a non-technical audience. Only the analyst gets the evmquery tool: ```python portfolio_reporter = Agent( role="Portfolio Reporter", goal="Turn raw blockchain data into clear, readable summaries for non-technical users.", backstory=( "You translate DeFi numbers into plain English. " "You never invent data — you wait for the analyst's findings before writing." ), llm=llm, ) fetch_task = Task( description=( "Use evmquery_read to fetch the USDC balance and current ETH/USD price " "for 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum. " "Include the block number for each value." ), expected_output="Raw balance and price values with block numbers.", agent=blockchain_analyst, ) report_task = Task( description=( "Using only the analyst's findings, write a two-sentence portfolio snapshot " "in plain English. Include the block numbers to show data freshness." ), expected_output="A clear, jargon-free portfolio snapshot with block numbers.", agent=portfolio_reporter, context=[fetch_task], ) crew = Crew( agents=[blockchain_analyst, portfolio_reporter], tasks=[fetch_task, report_task], ) result = crew.kickoff() print(result) ``` `context=[fetch_task]` tells CrewAI that `report_task` depends on `fetch_task`'s output. The analyst runs first, fetches live data, and the result is passed as context to the reporter. The reporter never calls the chain directly — only the analyst has the tool. Assign `EvmqueryReadTool` only to agents that need chain access. Summariser or formatter agents that work only from prior task output don't need it — this prevents unnecessary tool calls and keeps each agent's role well-defined. ## Developers building DeFi-aware AI products If you are building production DeFi tooling for AI agents, the [developer resources page](/) covers the full evmquery REST API: multi-wallet batch reads using the CEL `map` operator, list-type context variables for scanning many addresses in one call, and struct field access for protocols that return complex return types. For AI agent workflows specifically, the [AI users page](/for/ai-users) covers the REST tool and MCP surfaces side by side. ## CrewAI tool vs MCP: picking the right surface | | CrewAI tool (this post) | MCP server | |---|---|---| | Language | Python | Any MCP client | | Setup | ~60 lines in your agent file | Paste one config block | | Multi-agent | Yes, assign per-agent | Single conversation surface | | Control | Full: schema, error handling, logging | Client manages the conversation | | Best for | Production crews, custom backends | Claude Desktop, Cursor, VS Code | If you are building a Python multi-agent system, the `BaseTool` approach gives full control over the Pydantic schema, error formatting, and result shaping before the model sees the value. 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 LangChain agents in Python](/blog/langchain-evm-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 --- # 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, BNB Chain, and Polygon, 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 and on-chain call count, which is useful for 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](/): 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, BNB Chain, and Polygon 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`. ## Rate limits The free tier has no monthly cap; usage is bounded by a rate limit instead. A `map` across any number of wallets is a single request regardless of list size, and a batched `balanceOf` scan counts once whether it covers one wallet or fifty. For applications that poll frequently, that per-request accounting 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) — no monthly cap, 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 has no monthly cap. ## 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, BNB Smart Chain, and Polygon evmquery supports Ethereum, Base, BNB Smart Chain, and Polygon. A user who runs positions on multiple networks needs two to four 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}") ``` That's two sequential calls per wallet. 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 has no monthly cap, but usage is bounded by a rate limit. A `.filter()` call over 10 wallets still counts toward that limit like any other request. At 15-minute intervals across a handful of wallets you'll stay comfortably inside it; tighten the interval or watch many more wallets and you may need to pace your requests. ## 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](/). --- # 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: no monthly cap. ## 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 has no monthly cap — a rate limit keeps things fair, not a quota. ## 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](/). 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: no monthly cap. ## 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 has no monthly cap, with no credit card required. A single contract read is one request, 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. ### 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: ```yaml 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. Request count 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](/): 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 has no monthly cap, 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 has no monthly cap — a rate limit keeps things fair, not a quota. ## 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](/) 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 are cheap, single round-trip calls; 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](/) 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, BNB Smart Chain, and Polygon. ## 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, including the block number and on-chain call count. - `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 through your browser. 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 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 when prompted. New to evmquery? The free tier has no monthly cap, 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 support browser sign-in 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. Browser sign-in 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: ```yaml 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: ```yaml expression: bayc.ownerOf(solInt(7890)) == solAddress("0xd8dA…6045") ``` Returns `true` or `false` in one round trip. 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](/) — 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. For a deeper look at exactly where that read logic stops being trivial, see the [dedicated Alchemy comparison](/blog/alchemy-alternative). ## 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](/) 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 Updated: 2026-08-05 Author: evmquery team Category: reference 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. - Deployed at `0xcA11bde05977b3631167028862bE2a173976CA11` on Ethereum, Base, BNB Smart Chain, and 250+ other EVM chains, same address everywhere (one documented exception: Ancient8 redeployed after a compromised deployer key). - `aggregate3(Call3[])` never reverts the whole batch on one failing sub-call when `allowFailure: true` is set per-call; the original `aggregate(Call[])` reverts the entire batch if any sub-call fails. - `aggregate3Value(Call3Value[])` adds a `value` field per call for batching calls that send ETH alongside a read, rarely needed for pure reads. - Gas per call in a batch is close to the sum of the individual calls' gas: Multicall3's win is round trips, not gas. 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. ## Deployment addresses Multicall3 sits at the same address on every chain below, and on 250+ more not listed here (the full, current list lives in the [`mds1/multicall3` deployments file](https://github.com/mds1/multicall3/blob/main/deployments.json)). The one documented exception is Ancient8, which redeployed at a different address after its original deployer key was compromised. | Chain | Chain ID | Address | Block deployed | |---|---|---|---| | Ethereum Mainnet | 1 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [14,353,601](https://etherscan.io/tx/0x00d9fcb7848f6f6b0aae4fb709c133d69262b902156c85a473ef23faa60760bd) | | Base | 8453 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [5,022](https://basescan.org/tx/0x07471adfe8f4ec553c1199f495be97fc8be8e0626ae307281c22534460184ed1) | | Optimism | 10 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [4,286,263](https://optimistic.etherscan.io/tx/0xb62f9191a2cf399c0d2afd33f5b8baf7c6b52af6dd2386e44121b1bab91b80e5) | | Polygon | 137 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [25,770,160](https://polygonscan.com/tx/0x25d385667b12d6992742127dc7682e570136397806e2773dc47922eba0001989) | | Arbitrum One | 42161 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [7,654,707](https://arbiscan.io/tx/0x211f6689adbb0f3fba7392e899d23bde029cef532cbd0ae900920cc09f7d1f32) | | BNB Smart Chain | 56 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [15,921,452](https://bscscan.com/address/0xcA11bde05977b3631167028862bE2a173976CA11#code) | | Avalanche C-Chain | 43114 | `0xcA11bde05977b3631167028862bE2a173976CA11` | [11,907,934](https://snowtrace.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code) | ## 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. ## Python (web3.py) web3.py has no first-party multicall helper either, so you build the `Contract` yourself against the `aggregate3` ABI fragment and encode each sub-call with a second, address-less contract instance for the target ABI. ```python from web3 import Web3 MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11" MULTICALL3_ABI = [{ "name": "aggregate3", "type": "function", "stateMutability": "payable", "inputs": [{ "name": "calls", "type": "tuple[]", "components": [ {"name": "target", "type": "address"}, {"name": "allowFailure", "type": "bool"}, {"name": "callData", "type": "bytes"}, ], }], "outputs": [{ "name": "returnData", "type": "tuple[]", "components": [ {"name": "success", "type": "bool"}, {"name": "returnData", "type": "bytes"}, ], }], }] ERC20_ABI = [{ "name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}], }] w3 = Web3(Web3.HTTPProvider(rpc_url)) multicall = w3.eth.contract(address=MULTICALL3, abi=MULTICALL3_ABI) erc20 = w3.eth.contract(abi=ERC20_ABI) # no address: used only to encode/decode tokens = [usdc, usdt, dai] calls = [ (token, True, erc20.encode_abi("balanceOf", args=[holder])) for token in tokens ] results = multicall.functions.aggregate3(calls).call() balances = [ w3.to_int(r[1]) if r[0] else None for r in results ] ``` The gotchas mirror the Ethers version: pass tuples, not dicts, for `calls`, set `allowFailure=True` on every entry unless you want one bad call to revert the batch, and check `r[0]` (`success`) before touching `r[1]` (`returnData`). ## 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](/) 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](/) 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 has no monthly cap; 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 has no monthly cap — 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](/) 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](/). --- # 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 has no monthly cap. ## 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, BNB Smart Chain, and Polygon — the networks the engine currently serves. The corresponding ids (if you ever set the node via an expression) are `evm_ethereum`, `evm_base`, `evm_bnb_mainnet`, and `evm_polygon`. 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.** There's no monthly cap, but usage is bounded by a rate limit. One or two recipes on 5-minute schedules stay comfortably inside it; a 10-second-interval scanner may need to pace its requests. ## 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.