buildmymcpserver/apps/api/src/lib/rate-limit.ts

63 lines
1.9 KiB
TypeScript
Raw Normal View History

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<RateLimitResult> {
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<Plan, number> = {
hobby: 5,
pro: 40,
team: 50,
enterprise: 200,
};
export const BUILD_DAILY_LIMIT: Record<Plan, number> = {
hobby: 3,
pro: 20,
team: 30,
enterprise: 100,
};