Saphan StudioDocs
Cost management

Before a dollar is spent

Capacity checks, usage windows, the refusals that happen before a run spawns, and an honest statement of which ceiling is enforced by whom.

Two numbers ride on every dispatch: a prediction of what the work should cost, and a ceiling derived from it at twice that figure. What each of those is, who declares them, and what happens when a run reaches the ceiling is a topic of its own and is not repeated here.

This page is everything else that happens before the money moves — the capacity checks, the probes, and the refusals. They exist because the cheapest run is the one that never started, and the second cheapest is the one that was stopped a second before it did.

Which ceiling is actually enforced, and by whom

⚠ This is stated per backend, because it differs per backend, and a budget holder who assumes otherwise will be surprised exactly once.

What holds the run
Claude Code — today the only backend whose vendor CLI accepts a spend limitThe ceiling is composed onto the vendor's own budget option. A run that reaches it ends, under its own named status (budget-exhausted) — distinct from a vendor error, and distinct from a provider limit.
Every other backend — Codex, Droid, Qwen Code, plain commands, OpenAI-compatible endpoints⛔ There is no pre-enforcement. The ceiling is recorded on the run and nothing stops the run at a dollar figure. On an OpenAI-compatible endpoint the ceiling is compared against the actual after the run and the comparison is written on the record; elsewhere it is a recorded number only.
Every backend, unconditionallyThe wall clock. Every run carries a deadline and is killed at it. This is the engine's own backstop and it does not depend on any vendor's cooperation.

The remediation when a ceiling is hit is the same in both cases — re-predict and re-run, or split the work, as a deliberate decision — but the moment you learn differs, and on a backend in the second row you learn afterwards.

Capacity is checked before commitment, as a report

saphan fleet headroom --estimate-usd 5.00 [--model <model>] [--backend claude-code|codex]

Given an expected cost (required) and optionally one model, this probes every applicable place on this host — a Claude Code or Codex harness — for room to run and reports what it found. It is a report and never a dispatch: there is no way to make it select a winner, and no way to chain it into a run.

⚠ The report is not free. Every probe is a real, billable run through the same door as any other run, booked like one. Two disciplines make the report worth reading:

  • A place counts as free only on a positive assertion. A successful recorded probe with its own liveness marker present. ⛔ The absence of a failure is never read as a pass.
  • "Could not measure" is a first-class answer, never collapsed into "not free". Those are opposite conclusions and they lead to opposite acts.

Subscription windows are measured, not assumed

saphan fleet usage

A flat-rate plan is not unlimited — it is windowed, with capacity that refills on the plan's own cadence. This reads each seat's own usage and answers in three states: free, exhausted, or unknown, keeping the vendor's own reset wording verbatim rather than paraphrasing it into something that might be wrong. Claude Code and Codex seats are read; a seat of any other backend is reported as unsupported, by name.

The usage read itself costs nothing. ⚠ A Claude Code seat whose read comes back unusable can cost one small model turn — at most one per seat per 30 minutes — and every row says whether it did. A running server refreshes these readings on its own every fleet.usage_refresh_minutes (15 by default, 0 disables), and that refresh never buys a turn.

Every reading is persisted, and an ordinary run that hits a provider limit extinguishes a stale "free" verdict at no extra cost — the fleet's own work keeps the report honest between probes.

⚠ A window nobody could put a number on is marked as unread on its own row, rather than left to read as a window with room. An absent percentage and a low percentage are opposite answers, and only the mark tells them apart.

The report itself blocks nothing. What the readings do at dispatch time is the next section.

The pre-flight gate: what is wired, and what is not

A real dispatch can opt in to a pre-flight that runs the capacity probe and refuses the spawn when no probed place is free. The refusal enumerates every place it probed with that place's own reason, so the answer is actionable rather than a verdict. The pre-flight's own cost is stated on the option itself — nothing about capacity checking is silent, including its price.

The option is --headroom-preflight on saphan run. ⚠ It costs roughly six cents per probed place, plus that probe's own wall clock, which is why it is opt-in.

Independently of that, a prediction at or above a configurable threshold (headroom.warn_usd, five dollars by default) flags the dispatch for visibility. A warning, never a block.

A seat's own latest usage reading is read at every dispatch onto a seat. When that reading says the seat's window is spent and another seat on the same backend that would take the same role currently reads room, the dispatch is refused before the seat is reserved, naming the other seat and the reset time. When no other seat has room, the dispatch is admitted with a note on the record that it may die at the provider limit — capacity is not idled for want of an alternative. A seat with no reading at all is admitted with a note: absence of a measurement is not a refusal.

⚠ A spent weekly window is a fact about the subscription, not the seat. A seat funded by vendor credits reads identically and still runs. For that case saphan run --force-usage-reading overrides the refusal as an owner act, and the refusal it overrode is written onto the run's own record.

Two refusals that happen before the spawn

A dead session is refused before it burns capacity. Before casting work onto a Claude Code seat, that seat's credential is checked with the vendor's own free usage read — no model turn, and a good answer is reused for at most two minutes — and the spawn is refused if the session is already dead. A named refusal before the money moves, rather than a vendor error after it.

A step that asks for a model it does not need is refused outright. Asked through saphan route <step> --request-client, a step your routing policy places on the deterministic tier is refused, with its own exit code (3), and the refusal is booked in the ledger at zero cost. ⚠ This is a separate check you run, not a gate inside saphan run: a dispatch does not consult the routing policy today.

Declaring who pays for a seat

A seat is one identity on one machine slot, with its own profile and its own session — the unit by which several vendor accounts are driven in parallel from one machine. Each seat carries a billing class, set by an owner act on the seat's own signed row (saphan machine seat … --billing-class <class>):

ClassMeaning
subscriptionFlat-rate capacity somebody already paid for; the marginal run inside the window is effectively pre-paid
meteredInvoiced per token, from the first token
local-energyA model you host yourself: no vendor bills anything, and what is scarce is time and the host's capacity

⚠ An untagged seat reads back as unknown — treat unknown exactly like metered. The conservative reading is the safe one: assuming a seat is free is how an unbudgeted invoice arrives. ⚠ The class is a declaration on the seat and appears in reports; no dispatch check reads it today, so it does not by itself stop or route anything.

The economic consequence is the point of the feature. When most work runs on flat-rate accounts, metered APIs become a deliberate overflow rather than the default meter — and a model behind an endpoint you run yourself extends the ladder down to electricity, with ordinary deterministic tooling closing it at zero.

Declared zeros live in one place

A zero that means "this genuinely has no marginal cost" and a zero that means "nobody knows" must never look the same in a report. So the rate for a backend and model — or the declaration that there is no marginal rate at all — is written by a human into the pricing registry, and the engine only ever reads it.

  • A declared zero books a real, countable zero, with the declaration named on the row.
  • A model with no declaration books unknown, and is named in every rollup until somebody declares it.
  • One structural exception exists so an unregistered local model cannot masquerade as vendor spend: a run against a loopback endpoint with no declaration is classed as electricity from the observed fact of the loopback itself.

What is not built

  • ⚠ Ceilings are per run only. There is no envelope above the run — no stream-level or portfolio-level budget with its own threshold, and no monthly number burned down over time. Tracking a larger envelope is done by hand against the reads.
  • ⚠ Nothing warns you mid-run. A run's spend crossing a fraction of its ceiling raises no notification; you read the number at close.

Next