Agent Cloud Billing
Billing service for cloud agent resource consumption. Extends the EndpointCredits credit system with per-agent, per-customer metering across six resource types.
Refs #1981, #1983
Overview
Agent Cloud Billing tracks every resource an agent consumes on behalf of a customer. Each usage event is recorded in the agent_resource_usage table with a calculated credit cost. Credits are deducted from the customer's balance in credit_transactions.
Agent performs work
|
v
record_usage() called with resource_type + quantity
|
v
Credit cost calculated (quantity * credits_per_unit)
|
v
AgentResourceUsage row inserted
|
v
deduct_credits() removes credits from customer balance
Usage recording and credit deduction are two independent steps, and a
given call path may do one without the other. For A2A specifically, the router
records the usage event via record_usage() and performs its own deduction
against zerodb_projects.credit_balance — it does not call this service's
deduct_credits() (which writes to credit_transactions). So an
agent_resource_usage row does not by itself imply a matching
credit_transactions row.
Resource Types and Pricing
Six resource types are metered. Each has a fixed credit-per-unit rate.
| Resource Type | Credits per Unit | Unit | Description |
|---|---|---|---|
compute | 2 | seconds | Agent CPU execution time |
memory_ops | 5 | operations | ZeroMemory read/write operations |
vector_search | 8 | queries | Vector similarity search queries |
storage | 0.001 | bytes | Object/file storage consumption |
a2a | 3 | messages | Agent-to-agent communication messages |
postgresql | 20 | queries | Dedicated PostgreSQL queries |
Which resource types are metered through this service today
The six types above are not all written by the same code path, and the rates
below are not all the rates actually applied. Read this before assuming a
credit_cost value in agent_resource_usage came from the table above.
| Resource Type | Written by | Uses the rates above? |
|---|---|---|
a2a | AgentA2ARouterService.route_direct(), per message, in real time | Yes |
compute | aggregate_agent_usage Celery task, every 5 min | Yes |
memory_ops | aggregate_agent_usage Celery task, every 5 min | Yes |
vector_search | Not yet metered -- see note below | N/A |
storage | aggregate_agent_usage Celery task, every 5 min | Yes |
postgresql | aggregate_agent_usage Celery task, every 5 min | Yes |
a2a is recorded through AgentCloudBillingService.record_usage(),
synchronously at the moment the message is routed (Refs #9309). Four of the
remaining five (compute, memory_ops, storage, postgresql) are written
by the aggregate_agent_usage Celery task (app/tasks/agent_usage_aggregation.py)
every 5 minutes.
vector_search is not currently metered (core#9332). It has a rate in the
pricing table above and is accepted as a valid resource_type by
calculate_credit_cost() / record_usage(), but no code path anywhere in the
backend actually calls either with resource_type="vector_search" today --
not the Celery task (its TRACE_TYPE_TO_RESOURCE map has no vector_search
entry, and no agent_traces row is ever written with that trace type either),
and not any of the platform's vector-search services (hybrid_search_service.py,
optimized_vector_search.py, semantic_search.py, the agent framework's
vector_search_tool.py). Vector searches an agent performs today are not
billed against the customer's credit balance. Treat the vector_search row
in the pricing table as the rate that will apply once metering is wired up,
not as a rate currently being charged.
Resolved (Refs #9321): the Celery task used to declare its own,
independent CREDIT_PRICING table with different rates AND different units
(compute priced per-60-seconds instead of per-second; storage priced
per-MB instead of per-byte, while the stored row's unit column claimed
"bytes" either way). CLOUD_PRICING above is now the single source of
truth — the Celery task imports it directly and converts its own source data
(container-seconds, trace duration, storage MB) into the units
CLOUD_PRICING expects before pricing. SUM(credit_cost) across
agent_resource_usage is now safe to compute: every row, from either code
path, is priced on the same scale.
Echo developer markup is now applied inside record_usage() itself, so it
covers every resource type uniformly — previously only the Celery path
applied markup (via its own _apply_markup_and_deduct), so a developer
earned nothing on their agents' a2a traffic specifically. The markup
breakdown (base cost, developer earnings, platform fee, total) is stored
under metadata.markup on every row.
Cost Examples
| Operation | Quantity | Calculation | Credit Cost |
|---|---|---|---|
| 60 seconds of compute | 60 | 60 * 2 | 120.000 |
| 10 memory operations | 10 | 10 * 5 | 50.000 |
| 5 vector searches (not yet billed -- see above) | 5 | 5 * 8 | 40.000 |
| 1 MB storage (1,048,576 bytes) | 1048576 | 1048576 * 0.001 | 1048.576 |
| 25 agent-to-agent messages | 25 | 25 * 3 | 75.000 |
| 3 PostgreSQL queries | 3 | 3 * 20 | 60.000 |
All credit costs are calculated as Decimal values and quantized to 3 decimal places. The vector-search row is the formula calculate_credit_cost() would apply if called with resource_type="vector_search" -- as the note above explains, nothing calls it with that value today, so this row is illustrative only, not a cost customers are actually charged.
AgentResourceUsage Model
The agent_resource_usage table stores every metered event.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| agent_id | VARCHAR(255) | Agent identifier (indexed) |
| customer_id | UUID | Foreign key to users.id (indexed) |
| resource_type | VARCHAR(32) | One of the six resource types |
| quantity | NUMERIC | Amount of resource consumed |
| unit | VARCHAR(32) | Unit of measurement (seconds, operations, etc.) |
| credit_cost | NUMERIC | Calculated credit cost for this usage |
| period_start | DATETIME | Start of the metering period |
| period_end | DATETIME | End of the metering period |
| metadata | JSON | Optional additional context |
| created_at | DATETIME | Record creation timestamp |
Service API
AgentCloudBillingService
Instantiated with a SQLAlchemy Session. All methods operate within the caller's transaction.
from app.services.agent_cloud_billing_service import AgentCloudBillingService
service = AgentCloudBillingService(db=session)
calculate_credit_cost(resource_type, quantity)
Calculate the credit cost for a given resource type and quantity without recording usage.
Parameters:
resource_type(str) -- One of:compute,memory_ops,vector_search,storage,a2a,postgresqlquantity(Decimal) -- Amount of resource consumed
Returns: Decimal -- credit cost quantized to 3 decimal places
Raises: ValueError if the resource type is unknown
cost = service.calculate_credit_cost("vector_search", Decimal("15"))
## cost = Decimal("120.000")
record_usage(agent_id, customer_id, resource_type, quantity, period_start, period_end, metadata=None)
Record a resource usage event and calculate its credit cost. Inserts a row into agent_resource_usage and flushes (but does not commit -- the caller controls the transaction).
Parameters:
agent_id(str) -- Agent identifiercustomer_id(str) -- Customer UUID as stringresource_type(str) -- One of the six valid resource typesquantity(float) -- Amount consumed (must be positive)period_start(datetime) -- Start of metering periodperiod_end(datetime) -- End of metering period (must be after start)metadata(dict, optional) -- Additional context
Returns: Dictionary with the recorded usage details:
{
"id": "uuid-string",
"agent_id": "aurora",
"customer_id": "customer-uuid",
"resource_type": "compute",
"quantity": 120.0,
"unit": "seconds",
"credit_cost": 240.0,
"period_start": "2026-06-01T00:00:00",
"period_end": "2026-06-01T01:00:00"
}
Raises:
ValueErrorif resource_type is unknownValueErrorif period_end is not after period_startValueErrorif quantity is not positive
deduct_credits(customer_id, credits, description)
Deduct credits from a customer's balance via the credit_transactions table. Checks the current balance before deducting and returns False if insufficient.
Parameters:
customer_id(str) -- Customer UUID as stringcredits(float) -- Number of credits to deduct (positive value)description(str) -- Reason for the deduction
Returns: True if deduction succeeded, False if insufficient balance
Raises: ValueError if credits is not positive
success = service.deduct_credits(
customer_id="customer-uuid",
credits=240.0,
description="Agent aurora compute: 120s execution"
)
get_agent_usage_summary(agent_id, period_start, period_end)
Retrieve aggregated usage for a specific agent over a time period, grouped by resource type.
get_agent_usage_summary() and get_customer_cloud_spend() are service-layer
methods with no REST endpoint of their own. To read per-agent usage over HTTP,
use GET /api/v1/cloud/observability/costs/{agent_id} — it queries the same
agent_resource_usage table and returns the same per-resource-type breakdown.
There is currently no customer-level (all-agents) equivalent. Tracked in
core#9320.
Note also that GET /api/v1/billing/agent-usage despite its name does not
read agent_resource_usage — it reads agent_traces and estimates cost from
token counts, which is a different data source and a different number.
Parameters:
agent_id(str) -- Agent identifierperiod_start(datetime) -- Start of query periodperiod_end(datetime) -- End of query period
Returns:
{
"agent_id": "aurora",
"period_start": "2026-06-01T00:00:00",
"period_end": "2026-06-01T23:59:59",
"total_credit_cost": 510.0,
"total_records": 42,
"by_resource_type": {
"compute": {
"total_quantity": 300.0,
"total_credit_cost": 600.0,
"unit": "seconds",
"record_count": 15
},
"vector_search": {
"total_quantity": 50.0,
"total_credit_cost": 400.0,
"unit": "queries",
"record_count": 12
}
}
}
get_customer_cloud_spend(customer_id, period_start, period_end)
Retrieve total cloud spend for a customer across all agents, grouped by agent and resource type.
Parameters:
customer_id(str) -- Customer UUID as stringperiod_start(datetime) -- Start of query periodperiod_end(datetime) -- End of query period
Returns:
{
"customer_id": "customer-uuid",
"period_start": "2026-06-01T00:00:00",
"period_end": "2026-06-30T23:59:59",
"total_credit_cost": 1250.0,
"total_records": 87,
"by_agent": {
"aurora": {
"total_credit_cost": 800.0,
"record_count": 52,
"by_resource_type": {
"compute": {
"total_quantity": 200.0,
"total_credit_cost": 400.0,
"unit": "seconds",
"record_count": 20
}
}
},
"sage": {
"total_credit_cost": 450.0,
"record_count": 35,
"by_resource_type": {}
}
}
}
Budget Alerts and Cost Management
Monitoring Spend
Use get_customer_cloud_spend to monitor a customer's total spend across all agents. Compare against budget thresholds to trigger alerts:
spend = service.get_customer_cloud_spend(
customer_id="customer-uuid",
period_start=month_start,
period_end=month_end,
)
if spend["total_credit_cost"] > budget_threshold:
# Trigger budget alert notification
pass
Pre-flight Balance Checks
Before executing expensive operations, check whether the customer has sufficient credits:
estimated_cost = service.calculate_credit_cost("postgresql", Decimal("10"))
## Check balance before proceeding
can_deduct = service.deduct_credits(
customer_id="customer-uuid",
credits=float(estimated_cost),
description="Agent sage: 10 PostgreSQL queries"
)
if not can_deduct:
# Reject the operation or notify the customer
pass
Per-Agent Cost Attribution
Use get_agent_usage_summary to attribute costs to individual agents. This enables:
- Identifying which agents consume the most resources
- Optimizing expensive agents (e.g., reducing unnecessary vector searches)
- Billing breakdowns for customers running multiple agents
Data Lake Integration
Agent resource usage is exported to the data lake as the platform/agent_resource_usage partition. This enables historical analysis and trend detection via the lake query endpoint:
SELECT agent_id, resource_type, SUM(credit_cost) as total_cost
FROM platform_agent_resource_usage
GROUP BY agent_id, resource_type
ORDER BY total_cost DESC
See docs/api/DATA_LAKE_API.md for lake query details.
Pydantic Schemas
AgentResourceUsageCreate
Request schema for creating a usage record.
| Field | Type | Required | Constraints |
|---|---|---|---|
| agent_id | str | yes | 1-255 chars |
| customer_id | UUID | yes | |
| resource_type | enum | yes | One of the 6 resource types |
| quantity | Decimal | yes | Must be > 0 |
| unit | enum | yes | Matching unit for type |
| credit_cost | Decimal | yes | Must be >= 0 |
| period_start | datetime | yes | |
| period_end | datetime | yes | Must be after period_start |
| metadata | dict | no | Default: empty dict |
AgentResourceUsageResponse
Response schema returned by API endpoints.
Includes all fields from AgentResourceUsageCreate plus:
| Field | Type | Description |
|---|---|---|
| id | UUID | Record primary key |
| created_at | datetime | Creation timestamp |
AgentResourceUsageSummary
Aggregated summary schema.
| Field | Type | Description |
|---|---|---|
| agent_id | str | Agent identifier |
| resource_type | enum | Resource type |
| total_quantity | Decimal | Sum of quantity |
| total_credit_cost | Decimal | Sum of credit cost |
| unit | enum | Unit of measurement |
| period_start | datetime | Summary period start |
| period_end | datetime | Summary period end |
| record_count | int | Number of usage records |
Spending Caps (Budgets)
Separate from credit metering and x402 wallets, you can set a USD-denominated spending cap
that gates real-time A2A operations — POST /cloud/a2a/{agent_id}/message and POST /cloud/a2a/route.
If the cap is exceeded, the operation is blocked with 402 Payment Required before it
executes, not after.
This is the mechanism to understand if you're building agents that call /cloud/a2a/* and get
an unexpected 402 — a budget gate is almost certainly why, not a credit-balance issue.
A2A spend is attributed to the authenticated caller, not the Registry agent being messaged —
both /cloud/a2a/{agent_id}/message and /cloud/a2a/route derive the budget's identity key
server-side as user-<your user id>. Creating a budget with "agent_id": "agent-xxxxxxxxxxxx"
(a real agent you registered via POST /cloud/agents/register) has no effect whatsoever on
A2A spend — the gate will never check that row. To actually cap your A2A spend, set
"agent_id": "user-<your own user id>" when creating the budget (see Refs #9302).
Base path: /api/v1/billing
POST /budgets
Create a spending cap. For an A2A spending cap specifically, agent_id must be
user-<your user id> — see the warning above.
{
"agent_id": "user-3433680a-bfbe-468c-8bd7-42f83ee62ae5",
"limit_usd": 50.00,
"period": "month",
"max_task_cost_usd": 2.00,
"auto_pause": true
}
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | yes | Identity key this budget applies to. For A2A gating, this is user-<your user id> (see warning above), not a Registry agent_id. |
limit_usd | float | yes | Total spend allowed per period (> 0, ≤ 1,000,000) |
period | string | yes | day, week, or month |
max_task_cost_usd | float | no | Reject any single operation costing more than this |
auto_pause | bool | no (default false) | Automatically pause the agent when the period limit is hit |
Response (201 Created): a BudgetItem — same fields plus id, current_spend_usd (starts
at 0), created_at, updated_at.
GET /budgets
List budgets, optionally filtered by agent_id query param.
How the gate works
Every budgeted operation checks two things, in order:
- Per-task cap: if
max_task_cost_usdis set and this operation's estimated cost exceeds it, reject immediately regardless of remaining period budget. - Period cap: if
current_spend_usd + estimated_cost > limit_usd, reject. Ifauto_pauseis enabled, the agent is also paused (its registration is suspended) so subsequent calls fail fast instead of repeatedly hitting the same budget check.
An agent with no budget row at all has no spending cap — budgets are opt-in, not a default restriction.
Agent Wallets (x402)
In addition to credit-based billing, agents can hold USDC wallets for x402 micropayments. Wallets provide per-agent spending isolation with configurable limits. Refs #5008
Wallet Spending Guards
Each wallet enforces three layers of protection:
| Guard | Default | Description |
|---|---|---|
| Single payment cap | $10.00 | Maximum amount per individual transaction |
| Period spending limit | $100.00/month | Maximum total spend per billing period |
| Balance check | — | Transaction rejected if wallet balance insufficient |
Wallet Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/cloud/x402/wallets | Create per-agent wallet |
GET | /api/v1/cloud/x402/wallets/{agent_id} | Get wallet balance and limits |
POST | /api/v1/cloud/x402/wallets/{agent_id}/fund | Add funds to wallet |
POST | /api/v1/cloud/x402/wallets/{agent_id}/withdraw | Withdraw from wallet |
PUT | /api/v1/cloud/x402/wallets/{agent_id}/limits | Update spending limits |
GET | /api/v1/cloud/x402/wallets/{agent_id}/transactions | Transaction history |
Integration with Credit Billing
There are three independent billing/spend systems, each covering a different concern:
- Credits (
AgentCloudBillingService) meter platform resource consumption (compute, vectors, storage, etc.) and deduct from the customer's credit balance. - Budgets (
/billing/budgets, above) are a real-time USD spending cap that gates operations like A2A messages before they execute — independent of credit balance. - Wallets hold USDC for tool marketplace payments via x402.
An agent can consume platform credits for infrastructure, be gated by a USD budget cap on A2A
traffic, AND spend USDC for external tool calls — all at once, as separate concerns. Use
get_customer_cloud_spend for credit costs, GET /billing/budgets for spending-cap status, and
the wallet transaction history for x402 payment costs.
Full wallet API documentation: Agent402 API — Agent Wallets
Key Source Files
| File | Purpose |
|---|---|
src/backend/app/services/agent_cloud_billing_service.py | Core billing service with pricing and metering |
src/backend/app/models/agent_resource_usage.py | SQLAlchemy model for agent_resource_usage table |
src/backend/app/schemas/agent_resource_usage.py | Pydantic schemas (ResourceType, ResourceUnit, Create/Response/Summary) |
src/backend/app/services/agent_wallet_service.py | Per-agent USDC wallet service with spending guards |
src/backend/app/services/x402_revenue_share_service.py | Marketplace 90/5/5 revenue split service |