Per-spec cost visibility with operator spend ceiling
Users see the dollar cost of each spec they authored, computed post-hoc from the agent usage ledger. Operators can set a per-period spend ceiling per org; when crossed, new agent sessions are refused at the door while in-flight turns finish. No mid-turn kill, no running ticker, no fabricated numbers for unpriced models.
“I can't tell a user what a spec costs them and I can't stop one from costing us a fortune.”
The grilling
I can't tell a user what a spec costs them and I can't stop one from costing us a fortune. Before you ask me anything, go read how spend works in this codebase today. Specifically: what's recorded per turn, what the columns on the org row mean, what actually gates a free user, and whether any budget number in there is enforced anywhere. Be careful on that last one — read the code, don't trust the column names. Then report and grill me. Feature scale: what the product owes a user about what their runs cost.
Let me explore the codebase first. Good structure. Let me dig into the spend-related files. Now let me look at the database schema for the orgs table and the `recordSpend` function to see what columns exist and whether any budget numbers are actually enforced. Let me look at the database schema for the `orgs` table and the `agent_usage_ledger` table. Let me check what the billing UI currently shows the user, and also look at how the settings page renders org data. Let me also check the configuration env settings and how existing usage-metering gallery spec looks. Here's what I found after reading the code across `turn-spend.ts`, `pricing.ts`, `stripe-client.ts`, `stripe.ts` (the routes), `spec-quota.ts`, the database schema, and the UI components. **What's recorded per turn:** Every agent turn gets priced in micro-dollars (via `costMicros` in `pricing.ts`, using a hardcoded per-model rate table) and written to `agent_usage_ledger` with token counts, model, cost, and an error flag. The org's `periodSpendMicros` accumulates in the same atomic UPDATE. **What the org-row columns mean:** - `periodSpendMicros` — running backend-cost total this period (tracks to the micro-dollar) - `periodAlertMicros` — soft threshold that fires an operator alert (free: $1, paid: $20). Pages ops, never blocks - `periodBudgetMicros` — **legacy retired hard cap.** The code explicitly says "no longer enforced anywhere" and "nothing reads it." It's a dead column kept for row readability only. - `specLimit` / `specsCreatedTotal` — the ONLY actual gate: free tier capped at 10 specs lifetime **What actually gates a free user:** Only the 10-spec lifetime limit. The `credits_exhausted` concept is documented as "retired" in the toast handler. Nothing anywhere blocks a turn based on spend. A free user running Opus ($15/$75 per 1M tokens) can rack up hundreds of dollars in backend cost and the system just takes it. **What the user sees today:** Nothing about cost. The pricing page promises "unlimited — no per-run charges and no overage billing." The settings UI deliberately hides the usage meter: "The paid plan is unlimited... so we deliberately do NOT render a usage meter here." The billing snapshot API returns only subscription status, spec counts, and period dates — zero spend data to the frontend. So your read is correct. The ledger has granular per-turn cost data that could power any visibility or gating feature, but right now it's invisible to the user and unenforced. The old hard-cap column (`periodBudgetMicros`) was explicitly stripped of all enforcement. Two gaps to nail down before I write the first block: The code tells a clear story: you have a complete per-turn costing pipeline that feeds a ledger, but it's invisible to the user and unenforced. The old `periodBudgetMicros` cap was deliberately retired and nothing replaced it. The first question above cuts to the core choice: do you want to just show users what things cost, or actually let them enforce a limit?
Files the agent read
- read_repo_structure whole repo
- read apps/api/src/turn-spend.ts
- read apps/api/src/pricing.ts
- read apps/api/src/spec-quota.ts
- read apps/web/src/lib/handle-credits-exhausted.ts
- read apps/api/src/metrics.ts
- read apps/api/src/routes/stripe.ts
- find **/schema.ts
- read apps/api/src/stripe-client.ts
- find **/db/schema.ts
- find packages
- ls packages
- grep apps/api
- grep apps/api/package.json
- ls packages
- grep "db"
- ls db
- ls db/schema
- read db/schema/index.ts
- read apps/web/src/components/BillingBanner.tsx
- grep apps/web/src
- read apps/api/src/config.ts
- grep apps/api
You read it correctly and I want to make sure you state it plainly, because it's the crux: periodBudgetMicros is legacy and enforces nothing, the alert threshold only pages an operator, and the actual free-tier limit is a lifetime spec count. So today spend is measured and never acted on. You've asked whether to make periodBudgetMicros a real hard cap again, and you recommended it as the smallest change. No. I'm overruling that, and it's the most important call in this spec. A hard cap on spend means a turn dies partway through. Think about what that leaves behind: a spec that's been half-rewritten by an agent, some blocks inserted, a section score updated, and no way for the user to tell what happened or get back to a coherent state. And it happens at the moment they're most engaged. We'd be taking the product away from someone mid-sentence to save a few cents, and the fix — "upgrade" — arrives as a wall rather than an offer. That's a churn machine. The free tier already has a limit that's enforceable without any of that damage: the spec count, checked before anything starts. A refusal at the door is honest. A refusal halfway through is destructive. Grill me on what the feature is instead.
Noted and accepted. The hard-cap-overwrite-mid-turn is ruled out. The "refusal at the door" model you described for the spec cap is the right analogy — check before anything starts, so the user never gets half-rewritten work and a "pay up" wall. That gives two entirely different mechanisms for the "stop costing a fortune" half of your problem. Let me ask the right question now that the first bad answer is off the table. You ruled out the destructive mid-turn kill. The cleanest alternatives all follow the "refusal at the door" spec-cap pattern — check before the turn starts, not during. The question above asks which mechanism you want. My recommendation is B because it matches your existing product model (free tier has limits checked before work starts, paid is unlimited) while giving you the cost protection you need. Visibility alone (A) solves the "can't tell them" gap but leaves the "can't stop them" gap wide open.
Right, so here's what I want. Two things, and only the first is a control. One: visibility for the user. What this spec cost to author, in dollars, shown to the person who authored it. Not tokens — nobody converts tokens to money in their head — and not a running per-keystroke ticker, which just makes people anxious and stop using it. A number they can look at after the fact. Two: an operator-facing ceiling that acts on the org, not on the turn. If an org's spend in a period crosses a threshold, the next thing that can be refused is refused at a boundary where nothing is destroyed — starting a new authoring session, not continuing one. Any turn already in flight runs to completion. That's the constraint I want recorded and it's absolute: no spend control in this feature may terminate work that is already underway. Degradation happens at the start of new work. And since the pricing module throws on an unknown model rather than guessing — keep that. If we can't price a model we say so; we do not show a user a number we made up.
That's a clear, internally consistent answer. Let me capture it into blocks and score intent. The key distinctions I hear: - **Visibility** is per-spec, post-hoc, in dollars, and honest about unknowns - **The ceiling** is operator-set, org-level, checked at session start, never mid-turn - The fail-loud pricing rule carries into the UI The rejected block prescribed route names I hadn't verified — and the behavior is already covered by "No mid-turn spend kill." Let me just add the per-org constraint. Let me now score the intent section. We have three goals and four constraints that crisply define the boundaries the user set. Intent is scored and clean — 0.9 with no blocking findings. Here's what's captured in the spec so far: **Goals** - Per-spec authoring cost in dollars (post-hoc from the ledger, shown to the spec's author) - Operator-set per-period spend ceiling (checked at session start, never mid-turn) - Priced-model integrity in the UI (unknown models show "Unable to price," never $0 or a guess) **Constraints** - No mid-turn spend kill — ceiling only refuses new sessions, not in-flight turns - No running cost ticker — post-hoc, settled numbers only - No user-facing budget configuration — the ceiling is operator-only - Dollar-denominated — no tokens or micro-dollars shown to users - Per-org, not per-user Ready to move to the acceptance section when you are, or grill me on anything I'm missing.
insert_blocksunsupported state mechanicRequirement invents durable identity/access/lifecycle mechanic(s) absent from authoritative PM or repository evidence: session/account/access termination. Preserve the confirmed outcome without adding a storage, revocation, queryability, or presentation mechanism; ask only if it materially changes behavior.
Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.
Last call, on who sees what. The dollar figure goes to the person whose org paid for it, and on a personal workspace that's just the user. There's no per-member cost breakdown and no leaderboard of who spent what — explicit non-goal. That turns a cost display into a surveillance feature and changes how people use the product, and not for the better. Also non-goal: no pre-flight estimate before a run. I don't believe we can predict what a grilling session costs before we know how long the conversation goes, and a wrong estimate is worse than none — people will treat it as a quote and be angry when it's off. One acceptance criterion I want stated in measurable terms: the cost shown for a spec must reconcile to the sum of the ledger rows for that spec's turns. If those two numbers can disagree, the display is fiction. Score acceptance and generate the tasks.
Clear. Let me capture the non-goals and acceptance criteria, then score and generate tasks. Now let me score both remaining sections. All three sections pass. Let me verify the agent session route for the task titles, then generate. Both tasks created. Now the complete trace graph. Everything is committed and validated. Here's the final spec: **Title:** Per-spec cost visibility with operator spend ceiling **2 tasks, 6 acceptance criteria, 3 goals, 7 constraints — full trace coverage.** **Task 1: Show per-spec authoring cost in the spec detail view** Aggregates `agent_usage_ledger.costMicros` per spec into a dollar total rendered in the spec view post-hoc. Unpriced models show "Unable to price." Must reconcile exactly to `SUM(costMicros)`. **Task 2: Gate POST /v1/agent/sessions behind the per-org spend ceiling** Reads the org's ceiling and current `periodSpendMicros` at session start. Rejects new sessions when spend is at or above the ceiling. In-flight turns complete without interruption. Ceiling of 0 = unlimited; changes don't retroactively apply. The key product decisions this rests on: - **No mid-turn kill** — the ceiling refuses new sessions at the door, never terminates work in progress (you called this "the most important call in the spec") - **Dollar-denominated, post-hoc, no ticker** — users see a settled number, not a running anxiety meter - **No pre-flight estimates** — if we can't predict accurately, we don't show a number that will be treated as a quote - **No per-member breakdown** — cost is spec-level and org-level only, never a surveillance tool - **Fail-loud pricing extends to the UI** — unknown models show "Unable to price," never $0 or a fabricated figure
create_tasktask description sprawlThis task description runs 4 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.
Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.
create_tasktask description sprawlThis task description runs 6 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.
Files the agent read
- grep apps/api/src/routes/agent.ts
- ls apps/web/src/routes/dashboard
What it asked
Stop a user, or tell them the number?
- ARead-only visibility
User sees per-run cost and period spend, no ability to set or enforce a budget
- BRead + hard stopAgent's pick
User sees cost AND can set a period budget that blocks further turns when hit
- CRead + soft warning
User sees cost and a user-configurable soft threshold that warns but never blocks
You read it correctly and I want to make sure you state it plainly, because it's the crux: periodBudgetMicros is legacy and enforces nothing, the alert threshold only pages an operator, and the actual free-tier limit is a lifetime spec count. So today spend is measured and never acted on. You've asked whether to make periodBudgetMicros a real hard cap again, and you recommended it as the smallest change. No. I'm overruling that, and it's the most important call in this spec. A hard cap on spend means a turn dies partway through. Think about what that leaves behind: a spec that's been half-rewritten by an agent, some blocks inserted, a section score updated, and no way for the user to tell what happened or get back to a coherent state. And it happens at the moment they're most engaged. We'd be taking the product away from someone mid-sentence to save a few cents, and the fix — "upgrade" — arrives as a wall rather than an offer. That's a churn machine. The free tier already has a limit that's enforceable without any of that damage: the spec count, checked before anything starts. A refusal at the door is honest. A refusal halfway through is destructive. Grill me on what the feature is instead.
Visibility, allowance gate, or both?
- AVisibility only
Show per-turn cost and period spend in UI. No enforcement — users self-regulate.
- BVisibility + spend allowanceAgent's pick
Show cost AND a free-tier monthly allowance. Turns refused at the door when allowance is gone. Like the spec cap but for spend.
- CVisibility + user budget
Show cost AND let any user set their own period budget. Turns refused at the door when budget is hit. Overridable.
Right, so here's what I want. Two things, and only the first is a control. One: visibility for the user. What this spec cost to author, in dollars, shown to the person who authored it. Not tokens — nobody converts tokens to money in their head — and not a running per-keystroke ticker, which just makes people anxious and stop using it. A number they can look at after the fact. Two: an operator-facing ceiling that acts on the org, not on the turn. If an org's spend in a period crosses a threshold, the next thing that can be refused is refused at a boundary where nothing is destroyed — starting a new authoring session, not continuing one. Any turn already in flight runs to completion. That's the constraint I want recorded and it's absolute: no spend control in this feature may terminate work that is already underway. Degradation happens at the start of new work. And since the pricing module throws on an unknown model rather than guessing — keep that. If we can't price a model we say so; we do not show a user a number we made up.
Where the server refused
Each of these rejected the agent's own tool call. The agent had to fix its output and try again before the spec could advance — this is the gate, not a prompt the model can talk past.
insert_blocksunsupported state mechanicRequirement invents durable identity/access/lifecycle mechanic(s) absent from authoritative PM or repository evidence: session/account/access termination. Preserve the confirmed outcome without adding a storage, revocation, queryability, or presentation mechanism; ask only if it materially changes behavior.
Remove or reword the unsupported claim: keep only content the PM stated, selected, or the repository verified. If the claim is load-bearing, ask the PM instead of asserting it. This gate family will not block again this session for a rewrite without the claim — but resubmitting this same rejected content is rejected every time; retry once without the claim, then move on; do not loop on rewording. Block inserts are atomic: nothing from this call was committed. Retry the batch without the rejected claim.
create_tasktask description sprawlThis task description runs 4 sentences — past the 1-3 sentence capability handoff. Keep the outcome, the surfaces it touches, and what proves it works; cut restated acceptance assertions, step lists, and implementation detail the blocks and trace graph already carry.
Stop the current edit sequence. Treat this as a plan-shape failure, not a wording issue: re-plan from the acceptance criteria into the smallest set of coarse, independently shippable user slices before retrying — as few as the distinct capabilities require (commonly 1-3, hard cap 7, no minimum); one narrow regression fix is exactly 1 slice. If two slices are steps of the same capability, merge them into one. If bad tasks or blocks already exist, update/delete that bad shape before adding more.
What it had to clear
Every section had to score at or above 0.90 before create_task would run at all. This is the gate.
Intent is fully bounded: two clear user outcomes (visibility + operator ceiling) plus a pricing-integrity goal. Constraints explicitly rule out mid-turn kill, running ticker, user-facing budget config, and token displays — matching the PM's stated non-goals. Only the ceiling is operator-configured; users see cost post-hoc.
Seven constraints covering all boundaries: no mid-turn kill, no running ticker, no user budget config, dollar-only display, per-org ceiling, no per-member breakdown, no pre-flight estimates. All derived directly from PM decisions. No technical implementation constraints invented — the what is clear, the how is left to tasks.
Six acceptance criteria: reconciliation to ledger (PM's explicit check), ceiling rejection of new sessions, in-flight turn immunity, unpriced model handling, post-hoc display behavior, and ceiling persistence. All are measurable pass/fail signals.
The spec it produced
How it hangs together
Operator-set per-period spend ceiling
- constrainsNo mid-turn spend kill
- constrainsNo user-facing budget configuration
- constrainsCeiling is per-org, not per-user
- validatesCeiling refuses new sessions when crossed
- validatesIn-flight turns complete despite crossing
- validatesOperator ceiling config is durable
Per-spec authoring cost shown in dollars
- constrainsNo running cost ticker in the UI
- constrainsDollar-denominated user-facing cost
- constrainsNo per-member cost breakdown
- constrainsNo pre-flight cost estimates
- validatesCost reconciles to agent_usage_ledger
- validatesCost shown in the spec view, post-hoc
Priced-model integrity in the UI
- constrainsDollar-denominated user-facing cost
- validatesUnpriced model shows Unable to price
Cost reconciles to agent_usage_ledger
- implementsShow per-spec authoring cost in the spec detail view
Unpriced model shows Unable to price
- implementsShow per-spec authoring cost in the spec detail view
Cost shown in the spec view, post-hoc
- implementsShow per-spec authoring cost in the spec detail view
Ceiling refuses new sessions when crossed
- implementsGate POST /v1/agent/sessions behind the per-org spend ceiling
In-flight turns complete despite crossing
- implementsGate POST /v1/agent/sessions behind the per-org spend ceiling
Operator ceiling config is durable
- implementsGate POST /v1/agent/sessions behind the per-org spend ceiling
What the gate released
Show per-spec authoring cost in the spec detail view
Aggregate `agent_usage_ledger.costMicros` per specId into a dollar total and render it in the spec detail view post-hoc. When any turn uses a model absent from `MODEL_PRICING`, show "Unable to price" instead of $0 or a fabricated total. The display must exactly reconcile to `SUM(costMicros) / 1_000_000` — a mismatch means the display is wrong. Automated assertions: - Cost reconciles to agent_usage_ledger — If the two numbers can disagree, the display is fiction — this is a direct reconciliation check, not an approximation - Unpriced model shows Unable to price — no entry in MODEL_PRICING , the spec's cost display shows an explicit Unable to price label rather than a dollar total or $0 — never recorded, AND the case where a turn was recorded with a model name unknown to the current pricing table - Cost shown in the spec view, post-hoc — It is not displayed as a real-time ticker, per-turn counter, or during agent activity
Gate POST /v1/agent/sessions behind the per-org spend ceiling
Read the org's per-period ceiling and current `periodSpendMicros` when `POST /v1/agent/sessions` is called. If spend is at or above the ceiling, reject the session with a clear limit-reached error. A ceiling value of 0 or unset means unlimited. In-flight turns already running must not be interrupted — this check fires only at session start. The ceiling value is durable and read on every session-start check; changing it does not retroactively affect already-crossed periods. Automated assertions: - Ceiling refuses new sessions when crossed - In-flight turns complete despite crossing — without interruption — No agent turn, tool call, or spec mutation is aborted or rolled back due to the crossing — the ceiling only applies at session start - Operator ceiling config is durable — no ceiling (unlimited) — does not retroactively apply to already-crossed periods
zenorm workYour coding agent claims the task, implements it against the full spec, and gates itself on typecheck, lint, and tests.
A real request, the questions the agent asked, and every place the server refused its output. No product announcements.