Integration

Live Onchain Data in Azure AI Foundry: Build an EVM Blockchain Tool

Wire a live EVM blockchain tool into an Azure AI Foundry agent using FunctionTool — token balances, oracle prices, and Aave positions from a natural language prompt.

evmquery team · · 7 min read
Azure AI Foundry plus evmquery: live EVM blockchain data as a function tool

Azure AI Foundry gives you a managed agent runtime backed by Azure’s model catalog — GPT-4o, Mistral, Llama, and more — with built-in thread state, persistent message history, and enterprise auth via Entra ID. What it does not give you is live blockchain data. Ask a Foundry agent for your current WETH balance and it will either refuse or guess. That number lives on-chain, changes every block, and has never been in any training corpus.

A single custom function tool fixes this. In under 40 lines you can give any Azure AI Foundry agent the ability to read ERC-20 balances, DeFi positions, and oracle prices directly from the chain.

TL;DR

pip install azure-ai-projects azure-ai-agents azure-identity requests, define an evmquery_read function with a docstring, pass it to FunctionTool(functions={evmquery_read}), and your Azure AI Foundry agent can answer questions about live token balances, ETH prices, and Aave health factors. Free tier: 2,000 credits/month, no credit card needed.

How Azure AI Foundry function tools work

Azure AI Foundry agents follow an OpenAI Assistants-style thread and run model. Instead of one-shot completions, the agent operates on a persistent thread (conversation history) and executes each user turn as a run. During a run the model can emit tool calls; your application handles them and submits the outputs back before the run continues.

The FunctionTool class from azure-ai-agents bridges your Python functions and that dispatch loop. Pass a set of callables to FunctionTool(functions={...}) and the SDK introspects each function’s type annotations and docstring to build the JSON schema the model sees. The execute(tool_call) method handles the dispatch: it looks up the function by name, deserializes the arguments, calls the function, and returns the string output to pass back to the run.

The polling loop is more explicit than in frameworks like Agno or LangChain, but it gives you full control: you decide when tool calls execute, can log each invocation, and can reject or transform inputs before they reach the external API.

The evmquery_read tool function

evmquery’s REST API takes a chain identifier, a named contract map, and a CEL expression. The response includes the decoded result and the block number it was read from. One format detail: the API requires contract entries to be {"address": "0x..."} objects, not bare strings. The function below accepts plain address strings from the model and converts them internally.

import os
import time
from typing import Optional

import requests
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential
from azure.ai.agents.models import (
    FunctionTool,
    RequiredFunctionToolCall,
    SubmitToolOutputsAction,
    ToolOutput,
)

