Grant extra voice minutes to a specific account

Add a per-account hard-limit override via getVoiceHardLimitSeconds so
allow-listed user IDs get an additional 5 hours on top of the 5-hour
hard cap, threaded through both the voice usage check and status routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kirill Dubovitskiy
2026-06-01 00:24:47 -07:00
parent 812f4e1bc3
commit 450f29e983
@@ -7,8 +7,19 @@ import { log } from "@/utils/log";
const VOICE_FREE_LIMIT_SECONDS = 1200; // 20 minutes free tier per 30 days (~$0.76 cost)
const VOICE_HARD_LIMIT_SECONDS = 18000; // 5 hours absolute cap per 30 days (even with subscription)
const VOICE_MAX_CONVERSATIONS = 100; // Max conversations trackable per 30 days (ElevenLabs page_size limit)
const VOICE_EXTRA_LIMIT_SECONDS = 5 * 60 * 60;
const VOICE_EXTRA_LIMIT_PUBLIC_IDS = new Set([
"cmp66x5u018d9wz0unf56tp07",
]);
const ELEVEN_LABS_API = "https://api.elevenlabs.io/v1/convai";
function getVoiceHardLimitSeconds(userId: string): number {
if (VOICE_EXTRA_LIMIT_PUBLIC_IDS.has(userId)) {
return VOICE_HARD_LIMIT_SECONDS + VOICE_EXTRA_LIMIT_SECONDS;
}
return VOICE_HARD_LIMIT_SECONDS;
}
function deriveElevenUserId(happyUserId: string): string {
const hmac = crypto.createHmac("sha256", process.env.HANDY_MASTER_SECRET!);
hmac.update(happyUserId);
@@ -109,10 +120,11 @@ export function voiceRoutes(app: Fastify) {
}
const elevenUserId = deriveElevenUserId(userId);
const hardLimitSeconds = getVoiceHardLimitSeconds(userId);
// Check usage from ElevenLabs directly
const { usedSeconds, conversationCount } = await getVoiceUsage(elevenLabsApiKey, elevenUserId);
log({ module: 'voice' }, `User ${userId}: ${usedSeconds}s used, ${conversationCount} convos (free=${VOICE_FREE_LIMIT_SECONDS}s, hard=${VOICE_HARD_LIMIT_SECONDS}s)`);
log({ module: 'voice' }, `User ${userId}: ${usedSeconds}s used, ${conversationCount} convos (free=${VOICE_FREE_LIMIT_SECONDS}s, hard=${hardLimitSeconds}s)`);
// Conversation count cap — we can only track 100 per query (ElevenLabs page_size limit)
if (conversationCount >= VOICE_MAX_CONVERSATIONS) {
@@ -120,18 +132,18 @@ export function voiceRoutes(app: Fastify) {
allowed: false as const,
reason: 'voice_conversation_limit_reached' as const,
usedSeconds,
limitSeconds: VOICE_HARD_LIMIT_SECONDS,
limitSeconds: hardLimitSeconds,
agentId,
});
}
// Hard cap — 5 hours, no exceptions
if (usedSeconds >= VOICE_HARD_LIMIT_SECONDS) {
// Hard cap — normally 5 hours, with account-specific credits applied.
if (usedSeconds >= hardLimitSeconds) {
return reply.send({
allowed: false as const,
reason: 'voice_hard_limit_reached' as const,
usedSeconds,
limitSeconds: VOICE_HARD_LIMIT_SECONDS,
limitSeconds: hardLimitSeconds,
agentId,
});
}
@@ -182,7 +194,7 @@ export function voiceRoutes(app: Fastify) {
agentId,
elevenUserId,
usedSeconds,
limitSeconds: usedSeconds >= VOICE_FREE_LIMIT_SECONDS ? VOICE_HARD_LIMIT_SECONDS : VOICE_FREE_LIMIT_SECONDS,
limitSeconds: usedSeconds >= VOICE_FREE_LIMIT_SECONDS ? hardLimitSeconds : VOICE_FREE_LIMIT_SECONDS,
});
} catch (error) {
log({ module: 'voice' }, `ElevenLabs request error for user ${userId}: ${error}`);
@@ -211,6 +223,7 @@ export function voiceRoutes(app: Fastify) {
}
const elevenUserId = deriveElevenUserId(userId);
const hardLimitSeconds = getVoiceHardLimitSeconds(userId);
try {
const [{ usedSeconds, conversationCount }, subscribed] = await Promise.all([
@@ -219,7 +232,7 @@ export function voiceRoutes(app: Fastify) {
]);
return reply.send({
usedSeconds,
limitSeconds: subscribed ? VOICE_HARD_LIMIT_SECONDS : VOICE_FREE_LIMIT_SECONDS,
limitSeconds: subscribed ? hardLimitSeconds : VOICE_FREE_LIMIT_SECONDS,
conversationCount,
conversationLimit: VOICE_MAX_CONVERSATIONS,
elevenUserId,