Integration

Live Onchain Data in LangChain: Build a Custom EVM Blockchain Tool in 30 Lines

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.

evmquery team · · 7 min read
LangChain plus evmquery: live EVM blockchain data as a custom agent tool

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.

TL;DR

pip install langchain-core langchain-anthropic langgraph requests, decorate a function with @tool, and POST to https://api.evmquery.com/api/v1/query. Any LangChain agent can then read live USDC balances, ETH prices from Chainlink, or Aave health factors on Ethereum, Base, or BNB. No ABIs, no RPC node, no web3.py. Free tier: 2,000 credits/month.

How LangChain tools work

LangChain tools are Python functions decorated with @tool from langchain_core.tools. The decorator reads the docstring and type annotations to generate the tool’s name, description, and parameter schema automatically. When a model decides it needs external data, it emits a structured tool call; LangChain validates the arguments, calls the function, and feeds the result back to the model.

For blockchain queries this is the right primitive. The model knows it cannot answer “what is my USDC balance right now?” from training data. That number changes every block and was never in any corpus. A properly scoped tool gives the model a path to the real answer without you predicting every possible query in application code.

LangChain’s BlockchainDocumentLoader was designed for a different job: loading NFT metadata into a vector store for retrieval. It requires Alchemy, only supports a fixed set of schemas, and returns documents, not structured values. This tool fills the gap for any contract read at runtime.

What you’ll build

A single evmquery_read tool that any LangChain agent or chain can invoke. The tool takes a chain, a contract map, a CEL expression, and optional context variables. It calls the evmquery REST API and returns the decoded result and the block number it was read from.

If you are building an AI-powered DeFi dashboard, a portfolio monitor, or any agent that needs current contract state, this is the integration surface. If you want to query the chain directly from Claude Desktop or Cursor without writing any code, the evmquery MCP server is the faster path.

Setup

pip install langchain-core langchain-anthropic langgraph requests
export ANTHROPIC_API_KEY=sk-ant-...
export EVMQUERY_API_KEY=eq_...

Get a free evmquery key at https://app.evmquery.com/onboarding?plan=free. The free tier covers 2,000 credits per month, roughly 1,000 typical contract reads.

Defining the evmquery tool

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<sol_address>" 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<sol_address>). 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

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.

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

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<sol_address> 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:

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:

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.

Multiple tools

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:

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 covers the REST tool and MCP surfaces side by side.

LangChain tool vs MCP: picking the right surface

LangChain tool (this post)MCP server
LanguagePythonAny MCP client
Setup30 lines in your agent filePaste one config block
ControlFull: schema, error handling, loggingClient manages the conversation
Multi-tool agentsYes, composable with other toolsLimited to the MCP surface
Best forProduction agents, custom backendsClaude 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 gets you there in under five minutes.

Next steps

Two minutes to your first live chain read

2,000 credits a month, no credit card required. Grab a free API key and run your first expression against the live chain today.