Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .cursor/rules/project-context.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ Single cron endpoint `POST /api/cron` does both: collect → detect → alert in
| `src/lib/collector.ts` | Data collection pipeline (members, daily usage, spending, events, analytics, groups) |
| `src/lib/anomaly/detector.ts` | Orchestrator — runs all 3 detection layers, deduplicates |
| `src/lib/anomaly/thresholds.ts` | Layer 1: static limits (spend, requests, tokens) |
| `src/lib/anomaly/zscore.ts` | Layer 2: statistical z-score vs team mean |
| `src/lib/anomaly/trends.ts` | Layer 3: personal spikes, drift above P75, model shift |
| `src/lib/anomaly/zscore.ts` | Layer 2: statistical z-score on daily spend vs active team mean |
| `src/lib/anomaly/trends.ts` | Layer 3: daily spend spikes, cycle spend outliers |
| `src/lib/incidents.ts` | Incident lifecycle: create, alert, acknowledge, resolve + MTTD/MTTI/MTTR |
| `src/lib/alerts/slack.ts` | Slack webhook with block-kit messages |
| `src/lib/alerts/email.ts` | SMTP email with HTML templates |
| `src/lib/alerts/slack.ts` | Slack bot token + chat.postMessage with block-kit messages |
| `src/lib/alerts/email.ts` | Email alerts via Resend with HTML templates |
| `src/lib/date-utils.ts` | Date formatting helpers (formatDateShort, formatDateTick, formatDateLabel) |
| `src/lib/format-utils.ts` | Model name shortening (MODEL_MAP regex → short labels) |
| `src/app/api/cron/route.ts` | Main cron endpoint (collect + detect + alert) |
Expand Down Expand Up @@ -85,5 +85,5 @@ members, daily_usage, spending, usage_events, anomalies, incidents, config, coll
## Important Caveats

- API response shapes may not exactly match the live Cursor API. If real responses differ, update `src/lib/types.ts` and `src/lib/cursor-client.ts`.
- Trend detection can produce duplicate dedup keys (spike + drift both emit `trend:tokens` for same user) — first one wins, second is silently dropped.
- Trend detection can produce duplicate dedup keys (spend spike + cycle outlier both emit `trend:spend` for same user) — first one wins, second is silently dropped.
- CLI scripts (`npm run collect`, `npm run detect`) do NOT auto-load `.env` — use `source .env && export CURSOR_ADMIN_API_KEY` or run via the Next.js dev server cron endpoint instead.
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
CURSOR_ADMIN_API_KEY=key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

SLACK_WEBHOOK_URL=
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_CHANNEL_ID=C0123456789

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=cursor-tracker@yourcompany.com
DASHBOARD_URL=http://localhost:3000

RESEND_API_KEY=
RESEND_FROM=Cursor Tracker <alerts@resend.dev>
ALERT_EMAIL_TO=team-lead@yourcompany.com

