Costs and budgets
Esy records cost per provider call, not per run, and every number has a documented source. Budgets sit in front of execution: a run that would breach one is refused before it spends anything.
Cost is not price
What you see here is cost — what the providers charged to make the thing. It is not a price to a customer, and Esy does not compute margin for you. Every figure traces back to a model, a quantity, and a unit rate that was in effect at the time.
What one run costs
A finished run itemises its own bill. This is the real ledger from the Quickstart run:
"totalCosts": {
"estimatedUsd": 0.007749,
"actualUsd": 0.007749,
"currency": "USD",
"status": "provider_reported",
"providerSteps": [
{ "provider": "openai", "operation": "Render illustration",
"model": "gpt-image-2.5-sunburst", "actualCostUsd": 0.0046 },
{ "provider": "anthropic", "operation": "Classify asset",
"model": "claude-haiku-4-5-20251001", "actualCostUsd": 0.000884 },
{ "provider": "anthropic", "operation": "Text gate",
"model": "claude-haiku-4-5-20251001", "actualCostUsd": 0.00226 },
{ "provider": "cloudflare_r2", "operation": "storage.upload",
"model": null, "actualCostUsd": 0.0000045 }
]
}The render was $0.0046 and the two checks around it were $0.0031 together — the quality apparatus cost two-thirds of what the image did. That ratio is normal for gated workflows, and it is the thing to look at before concluding a workflow is expensive.
The three cost states
Each ledger row moves through the same three states. There are three — not four.
| State | When | What it means |
|---|---|---|
estimated | Before the call | Priced from the model’s published rate and an expected quantity. This is what budgets check. |
provider_reported | Immediately after | The provider told us the real quantity. Usually close to the estimate; occasionally not. |
reconciled | Later, against the invoice | Confirmed against what we were actually billed. The final word. |
Reading spend
GET /v1/costs aggregates across runs. Filter by workspace, project, workflow, or period to get the rollup you need rather than summing ledgers yourself.
A collection adds one more rollup: every attempt at every member carries its own cost, so a pack reports what it cost across all the orders it placed, and a failed attempt can be refunded by its id.
Budgets
A budget is a limit attached to a scope, with a period and an enforcement mode. It is evaluated before a run executes, so a refusal costs nothing.
curl -X POST https://api.esy.com/v1/budgets \
-H "Authorization: Bearer $ESY_API_KEY" \
-H "content-type: application/json" \
-d '{
"workspaceId": "9a1b6d4c-…",
"name": "Clip art, monthly",
"workflowId": "generate-clip-art-asset-v2",
"limitUsd": 50.0,
"period": "monthly",
"perRunCapUsd": 0.35,
"enforcementMode": "hard_stop"
}'| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | uuid | required | Every budget belongs to a workspace. |
projectId | uuid | optional | Narrows the budget to one project. |
workflowId | string | optional | Narrows it to one workflow. There is no scope field to send — the scope is inferred from which of these ids you set, and comes back on the response as workspace, project, or workflow. |
limitUsd | number | required | The ceiling for one period. |
period | enum | optional | total, daily, weekly, or monthly. |
perRunCapUsd | number | optional | Refuses any single run estimated above this, regardless of remaining budget. |
enforcementMode | enum | optional | What happens at the limit — see below. |
Enforcement modes
| Mode | At the limit | Use when |
|---|---|---|
hard_stop | Refuses with 402. Nothing is spent. | You want a real ceiling. |
allow_overage | Keeps going until overageUsd is also used up. | A limit with a deliberate grace margin. |
allow_one_more | Permits exactly one more run, then refuses. | You would rather finish the item in flight than truncate it. |
track_only | Never refuses. Records everything. | You want the number before you want the brake. |
What a refusal looks like
HTTP 402
{
"detail": {
"code": "budget_exceeded",
"reason": "hard_stop",
"budgetId": "budget-…",
"scope": "workspace",
"enforcementMode": "hard_stop",
"limitUsd": 50.0,
"spendUsd": 49.82,
"runEstimateUsd": 0.28,
"remainingUsd": 0.18
}
}The body carries everything you need to explain the refusal to a person: which budget, which rule, the limit, the spend so far, and what this run would have cost. reason is one of per_run_cap_exceeded, hard_stop, allow_overage_exceeded, or allow_one_more_exhausted.
Refusals are durable. GET /v1/budgets/{budget_id}/refusals lists what a budget turned away — useful when someone asks why a batch came back short.
Estimating before you commit
For a single run, the server estimates for you at creation and refuses with a 402 if a budget would be breached — nothing is spent. For batches, use a Generation Order: it is created in the planned state with estimatedCostUsd already filled in, and the budget is checked both when you create it and again when you start it. Read the estimate, then decide — finding the limit at item 400 of 500 is an expensive way to learn it.
- Cost is recorded per provider call, with the model, quantity, and rate behind every figure.
- Three cost states:
estimated→provider_reported→reconciled. - Budgets refuse before execution, so a
402is free — but afailedrun is not. - Checks and gates are often the larger half of a workflow’s bill. Look at
providerStepsbefore optimising the wrong thing.
