How to Add ATXP to Pydantic AI
Pydantic AI’s defining trait is that it treats type safety as a first-class concern — not an afterthought. Every tool input, every tool output, every agent result is typed and validated at runtime. That discipline makes it one of the cleanest frameworks for wiring in payment infrastructure, because when you know exactly what a tool is passing in and returning, adding a billing layer is structural rather than bolt-on.
This guide covers the complete Pydantic AI agent payments integration: defining payment-gated tools with typed inputs and outputs, injecting ATXP credentials through Pydantic AI’s dependency system, and using structured results to log per-tool-call cost in a way you can actually audit.
Why Pydantic AI’s Type System Changes the Payment Integration
Most agent frameworks treat tools as loosely typed functions. The agent sends a string, the tool parses it however it wants, and returns something that may or may not be validated. Pydantic AI is different.
Tools in Pydantic AI are Python functions with full type annotations. The framework uses Pydantic’s v2 validation engine to enforce those types at runtime — before your tool logic executes. Parameters become a Pydantic model automatically, which means:
- Automatic JSON schema generation for the LLM
- Runtime validation before your code runs
- Clear rejection errors when input doesn’t match expected types
- IDE autocompletion and static analysis throughout
This matters for payments because you know exactly what a tool call is requesting before it executes. The LLM can’t send a malformed tool call that triggers a billable API hit against a corrupted payload. Type safety and payment safety reinforce each other.
Pydantic (the underlying library) processes over 200 million Python package downloads per month as of 2025, and Pydantic AI inherited that community immediately on launch in late 2024. It’s now one of the fastest-growing type-safe agent frameworks in the Python ecosystem.
How ATXP Fits Into the Pydantic AI Model
ATXP gives your agent a wallet with ATXP credits. When the agent calls a paid external API — web search, image generation, SMS, email — the request routes through ATXP. Credits are debited per call. You get receipts, a spend ledger, and configurable spending limits.
For Pydantic AI, the integration has three structural pieces:
- Dependency injection — ATXP client lives in your
Depsdataclass, injected into every tool viaRunContext - Typed tools — every paid tool receives the ATXP client from context, calls the API, and returns a typed result
- Structured output — cost tracking lives in your result type so you get per-invocation cost attribution in every run
Setup
Install dependencies:
pip install pydantic-ai httpx
Get your ATXP API key at atxp.ai. Fund your agent wallet with credits (via Stripe or USDC). Then export the key:
export ATXP_API_KEY=your_key_here
Defining the Deps Dataclass
Pydantic AI’s dependency injection uses a typed Deps dataclass passed into every tool via RunContext[Deps]. This is where your ATXP client lives — explicitly passed, never global.
from dataclasses import dataclass
import httpx
@dataclass
class AgentDeps:
atxp_client: httpx.AsyncClient
atxp_api_key: str
budget_cap_usd: float = 1.00 # hard cap per run
The budget_cap_usd field is intentional. Every run gets a declared budget ceiling passed to ATXP with each request. When the ceiling is reached, ATXP returns a 402 — the agent can’t overspend because enforcement is server-side, not a local check that could be bypassed. See how ATXP’s IOU model caps agent spending for the full model.
Building the Agent
from pydantic_ai import Agent
from pydantic import BaseModel
class ResearchResult(BaseModel):
query: str
summary: str
sources: list[str]
total_cost_usd: float
agent: Agent[AgentDeps, ResearchResult] = Agent(
'anthropic:claude-sonnet-4-6',
deps_type=AgentDeps,
result_type=ResearchResult,
system_prompt=(
"You are a research agent. Use the web_search tool to find current information. "
"Always include source URLs in sources and report the total ATXP cost used."
),
)
Agent[AgentDeps, ResearchResult] makes the dependency shape and result shape explicit at the type level. Pydantic AI validates both at runtime. result.data will always be a valid ResearchResult — or the run fails with a clear validation error, not a silent bad state.
Defining Payment-Gated Tools
The web_search tool takes RunContext[AgentDeps] as its first argument (the framework injects this), followed by typed parameters for the actual search. The Field annotations generate the JSON schema the LLM uses to construct calls.
from pydantic_ai import RunContext
from pydantic import Field
@agent.tool
async def web_search(
ctx: RunContext[AgentDeps],
query: str = Field(description="The search query to execute"),
max_results: int = Field(default=5, ge=1, le=20, description="Number of results to return"),
) -> dict:
"""Search the web for current information."""
response = await ctx.deps.atxp_client.post(
"https://api.atxp.ai/v1/search",
headers={"Authorization": f"Bearer {ctx.deps.atxp_api_key}"},
json={
"query": query,
"max_results": max_results,
"budget_cap": ctx.deps.budget_cap_usd,
},
)
response.raise_for_status()
data = response.json()
return {
"results": data["results"],
"cost_usd": data["cost"],
"sources": [r["url"] for r in data["results"]],
}
The ge=1, le=20 constraint on max_results is enforced by Pydantic validation before the function body runs. The LLM sees that constraint in the JSON schema and is less likely to generate an out-of-range value. When it does, it gets a validation error rather than a billable call with a bad parameter.
Adding Image Generation
Same pattern, different ATXP endpoint — and note the return type is a Pydantic model rather than a raw dict:
class ImageResult(BaseModel):
url: str
prompt: str
cost_usd: float
@agent.tool
async def generate_image(
ctx: RunContext[AgentDeps],
prompt: str = Field(description="Detailed image generation prompt"),
size: str = Field(default="1024x1024", pattern=r"^\d+x\d+$"),
) -> ImageResult:
"""Generate an AI image. Returns the hosted image URL."""
response = await ctx.deps.atxp_client.post(
"https://api.atxp.ai/v1/image",
headers={"Authorization": f"Bearer {ctx.deps.atxp_api_key}"},
json={"prompt": prompt, "size": size},
)
response.raise_for_status()
data = response.json()
return ImageResult(
url=data["url"],
prompt=prompt,
cost_usd=data["cost"],
)
Returning a Pydantic model means the framework validates the return value against ImageResult before passing it back to the LLM. If ATXP returns unexpected data, you catch it at the tool boundary — not somewhere downstream where cost_usd is unexpectedly None.
Running the Agent
import asyncio
import os
async def main():
async with httpx.AsyncClient(timeout=30.0) as client:
deps = AgentDeps(
atxp_client=client,
atxp_api_key=os.environ["ATXP_API_KEY"],
budget_cap_usd=0.50,
)
result = await agent.run(
"Research the current state of agentic commerce and summarize key trends.",
deps=deps,
)
# result.data is typed as ResearchResult — IDE knows its shape
print(f"Summary: {result.data.summary}")
print(f"Sources: {result.data.sources}")
print(f"ATXP tool cost: ${result.data.total_cost_usd:.4f}")
print(f"LLM tokens: {result.usage()}")
asyncio.run(main())
result.usage() gives you the model-side token spend. result.data.total_cost_usd gives you the ATXP tool call spend. Track both if you’re doing real cost accounting — LLM costs and tool costs are distinct line items that need to be tracked separately.
Structured Cost Tracking Across Multiple Tools
For agents that call several tools per run, the cleanest pattern accumulates cost per tool and surfaces it in the result:
from pydantic import BaseModel, Field as PydanticField
class ToolCostEntry(BaseModel):
tool: str
call_count: int
cost_usd: float
class ResearchResult(BaseModel):
summary: str
sources: list[str]
tool_costs: list[ToolCostEntry] = PydanticField(default_factory=list)
@property
def total_cost_usd(self) -> float:
return sum(e.cost_usd for e in self.tool_costs)
Include this in your system prompt: “Track the cost of each tool call. In your final response, include a tool_costs list with one entry per tool type used.”
The LLM will populate tool_costs naturally because the result type schema makes the structure explicit. You get per-tool cost attribution in every run — which is the data you need when a run is more expensive than expected. You’ll know whether it was the search calls or the image generation.
Comparing Pydantic AI Integration to Other Frameworks
| Framework | Tool type safety | Dependency injection | Structured results | ATXP integration effort |
|---|---|---|---|---|
| Pydantic AI | Full (Pydantic v2) | Native RunContext[Deps] | Typed result model | Low — clean deps pattern |
| LangChain | Partial (tool schema) | Callbacks / config | Variable | Medium — more ceremony |
| LlamaIndex | Partial (FunctionTool) | No native pattern | Variable | Medium |
| OpenAI Agents SDK | Moderate | None native | Function annotations | Medium |
| CrewAI | Low | None native | String outputs | Higher — needs wrapper |
The deps injection pattern means you never have global state or environment variables inside tool functions. The ATXP client is always explicitly passed — testable, replaceable, auditable. This is the correct architecture for credential blast radius control.
Budget Design: Handling the Cap Mid-Task
Setting budget_cap_usd=0.50 means ATXP returns a 402 when any call would push spend over that ceiling. Handle it explicitly with ModelRetry so the LLM can finish gracefully rather than crash:
from pydantic_ai.exceptions import ModelRetry
@agent.tool
async def web_search(ctx: RunContext[AgentDeps], query: str) -> dict:
try:
response = await ctx.deps.atxp_client.post(
"https://api.atxp.ai/v1/search",
headers={"Authorization": f"Bearer {ctx.deps.atxp_api_key}"},
json={"query": query, "budget_cap": ctx.deps.budget_cap_usd},
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
if e.response.status_code == 402:
raise ModelRetry("Budget cap reached. Summarize findings from searches already completed.")
raise
ModelRetry passes the message back to the LLM as a tool result it can reason about — rather than a hard exception that terminates the run. The agent responds intelligently to the cap rather than failing silently. For deeper budget architecture decisions, see How to Give an AI Agent a Budget.
Production Patterns
Credential isolation: The ATXP key lives in AgentDeps, constructed at call time from environment variables. Different runs use different instances — scoped budgets, different keys for test vs. prod, or per-user wallet isolation. Nothing is global.
Per-task budget sizing: Create a new AgentDeps per task with an appropriate ceiling. A research task triggered by a $5 user action shouldn’t have a $50 budget. Size the cap to the expected task cost with reasonable headroom.
Testing without HTTP: The deps pattern makes testing clean — inject a mock ATXP client without patching environment variables or monkeypatching the HTTP layer:
import httpx
class MockATXPClient:
async def post(self, url: str, **kwargs) -> httpx.Response:
return httpx.Response(200, json={
"results": [{"url": "https://example.com", "snippet": "test"}],
"cost": 0.001,
})
test_deps = AgentDeps(
atxp_client=MockATXPClient(),
atxp_api_key="test-key",
budget_cap_usd=0.10,
)
Why Not Just Use API Keys Directly?
The obvious alternative: skip ATXP, manage API keys for each service yourself. Here’s the actual comparison at production scale:
| Approach | Credential management | Spend visibility | Budget enforcement | API surface |
|---|---|---|---|---|
| ATXP | Single API key | Full ledger, per-call receipts | Hard cap enforced server-side | One integration |
| Direct keys | One key per service | Fragmented, per-service dashboards | Soft limits or none | One per service |
At small scale — one agent, one service — direct keys are fine. At production scale with multiple agents calling multiple services across multiple developers, the credential sprawl and spend opacity become real operational problems. ATXP eliminates the category. The architectural argument is in How to Build an AI Agent Without API Keys.
Get Started With ATXP
Set up your agent wallet and get an API key at atxp.ai. First-time accounts get $5 in free credits to test the integration end-to-end before committing.
FAQ
Does Pydantic AI support async tool calls with ATXP?
Yes. Pydantic AI natively supports async def tools, and ATXP’s API endpoints work with any async HTTP client. All examples above use httpx.AsyncClient. Synchronous tools work too — swap in httpx.Client and remove the async/await keywords.
Can I use Pydantic AI with models other than Anthropic or OpenAI?
Yes. Pydantic AI supports Google Gemini, Mistral, Ollama, and others via a unified model interface. ATXP is fully model-agnostic — it handles tool call payments regardless of which model is running the agent. The integration above works identically with 'google-gla:gemini-2.0-flash' or any other supported model string.
How do I get per-tool cost attribution across a multi-tool run?
Use the list[ToolCostEntry] pattern in your result type (shown in the cost tracking section above). Instruct the agent in its system prompt to track costs per tool and populate tool_costs in its response. Because the result schema is explicit, the LLM fills it reliably. You get a cost breakdown by tool in every RunResult.
What happens if my agent hits the budget cap in the middle of a task?
ATXP returns HTTP 402. Without explicit handling, Pydantic AI raises an exception and the run terminates. With ModelRetry handling (shown above), the LLM receives the budget exhaustion message as a tool result and can finish gracefully — summarizing what it found before the cap hit rather than crashing. Use ModelRetry for any tool where partial results are useful.
Can I scope different budgets to different tools in the same agent?
The cleanest way is to put per-tool budget limits in your AgentDeps and pass the appropriate limit in each tool’s ATXP request. For example, budget_cap_search_usd and budget_cap_image_usd as separate fields in your dataclass. This gives you independent ceilings for cheap (search) vs. expensive (image gen, video) tools within a single agent run.