{"record":{"id":"8c660005bab58df9","repo":"HeyPuter/puter","slug":"insufficient-funds-8c6600","errorCode":"insufficient_funds","errorMessage":"Insufficient funds","messagePattern":"Insufficient funds","errorType":"http","errorClass":"HttpError","httpStatus":402,"severity":"error","filePath":"src/backend/drivers/ai-tts/providers/awsPolly/AWSPollyTTSProvider.ts","lineNumber":264,"sourceCode":"\n        if (typeof text !== 'string' || text.trim() === '') {\n            throw new HttpError(400, 'Missing required field: text', {\n                legacyCode: 'field_required',\n                fields: { key: 'text' },\n            });\n        }\n\n        const actor = Context.get('actor')!;\n        const usageType = `aws-polly:${engine}:character`;\n        const ucentsPerChar = AWS_POLLY_COSTS[engine] ?? 0;\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        // Resolve voice\n        let voice = voiceArg ?? undefined;\n\n        if (!voice && language) {\n            voice =\n                (await this.getLanguageAppropriateVoice(language, engine)) ??\n                undefined;\n        }\n\n        if (!voice) {\n            voice = await this.getDefaultVoiceForEngine(engine);\n        }\n\n        const client = this.getClient();","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-tts/providers/awsPolly/AWSPollyTTSProvider.ts#L246-L282","documentation":"Credit gate in AWSPollyTTSProvider.synthesize. After resolving the engine it computes totalCost = ucentsPerChar (from AWS_POLLY_COSTS[engine]) times text.length and calls meteringService.hasEnoughCredits(actor, totalCost). If false, it throws HTTP 402 (legacyCode insufficient_funds). Usage is metered per character after synthesis.","triggerScenarios":"An actor whose balance is below the per-character cost of the requested text for the chosen engine. Neural/generative/long-form engines cost more per character than standard, so the same text can pass on 'standard' but fail on 'generative'.","commonSituations":"Long text on a premium engine exhausting a small balance; free-tier users hitting neural voice limits; switching from standard to generative without budget awareness.","solutions":["Top up the actor/workspace balance.","Shorten the text or split it into smaller chunks.","Use a cheaper engine ('standard') where acceptable.","Pre-check the balance and estimate cost (chars * AWS_POLLY_COSTS[engine]) before submitting."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Estimate Polly cost before calling. Mirror AWS_POLLY_COSTS (microcents/char).\nconst AWS_POLLY_COSTS = { standard: 100, neural: 200, 'long-form': 100, generative: 300 };\nfunction estimateUcents(text, engine) {\n  return (AWS_POLLY_COSTS[engine] ?? 0) * text.length;\n}\n// Compare estimateUcents(text, engine) to the user's balance before submitting.","typeGuard":null,"tryCatchPattern":"try {\n  await driver.synthesize({ text, provider: 'aws-polly', engine });\n} catch (e) {\n  if (e?.status === 402 || e?.fields?.legacyCode === 'insufficient_funds') {\n    // prompt top-up; consider retrying on 'standard' engine or shorter text\n  } else throw e;\n}","preventionTips":["Show estimated cost (chars * rate) in the UI before the user submits.","Prefer 'standard' for drafts/long text; reserve 'generative'/'neural' for final output.","Keep the user's balance visible so the 402 is never a surprise."],"tags":["aws-polly","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"}