feat(exa): add $10/month free allowance with credit billing for overages#2169
feat(exa): add $10/month free allowance with credit billing for overages#2169
Conversation
Implement per-user monthly Exa usage tracking with a free tier + overage model: - First $10/month is free for all authenticated users - Beyond $10, usage is charged to the user's (or org's) Kilo credit balance - Streaming disabled to guarantee cost tracking via costDollars in JSON responses - Two-table storage strategy: pre-aggregated counter (O(1) lookups) + partitioned audit log
Code Review SummaryStatus: 4 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Other Observations (not in diff)
Files Reviewed (40 files)
Reviewed by gpt-5.4-20260305 · 950,299 tokens |
Add free_allowance_microdollars column to exa_monthly_usage with lock-in semantics: the allowance is set on the first request of the month (INSERT) and not overwritten on subsequent requests (excluded from ON CONFLICT UPDATE). This enables per-user tiered allowances in the future by modifying a single pure function (getExaFreeAllowanceMicrodollars) without changing the billing flow. The route handler now uses the stored allowance instead of the global constant, and the 402 error message reflects the actual allowance.
Add organization_id to exa_monthly_usage so charged amounts are tracked per-context (personal vs org). Both recompute functions now include total_charged_microdollars from exa_monthly_usage in cumulative usage, preventing paid Exa charges from vanishing on balance recomputation. Squashes branch migrations 0077+0078 into a single 0077.
The route now passes readDb as a 3rd argument to getBalanceAndOrgSettings, but two toHaveBeenCalledWith assertions only expected 2 arguments. The failing assertion triggers Jest's pretty-printer on the real Drizzle client (a Proxy-backed object), which hangs indefinitely.
Replace the exa_monthly_usage lump-sum approach with a chronological merge-sort of exa_usage_log records into the usage stream. This gives correct credit-expiration baselines when Exa charges are interleaved with credit grants/expirations. - Stop dropping old exa_usage_log partitions (retain indefinitely) - Register exa-partition-maintenance cron in vercel.json - Promote exa_usage_log insert from fire-and-forget to required - Recompute functions merge-sort exa_usage_log with microdollar_usage
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
markijbema's Assessment (expanded)Bookkeeping & Recoverability Review3)
8) Double-counting risk:
Confirmed bullshit. Comparison: Exa bookkeeping vs. microdollar_usage bookkeeping
Same false alarm as #9 above. 5) The
6) Free allowance is per-user across all contexts, but charges are per-context: The free allowance check aggregates usage across personal AND org rows. So if a user uses $6 personally and $5 through an org, they've used $11 total and the next request is "paid." But the charge goes to whichever context the request is made in. A user could consume their free tier entirely through org requests, then a personal request would be "paid" and charged to their personal balance.
Architecture Review: Custom Exa Billing vs. Reusing Existing Free-Tier InfrastructureThe bot review noted that the custom approach creates a parallel billing pipeline. The personal path does a bare
Plan vs. Implementation Review6) Plan said
Verified: no double-counting. Same root cause as #9 and Comparison #1 — Security Review: Exa Free Tier Abuse VectorsThe review identified 5 abuse vectors: (1) request abort = free usage, (2) no error handling in
Action Items Summary
Update: Items #9 (Bookkeeping), #1 (Comparison), and #7 (Plan vs Impl) were all the same false alarm — |
markijbema
left a comment
There was a problem hiding this comment.
Some inline human comments from me, and I think the llm analysis found some valid points summarized in the last post, but I don't think any of them are really blocking
…equest leak If exa_usage_log insert fails (e.g. missing partition), the counter was already incremented but deductFromBalance never ran — giving the user a free paid request with no log row for recompute to recover from. Reordering so the log insert happens first ensures a failed insert leaves no side effects, and any later failure is recoverable.
… billing Recompute already picks up personal Exa charges from exa_usage_log, so a microdollar_usage row would double-count.
…oss contexts Org usage counts toward the same free tier as personal usage. Once exhausted, the charge goes to whichever context makes the request. This prevents gaming via multiple orgs.
We only clone responses that have |
Summary
Adds billing for the Exa proxy endpoints. Each user gets $10 in free credits per month, and usage is tracked so we can recompute balances. The $10 is configurable, but only on a monthly basis.
Storage strategy
Two tables to avoid the unbounded row growth that
microdollar_usagesuffers from:exa_monthly_usage— pre-aggregated counter (one row per user per month). The pre-request balance check reads this single row (O(1) forever).exa_usage_log— per-request audit trail, range-partitioned by month oncreated_at. Never queried in the hot path.Verification
pnpm typecheck— passespnpm test -- apps/web/src/app/api/exa— 23 tests pass (auth, paths, streaming stripped, allowance, balance check, cost recording)pnpm test -- apps/web/src/lib/exa-usage.test.ts— 8 tests pass (counter reads, upserts, balance deduction)pnpm format+pnpm lint— cleantotal_cost_microdollarsto10000000Also made sure that the
charged_to_balanceprop is correctly set:Reviewer notes
I feel confident that I've tested most of the edge cases for this feature, but I'd love a second pair of eyes on the logic, as it's likely to be costly if we make a mistake with the accounting.