HeyPuter/puter · error · HttpError
insufficient_funds
insufficient_funds
Error message
Insufficient funds
What it means
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.
Source
Thrown at src/backend/drivers/ai-tts/providers/awsPolly/AWSPollyTTSProvider.ts:264
if (typeof text !== 'string' || text.trim() === '') {
throw new HttpError(400, 'Missing required field: text', {
legacyCode: 'field_required',
fields: { key: 'text' },
});
}
const actor = Context.get('actor')!;
const usageType = `aws-polly:${engine}:character`;
const ucentsPerChar = AWS_POLLY_COSTS[engine] ?? 0;
const totalCost = ucentsPerChar * text.length;
const usageAllowed = await this.meteringService.hasEnoughCredits(
actor,
totalCost,
);
if (!usageAllowed) {
throw new HttpError(402, 'Insufficient funds', {
legacyCode: 'insufficient_funds',
});
}
// Resolve voice
let voice = voiceArg ?? undefined;
if (!voice && language) {
voice =
(await this.getLanguageAppropriateVoice(language, engine)) ??
undefined;
}
if (!voice) {
voice = await this.getDefaultVoiceForEngine(engine);
}
const client = this.getClient();View on GitHub (pinned to 908ec23eda)
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.
Defensive patterns
Strategy: try-catch
Validate before calling
// Estimate Polly cost before calling. Mirror AWS_POLLY_COSTS (microcents/char).
const AWS_POLLY_COSTS = { standard: 100, neural: 200, 'long-form': 100, generative: 300 };
function estimateUcents(text, engine) {
return (AWS_POLLY_COSTS[engine] ?? 0) * text.length;
}
// Compare estimateUcents(text, engine) to the user's balance before submitting. Try / catch
try {
await driver.synthesize({ text, provider: 'aws-polly', engine });
} catch (e) {
if (e?.status === 402 || e?.fields?.legacyCode === 'insufficient_funds') {
// prompt top-up; consider retrying on 'standard' engine or shorter text
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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'.
Common situations: 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.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/8c660005bab58df9.
Report an issue: GitHub.