Audit — charge billing (visiteurs et orgs)
Lecture seule sur le code, plus le trafic et une journee de facture Vercel. Ce document et trois items de backlog sont la sortie. Perimetre : le ledger de credits, les crons daily-bonus et reset-credits, les plafonds…
Lecture seule sur le code, plus le trafic et une journee de facture Vercel. Ce document et trois items de backlog sont la sortie.
Perimetre : le ledger de credits, les crons
daily-bonusetreset-credits, les plafonds qui depensent de l'argent provider (scan public, discovery, spy-full), le rate limit MCP Free, le pool Postgres.Etat du depot :
origin/maina463d5171e, 23 septembre 2026. Trafic : Vercel Web Analytics, projetboostecom.app, production, 24 aout au 23 septembre 2026 (le 23 est une journee partielle, releve vers 02:30 UTC). Facture : team BoostEcom, fenetre 31 aout 07:00 au 1er septembre 07:00 UTC.
Verdict
Le site et environ 1 000 organisations payantes tiennent. Quelques
milliers de scans anonymes, non. Le premier job billing qui rate du
travail est daily-bonus, vers 1 500 a 3 000 orgs payantes actives le
meme jour, et il rate en sautant la queue.
La prod est deux ordres de grandeur en dessous : 4 329 pages vues sur 31 jours, 18 visiteurs au pic d'une journee.
Ce qui a ete verifie
| Sonde | Resultat |
|---|---|
| Double depense d'un meme solde | fermee : pg_advisory_xact_lock + ligne hold (credits-check.ts) |
| Double grant mensuel / bonus | ferme : MonthlyReset(orgId, year, month), DailyBonus(orgId, day) |
| Overdraft au-dela du solde | ferme : refus si l'estime depasse le solde, abort mid-stream |
| Free qui brule le Gateway via le chat | ferme : FREE_TIER refuse avant le solde |
daily-bonus a 300 s | sequentiel, pas de curseur repris au timeout |
reset-credits a 300 s | meme forme, mais seulement les ancres dues ce jour (~1/30) |
| Scan public anonyme | 500/jour, 50 en vol, 600 minutes Browserbase / mois |
| Discovery payant / spy-full | 100 / mois et 120 / mois, globaux |
| MCP Free | 50/min et 2 000/jour par org, rien sur la somme |
Pool pg | max laisse au defaut (10 / instance), attachDatabasePool non cable (data-platform/0528) |
| Webhooks Stripe | claim idempotent StripeEvent. Le volume de quelques milliers d'abos n'est pas le sujet |
Constat 1 — les visiteurs qui lisent ne coutent pas le ledger
Chaque page est rendue a la requete (nonce CSP, ISR inerte). 5 000 visiteurs par jour, c'est une facture Fluid, pas une facture Stripe.
La journee de facture observee est a $6.47, dont $4.79 de Build CPU. Ce n'est pas un run-rate : un build n'a rien a voir avec les visiteurs, et les 67 crons sont deja dans les lignes Fluid ($0.78 de memoire provisionnee, $0.22 de CPU actif, $0.01 d'invocations). On ne multiplie pas cette journee par 30, ni par un ratio de visiteurs.
Constat 2 — les surfaces qui depensent s'arretent a quelques centaines de scans
| Surface | Plafond | Fichier |
|---|---|---|
| Scan public Browserbase | 500/jour, 50 concurrents, 600 min/mois | src/features/tracking/lib/public-scan-budget.ts |
| Sondes payantes d'onboarding | 100/mois, global | src/services/billing/discovery-scan-budget.ts |
| Spy-full | 120/mois, global | src/services/algorithms/intelligence/spy-full-budget.ts |
600 minutes a 110 s de plafond par scan, c'est de l'ordre de 300 scans
complets par mois si chacun va au bout. Les defauts sont le produit.
Les relever est une depense, pas un correctif. Item
billing/2964.
Constat 3 — daily-bonus est le mur des orgs payantes
src/app/api/cron/daily-bonus/route.ts : maxDuration = 300, chunks de
500, puis pour chaque org eligible un await de transaction, un
await d'audit, un await d'invalidation de cache. Pas de curseur
persiste. Au timeout, les ids qui trient en dernier ne recoivent pas
le bonus du jour. @@unique([orgId, day]) empeche le double paiement
au retry, il ne rattrape pas la queue. Vercel ne rejoue pas le cron.
La latence par grant n'a pas ete tracee. A 80-400 ms par org (trois
allers-retours), 300 s couvre 750 a 3 750 grants. A 200 ms et 50 %
d'orgs actives (hypothese login Pro du financial-model.md), le mur
est vers 3 000 orgs payantes. Le jour ou tout le monde a ete actif
dans les 24 h, vers 1 500. Item platform-ops/2960.
reset-credits a la meme forme mais ne voit que l'anniversaire. Il
tient plus loin, y compris le jour ou les ancres 29-31 se rabattent.
Constat 4 — la marge tient, meme si tout le monde brule tout
Prix et grants lus dans PLAN_PRICING (src/types/billing-plans.ts).
Markup 1.5×. Stripe 2.9 % + $0.30. Infra tenue au $0.50/org du modele,
qui n'est pas une mesure.
| Plan | Prix | Marge modele | Marge si 100 % du grant et 100 % du bonus |
|---|---|---|---|
| Pro | $79 | 69 % ($54.58) | 29 % ($23.24) |
| Max 5x | $199 | 71 % ($140.70) | 17 % ($33.10) |
| Max 20x | $399 | 80 % ($319.30) | 22 % ($87.30) |
Mix de planification, pas le ledger reel : 800 Pro, 150 Max 5x, 50
Max 20x. MRR $113 000. Contribution ~$81k au taux du modele,
~$28k si tout est brule. Les credits achetes restent ~30 % au pire
cas (tableau derive de financial-model.md).
Le cap de concurrence IA (1 / 2 / 5 / 10) fail open si Redis ne repond pas. Le quota AI Gateway n'est pas dans le depot.
Constat 5 — le Free ne brule pas le chat, il peut bruler la somme des plafonds MCP
PLAN_MCP_RATE_LIMITS : Free 50/min, 2 000/jour, par org. Mille orgs
Free qui saturent le plafond, c'est 2 millions d'appels MCP par jour,
chacun une fonction et une lecture Store. Item integrations/2962.
Ce que cet audit n'a pas mesure
- connexions Neon en prod
- depense AI Gateway
- nombre de clients Stripe
- latence reelle d'un grant
daily-bonus