> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fiveninelabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Budgets and priced proposals

> max_spend_usd is a hard per-run ceiling; anything that would cost more money comes back priced for confirmation, never silently billed.

Two rules govern every dollar on this API:

1. **You state the ceiling before anything runs.** `max_spend_usd` is required on
   every spending call.
2. **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](/errors/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"` on
  `search-done` — stated, like every stop reason.
* **Per-run, not per-session.** `Session.spend_usd` is 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.

Per-key caps bound the ceiling: `max_spend_usd` above the key's
`max_spend_usd_per_session` fails with [403 session\_cap\_exceeded](/errors/session_cap_exceeded);
a key over its calendar-month budget fails with
[403 monthly\_cap\_exceeded](/errors/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:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/turns \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"text": "also check San Antonio shops"}'
```

```json theme={null}
{
  "error": {
    "type": "proposal",
    "code": "proposal_required",
    "message": "This follow-up changes the merchant set and needs 3 new passes (~$1.20).",
    "doc_url": "https://docs.fiveninelabs.com/errors/proposal_required",
    "request_id": "req_2xk91a",
    "proposal": {
      "turn": 4,
      "estimated_usd": 1.2,
      "passes": 3,
      "re_resolve": true,
      "kept_rows": 228,
      "expires_at": "2026-08-26T15:32:07Z"
    }
  }
}
```

**Why a 409?** So a naive retry loop can never silently spend. The proposal is
recorded server-side; nothing has run and nothing has been charged.

## Confirming a proposal

Re-POST the same turn with `"confirm": true`:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/turns \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"text": "also check San Antonio shops", "confirm": true}'
```

Details that matter:

* **Fresh `Idempotency-Key`.** The confirm is a different body, so it needs a new key.
  Reusing the original key gets
  [409 idempotency\_conflict](/errors/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, send
`max_spend_usd` on the turn itself:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/turns \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"text": "also check San Antonio shops", "max_spend_usd": 2}'
```

A proposal whose estimate is at or under your stated ceiling runs immediately — no
409\. If the estimate exceeds it, you get the proposal as usual.

## 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 `turn` can
  no longer be confirmed.
* **Turns are not queued.** A turn posted while a run is in flight (status `running`
  or `judging`) returns [409 session\_busy](/errors/session_busy) with the live
  status. Wait for the terminal — stream, long-poll, or webhook — then post.
* If you use webhooks, `session.proposal` delivers 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: every `reserve`, `settle`, and `release`, 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.

Stream events, receipts, session `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

<Warning>
  **`sum(receipts) ≤ spend_usd`. Do not treat the receipts sum as the bill.**

  A receipt is issued **per delivered source pass** — one source that reported
  `delivered: true` produces one receipt with that pass's measured cost, claim, record
  count and duration. Two classes of real, billed spend sit outside that surface:

  * **Orchestration spend.** The model calls that parse your query, resolve merchants,
    route dispatch and adjudicate the answer are not a source pass, so by design they
    carry **no per-source receipt**. They stream as [`cost` events](/guides/events), and
    they are summed into `spend_usd` and settled through the ledger like everything else.
  * **Passes that delivered nothing.** A pass stopped by a deadline, an anti-bot wall or
    a dead transport reports `delivered: false, records: 0` with a stated reason, and is
    billed its measured cost — but issues no receipt, because there is no delivery to
    attest.

  `spend_usd` and `GET /v1/usage` are the **complete** account of what you were charged;
  receipts are the **itemisation of delivered work**. Reconciling `sum(receipts)` against
  `spend_usd` will leave a remainder, and it is these two classes — on a measured run it
  has been as much as \~30% of the bill, concentrated in failed passes.

  Nothing here is an overcharge: the arithmetic reconciles, the ceiling holds, and the
  reservation releases in full. It is a **disclosure gap**. If you are building a
  reconciliation or chargeback view, read `spend_usd` or the ledger — never the receipts
  sum.
</Warning>

**Planned** (each gets a dated [changelog](/changelog) entry when it ships): an
**`orchestration` 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.
