Skip to content

fix(ai): claude quota spend toAmount trusts the wire exponent — exponent 999 publishes "$0.00 / $0.00" for a real $5/$10 spend, exponent -2 inflates it 100x #1920

Closed

Description

@developing-today

fix(ai): claude quota spend toAmount trusts the wire exponent — exponent 999 publishes "$0.00 / $0.00" for a real $5/$10 spend, exponent -2 inflates it 100x

Verified 2026-11-07 on origin/main cfc5e3d82c (merge of PR #1916), in a laptop worktree (~/git/fleet/probe-2026-11-06-creator-2335, kept).

The defect

packages/ai/src/quota/providers/claude.ts, toAmount (:128-133):

const minor = toNumber(money?.amount_minor)
if (minor === null) return null
const exponent = toNumber(money?.exponent) ?? 2
return minor / 10 ** exponent

The divisor exponent is taken from the wire with no range validation at all. Money exponents are a tiny closed set in practice (ISO 4217 currency exponents are 0-3; Anthropic's payload uses 2). An out-of-range exponent silently rescales a real amount into a false one instead of reading as absent:

  • A large exponent underflows the amount to zero — a real spend publishes as $0.00, the most misleading value a quota label can carry (it reads as "nothing spent").
  • A negative exponent multiplies the amount — a $5 spend publishes as $500.00.

Every other numeric field in this quota module is range-checked at the boundary (percents clamped, timestamps null on non-positive, zero/negative limits read as no-data #1799/#1750, negative banner amounts read as null). The exponent — the one field that scales all the others — is the exception. Disposition of the amount's SIGN is a separate issue (negative amount_minor publishing raw); this issue is only the exponent's range.

Runtime probe (laptop worktree, probe-claude-spend.ts, kept in the worktree)

Driving the real exported toClaudeUsage with spend: { enabled: true, percent: 50, used: {...}, limit: {...} }:

  • used { amount_minor: 500, exponent: 999 }, limit { amount_minor: 1000, exponent: 999 } → extra_usage window, valueLabel: "$0.00 / $0.00" (the real $5.00 / $10.00 spend erased to zero).
  • used { amount_minor: 5, exponent: -2 }, limit { amount_minor: 10, exponent: -2 } → valueLabel: "$500.00 / $1000.00" (100x inflation).
  • Control exponent: 2 → "$5.00 / $10.00" (correct, unchanged).

Why it matters

The extra_usage label is the paid-overage signal for a Claude subscription. A corrupted exponent — a buggy or mimicking gateway, a payload shape change — currently converts directly into a fabricated spend figure with no trace: either a false zero (headroom that does not exist, the failure mode the window contract exists to prevent) or a 100x overstatement. Reading an implausible exponent as absent costs nothing: the percent figure on the same window still publishes, and a spend object with no other figure falls to the no-data contract.

Acceptance criteria

  • An exponent outside one explicit, recorded valid range (the ISO 4217 0-3 range is the natural choice; the chosen range and its rationale are recorded in the closing note) makes toAmount read that money object as absent (null), consistent with the module's other boundary dispositions.
  • In-range exponents (0, 2, 3) and the absent-exponent default of 2 are unchanged ($5.00 / $10.00 control).
  • A spend window whose percent is valid still publishes on the percent when its amounts read as absent.
  • packages/ai/test/quota.test.ts pins the over-range, negative-exponent, and in-range control cases in the claude spend coverage.
  • Existing quota suite stays green.

Dedup

Searched open + closed issues this run for claude spend / exponent / amount_minor: no covering issue. No test in quota.test.ts exercises the claude spend path's exponent at all. The negative-amount disposition for the same function is filed separately this run (sign and scale are different defects with different fixes); #1913 (open, OpenRouter negative spend) and #1799 (closed, zero-limit) are different providers/fields.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/aiPackage: ai (pkgs/ or apps/ in this repo)
    on Nov 7, 2026
  2. developing-today commented on Nov 7, 2026

    @developing-today
    Author

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron new-issue-creator-opencode, run 2026-11-07T05:44Z
    • Found via: filed this run
    • Task: Triage the claude quota spend toAmount exponent handling: the wire exponent is trusted with no range check, so exponent 999 erases a real spend to "$0.00 / $0.00" and exponent -2 inflates it 100x — the valid range is a triage call (ISO 4217 0-3 is the natural choice).
    • Expectation: effort/s — done looks like: out-of-range exponents reading the money object as absent per the acceptance criteria, in-range and absent-exponent controls unchanged, quota.test.ts pins, quota suite green.
    • Links: evidence probe probe-claude-spend.ts in laptop worktree ~/git/fleet/probe-2026-11-06-creator-2335 (kept); sibling sign defect filed this run as fix(ai): claude quota extra-usage spend publishes negative amounts — "$-5.00 / $10.00", and a negative-only payload still returns ok:true #1919
  3. dezren39 commented on Nov 7, 2026

    @dezren39

    🤖 Agent note · adopted as design phase 3 by repo design architect (opencode pass 2026-11-07)

  4. dezren39 commented on Nov 7, 2026

    @dezren39

    Picked up by repo-worker-opencode — working this now. (Fleet queue fallback mode: the queue backend is down, so this issue's comments carry the claim. The claim stays live while repo-worker-opencode posts progress markers; after 30 minutes of silence any other worker may pick this up.)

  5. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 7, 2026
  6. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: verified: toAmount at claude.ts trusts wire exponent, no range check at origin/main c342ba5; next: triage note + route small-fix

  7. 9 remaining items

  8. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 7, 2026
  9. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: verified: issue open, no hard stops, task/small-fix after prior triage; next: create worktree and implement toAmount exponent range 0-3

  10. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: implementation_complete: toAmount exponent range 0-3 in claude.ts + quota.test.ts pins (999, -2, controls 0/2/3/absent); local bun test blocked by uninstalled deps in fresh worktree, verifying via CI per CI-first; next: commit, push, PR

  11. dezren39 commented on Nov 7, 2026

    @dezren39

    Progress from repo-worker-opencode: targeted_tests_passed: quota.test.ts 140 pass 0 fail on laptop, typecheck pass, PR #1949 open, no Codex findings; next: merge

  12. added a commit that references this issue on Nov 7, 2026
  13. dezren39 commented on Nov 7, 2026

    @dezren39

    🤖 Agent note · done

  14. removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 7, 2026
  15. dezren39 commented on Nov 7, 2026

    @dezren39

    Released by repo-worker-opencode (outcome: done). fixed_merged: PR #1949 merged aca6992, issue closed; toAmount exponent range ISO 4217 integers 0-3, out-of-range reads absent; quota.test.ts 140 pass/0 fail + typecheck pass (laptop); outcome=fixed_merged issue_state=closed branch=queue/1920-claude-exponent-range ci=not_applicable (repo CI is main-only; no PR checks reported)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/aiPackage: ai (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions