import type { Plan } from '@bmm/llm'; import { getRedis } from './redis.js'; const DAY_SEC = 24 * 60 * 60; function todayKey(): string { return new Date().toISOString().slice(0, 10); } export interface RateLimitResult { ok: boolean; remaining: number; resetIn: number; } /** * Daily counter via Redis INCR. Atomic — no race window between read & write. * First INCR (count === 1) sets the TTL so the key auto-rolls at midnight UTC. */ export async function checkDailyLimit( scope: string, userId: string, max: number, ): Promise { const key = `ratelimit:${scope}:${userId}:${todayKey()}`; const redis = getRedis(); const count = await redis.incr(key); if (count === 1) await redis.expire(key, DAY_SEC); const ttl = await redis.ttl(key); return { ok: count <= max, remaining: Math.max(0, max - count), resetIn: ttl > 0 ? ttl : DAY_SEC, }; } // Per-tier daily limits on the two LLM-priced actions. // Preview = ~€0.002-0.115/call (model-dependent) · Build = ~€0.005-0.22/call. // // Caps are set so that even a max-usage power-user stays profitable at the // tier's price point. Critical for Team/Enterprise where Sonnet/Opus tokens // add up fast — a runaway Bot with a Team subscription could otherwise // out-cost the €199 monthly revenue. Math (max-case): // Pro: 40 prev × €0.020 × 30 = €24/mo → margin €25 (~50%) // Team: 50 prev × €0.058 × 30 = €87/mo → margin €112 (~56%) // Enterprise: 200 prev × €0.060 × 30 = €360/mo → margin €639 (~64%) // Build caps are looser because the 24h cache TTL means most builds are // cache-HITS (no LLM call) — the cap is mostly about runner-port / hosting // budget, not token cost. export const PREVIEW_DAILY_LIMIT: Record = { hobby: 5, pro: 40, team: 50, enterprise: 200, }; export const BUILD_DAILY_LIMIT: Record = { hobby: 3, pro: 20, team: 30, enterprise: 100, };