- You state the ceiling before anything runs.
max_spend_usdis required on every spending call. - Nothing spends without you having priced it. A follow-up that needs new passes is recorded, not run, and comes back as a priced proposal for you to confirm.
max_spend_usd — the per-run ceiling
Every spending run — the first turn, a confirmed re-aim, a refresh, an extraction —
carries its own max_spend_usd. It is:
- A reservation. The amount is reserved against your balance before the engine dispatches anything. If the balance can’t cover it, you get 402 insufficient_credit and nothing runs.
- A hard ceiling. Past the ceiling, no new source pass is dispatched, whatever the
agent’s routing model wants. The run ends with
stop: "spend-budget"onsearch-done— stated, like every stop reason. - Per-run, not per-session.
Session.spend_usdis the informational lifetime sum across a session’s runs; nothing enforces a lifetime ceiling. Each confirmed proposal or refresh reserves its own amount. - Settled at actuals. On terminal, the run settles at measured cost (usually below
the cap) and the rest of the reservation is released. A pass whose receipt says
cost_basis: "unavailable"settles at $0 — only measured cost is billed.
max_spend_usd above the key’s
max_spend_usd_per_session fails with 403 session_cap_exceeded;
a key over its calendar-month budget fails with
403 monthly_cap_exceeded. Caps are permissions, not
rates — set them per key in the dashboard.
Follow-up turns: free until they aren’t
POST /v1/sessions/{id}/turns { "text": "…" } routes your follow-up:
- Narrows and asks are free. They answer immediately from the session’s delivered corpus — no model dispatch, no charge, no proposal.
- A re-aim costs money — it changes what the session is looking for and needs new source passes. It is never run on the spot. Instead:
Confirming a proposal
Re-POST the same turn with"confirm": true:
- Fresh
Idempotency-Key. The confirm is a different body, so it needs a new key. Reusing the original key gets 409 idempotency_conflict, not a silent spend. - The confirmed run reserves its own estimate — a fresh reservation, checked against your balance like any other spending run.
- Proposals expire after 24 hours. A confirm after expiry doesn’t run at stale
prices — the server responds with a fresh proposal (new estimate, new
expires_at) to confirm instead.
Pre-authorizing: skip the round-trip
If you’d rather state a budget than click through a confirmation, sendmax_spend_usd on the turn itself:
The proposal state machine (deliberately small)
- At most one proposal is outstanding per session. A new non-confirm turn voids
the outstanding proposal — the newest ask wins, and the voided proposal’s
turncan no longer be confirmed. - Turns are not queued. A turn posted while a run is in flight (status
runningorjudging) returns 409 session_busy with the live status. Wait for the terminal — stream, long-poll, or webhook — then post. - If you use webhooks,
session.proposaldelivers the proposal to your backend so a human (or your agent) can decide asynchronously.
Where the money story is auditable
GET /v1/balance— credits and open reserves, live.GET /v1/usage— the ledger: everyreserve,settle, andrelease, each settle citing the run whose signed receipts justify it.GET /v1/sessions/{id}/receipts— the per-source receipts themselves, with cost, cost basis, and evidence pointers.
spend_usd, the ledger, and your invoice all speak
the same currency — the customer’s price.
What a receipt covers — and what it does not
Planned (each gets a dated changelog entry when it ships): anorchestration receipt line-item covering the run’s own model spend, so
sum(receipts) == spend_usd becomes a number you can assert on; and a stated line item
for a failed pass, so un-delivered work is accounted for explicitly rather than
being silently absent.