Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .cursor/rules/project-context.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Single cron endpoint `POST /api/cron` does both: collect → detect → alert in
| `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/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/slack.ts` | Slack bot token + chat.postMessage with block-kit messages |
| `src/lib/alerts/email.ts` | SMTP email 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) |
Expand Down
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
34 changes: 16 additions & 18 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 @@ -69,7 +68,7 @@ Every alert includes who, what model, how much, and a link to their dashboard pa

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

Expand All @@ -91,8 +90,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
- **Email**: HTML-formatted alerts via [Resend](https://resend.com) (one API key, no SMTP config)

### Web Dashboard

Expand Down Expand Up @@ -136,20 +135,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 @@ -258,9 +256,9 @@ 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 |
| 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 |
| Z-score window | 7 days | Historical window for statistical comparison |
| Spike multiplier | 3.0x | Alert when today > N× user's personal average |
Expand Down
94 changes: 73 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@
"@tailwindcss/postcss": "^4.1.18",
"better-sqlite3": "^12.6.2",
"next": "^16.1.6",
"nodemailer": "^8.0.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.7.0",
"resend": "^6.9.2",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
Expand All @@ -85,7 +85,6 @@
"@semantic-release/git": "^10.0.1",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.2.3",
"@types/nodemailer": "^7.0.10",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.2",
Expand Down
4 changes: 3 additions & 1 deletion src/app/api/cron/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export async function POST(request: Request) {

if (detectionResult.newAnomalies.length > 0) {
const pairs = processNewAnomalies(detectionResult.newAnomalies);
const alertResult = await sendAlerts(pairs);
const alertResult = await sendAlerts(pairs, {
dashboardUrl: process.env.DASHBOARD_URL,
});
results.alerts = alertResult;
}
} catch (error) {
Expand Down
17 changes: 14 additions & 3 deletions src/app/settings/settings-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function SettingsClient({ config: initial }: SettingsClientProps) {
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<Section title="Static Thresholds" description="Hard limits that trigger immediate alerts">
<Section title="Static Thresholds" description="Hard limits per user. Set to 0 to disable.">
<Field
label="Max spend / cycle"
value={config.thresholds.maxSpendCentsPerCycle}
Expand All @@ -71,7 +71,12 @@ export function SettingsClient({ config: initial }: SettingsClientProps) {
thresholds: { ...config.thresholds, maxSpendCentsPerCycle: v },
})
}
suffix={`$${(config.thresholds.maxSpendCentsPerCycle / 100).toFixed(0)}`}
suffix={
config.thresholds.maxSpendCentsPerCycle > 0
? `$${(config.thresholds.maxSpendCentsPerCycle / 100).toFixed(0)}`
: undefined
}
hint={config.thresholds.maxSpendCentsPerCycle === 0 ? "disabled" : undefined}
unit="cents"
/>
<Field
Expand All @@ -83,6 +88,7 @@ export function SettingsClient({ config: initial }: SettingsClientProps) {
thresholds: { ...config.thresholds, maxRequestsPerDay: v },
})
}
hint={config.thresholds.maxRequestsPerDay === 0 ? "disabled" : undefined}
unit="reqs"
/>
<Field
Expand All @@ -94,7 +100,12 @@ export function SettingsClient({ config: initial }: SettingsClientProps) {
thresholds: { ...config.thresholds, maxTokensPerDay: v },
})
}
suffix={`${(config.thresholds.maxTokensPerDay / 1_000_000).toFixed(1)}M`}
suffix={
config.thresholds.maxTokensPerDay > 0
? `${(config.thresholds.maxTokensPerDay / 1_000_000).toFixed(1)}M`
: undefined
}
hint={config.thresholds.maxTokensPerDay === 0 ? "disabled" : undefined}
unit="tokens"
/>
</Section>
Expand Down
Loading
Loading