CRON_SECRET=your-secret-for-cron-endpoint
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ jobs:
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/
.next/
dist/
data/*.db
!data/mock.db
data/*.db-journal
data/*.db-shm
data/*.db-wal
Expand Down
19 changes: 19 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@
"color": "terminal.ansiCyan"
}
},
{
"label": "Dev Server (Mock Data)",
"type": "shell",
"command": "npm run dev:mock -- --turbopack -p 3456",
"problemMatcher": [],
"isBackground": true,
"presentation": {
"group": "cursor-tracker",
"panel": "dedicated",
"showReuseMessage": false,
"clear": true,
"reveal": "always",
"focus": false
},
"icon": {
"id": "globe",
"color": "terminal.ansiYellow"
}
},
{
"label": "Collect Data",
"type": "shell",
Expand Down
79 changes: 43 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
<h1 align="center">Cursor Usage Tracker</h1>

<p align="center">
AI spend monitoring, anomaly detection, and alerting for teams on Cursor Enterprise.<br>
Know who's burning through your budget before the invoice tells you.
Know who's burning through your AI budget before the invoice tells you.
</p>

<p align="center">
Expand All @@ -21,7 +20,7 @@

---

## Why This Exists
## AI Spend Is a Blind Spot

Engineering costs used to be two things: headcount and cloud infrastructure. You had tools for both. Then AI coding assistants showed up, and suddenly there's a third cost center that nobody has good tooling for.

Expand Down Expand Up @@ -51,13 +50,12 @@ Developer uses Cursor → API collects data hourly → Engine detects anomaly

### How It Works

| What happens | Example |
| -------------------------------------------- | ------------------------------------------------------- |
| A developer exceeds the spend limit | `Bob spent $82 this cycle (limit: $50)` → Slack alert |
| Someone's usage is 3x their personal average | `Token spike: 4.2x Alice's 7-day average` → Slack alert |
| A user is statistically far from the team | `Bob: 2.8 std devs above team mean` → Slack alert |
| Someone shifts to expensive models | `Opus usage jumped from 5% to 45%` → Slack alert |
| Usage drifts above team P75 for days | `Above team P75 for 5 of last 6 days` → Slack alert |
| What happens | Example |
| ------------------------------------------ | ----------------------------------------------------------------------------- |
| A developer exceeds the spend limit | `Bob spent $82 this cycle (limit: $50)` → Slack alert |
| Someone's daily spend spikes | `Alice: daily spend spiked to $214 (4.2x her 7-day avg of $51)` → Slack alert |
| A user's cycle spend is far above the team | `Bob: cycle spend $957 is 5.1x the team median ($188)` → Slack alert |
| A user is statistically far from the team | `Bob: daily spend $214 is 3.2σ above team mean ($42)` → Slack alert |

Every alert includes who, what model, how much, and a link to their dashboard page so you can investigate immediately.

Expand All @@ -67,11 +65,11 @@ Every alert includes who, what model, how much, and a link to their dashboard pa

### Three-Layer Anomaly Detection

| Layer | Method | What it catches |
| -------------- | ------------- | --------------------------------------------------------------------------- |
| **Thresholds** | Static limits | Spend > $50/cycle, > 500 requests/day, > 5M tokens/day |
| **Z-Score** | Statistical | User 2+ standard deviations above team mean |
| **Trends** | Behavioral | Personal spikes, sustained drift above P75, model shift to expensive models |
| Layer | Method | What it catches |
| -------------- | ------------- | ----------------------------------------------------------------------------- |
| **Thresholds** | Static limits | Optional hard caps on spend, requests, or tokens (disabled by default) |
| **Z-Score** | Statistical | User daily spend 2.5+ standard deviations above team mean (active users only) |
| **Trends** | Spend-based | Daily spend spikes vs personal average, cycle spend outliers vs team median |

### Incident Lifecycle (MTTD / MTTI / MTTR)

Expand All @@ -91,8 +89,8 @@ Anomaly Detected ──→ Alert Sent ──→ Acknowledged ──→ Resolved

### Rich Alerting

- **Slack**: Block Kit messages with severity, user, model, value vs threshold, and dashboard links
- **Email**: HTML-formatted alerts with the same context
- **Slack**: Block Kit messages via bot token (`chat.postMessage`) with severity, user, model, value vs threshold, and dashboard links. Batches alerts automatically (individual messages for 1-3 anomalies, single summary for 4+).
- **Email**: HTML-formatted alerts via [Resend](https://resend.com) (one API key, no SMTP config)

### Web Dashboard

Expand All @@ -116,7 +114,16 @@ Anomaly Detected ──→ Alert Sent ──→ Acknowledged ──→ Resolved
| Admin API key | Cursor dashboard → Settings → Advanced → Admin API Keys |
| Node.js 18+ | [nodejs.org](https://nodejs.org) |

### 1. Clone and install
### 1. Set up

**Option A: One command**

```bash
npx cursor-usage-tracker my-tracker
cd my-tracker
```

**Option B: Manual clone**

```bash
git clone https://github.com/ofershap/cursor-usage-tracker.git
Expand All @@ -136,20 +143,19 @@ Edit `.env`:
# Required
CURSOR_ADMIN_API_KEY=your_admin_api_key

# Alerting (at least one recommended)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxx
# Alerting — Slack (at least one alerting channel recommended)
SLACK_BOT_TOKEN=xoxb-your-bot-token # bot token with chat:write scope
SLACK_CHANNEL_ID=C0123456789 # channel to post alerts to

# Dashboard URL (used in alert links)
DASHBOARD_URL=http://localhost:3000

# Optional
CURSOR_ANALYTICS_API_KEY=your_analytics_key # for Insights page (DAU, model breakdowns, MCP)
CRON_SECRET=your_secret_here # protects the cron endpoint
DASHBOARD_PASSWORD=your_password # optional basic auth for the dashboard

# Email alerts (optional)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASS=app_password
SMTP_FROM=cursor-tracker@yourcompany.com
# Email alerts via Resend (optional)
RESEND_API_KEY=re_xxxxxxxxxxxx
ALERT_EMAIL_TO=team-lead@company.com
```

Expand Down Expand Up @@ -256,15 +262,16 @@ flowchart TB

All detection thresholds are configurable via the Settings page or the API:

| Setting | Default | What it does |
| -------------------- | ------- | ------------------------------------------------- |
| Max spend per cycle | $200 | Alert when a user exceeds this in a billing cycle |
| Max requests per day | 200 | Alert on excessive daily request count |
| Max tokens per day | 5M | Alert on excessive daily token consumption |
| Z-score multiplier | 2.5 | How many standard deviations above mean to flag |
| Z-score window | 7 days | Historical window for statistical comparison |
| Spike multiplier | 3.0x | Alert when today > N× user's personal average |
| Drift days above P75 | 3 | Consecutive days above team P75 to flag |
| Setting | Default | What it does |
| ------------------------ | ------- | -------------------------------------------------------------- |
| Max spend per cycle | 0 (off) | Alert when a user exceeds this in a billing cycle |
| Max requests per day | 0 (off) | Alert on excessive daily request count |
| Max tokens per day | 0 (off) | Alert on excessive daily token consumption |
| Z-score multiplier | 2.5 | How many standard deviations above mean to flag (spend + reqs) |
| Z-score window | 7 days | Historical window for statistical comparison |
| Spend spike multiplier | 5.0x | Alert when today's spend > N× user's personal daily average |
| Spend spike lookback | 7 days | How many days of history to compare against |
| Cycle outlier multiplier | 10.0x | Alert when cycle spend > N× team median (active users only) |

---

Expand Down
52 changes: 52 additions & 0 deletions bin/create.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env node

import { execSync } from "node:child_process";
import { existsSync, copyFileSync } from "node:fs";
import { resolve, basename } from "node:path";

Check failure on line 5 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'basename' is defined but never used

Check failure on line 5 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'basename' is defined but never used

const REPO = "https://github.com/ofershap/cursor-usage-tracker.git";
const dirName = process.argv[2] || "cursor-usage-tracker";

Check failure on line 8 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'process' is not defined

Check failure on line 8 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'process' is not defined
const targetDir = resolve(process.cwd(), dirName);

Check failure on line 9 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'process' is not defined

Check failure on line 9 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'process' is not defined

function run(cmd, opts = {}) {
execSync(cmd, { stdio: "inherit", ...opts });
}

function log(msg) {
console.log(`\n\x1b[36m${msg}\x1b[0m`);

Check failure on line 16 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'console' is not defined

Check failure on line 16 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'console' is not defined
}

if (existsSync(targetDir)) {
console.error(`\x1b[31mDirectory "${dirName}" already exists.\x1b[0m`);

Check failure on line 20 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'console' is not defined

Check failure on line 20 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'console' is not defined
process.exit(1);

Check failure on line 21 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'process' is not defined

Check failure on line 21 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'process' is not defined
}

log(`Cloning cursor-usage-tracker into ${dirName}...`);
run(`git clone --depth 1 ${REPO} "${targetDir}"`);

log("Installing dependencies...");
run("npm install", { cwd: targetDir });

const envExample = resolve(targetDir, ".env.example");
const envFile = resolve(targetDir, ".env");
if (existsSync(envExample) && !existsSync(envFile)) {
copyFileSync(envExample, envFile);
}

log("Done! Next steps:");
console.log(`

Check failure on line 37 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (20)

'console' is not defined

Check failure on line 37 in bin/create.mjs

View workflow job for this annotation

GitHub Actions / test (22)

'console' is not defined
cd ${dirName}

1. Edit .env and add your CURSOR_ADMIN_API_KEY
Get it from: Cursor dashboard → Settings → Advanced → Admin API Keys

2. Start the dashboard:
npm run dev

3. Collect your first data:
npm run collect

4. Open http://localhost:3000

Docs: https://github.com/ofershap/cursor-usage-tracker
`);
Binary file added data/mock.db
Binary file not shown.
Loading
Loading