Commit 6aa4e3b
authored
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
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
* feat(exa): store per-user free allowance on monthly usage row
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.
* feat(exa): route exa API through global backend and use read replica for reads
* fix(exa): make recomputeBalance account for paid Exa usage
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.
* fix(exa): match readDb arg in getBalanceAndOrgSettings assertions
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.
* style: format unformatted files
* fix(exa): use per-request exa_usage_log for balance recomputation
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
* chore(db): remove branch-local migration 0077 before merging main
* chore(db): regenerate exa migration as 0078 after merging main
* Prevent redirects to global app in other envs
* fix(web): use VERCEL_ENV instead of NODE_ENV for production rewrite check
* refactor(exa): use date-fns format instead of hand-rolled date helpers
* Remove stupid comment
* refactor: simplify mergeSortedByCreatedAt to concat+sort
* fix(exa): insert usage log before upserting counter to prevent free-request 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.
* docs(exa): warn against inserting into microdollar_usage for personal billing
Recompute already picks up personal Exa charges from exa_usage_log,
so a microdollar_usage row would double-count.
* docs(exa): document that free allowance is intentionally per-user across 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.
* Remove the exa plans
* chore(db): remove branch-local migration 0078 before merging main
* chore(db): regenerate exa migration as 0078 after merging main1 parent 5b67fa6 commit 6aa4e3b
File tree
17 files changed
+16777
-97
lines changed- .plans
- apps/web
- src
- app/api
- cron/exa-partition-maintenance
- exa/[...path]
- lib
- packages/db/src
- migrations
- meta
17 files changed
+16777
-97
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
54 | 58 | | |
55 | 59 | | |
56 | 60 | | |
| |||
Lines changed: 59 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
0 commit comments