{"record":{"id":"7ea44fce826e41e2","repo":"HeyPuter/puter","slug":"insufficient-funds-7ea44f","errorCode":"insufficient_funds","errorMessage":"Insufficient funds","messagePattern":"Insufficient funds","errorType":"http","errorClass":"HttpError","httpStatus":402,"severity":"error","filePath":"src/backend/drivers/ai-tts/providers/elevenlabs/ElevenLabsTTSProvider.ts","lineNumber":248,"sourceCode":"                    fields: { key: 'model', expected, got: modelId },\n                },\n            );\n        }\n\n        const desiredFormat =\n            output_format || response_format || DEFAULT_OUTPUT_FORMAT;\n\n        const actor = Context.get('actor')!;\n        const usageKey = `elevenlabs:${modelId}:character`;\n        const ucentsPerChar = ELEVENLABS_TTS_COSTS[modelId];\n        const totalCost = ucentsPerChar * text.length;\n\n        const usageAllowed = await this.meteringService.hasEnoughCredits(\n            actor,\n            totalCost,\n        );\n        if (!usageAllowed) {\n            throw new HttpError(402, 'Insufficient funds', {\n                legacyCode: 'insufficient_funds',\n            });\n        }\n\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const payload: any = {\n            text,\n            model_id: modelId,\n            output_format: desiredFormat,\n        };\n\n        const finalVoiceSettings = voice_settings ?? voiceSettings;\n        if (finalVoiceSettings) {\n            payload.voice_settings = finalVoiceSettings;\n        }\n\n        const response = await this.request(`/v1/text-to-speech/${voiceId}`, {\n            method: 'POST',","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-tts/providers/elevenlabs/ElevenLabsTTSProvider.ts#L230-L266","documentation":"Credit gate in ElevenLabsTTSProvider.synthesize. totalCost = ELEVENLABS_TTS_COSTS[modelId] * text.length; if meteringService.hasEnoughCredits(actor, totalCost) is false, it throws HTTP 402 (legacyCode insufficient_funds). Billing is per character, and different models carry different per-character rates.","triggerScenarios":"An actor whose balance is below the per-character cost of text on the chosen model. Premium models (e.g. eleven_v3) cost more per character, so the same text can pass on eleven_flash_v2_5 but fail on a pricier model.","commonSituations":"Long-form narration on a premium model exhausting a small balance; free-tier users hitting limits; switching to a costlier model without budget awareness.","solutions":["Top up the actor/workspace balance.","Shorten the text or batch it.","Switch to a cheaper model (e.g. eleven_flash_v2_5).","Pre-estimate cost as chars * ELEVENLABS_TTS_COSTS[model] before submitting."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Estimate ElevenLabs cost (per-character) before calling.\nconst ELEVENLABS_TTS_COSTS = {\n  'eleven_multilingual_v2': 190,\n  'eleven_flash_v2_5': 50,\n  'eleven_turbo_v2_5': 100,\n  'eleven_v3': 300,\n};\nfunction estimateUcents(text, model) {\n  return ELEVENLABS_TTS_COSTS[model] * text.length;\n}\n// Compare to the user's balance; switch to a cheaper model if it won't fit.","typeGuard":null,"tryCatchPattern":"try {\n  await driver.synthesize({ text, provider: 'elevenlabs', model });\n} catch (e) {\n  if (e?.status === 402 || e?.fields?.legacyCode === 'insufficient_funds') {\n    // prompt top-up, or retry on eleven_flash_v2_5 with the same text\n  } else throw e;\n}","preventionTips":["Prefer eleven_flash_v2_5 for long/draft text; reserve pricier models for final output.","Show estimated cost (chars * rate) before submission.","Keep the balance visible so the 402 isn't unexpected."],"tags":["elevenlabs","tts","billing","credits","pre-flight"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}