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.
TL;DR
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: 2,000 credits/month.
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 is a faster path.
Setup
pip install langgraph langchain-core langchain-anthropic 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
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
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:
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:
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:
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:
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:
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.
Wallet balance queries
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.
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 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 — the same
@toolpattern without the graph runtime, for simpler single-step agents - evmquery MCP server — connect directly to Claude Desktop or Cursor without writing any Python
- Blockchain monitoring in Python — schedule periodic contract reads with the REST API directly
- Query EVM contract data from Python — REST API fundamentals if you are building without a framework