EVMQUERY_API = "https://api.evmquery.com/api/v1/query"


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. One of evm_ethereum, evm_base, evm_bnb_mainnet.
        contracts: Map of short name to contract address (0x hex string).
                   Example: {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
        expression: CEL expression evaluating contract state. Contract short
                    names become variables. Example:
                    "formatUnits(usdc.balanceOf(wallet), usdc.decimals())"
        context: Optional map of variable names to 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"

    contract_schema = {k: {"address": v} for k, v in contracts.items()}

    body: dict = {
        "chain": chain,
        "schema": {"contracts": contract_schema},
        "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']})"

Two implementation notes:

  • The docstring is the schema. The Azure AI Agents SDK passes it directly to the model as the tool description. Scope constraints (“Do NOT use for historical data or event logs”) reduce misrouted calls without any extra routing code.
  • 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 at call time.

Wiring the tool into an agent

With the function defined, the rest is standard Foundry boilerplate: create the FunctionTool, create the agent, open a thread, post a message, run it, poll until done.

user_functions = {evmquery_read}
functions = FunctionTool(functions=user_functions)

project_client = AIProjectClient(
    endpoint=os.environ["PROJECT_ENDPOINT"],
    credential=DefaultAzureCredential(),
)

with project_client:
    agents_client = project_client.agents

    agent = agents_client.create_agent(
        model=os.environ["MODEL_DEPLOYMENT_NAME"],
        name="evm-data-agent",
        instructions=(
            "You are a blockchain data assistant. When asked about token balances, "
            "DeFi positions, or current protocol metrics, use evmquery_read to fetch "
            "live data from the chain. Always include the block number in your reply."
        ),
        tools=functions.definitions,
    )

    thread = agents_client.threads.create()
    agents_client.messages.create(
        thread_id=thread.id,
        role="user",
        content="What is the current ETH/USD price from the Chainlink oracle?",
    )

    run = agents_client.runs.create(thread_id=thread.id, agent_id=agent.id)

    while run.status in ("queued", "in_progress", "requires_action"):
        time.sleep(1)
        run = agents_client.runs.get(thread_id=thread.id, run_id=run.id)

        if run.status == "requires_action" and isinstance(
            run.required_action, SubmitToolOutputsAction
        ):
            tool_outputs = []
            for tool_call in run.required_action.submit_tool_outputs.tool_calls:
                if isinstance(tool_call, RequiredFunctionToolCall):
                    output = functions.execute(tool_call)
                    tool_outputs.append(
                        ToolOutput(tool_call_id=tool_call.id, output=output)
                    )
            agents_client.runs.submit_tool_outputs(
                thread_id=thread.id, run_id=run.id, tool_outputs=tool_outputs
            )

    messages = agents_client.messages.list(thread_id=thread.id)
    for msg in messages:
        if msg.role == "assistant" and msg.text_messages:
            print(msg.text_messages[-1].text.value)

    agents_client.delete_agent(agent.id)

Set three environment variables before running:

export PROJECT_ENDPOINT="https://<resource>.ai.azure.com/api/projects/<project>"
export MODEL_DEPLOYMENT_NAME="gpt-4o"
export EVMQUERY_API_KEY="eq_..."

PROJECT_ENDPOINT is found on the Overview page of your Azure AI Foundry project. DefaultAzureCredential authenticates via az login in development; in production it picks up managed identity automatically.

Install

pip install azure-ai-projects azure-ai-agents azure-identity requests

Three live queries to try

All expressions below were validated against the live chain before publication. Block numbers will differ when you run them.

evmquery_read(
    chain="evm_ethereum",
    contracts={"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"},
    expression="formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())",
)
# → "2106.9577767 (block 25133961)"

No context needed — this is a pure contract read. The address is the canonical Chainlink ETH/USD aggregator on Ethereum mainnet.

ERC-20 balance check

evmquery_read(
    chain="evm_ethereum",
    contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
    expression="formatUnits(usdc.balanceOf(wallet), usdc.decimals())",
    context={"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
)
# → "0.11674 (block 25133961)"

formatUnits reads decimals() directly from the contract — USDC uses 6, DAI uses 18, WBTC uses 8. The expression adapts without hard-coding the scale.

USDC total supply on Base

evmquery_read(
    chain="evm_base",
    contracts={"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"},
    expression="formatUnits(usdc.totalSupply(), usdc.decimals())",
)
# → "4273730639.504659 (block 46230421)"

Switching chains is a single field change. The same CEL expression pattern works across Ethereum, Base, and BNB Smart Chain without modification.

Struct reads and Aave positions

evmquery expressions can traverse struct return values using dot notation. Aave’s getUserAccountData returns a six-field struct; you can read the health factor in isolation:

evmquery_read(
    chain="evm_ethereum",
    contracts={"aave": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"},
    expression="formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)",
    context={"wallet": "0x..."},
)

A healthFactor above 1.0 is safe; below 1.0 triggers liquidation. For wallets with no active borrow, evmquery returns the maximum uint256 divided by 1e18 — approximately 1.16e59 — which represents infinite health, not an error. Include a note in your agent’s system prompt so the model interprets this correctly.

To read the complete position — collateral, debt, available borrows, LTV, and liquidation threshold — drop the .healthFactor field:

expression="aave.getUserAccountData(wallet)"

The response is a JSON object with all six fields. The model can compute ratios, flag risk, or summarise the position without any additional parsing in your code.

Developers building DeFi tooling can find more expression patterns in the developer resources. For agentic use cases that span multiple frameworks, the AI users page covers the REST tool and MCP surfaces side by side.

Multi-wallet scans

Pass a list to context and use the CEL map macro to scan multiple wallets in a single API call:

evmquery_read(
    chain="evm_ethereum",
    contracts={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
    expression="wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))",
    context={
        "wallets": [
            "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
            "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8",
            "0x40B38765696e3d5d8d9d834D8AaD4bB6e418E489",
        ]
    },
)

Passing a Python list triggers list<sol_address> automatically via the _type helper. The CEL map macro executes in a single round-trip — not one call per wallet.

Azure AI Foundry tool vs MCP

Foundry function tool (this post)MCP server
LanguagePythonAny MCP client
AuthAzure Entra ID or managed identityOIDC or API key
Setup~40 lines in your agent fileOne config block
ControlFull: schema, error handling, loggingClient manages conversation
Multi-tool agentsYes, composableLimited to MCP surface
Best forProduction Azure workloadsClaude Desktop, Cursor, VS Code

If you are building a production Python agent on Azure infrastructure, the function tool gives full control over schema design, error surfacing, and result formatting. 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.