{"record":{"id":"16cefe267944886e","repo":"HeyPuter/puter","slug":"insufficient-funds-16cefe","errorCode":"insufficient_funds","errorMessage":"Insufficient credits","messagePattern":"Insufficient credits","errorType":"http","errorClass":"HttpError","httpStatus":402,"severity":"error","filePath":"src/backend/drivers/ai-speech2txt/providers/xai/XAISpeechToTextProvider.ts","lineNumber":161,"sourceCode":"            );\n            fileBuffer = loaded.buffer;\n            filename = loaded.filename || 'audio.mp3';\n            mimeType = loaded.mimeType || 'audio/mpeg';\n        }\n\n        // Pre-flight credit check. For URLs we can't know the duration\n        // upfront, so use a conservative 60-second estimate; actual usage\n        // is metered from the API response duration afterwards.\n        const estimatedSeconds = fileBuffer\n            ? Math.max(1, Math.ceil(fileBuffer.byteLength / 16000))\n            : 60;\n        const estimatedCost = UCENTS_PER_SECOND * estimatedSeconds;\n        const allowed = await this.deps.metering.hasEnoughCredits(\n            actor,\n            estimatedCost,\n        );\n        if (!allowed)\n            throw new HttpError(402, 'Insufficient credits', {\n                legacyCode: 'insufficient_funds',\n            });\n\n        // Build multipart form data\n        const formData = new FormData();\n\n        if (args.language) formData.append('language', args.language);\n        if (args.format !== undefined)\n            formData.append('format', String(args.format));\n        if (args.diarize) formData.append('diarize', 'true');\n        if (args.multichannel) formData.append('multichannel', 'true');\n        if (args.channels) formData.append('channels', String(args.channels));\n        if (args.audio_format)\n            formData.append('audio_format', args.audio_format);\n        if (args.sample_rate)\n            formData.append('sample_rate', String(args.sample_rate));\n\n        if (isUrl) {","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-speech2txt/providers/xai/XAISpeechToTextProvider.ts#L143-L179","documentation":"Pre-flight credit gate in the xAI STT provider. Before calling xAI it estimates cost as UCENTS_PER_SECOND (2778 microcents/s) times estimated duration — for uploaded files estimated as max(1, ceil(byteLength/16000)) seconds, for URLs a flat 60s — and rejects with HTTP 402 (legacyCode insufficient_funds) when metering.hasEnoughCredits(actor, estimatedCost) returns false. Actual usage is metered from the response duration afterwards.","triggerScenarios":"An authenticated actor with zero/negative balance calls xAI transcription on either an uploaded audio file or an HTTP URL, and the estimated cost exceeds their remaining credits. Large files push the estimate higher (byteLength/16000).","commonSituations":"New/free users with no balance; a workspace that burned through its grant; uploading a very long recording where the byte-based estimate alone exceeds the balance; URL inputs always assume 60s so even short clips need 60s worth of credit.","solutions":["Top up the user/workspace credit balance and retry.","Trim or compress the audio so the byte-based estimate fits the remaining balance.","For URL inputs, confirm at least 60s-equivalent credits are available since the estimate is fixed regardless of actual duration.","Use test_mode: true to validate the pipeline without consuming credits."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Estimate the xAI cost before calling, using the same heuristic the server uses.\nconst UCENTS_PER_SECOND = 2778;\n// for an uploaded file:\nconst estimatedSeconds = Math.max(1, Math.ceil(fileBytes / 16000));\n// for a URL the server assumes 60s:\n// const estimatedSeconds = 60;\nconst estimatedCostUcents = UCENTS_PER_SECOND * estimatedSeconds;\n// Compare against the user's balance via your billing/balance endpoint before calling.","typeGuard":null,"tryCatchPattern":"try {\n  const t = await driver.transcribe({ provider: 'xai', file });\n} catch (e) {\n  if (e?.status === 402 || e?.fields?.legacyCode === 'insufficient_funds') {\n    // prompt the user to top up; optionally retry after balance refresh\n  } else throw e;\n}","preventionTips":["Surface the user's remaining credits in the UI so they know before submitting.","Warn before uploading very large audio (the estimate scales with byte length).","For URL inputs, remember the server reserves 60s of credit regardless of true duration."],"tags":["billing","credits","xai","speech-to-text","pre-flight"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}