Skip to main content
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 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; 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:
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:
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, 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:
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 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

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, 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.
Planned (each gets a dated 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.