Skip to content

collect fails on /teams/spend: NOT NULL on included_spend_cents + wrong spend amounts (API shape changed) #19

@Sebastien-Meiffren

Description

@Sebastien-Meiffren

Summary

Running npm run collect fails to persist spending data because Cursor's /teams/spend API response shape has changed. Two related issues:

  1. The endpoint no longer returns includedSpendCents, causing a SQLite NOT NULL constraint violation.
  2. 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

npm run collect

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):

  • includedSpendCentsno 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.
  • overallSpendCentsnew 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions