The cost card
Unknown is a shape, not a number
An amount is a tagged union. A determinate amount carries an integer:0 and a renderer cannot print $0.00 for “we do not know”. The reasons are a closed set: no_historical_basis, model_price_unknown, environment_rate_unknown, variable_workload, component_unknown, environment_class_not_runnable.
The SDK carries the same rule into the type system:
basis says where the figures came from: historical, worker_limits, template_default or no_basis. amounts_are_illustrative is true wherever a deployment has configured rates that have not been measured.
Obol publishes no price on this card. A configured environment rate is deployment configuration; an unconfigured one is reported as unknown rather than guessed. The plan ladder carries entitlements and has no money field on it at all.
The hard stop is always exact
Cost is shown before every run, by enforcement
POST /worker-runs requires acknowledged_hard_stop, and it must equal the acknowledgement from a fresh estimate. A client that never rendered the card cannot produce the figures. A client whose card went stale gets the current one back:
409
obol.workers.run() does this for you, and re-runs your confirm callback against the new card first.
Plan limits refuse before anything starts
{"kind": "limited", "value": n} or {"kind": "unlimited"} — an unlimited limit has no value field, for the same reason an unknown amount has no number.
Three refusal reasons can arrive before a run starts. Each is typed, each names the rule, and none of them allocates anything:
Two plan caps are structural rather than per-period counts (ADR-0135). Each refuses the start. Neither ever cuts a running run short:
A Worker whose run timeout is longer than the plan allows is refused with
dimension: "run_timeout_seconds". A Free workspace whose environment time this month is used up is refused with dimension: "environment_seconds" before any environment is allocated. Schedules and webhooks also need a paid plan: schedules_allowed and webhooks_allowed are false on Free.
entitlement_exceeded is a pre-allocation refusal and has no counterpart in the run-failure taxonomy: there is no failure reason for “plan run allowance exhausted”, because a run that was refused never started and therefore never failed. A run that exhausts a budget mid-flight fails with budget_exhausted instead, and that failure carries most_expensive_step.What a demo costs
Nothing, and by construction rather than by policy.demo is set by control at the point of creation, the run’s estimate is a known zero, the budget row is written billable: false, and the metering path refuses to make a demo interval chargeable. Three independent places, because this is the one mistake that takes money from someone who was promised none.
Reading what a run actually cost
The run list carriesmeasured_total_micros_usd, which is null when nothing was measured. Render that as unknown, never as zero. The run page carries most_expensive_step on every run with measured steps, not only on a budget failure — “what did this cost me” gets asked more often than “why did it stop”.
Provisioning time is not billed; a wait that releases compute accrues no active time, and one that deliberately holds a live environment does. Whether a paused run still holds compute is environment.holds_compute on the run view, not something to infer from run state.