Skip to content

Commit fb8bb6c

Browse files
authored
fix(cursor): keep API usage dollars tied to plan limit (#289)
1 parent f1a1d3b commit fb8bb6c

2 files changed

Lines changed: 47 additions & 36 deletions

File tree

packages/agents-usage/src/collectors/cursor.test.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,10 @@ describe("parseCursorUsage", () => {
7474
expect(api.resetsAt).toBe(1_719_600_000_000);
7575
});
7676

77-
it("treats breakdown.total as real spend and derives the allowance from the percent", () => {
78-
// Live payload shape: used/limit/remaining clamp at the included $20 while
79-
// breakdown.total carries the real spend ($20 included + $8.75 bonus
80-
// consumed = $28.75). The authoritative 55.07% then implies a ~$52.21
81-
// allowance (28.75 / 0.5507), keeping the dollars consistent with the bar.
77+
it("treats breakdown.total as real spend against the vendor plan limit", () => {
78+
// Live payload: used/limit clamp at included $20 while breakdown.total is
79+
// real spend ($20 included + $8.75 bonus = $28.75). API % is a separate
80+
// meter — do not invent limit = spend / percent ($28.75 / 55% ≈ $52).
8281
const body = {
8382
membershipType: "pro",
8483
individualUsage: {
@@ -96,12 +95,11 @@ describe("parseCursorUsage", () => {
9695
const api = snap.windows.find((w) => w.id === "cursor-api")!;
9796
expect(api.usedPercent).toBeCloseTo(55.07);
9897
expect(api.used).toBeCloseTo(28.75);
99-
expect(api.limit).toBeCloseTo(52.21, 1);
98+
expect(api.limit).toBeCloseTo(20);
10099
});
101100

102-
it("derives the allowance when the clamped dollars disagree with the percent (no breakdown)", () => {
103-
// Older payloads without a breakdown: used/limit cap at the plan price
104-
// ($20/$20) while the percent says 44% consumed → allowance ≈ $45.45.
101+
it("keeps plan dollars when they disagree with API percent (no breakdown)", () => {
102+
// Clamped plan price $20/$20; API bar can still be 44% of a different pool.
105103
const body = {
106104
membershipType: "pro",
107105
individualUsage: {
@@ -112,7 +110,29 @@ describe("parseCursorUsage", () => {
112110
const api = snap.windows.find((w) => w.id === "cursor-api")!;
113111
expect(api.usedPercent).toBe(44);
114112
expect(api.used).toBeCloseTo(20);
115-
expect(api.limit).toBeCloseTo(45.45, 1);
113+
expect(api.limit).toBeCloseTo(20);
114+
});
115+
116+
it("shows overspend past the plan limit without inventing a percent-derived ceiling", () => {
117+
// User case: ~$35.61 total cost, $20 Pro included, API bar ~4.5%.
118+
// Old math: $35.61 / 0.04555 ≈ $782 nonsense allowance.
119+
const body = {
120+
membershipType: "pro",
121+
individualUsage: {
122+
plan: {
123+
used: 2000,
124+
limit: 2000,
125+
breakdown: { included: 2000, bonus: 1561, total: 3561 },
126+
autoPercentUsed: 22.37,
127+
apiPercentUsed: 4.555,
128+
},
129+
},
130+
};
131+
const snap = parseCursorUsage(body, {}, NOW);
132+
const api = snap.windows.find((w) => w.id === "cursor-api")!;
133+
expect(api.usedPercent).toBeCloseTo(4.555);
134+
expect(api.used).toBeCloseTo(35.61);
135+
expect(api.limit).toBeCloseTo(20);
116136
});
117137

118138
it("keeps the vendor limit when the dollars already agree with the percent", () => {

packages/agents-usage/src/collectors/cursor.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import type { UsageSnapshot, UsageWindow } from "../types";
1212
* Schema (per codexbar) of GET /api/usage-summary → individualUsage.plan:
1313
* { used (cents), limit (cents), breakdown { included, bonus, total },
1414
* totalPercentUsed, autoPercentUsed, apiPercentUsed } + billingCycleEnd +
15-
* membershipType. Surfaced as Auto and API breakdown windows. The dollar
16-
* allowance belongs to API usage; see `apiDollars` for how the clamped
17-
* dollar fields are reconciled with the authoritative percents.
15+
* membershipType. Surfaced as Auto and API windows. API dollars use real
16+
* spend over the vendor plan limit; the bar uses apiPercentUsed separately.
1817
*/
1918

2019
export const CURSOR_USAGE_ENDPOINT = "https://cursor.com/api/usage-summary";
@@ -63,21 +62,22 @@ function clampPercent(value: number | undefined): number | undefined {
6362
}
6463

6564
/**
66-
* Cursor's plan dollar fields need reconciliation (verified against the live
67-
* payload and CodexBar's Cursor probe, which scrapes the same endpoint):
65+
* Cursor plan dollars vs API percent are different meters:
6866
*
69-
* - `used`/`limit`/`remaining` clamp at the nominal plan price (e.g. $20/$20
70-
* while the percent says 55%), so they understate real spend.
71-
* - `breakdown.total` is the total credits *consumed* (included + bonus spend),
72-
* not the allowance — treating it as the limit was CodexBar regression #240.
73-
* Prefer it as the real spend when it exceeds the clamped `used`.
74-
* - `apiPercentUsed` is the authoritative consumed fraction (it's what
75-
* Cursor's own dashboard messages report). When the dollar pair disagrees
76-
* with it, keep the spend and derive the allowance as `spend / percent` so
77-
* the dollar text always matches the bar (e.g. $28.75 / $52.21 at 55%,
78-
* instead of a clamped, full-looking $20 / $20).
67+
* - `used`/`limit` clamp at the nominal plan price (e.g. $20/$20) and can
68+
* understate real spend once bonus credit is consumed.
69+
* - `breakdown.total` is credits *consumed* (included + bonus spend), not the
70+
* allowance — treating it as the limit was CodexBar regression #240.
71+
* - `apiPercentUsed` drives the bar / "% by reset" pace; it is not a fraction
72+
* of the plan dollar cap. Deriving `limit = spend / apiPercent` invents a
73+
* nonsense ceiling (e.g. $35.61 / $775 at 4.5%) that is neither spend nor
74+
* the Pro included allowance.
75+
*
76+
* Surface honest money: real spend over the vendor plan limit. The bar may
77+
* then disagree with the dollar ratio — that is correct; they measure different
78+
* things.
7979
*/
80-
function apiDollars(plan: CursorPlanUsage, percent: number): { used?: number; limit?: number } {
80+
function apiDollars(plan: CursorPlanUsage): { used?: number; limit?: number } {
8181
const reportedUsed = centsToUsd(plan.used);
8282
const reportedLimit = centsToUsd(plan.limit);
8383
const breakdownTotal = centsToUsd(plan.breakdown?.total);
@@ -89,15 +89,6 @@ function apiDollars(plan: CursorPlanUsage, percent: number): { used?: number; li
8989
if (spend === undefined) {
9090
return reportedLimit !== undefined && reportedLimit > 0 ? { limit: reportedLimit } : {};
9191
}
92-
if (spend > 0 && percent > 0) {
93-
const impliedPercent =
94-
reportedLimit !== undefined && reportedLimit > 0 ? (spend / reportedLimit) * 100 : undefined;
95-
// 1-point tolerance so ordinary rounding drift in the reported percent
96-
// doesn't override a limit the spend already agrees with.
97-
if (impliedPercent === undefined || Math.abs(impliedPercent - percent) > 1) {
98-
return { used: spend, limit: spend / (percent / 100) };
99-
}
100-
}
10192
return {
10293
used: spend,
10394
...(reportedLimit !== undefined && reportedLimit > 0 ? { limit: reportedLimit } : {}),
@@ -146,7 +137,7 @@ export function parseCursorUsage(
146137
usedPercent: apiPercent,
147138
unit: "percent",
148139
currency: "USD",
149-
...apiDollars(plan, apiPercent),
140+
...apiDollars(plan),
150141
...withReset,
151142
});
152143
}

0 commit comments

Comments
 (0)