{"record":{"id":"9384f4240dfad8a1","repo":"HeyPuter/puter","slug":"insufficient-funds-9384f4","errorCode":"insufficient_funds","errorMessage":"Insufficient credits","messagePattern":"Insufficient credits","errorType":"http","errorClass":"HttpError","httpStatus":402,"severity":"error","filePath":"src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts","lineNumber":198,"sourceCode":"\n        // Metering: estimate duration from file size if we don't parse metadata.\n        // 16 kbit/s is a safe lower bound for speech audio; pre-check credits\n        // before we hit the ElevenLabs API. Post-usage we increment by the same\n        // estimate — duration parsing is deferred to v2.1 if needed.\n        const estimatedSeconds = Math.max(\n            1,\n            Math.ceil(loaded.buffer.byteLength / 16000),\n        );\n        const usageKey = `elevenlabs:${modelId}:second`;\n        const ucentsPerSecond = VOICE_CHANGER_COSTS[usageKey] ?? 0;\n        const estimatedCost = ucentsPerSecond * estimatedSeconds;\n\n        const hasCredits = await this.services.metering.hasEnoughCredits(\n            actor,\n            estimatedCost,\n        );\n        if (!hasCredits) {\n            throw new HttpError(402, 'Insufficient credits', {\n                legacyCode: 'insufficient_funds',\n            });\n        }\n\n        const formData = new FormData();\n        const blob = new Blob([loaded.buffer as BlobPart], {\n            type: loaded.mimeType ?? 'application/octet-stream',\n        });\n        formData.append('audio', blob, loaded.filename);\n        formData.append('model_id', modelId);\n\n        const settings = args.voice_settings ?? args.voiceSettings;\n        if (settings !== undefined && settings !== null) {\n            formData.append(\n                'voice_settings',\n                typeof settings === 'string'\n                    ? settings\n                    : JSON.stringify(settings),","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts#L180-L216","documentation":"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.","triggerScenarios":"A user with zero or near-zero balance calling convert on a multi-second clip; the byte-based estimate exceeds the remaining credits.","commonSituations":"Free-tier users; large audio files pushing the byte-based duration estimate up; org accounts with depleted budgets.","solutions":["Top up the user's credit balance.","Trim or compress the audio to lower the byte-based duration estimate (and the real cost).","Pre-check the balance and gate the UI; show the estimated cost before submit."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// mirror the server's byte-based estimate\nconst secs = Math.max(1, Math.ceil(audioByteLength / 16000));\nconst UCENTS_PER_SEC = 300000 * 0.9; // eleven_multilingual_sts_v2 / eleven_english_sts_v2\nif (userBalanceUcents < secs * UCENTS_PER_SEC) {\n  throw new Error('insufficient credits — top up');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await driver.convert(args);\n} catch (e) {\n  if (e?.legacyCode === 'insufficient_funds' || e?.status === 402) {\n    showTopUpPrompt();\n  } else throw e;\n}","preventionTips":["Show the estimated cost in the UI before submit.","Check balance before starting long uploads.","Trim/compress audio to lower the byte-based estimate."],"tags":["billing","credits","speech2speech"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}