Summary
Running npm run collect fails to persist spending data because Cursor's /teams/spend API response shape has changed. Two related issues:
- The endpoint no longer returns
includedSpendCents, causing a SQLite NOT NULL constraint violation.
- The endpoint now returns
spendCents = 0 for users under their plan limit and exposes the actual cycle total in a new field, overallSpendCents. As a result, even after working around the constraint error, all stored spend values would be 0.
Reproduction
Output:
[collect] Fetching spending data...
[collect] Failed to collect spending: NOT NULL constraint failed: spending.included_spend_cents
...
[collect] Errors:
- Failed to collect spending: NOT NULL constraint failed: spending.included_spend_cents
Spending: 0 is reported, while every other collector (daily usage, groups, usage events, analytics) succeeds.
Root cause
A live response from POST /teams/spend (page 1, pageSize 3) currently looks like:
{
"teamMemberSpend": [
{
"userId": "user_...",
"spendCents": 0,
"overallSpendCents": 860,
"fastPremiumRequests": 0,
"name": "...",
"email": "...",
"profilePictureUrl": null,
"role": "member",
"monthlyLimitDollars": 30,
"hardLimitOverrideDollars": 200,
"effectivePerUserLimitDollars": 30
}
],
"subscriptionCycleStart": 1779062400000,
"totalMembers": 50,
"totalPages": 17,
"limitedUsersCount": 0,
"maxUserSpendCents": 0
}
Differences from what the codebase expects (see MemberSpend in src/lib/types.ts and upsertSpending in src/lib/data/sqlite.ts):
includedSpendCents — no longer present. The collector binds undefined, which better-sqlite3 sends as NULL, hitting included_spend_cents INTEGER NOT NULL.
spendCents — semantics appear to have changed. It now seems to represent the overage above the plan-included amount, and is 0 for everyone within plan.
overallSpendCents — new field. Represents the cycle-to-date total (what the dashboard previously showed as spend_cents).
- New fields seen on members:
profilePictureUrl, effectivePerUserLimitDollars.
Suggested fix
In src/lib/cursor-client.ts::getSpending, normalize the response so the rest of the pipeline keeps working:
spendCents := overallSpendCents ?? spendCents (total cycle spend)
includedSpendCents := max(0, overallSpendCents - spendCents_overage) (or 0 if not derivable)
- Defensive
?? 0 / ?? null in upsertSpending bindings as a belt-and-suspenders fix against future schema changes.
Update src/lib/types.ts::MemberSpend and SpendResponse to document the new fields and the new semantics of spendCents. The API reference in .cursor/rules/cursor-api.mdc should also be refreshed — it currently lists only { email, spendCents, fastPremiumRequests } for this endpoint.
Environment
- Branch:
main @ 5935d11
- Node / SQLite: better-sqlite3
- Team size in repro: 50 members, 17 spend pages
Summary
Running
npm run collectfails to persist spending data because Cursor's/teams/spendAPI response shape has changed. Two related issues:includedSpendCents, causing a SQLiteNOT NULLconstraint violation.spendCents = 0for users under their plan limit and exposes the actual cycle total in a new field,overallSpendCents. As a result, even after working around the constraint error, all stored spend values would be0.Reproduction
Output:
Spending: 0is reported, while every other collector (daily usage, groups, usage events, analytics) succeeds.Root cause
A live response from
POST /teams/spend(page 1, pageSize 3) currently looks like:{ "teamMemberSpend": [ { "userId": "user_...", "spendCents": 0, "overallSpendCents": 860, "fastPremiumRequests": 0, "name": "...", "email": "...", "profilePictureUrl": null, "role": "member", "monthlyLimitDollars": 30, "hardLimitOverrideDollars": 200, "effectivePerUserLimitDollars": 30 } ], "subscriptionCycleStart": 1779062400000, "totalMembers": 50, "totalPages": 17, "limitedUsersCount": 0, "maxUserSpendCents": 0 }Differences from what the codebase expects (see
MemberSpendinsrc/lib/types.tsandupsertSpendinginsrc/lib/data/sqlite.ts):includedSpendCents— no longer present. The collector bindsundefined, which better-sqlite3 sends asNULL, hittingincluded_spend_cents INTEGER NOT NULL.spendCents— semantics appear to have changed. It now seems to represent the overage above the plan-included amount, and is0for everyone within plan.overallSpendCents— new field. Represents the cycle-to-date total (what the dashboard previously showed asspend_cents).profilePictureUrl,effectivePerUserLimitDollars.Suggested fix
In
src/lib/cursor-client.ts::getSpending, normalize the response so the rest of the pipeline keeps working:spendCents := overallSpendCents ?? spendCents(total cycle spend)includedSpendCents := max(0, overallSpendCents - spendCents_overage)(or0if not derivable)?? 0/?? nullinupsertSpendingbindings as a belt-and-suspenders fix against future schema changes.Update
src/lib/types.ts::MemberSpendandSpendResponseto document the new fields and the new semantics ofspendCents. The API reference in.cursor/rules/cursor-api.mdcshould also be refreshed — it currently lists only{ email, spendCents, fastPremiumRequests }for this endpoint.Environment
main@5935d11