{"record":{"id":"d680a6269d620d74","repo":"screenpipe/screenpipe","slug":"credits-exhausted-daily-limit-exceeded","errorCode":"credits_exhausted | daily_limit_exceeded","errorMessage":"hosted AI admission rejected","messagePattern":"hosted AI admission rejected","errorType":"http","errorClass":null,"httpStatus":429,"severity":"error","filePath":"packages/ai-gateway/src/index.ts","lineNumber":653,"sourceCode":"\t\t\t\t\tgate: 'per_minute',\n\t\t\t\t\ttier: authResult.tier,\n\t\t\t\t\taccountPlan: authResult.accountPlan,\n\t\t\t\t});\n\t\t\t\treturn rateLimit.response;\n\t\t\t}\n\n\t\t\tconst cloudflareGateway = isHostedChatGatewayEnabled(env);\n\t\t\tlet legacyRescueFallback = false;\n\t\t\t// Legacy mode retains the paid weighted-query admission gate. In\n\t\t\t// Cloudflare mode the provider-cost spend rules are authoritative for\n\t\t\t// this endpoint; Free's separate two-message lease remains above.\n\t\t\tlet usage: Awaited<ReturnType<typeof trackUsage>> | null = null;\n\t\t\tif (!cloudflareGateway) {\n\t\t\t\tconst ipAddress = request.headers.get('cf-connecting-ip') || undefined;\n\t\t\t\tusage = await trackUsage(env, authResult.deviceId, usageTier, authResult.userId, ipAddress, body.model);\n\t\t\t}\n\t\t\tif (usage && !usage.allowed) {\n\t\t\t\tconsole.warn('hosted AI admission rejected', {\n\t\t\t\t\tgate: 'daily_query',\n\t\t\t\t\ttier: authResult.tier,\n\t\t\t\t\taccountPlan: authResult.accountPlan,\n\t\t\t\t});\n\t\t\t\tconst creditsExhausted = (usage.creditsRemaining ?? 0) <= 0;\n\t\t\t\tconst allowanceError = {\n\t\t\t\t\tstatus: 429,\n\t\t\t\t\tcode: creditsExhausted ? 'credits_exhausted' : 'daily_limit_exceeded',\n\t\t\t\t};\n\t\t\t\tconst rescueFallbackBody = resolveBackgroundFallbackBody({\n\t\t\t\t\tenabled: isBackgroundRequest(request) && hasPaidHostedAiPlan(authResult),\n\t\t\t\t\terror: allowanceError,\n\t\t\t\t\tbody,\n\t\t\t\t\tenv,\n\t\t\t\t});\n\t\t\t\tif (rescueFallbackBody) {\n\t\t\t\t\tlegacyRescueFallback = true;\n\t\t\t\t\tbody = rescueFallbackBody;","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/screenpipe/screenpipe/blob/4ebf712990fee17eeaf904dacf749b6e96ac9bf3/packages/ai-gateway/src/index.ts#L635-L671","documentation":"Server-side log line from the gateway worker when the legacy (non-Cloudflare-gateway) daily weighted-query admission gate rejects a request: `trackUsage` returned `allowed: false`. The caller receives a 429 whose error code is `credits_exhausted` when the account has zero credits left, otherwise `daily_limit_exceeded` with used/limit/resets_at and upgrade options. Background/automation requests from paid hosted-AI plans may instead be rescued by downgrading the request body to a fallback model rather than failing.","triggerScenarios":"A device's weighted daily query count (per-model weight times count) reaches `limits.dailyQueries` for its tier, or credits drop to zero; e.g. a free-tier user consuming all free AI queries for the day, or an app with no credits calling after the free allowance is spent. Anonymous requests over the IP daily limit also land here via trackUsage.","commonSituations":"Heavy daily usage of hosted AI on the free tier (limit resets at the next UTC day); pipe/automation loops draining the daily quota early; spoofed/shared device IDs caught by IP-based tracking; a paid subscription lapsing so the account reverts to the free limit.","solutions":["Read the 429 body: `resets_at` tells when the free daily limit resets (UTC); wait for the reset, or check `credits_remaining` to distinguish `credits_exhausted` from `daily_limit_exceeded`.","Buy credits or subscribe (links are included in the 429's `upgrade_options.buy_credits` / `upgrade_options.subscribe`) to raise the effective daily allowance.","Reduce consumption: lower request volume, prefer cheaper/weight-0 models (getModelWeight multiplies usage by model weight), and batch or cache results instead of re-querying.","If this is a background/automation request from a paid hosted-AI plan, the gateway already attempts a fallback-model rescue; ensure the request is marked as background (and the plan qualifies) so it downgrades instead of failing."],"exampleFix":"// before: blind retry\nconst res = await callGateway(body);\n// after: honor daily-limit 429\nconst res = await callGateway(body);\nif (res.status === 429) {\n  const err = (await res.json()).error;\n  if (err === 'credits_exhausted') throw new Error('Buy credits at screenpi.pe/onboarding');\n  const waitMs = new Date(err.resets_at).getTime() - Date.now(); // daily_limit_exceeded: wait for reset\n}","handlingStrategy":"fallback","validationCode":"// before calling, check the quota endpoint / keep local counters\nconst res = await fetch(`${GATEWAY}/usage`);\nconst { used_today, limit_today, credits_remaining } = await res.json();\nif (used_today >= limit_today && credits_remaining <= 0) {\n  throw new Error('daily quota exhausted — upgrade at screenpi.pe/onboarding');\n}","typeGuard":null,"tryCatchPattern":"const res = await callGateway(body);\nif (res.status === 429) {\n  const payload = await res.json();\n  if (payload.error === 'credits_exhausted') {\n    throw new Error('no credits left: ' + payload.upgrade_url);\n  }\n  // daily_limit_exceeded: schedule retry at payload.resets_at\n  setTimeout(() => retry(), new Date(payload.resets_at).getTime() - Date.now());\n}","preventionTips":["Persist one device ID and authenticate so you get accurate quota accounting and higher tiers.","Prefer cheap/weight-0 models and cache results to stretch the daily allowance.","Track daily usage client-side against limit_today and stop before exhausting.","Handle credits_exhausted vs daily_limit_exceeded differently in client code.","Buy credits or subscribe before heavy automation days; use background requests on paid plans to get the fallback-model rescue."],"tags":["http-429","quota","credits","ai-gateway"],"backgroundTag":"daily-limit-exceeded","analyzedSha":"4ebf712990fee17eeaf904dacf749b6e96ac9bf3","analyzedAt":"2026-09-01T23:33:43.065Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}