Agent integration¶
Give an LLM agent a tool that hits paid APIs. Routeweiler handles the 402 transparently from inside the tool call.
Prerequisites¶
| Requirement | Notes |
|---|---|
| Agent framework | OpenAI Agents SDK shown; LangChain, Anthropic tool use, and MCP follow the same closure pattern |
| Funding source | Any rail will work. This recipe uses x402 on Base Sepolia. See x402 — Base USDC for wallet setup |
| A paid endpoint | Use the x402 test server for local testing |
| OpenAI API key | Required for the worked example; not needed for other frameworks |
Environment variables¶
export WALLET_KEY=0x<your-private-key>
export OPENAI_API_KEY=sk-...
export WEATHER_URL=http://127.0.0.1:4021/forecast
The pattern¶
Create the Routeweiler client once in the outer async with block and let each tool function close over it. Every tool invocation then shares the same funding source, budget envelope, and trace sink, so cost accounting and policy enforcement are automatic across the full agent run.
LLM → tool call → rw.get(url) → 402 Payment Required
↓
Routeweiler pays
↓
200 OK + receipt
← tool returns text
LLM continues
The agent never sees the payment. It only sees the 200 response body.
Worked example: OpenAI Agents SDK¶
import asyncio
import os
from agents import Agent, Runner, function_tool
from eth_account import Account
from routeweiler import (
BudgetExceededError,
Funding,
NoFeasibleRailError,
PolicyDeniedError,
Routeweiler,
)
from routeweiler.trace.sink_sqlite import TraceSink
WEATHER_URL = os.environ["WEATHER_URL"]
async def main() -> None:
wallet = Account.from_key(os.environ["WALLET_KEY"])
async with Routeweiler(
funding=[Funding.base_sepolia_usdc(wallet=wallet)],
trace_sink=TraceSink.sqlite("rw.db"),
agent_id="weather-agent",
) as rw:
@function_tool
async def fetch_weather(city: str) -> str:
"""Fetch the current weather for a city from a paid weather API."""
try:
resp = await rw.get(f"{WEATHER_URL}?city={city}")
return resp.text
except BudgetExceededError:
return "Budget cap reached — no more weather lookups this session."
except PolicyDeniedError as exc:
return f"Policy blocked the request: {exc}"
except NoFeasibleRailError:
return "No usable payment rail — check your funding source."
agent = Agent(
name="Weather Assistant",
model="gpt-4o-mini",
instructions="You help users check weather. Use the fetch_weather tool when asked.",
tools=[fetch_weather],
)
result = await Runner.run(agent, "What is the current weather in London?")
print(result.final_output)
asyncio.run(main())
Returning error strings (rather than raising) lets the model react and report back to the user gracefully. Raised exceptions inside a @function_tool abort the entire run.
Adding a budget envelope¶
When an LLM drives tool invocations there is no per-call human review, and the model may call the same tool repeatedly. A budget envelope caps total spend across the full run:
from routeweiler import BudgetEnvelope
async with Routeweiler(
funding=[Funding.base_sepolia_usdc(wallet=wallet)],
trace_sink=TraceSink.sqlite("rw.db"),
agent_id="weather-agent",
budget_envelope=BudgetEnvelope(
id="weather-agent-session",
cap_minor_units=100, # $1.00 in cents
cap_currency="usd",
allowed_rails=["x402"],
ttl_seconds=3_600,
),
) as rw:
...
Once the cap is hit, BudgetExceededError is raised on the next rw.get(...) call. The try/except inside the tool catches it and returns a string so the model can acknowledge the limit without crashing the run.
Server side (for local testing)¶
rw-demo/demo6_agent/server.py is a minimal FastAPI server that wraps GET /forecast with PaymentMiddlewareASGI and charges $0.0001 in Base-Sepolia USDC. Run it alongside the agent:
See x402 — Test endpoint for a fuller explanation of the server-side setup.
Pattern variants¶
The async with Routeweiler(...) as rw block and the tool closure work the same way in other frameworks:
Anthropic tool use
Define the tool schema as a dict, run the model loop yourself, and dispatch on tool_use content blocks. The tool handler body is await rw.get(url). Wrap the same async with Routeweiler(...) context around the loop.
LangChain
from langchain_core.tools import tool
@tool
async def fetch_weather(city: str) -> str:
"""Fetch weather from a paid API."""
resp = await rw.get(f"{WEATHER_URL}?city={city}")
return resp.text
Create rw before the agent executor and keep it alive for the duration of the run.
MCP server
Expose rw.get as an MCP tool so any MCP client (Claude Desktop, Claude Code) can use your agent's wallet without knowing anything about x402 or Routeweiler:
@server.call_tool()
async def fetch_weather(name: str, arguments: dict) -> list:
resp = await rw.get(f"{WEATHER_URL}?city={arguments['city']}")
return [TextContent(type="text", text=resp.text)]
What to check in the trace¶
Each tool-call invocation appends one row to trace_events in rw.db, keyed by agent_id:
import sqlite3, json
conn = sqlite3.connect("rw.db")
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM trace_events WHERE agent_id = 'weather-agent'"
).fetchall()
conn.close()
for row in rows:
payload = json.loads(row["payload"])
payment = payload["payment"]
print(f"Tx: {payment['proofValue']}")
print(f"Amount: {payment['amountNative']} base units = {payment['amountEnvelope']} USD")
print(f"Settled: {payment['settlementLatencyMs']} ms")
This gives you a per-tool-call audit trail for the entire agent run.
Error handling¶
Inside each tool, return errors as strings so the model can react:
from routeweiler import (
BudgetExceededError,
PolicyDeniedError,
NoFeasibleRailError,
RouteweilerError,
)
@function_tool
async def fetch_weather(city: str) -> str:
try:
resp = await rw.get(f"{WEATHER_URL}?city={city}")
return resp.text
except BudgetExceededError:
return "Budget cap reached — no more payments this session."
except PolicyDeniedError as exc:
return f"Policy blocked the request: {exc}"
except NoFeasibleRailError:
return "No usable rail — check funding source or policy."
Outside the agent run, catch RouteweilerError to handle unexpected failures without leaking internal details:
from routeweiler import RouteweilerError
try:
result = await Runner.run(agent, prompt)
except RouteweilerError as exc:
print(f"Payment infrastructure error: {exc}")