HeyPuter/puter · error · HttpError

insufficient_funds

insufficient_funds

Error message

Insufficient credits

What it means

Before calling ElevenLabs, the driver estimates cost from raw byte size (Math.ceil(byteLength/16000) seconds at the 16 kbps speech floor) and asks metering.hasEnoughCredits(actor, estimatedCost). If the user cannot cover the estimate it throws 402 / insufficient_funds to avoid a billable upstream call the user cannot pay for. Post-call, metering.incrementUsage charges the same estimate.

Source

Thrown at src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts:198

        // Metering: estimate duration from file size if we don't parse metadata.
        // 16 kbit/s is a safe lower bound for speech audio; pre-check credits
        // before we hit the ElevenLabs API. Post-usage we increment by the same
        // estimate — duration parsing is deferred to v2.1 if needed.
        const estimatedSeconds = Math.max(
            1,
            Math.ceil(loaded.buffer.byteLength / 16000),
        );
        const usageKey = `elevenlabs:${modelId}:second`;
        const ucentsPerSecond = VOICE_CHANGER_COSTS[usageKey] ?? 0;
        const estimatedCost = ucentsPerSecond * estimatedSeconds;

        const hasCredits = await this.services.metering.hasEnoughCredits(
            actor,
            estimatedCost,
        );
        if (!hasCredits) {
            throw new HttpError(402, 'Insufficient credits', {
                legacyCode: 'insufficient_funds',
            });
        }

        const formData = new FormData();
        const blob = new Blob([loaded.buffer as BlobPart], {
            type: loaded.mimeType ?? 'application/octet-stream',
        });
        formData.append('audio', blob, loaded.filename);
        formData.append('model_id', modelId);

        const settings = args.voice_settings ?? args.voiceSettings;
        if (settings !== undefined && settings !== null) {
            formData.append(
                'voice_settings',
                typeof settings === 'string'
                    ? settings
                    : JSON.stringify(settings),

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Top up the user's credit balance.
  2. Trim or compress the audio to lower the byte-based duration estimate (and the real cost).
  3. Pre-check the balance and gate the UI; show the estimated cost before submit.
Defensive patterns

Strategy: validation

Validate before calling

// mirror the server's byte-based estimate
const secs = Math.max(1, Math.ceil(audioByteLength / 16000));
const UCENTS_PER_SEC = 300000 * 0.9; // eleven_multilingual_sts_v2 / eleven_english_sts_v2
if (userBalanceUcents < secs * UCENTS_PER_SEC) {
  throw new Error('insufficient credits — top up');
}

Try / catch

try {
  await driver.convert(args);
} catch (e) {
  if (e?.legacyCode === 'insufficient_funds' || e?.status === 402) {
    showTopUpPrompt();
  } else throw e;
}

Prevention

When it happens

Trigger: A user with zero or near-zero balance calling convert on a multi-second clip; the byte-based estimate exceeds the remaining credits.

Common situations: Free-tier users; large audio files pushing the byte-based duration estimate up; org accounts with depleted budgets.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/9384f4240dfad8a1. Report an issue: GitHub.