Young's Mind
03 Decisions

Ruling 3 = 300 s, and the OpenCode budget trace

Young, 2026-10-10:

Ruling 3 = 300 s, and the OpenCode budget trace

Young, 2026-10-10:

"Ruling 3 — 300s (will refine when we have more data). Currently can only check on the opencode console. Via opencode cli opencode stats --days 7" — with the OpenCode v2 docs, including the Budgets page and the Budgets API.

I was wrong, and I am correcting it in place. My mira-go-exhaustion-trace.md §4 concluded "No Opencode balance API was found" and the design assumed none. There is one. It is documented, it is REST, it returns spend in microcents, and it names which budget was exceeded.


1. The correction — there IS a programmatic budget API

My earlier claimReality, per OpenCode's own docs
"No Opencode balance API was found"GET /api/v1/budgets/members — returns every member's limit_micro_cents, spent_micro_cents, exceeded, resets_at
"trust opencode stats... never synthesise a balance"still true — and now there is a live balance to read, so no synthesis is needed either

The response, per the docs:

{
  "user_id": "user_...",
  "email": "alice@example.com",
  "limit_micro_cents": "7525000000",
  "spent_micro_cents": "1250000000",
  "exceeded": false,
  "resets_at": "2026-10-01T00:00:00.000Z",
  "source": "custom"
}

100,000,000 microcents per US dollar, decimals as strings so precision is not lost. Spend and resets cover the current UTC calendar month.

Access requirement, worth noting: "Create a service account API key with All permissions. Inference-only keys cannot access budget data." So the trace needs a different key from the one the fleet runs inference on — which is consistent with our no-keys-in-config rule, and a second credential to govern.


2. Why this is better than the 429 breaker — and still not a replacement

This closes the gap I had named as unverified. It is a live, authoritative balance, month-scoped, with a reset date. It is not a synthesised figure.

SourceWhat it givesWhat it cannot
Budgets APIlimit vs spent, exceeded flag, reset dateit does not say which model or task spent it
429 breakerwhich model died, and the verbatim provider stringit does not know the balance
opencode stats --days 7cost by day and modelCLI-only, manual

So all three are needed, and they answer different questions. My §3 sequence gains a first line:

GET /api/v1/budgets/members          → limit, spent, exceeded, resets_at   ← NEW
    │  (the authority on whether, not on what)
    ▼
model returns 429 "provider.quota"   → which model died, verbatim reason    ← V2's prober
    │
    ▼
window_state = blocked, rollover_reason = the provider string              ← my fields
    │
    ▼
billing_instrument → zen_payg, the rollover auditable                      ← my fields

The Budget reached signal is also documented and it is exactly the shape we want: "New requests fail with HTTP 429... The response does not say which budget was reached" — Upstream request failed: Account budget exceeded. Two distinct 429 shapes to classify:

Provider stringMeaning
Account budget exceededa budget was hit — workspace, user, or service account, and the API deliberately does not say which
provider.quota "Rate limit exceeded."a per-model rate limit — the window, not the balance

That distinction is load-bearing for my fields. A budget-exceeded event should set window_state: blocked and record budget_exceeded: true; a per-model quota event is a narrower thing. Collapsing them is how the rollover becomes untraceable — the very thing you asked about.


3. A design consequence: window_state needs a third value

My ledger had ok · approaching · blocked. The budget API adds a distinct state, because a workspace budget blocks differently from a model quota:

StateTriggerMeaning
okunder bothroutable
approachingspent/limit crossing a thresholdstill routable, escalation should prefer free
quota_blockedper-model 429 provider.quotathat model is unavailable; another may work
budget_exceededAccount budget exceeded, or exceeded: truethe workspace is blocked — nothing works

quota_blocked and budget_exceeded must not be one value. They demand different responses: the first rolls over (your ruling), the second stops the fleet and waits for a reset or a raise.

And the docs confirm your rollover rule's risk: "Budgets are checked when a request starts. Requests already in progress finish, so spend can end slightly above the limit." So GO exhaustion is not a clean cut — in-flight work overshoots. My ledger's partial-spend probe is what catches that.


4. What counts toward a budget — and one entry that matters to us

SpendCounts
Paid Console modelsyes
Free modelsno
Go usage within the planno
Go extra usage paid from creditsyes

Read that carefully: GO usage within the plan does not count toward a Console budget. Which means:

  • If our GO usage stays inside the plan, the Console budget never sees it — and the budget API will report $0 spend while we are using the $10 package.
  • The exceeded flag therefore tracks credit-paid spend, not GO-plan spend.

So the balance API is not a complete picture of GO consumption, and this is precisely the distinction that matters to your rollover rule. The design must record two quantities separately:

  • go_plan_spend — usage inside the $10 plan, invisible to the budget API
  • go_credit_spend — credits spent beyond it, visible, and budget-capped

billing_instrument needs that split, not just go_allowance. Otherwise an "exhausted" GO will look like $0 spent and the rollover will fire late — or not at all. This is the strongest finding in this document and it comes from one row of Young's linked table.

Also useful: $0 blocks all spend that counts; budgets reset on the 1st at 00:00 UTC; and there is a PUT to set a custom budget and a DELETE to return to default — so the cap is programmatically enforceable, not just observable.


5. Ruling 3 recorded

The stuck duration is 300 s, per Young — with the honest note he attached: refine when we have more data.

This is the A2A orphaned-task watchdog, not the kanban stale rule. Hermes ships both:

MechanismNumberWhat it governs
A2A orphaned-task watchdog300 s ← RULINGa task that produced no reply
Watchdog sweep period60 show often we check
Kanban stale worker3600 sa claimed worker that stopped heartbeating

Two different stuck conditions, and Young picked the one that matches Stage 3.3: a task that stopped producing, not a worker that went quiet. That is the right choice — the 3600 s number governs worker liveness, and applying it to tasks would mean a stuck task waits an hour.

The proof stays as written: a task past its duration enters stuck without a human deciding, and the state names the cause. Now with a number attached, Stage 3.3's probe is buildable.


6. Not established

  • I have not called the Budgets API. No service-account key exists in my session, and every terminal call has failed on a stuck cwd. Everything above is from OpenCode's own docs, which is the best available source but is not a probe.
  • Whether our workspace actually has a Console budget set is unknown. "Without any budget, spend is unlimited" — so the exceeded flag may simply read false forever if no limit is configured. That is a setting to check, not an assumption.
  • The claim in §4 about GO-plan-invisibility is read from the "What counts" table, and I have not verified it against a real response showing $0 spent mid-plan.
Copyright © 2026