Plans
3 tiers payants + Free + Custom, naming Claude-like (Pro / Max 5x / Max 20x). Décision du 2026-09-25, qui remplace sur ce point l'ADR 0037 (« boutiques illimitées sur les plans payants ») : chaque palier inclut un…
3 tiers payants + Free + Custom, naming Claude-like (Pro / Max 5x / Max 20x). Décision du 2026-09-25, qui remplace sur ce point l'ADR 0037 (« boutiques illimitées sur les plans payants ») : chaque palier inclut un nombre de boutiques (Free 1, Pro 3, Max 5x 10, Max 20x 25, Custom négocié), les membres restent illimités sur tous les plans payants (1 sur Free), et il n'y a pas d'add-on : l'add-on boutique à $9 reste retiré (il pourra revenir plus tard, derrière l'ADR 0039). Au-delà des boutiques incluses, on passe au palier supérieur ou on demande Custom. « 5x / 20x » restent des noms, pas des comptes de stores. Vendre sur le marketplace exige un plan payant depuis la PR #1522 (acheter reste ouvert à tous). Credits inclus : montant fixe par palier ($49 / $149 / $299), découplé du prix de l'abonnement depuis v4.0 — voir ADR 0012. Daily bonus per-Org sur tous paid plans. Caps msg/5h supprimés.
Vue d'ensemble
| Plan | Prix/Org/mo | Stores | Users | Credits inclus (montant fixe) | Daily bonus | Workflows ‖ | Chat in-platform |
|---|---|---|---|---|---|---|---|
| Free | $0 | 1 | 1 | 0 | — | 0 | ❌ |
| Pro | $79 | 3 inclus | Illimités | $49 | $1/jour/Org | 1 | ✅ |
| Max 5x | $199 | 10 inclus | Illimités | $149 | $3/jour/Org | 5 | ✅ |
| Max 20x | $399 | 25 inclus | Illimités | $299 | $5/jour/Org | 20 | ✅ |
| Custom | Sur devis | Négocié | Illimités | Négocié | Négocié | Illimité | ✅ + SLA + CSM |
Les paliers se distinguent par les boutiques incluses et par l'usage. Boutiques incluses, credits inclus, daily bonus, tours IA simultanés (
PLAN_AI_CONCURRENCY), workflows concurrents (PLAN_USAGE_CAPS, une table par palier sans lien avec les stores), rate-limit MCP, minutes de scan, skills premium (Max 5x+), profondeur Intelligence (FEATURE_MIN_TIER) et détail affilié (Max 5x+). Chaque ligne est lue par le code qui l'applique.
Free — $0/mo
| Item | Valeur |
|---|---|
| Orgs | 1 (par compte user, illimité au-delà via création nouvelle Org) |
| Stores inclus | 1 (PLAN_LIMITS.free.stores) |
| Users | 1 (PLAN_LIMITS.free.seats, appliqué à l'invitation) |
| Credits | 0 (pas de credits offerts) |
| Chat in-platform (@Atlas + spécialistes) | ❌ Bloqué |
| Workflows / cron agents | ❌ Bloqués |
| Daily bonus | — |
| Clé MCP + MCP relay (Claude / ChatGPT / Cursor ou tout client MCP, via OAuth ou clé statique) | ✅ Activé |
| Connecteurs OAuth (Google, Meta, Shopify, Notion) | ✅ Activés |
| Dashboard (stores, audits, settings) | ✅ Activé |
| Marketplace (browse + achat one-shot) | ✅ Activé |
| Vendre sur le marketplace | ❌ Plan payant requis |
| Essai | Aucun |
| Rate-limit MCP | 50/min, 2k/jour anti-DoS + push upgrade |
| Annual discount | n/a |
Rôle (décision du propriétaire, 2026-10-02) : Free = pas d'essai, 0 crédit, pas de chat. Free existe pour créer un compte, obtenir une clé MCP et utiliser le MCP : brancher sa boutique à n'importe quel LLM. Coût AI zéro pour BoostEcom. Il ne vend pas sur le marketplace.
Vérifié dans le code (2026-10-02) : aucune porte payante ne se trouve sur ce chemin. La clé MCP statique (POST /api/integrations/shopify/custom-app/mcp-key) est gatée par le rôle sur la boutique, pas par le plan ; le consentement OAuth (/oauth/authorize, /api/oauth/*) et le serveur MCP (/api/mcp/[storeId]) ne lisent pas le plan, sauf pour le rate-limit (PLAN_MCP_RATE_LIMITS.free : 50/min, 2 000/jour). La boutique elle-même passe par resolveOrgQuota(orgId, "stores"), qui admet 1 boutique sur Free.
Conversion drivers :
- L'user veut le chat in-platform avec @Atlas → upgrade Pro
- L'user veut un 2e store ou un 2e membre → Pro (3 boutiques incluses, membres illimités sur tout plan payant).
- L'user veut vendre sur le marketplace → plan payant requis (PR #1522).
- L'user veut des workflows automatisés → upgrade Pro
- L'user achète sur le marketplace → revenue immédiat sans upgrade
Cible : développeurs, power users qui ont déjà Claude Pro/Max, e-merchants en évaluation, acheteurs marketplace (vendre exige un plan payant).
Pro — $79/mo (per Org)
| Item | Valeur |
|---|---|
| Credits inclus | $49 (montant fixe, découplé du prix depuis v4.0 — ADR 0012) |
| Daily bonus | $1/jour/Org ($30/mo max — jusqu'à $79/mo de credits potentiels : $49 sub + $30 bonus, sans lien avec le prix de l'abonnement bien qu'il tombe sur le même chiffre) |
| Stores | 3 inclus (au-delà : palier supérieur ou Custom, pas d'add-on) |
| Users | Illimités |
| Sessions | Pas de cap msg, pas de cap Opus/jour |
| Modèles disponibles | Tous (Mini / Pro / Max / Max Fast / Studio) |
| Workflows concurrents | 1 (PLAN_USAGE_CAPS.pro) |
| Agents autonomes (cron) | ✅ Activés (consomment credits) |
| Marketplace | Browse + achat + vente (M9+) |
| Tours IA simultanés | 2 (PLAN_AI_CONCURRENCY.pro) |
| MCP rate-limit | 500/min, 50k/jour |
| Annual discount | −20% disponible après J+30 |
| Cost allocation par store/agent/modèle | ✅ Dashboard breakdown |
Cible : marchand Shopify solo, freelance occasionnel, side project sur Shopify.
Max 5x — $199/mo (per Org)
| Item | Valeur |
|---|---|
| Credits inclus | $149 (montant fixe, découplé du prix) |
| Daily bonus | $3/jour/Org ($90/mo max — jusqu'à $239/mo de credits potentiels) |
| Stores | 10 inclus (au-delà : palier supérieur ou Custom, pas d'add-on) |
| Users | Illimités |
| Sessions | Pas de cap |
| Modèles disponibles | Tous + accès prioritaire aux dernières releases |
| Workflows concurrents | 5 (PLAN_USAGE_CAPS.max_5x) |
| Agents autonomes (cron) | ✅ Activés |
| Marketplace | Idem Pro + listings prioritaires côté vendeur |
| Tours IA simultanés | 5 (PLAN_AI_CONCURRENCY.max_5x) |
| MCP rate-limit | 1000/min, 100k/jour |
| Annual discount | −20% après J+30 |
Nouveautés vs Pro :
- Skills premium (
skill-access.ts), couche fournisseurs Intelligence, détail affilié par filleul - Daily bonus 3× ($3/jour vs $1)
- 5 tours IA simultanés (vs 2 sur Pro)
Cible : freelance Shopify multi-clients, e-merchant multi-marque, opérateur en croissance.
Max 20x — $399/mo (per Org)
| Item | Valeur |
|---|---|
| Credits inclus | $299 (montant fixe, découplé du prix) |
| Daily bonus | $5/jour/Org ($150/mo max — jusqu'à $449/mo de credits potentiels) |
| Stores | 25 inclus (au-delà : Custom, pas d'add-on) |
| Users | Illimités |
| Sessions | Pas de cap |
| Modèles disponibles | Tous |
| Workflows concurrents | 20 (PLAN_USAGE_CAPS.max_20x) |
| Agents autonomes (cron) | ✅ Activés (rate-limit augmenté) |
| Marketplace | Idem Pro |
| Tours IA simultanés | 10 (PLAN_AI_CONCURRENCY.max_20x) |
| MCP rate-limit | 5000/min, illimité jour |
| White-label | ❌ Pas inclus dans le plan : add-on enterprise sur devis (billing-enterprise.ts, contact_sales, engagement 24 mois) |
| Bulk operations | ❌ Pas implémenté : ni audit portfolio cross-stores ni déploiement multi-stores n'existent dans le code |
| Support | Email priorité (réponse < 24h ouvré) |
| Annual discount | −20% après J+30 |
Nouveautés vs Max 5x :
- Le plus gros pot de credits et MCP sans plafond journalier
- 10 tours IA simultanés (vs 5 sur Max 5x)
Cible : agence Shopify, opérateur multi-brand de scale, freelance sénior avec 10+ clients.
Custom — sur devis
| Item | Valeur |
|---|---|
| Prix | Négocié (typiquement $599+/mo) |
| Stores | Négocié (illimité possible) |
| Users | Illimités |
| Markup credits | 1.0× (au coût AI Gateway, pas de marge) — et le plancher de marge ne s'y applique pas : voir ci-dessous |
| Custom models | Fine-tuning, modèles privés via AI Gateway |
| SLA | 99.9% |
| Customer Success Manager | Dédié |
| Onboarding | Personnalisé |
| Compliance | SAML SSO, SOC 2, audit logs étendus |
Trigger automatique : un user atteint $1000+/mo de credit purchases pendant 2 mois consécutifs OU demande SSO/SOC 2 → email proposant Custom.
Cible : enterprise, brand DTC scale (10+ M$ ARR), agence Shopify Plus, multi-org corporate.
Annual billing (−20%)
- Disponible uniquement après J+30 (anti-churn rapide à perte)
- Credits délivrés mensuellement sur l'année (pas en bloc) pour éviter burn-out + churn
- Daily bonus inclus, indépendant du cycle annuel
- Stripe coupon dynamique appliqué au switch
| Plan | Mensuel | Annuel (−20%) | Equivalent mensuel |
|---|---|---|---|
| Pro | $79/mo | $750/an | ~ $62.50/mo |
| Max 5x | $199/mo | $1910/an | ~ $159.17/mo |
| Max 20x | $399/mo | $3830/an | ~ $319.17/mo |
Le total annuel est le prix reel, celui que
PLAN_PRICING.yearlyPricefacture :arrondi-au-multiple-de-10-inferieur(mensuel × 12 × 0,8). Sur Pro,79 × 12 × 0,8 = 758,40, arrondi au multiple de 10 inferieur donc $750 (et non $758 : une version anterieure de ce depot avait ecrit $758 dans le catalogue, corrige avant que ce document ne soit republie).L'equivalent mensuel est approximatif et derive, jamais l'inverse. Cette table a longtemps affiche
$39/moet$468/ansous l'ancien catalogue (prix = credits, Pro $49/mo) : le $468 venait de remultiplier par douze un taux mensuel deja arrondi vers le bas, et 39 × 12 = 468 ne retombe pas sur l'annuel reel de l'epoque. Ne pas « corriger » le total annuel pour le faire coller a l'equivalent mensuel — c'est le sens inverse, et c'est l'erreur d'origine (billing/0071).Le repricing v4.0 (ADR 0012) change le prix mensuel et donc le total annuel qui en derive ; il ne change rien au GRANT de credits, delivre mensuellement quel que soit le cycle de facturation (voir ci-dessus).
Les trois totaux annuels sont desormais ancres dans
scripts/check-doc-claims.mjs: ils ne peuvent plus diverger en silence.
La porte des 30 jours, et ce qu'elle mesure
La remise annuelle s'ouvre 30 jours apres la creation de l'ORGANISATION, pas du compte utilisateur. C'est l'organisation qui paie : l'abonnement lui appartient, la facture lui est adressee, et c'est la ligne que Stripe facture. Mesurer sur l'utilisateur laisserait un compte de deux ans creer une organisation neuve et l'engager sur un an au jour zero, ce qui est exactement l'engagement que le delai existe pour laisser murir.
La regle vit dans src/services/billing/billing-cadence.ts, lue par deux
appelants : le handler de checkout, qui refuse en 403 avec un code d'erreur
distinct d'un echec de paiement, et /pricing, qui ne doit pas proposer un
bouton qui sera refuse. Le refus porte la date d'ouverture et rappelle que le
mensuel est disponible tout de suite.
Il n'y a pas de trimestriel. Deux cadences sont vendues : mensuel et
annuel. Le trimestriel etait un onglet soon sans prix dans PLAN_PRICING
ni price Stripe ; le proprietaire l'a supprime le 2026-10-02
(billing/0373), avec priceIdQuarterly, ses trois variables
NEXT_PUBLIC_STRIPE_*_QUARTERLY et QUARTERLY_DISCOUNT.
Si la date de creation est illisible, le checkout passe. C'est le meme repli que partout ailleurs dans la facturation : le delai protege d'un achat impulsif, pas d'un attaquant, et rien ne s'obtient en se forgeant une organisation plus vieille que celle qu'on possede deja.
Pas de trial sur les paid plans
Le Free tier n'est pas un essai : un compte, une clé MCP, le MCP relay et 1 boutique, sans crédit ni chat. L'user branche sa boutique sur son propre LLM aussi longtemps qu'il veut, sans coûter $0.01 d'IA à BoostEcom.
Vérifié industrie :
- v0 : pas de trial, juste Free $5 credits
- Cursor : 14j trial mais churn élevé
- Linear : free tier seulement
- Claude : free tier (limited messages) seulement
Re-évaluation si conversion Free → Pro < 2% à M3 OU data utilisateurs sortie indique besoin de tester avant de payer.
Stores additionnels (retiré)
Retiré par l'ADR 0037
le 2026-09-25, et reste retiré après la décision de gouvernance du même jour
qui rétablit des boutiques incluses par palier (Free 1, Pro 3, Max 5x 10,
Max 20x 25, Custom négocié) : au-delà du compte inclus, on passe au palier
supérieur ou on demande Custom. Il n'y a rien à vendre à l'unité. L'add-on
pourra revenir plus tard, derrière l'ADR 0039 ; d'ici là, aucune ligne
Stripe ne le porte. Le module
services/billing/extra-stores.ts, le drapeau acceptExtraStoreCharge, la
constante ADDITIONAL_STORE_PRICE_MONTHLY et la variable
STRIPE_EXTRA_STORE_PRICE sont supprimés, et billing-every-door.test.ts
refuse qu'un fichier source les nomme à nouveau.
Étape opérateur. Aucun code n'appelle plus Stripe pour cette ligne, dans
un sens ni dans l'autre. Un abonnement qui porte encore un item au prix
extra-store (le Price ID noté dans docs/ops/stripe-account-setup.md) continue
d'être facturé tant qu'un opérateur ne le retire pas : lister ces items dans
le dashboard Stripe (Produits → le prix → Abonnements), puis retirer l'item
avec ou sans prorata, au choix. Le reste de l'abonnement ne
bouge pas (founding-price-lock.test.ts).
États d'abonnement : ce que Stripe dit, ce que le produit fait
Stripe a neuf statuts, le modèle local en avait quatre. Les cinq autres
étaient repliés sur past_due à trois endroits différents. Le repli allait
dans le bon sens (aucun n'accorde l'accès) mais rendait quatre situations
indistinguables, alors qu'une seule se résout en cliquant un lien.
Source de vérité : src/services/billing/subscription-status.ts.
| Statut | Accès | Stripe relance ? | Ce qui le résout | Ce qu'on dit |
|---|---|---|---|---|
active | oui | oui | rien | rien à signaler |
trialing | oui | oui | rien | rien à signaler |
incomplete | non | oui | authentification 3DS | « votre banque demande une confirmation », avec le lien |
incomplete_expired | non | non | nouveau checkout | l'abonnement n'a jamais démarré |
past_due | non | oui | nouveau moyen de paiement | « paiement refusé, mettez à jour votre carte » |
unpaid | non | non | régler le solde | les relances sont terminées, ne pas promettre un nouvel essai |
paused | non | non | rien | mis en pause volontairement, aucun incident |
canceled | non | non | nouveau checkout | résilié |
L'accès ne s'élargit jamais : ENTITLED_STATUSES reste active +
trialing, et un statut inconnu tombe sur past_due, jamais sur active.
subscription-status.test.ts épingle les deux propriétés.
Capacite IA : ce que les plans achetent vraiment
La matrice vendait « AI Gateway : Standard / Priority / Dedicated », et
config/plans.ts promettait une « Priority queue on AI Gateway ». Aucun
mecanisme de file ou de priorite n'existait : une recherche sur priority
dans l'orchestrateur ne renvoyait que l'ordre des outils dans un prompt
(billing/0120).
Ca ne pouvait pas non plus etre construit honnetement. Le fournisseur ordonnance son propre travail : une file de notre cote ne fait rien tant que nous ne sommes pas le goulot, et nous ne le sommes pas. Vendre une vitesse qu'on ne controle pas est precisement ce que cet audit existe pour arreter.
Ce que les plans achetent est donc de la capacite : combien de tours IA une organisation peut mener de front.
| Plan | Tours simultanes |
|---|---|
| Free | 1 |
| Pro | 2 |
| Max 5x | 5 |
| Max 20x | 10 |
| Custom | illimite |
Free reste a 1 et non a 0 : le palier gratuit est le haut de l'entonnoir, et « gouter @Atlas avant de payer » doit continuer de marcher.
Le compteur est un hash Redis turnId -> startedAt avec TTL, pas un
INCR/DECR. Un compteur fuit des qu'un tour meurt entre les deux (lambda
qui tombe, connexion coupee, onglet ferme) et l'organisation reste plafonnee
pour toujours, sans rien a inspecter. C'est exactement le defaut qu'il a
fallu reparer sur api/workflow/[id]/run. Ici, une entree plus vieille qu'un
tour plausible n'est pas comptee : un creneau perdu se repare tout seul.
Le plafond echoue ouvert : Redis injoignable, plan illisible, tout passe. C'est un dispositif d'equite, pas une frontiere de securite.
Skills library : Standard et Premium
/pricing vend une bibliotheque Standard sur Pro et Premium sur Max. Jusqu'en
aout 2026 aucune skill n'etait gatee : la meme bibliotheque se chargeait pour
tout le monde, Free compris (billing/0121).
Le critere retenu : est Premium ce qui coute cher a executer ou donne un avantage disproportionne ; est Standard ce qui sert a operer sa propre boutique.
| Skill | Bibliotheque | Pourquoi |
|---|---|---|
creative-ads | Premium | Generation creative de masse |
seo-audit | Premium | Intelligence concurrentielle sur le site d'un tiers |
tracking-audit | Premium | Idem |
create-shopify-brand | Premium | Construit une marque entiere depuis rien |
ship-shopify-store | Premium | Construit une boutique entiere depuis rien |
general | Standard | Le repli du routeur |
store-setup | Standard | Operer sa boutique |
theme-editor | Standard | Operer sa boutique |
pdp-conversion | Standard | Operer sa boutique |
product-catalog | Standard | Operer sa boutique |
Premium est inclus a partir de Max 5x. Le niveau vit dans le frontmatter
de chaque skill.md (tier: premium) ; l'absence vaut Standard, pour
qu'ajouter une skill ne la retire jamais silencieusement a quelqu'un.
Le refus est dit, pas subi. Une skill Premium demandee sur un plan qui ne l'inclut pas n'est pas seulement retiree de la selection : le prompt porte une note qui demande a @Atlas de repondre avec ce qu'il peut et de dire clairement que cette capacite demande un plan superieur. Une skill qui disparait en silence fait passer le produit pour faible au lieu de faire passer le plan pour petit.
Le gate echoue ouvert. Un plan illisible autorise tout : c'est une lecture de plus sur un chemin chaud, et la regle de facturation de ce depot est qu'une verification qui ne peut pas s'evaluer ne coupe pas un client payant.
Récap pricing
| Plan | Prix | Stores | Users | Credits | Daily bonus | Workflows ‖ | Markup credits | Annual |
|---|---|---|---|---|---|---|---|---|
| Free | $0 | 1 | 1 | 0 | — | 0 | n/a | n/a |
| Pro | $79 | 3 | ∞ | $49 | $1/jour | 1 | 1.5× | −20% |
| Max 5x | $199 | 10 | ∞ | $149 | $3/jour | 5 | 1.5× | −20% |
| Max 20x | $399 | 25 | ∞ | $299 | $5/jour | 20 | 1.5× | −20% |
| Custom | $599+ | Négocié | ∞ | Négocié | Négocié | ∞ | 1.0× | n/a |
Prix ≠ Credits depuis v4.0 (ADR 0012) : les deux colonnes ne coïncident plus, volontairement. Avant, elles étaient la même colonne redite deux fois.
Plans offerts (comped) : droit d'accès sans revenu
Un plan peut être accordé sans passer par Stripe : compte interne, partenaire, design partner, geste commercial. C'est un droit d'accès, pas un abonnement, et la plateforme traite les deux comme deux axes séparés :
| Axe | Question | Lu par | Ignore |
|---|---|---|---|
| Entitlement | qu'est-ce que cette org a le droit d'utiliser ? | resolveEntitlement() / requirePaidPlan() | la provenance du plan |
| Revenu | combien cette org paie-t-elle ? | isRevenueSubscription() | le plan et le statut |
Conséquence directe : une org en Max 20x offert a exactement les mêmes features qu'une org payante, et pèse $0 dans le MRR. Compter un plan offert comme du revenu invente de l'argent que personne ne facturera, c'est ce que faisaient les deux calculs de MRR avant août 2026.
Comment le MRR est derive
Une seule regle, un seul fichier : summarizeMrr() dans
src/services/billing/mrr.ts. Les deux
surfaces qui affichent du revenu recurrent la lisent — la home admin et le
job kpi-snapshot — precisement pour qu'elles ne puissent plus diverger.
MRR = Σ prix_mensuel_catalogue(plan) sur les abonnements actifs
tels que isRevenueSubscription(sub)
Le detail par plan sort du meme passage que le total, donc
Σ revenu_par_plan == MRR tient par construction et non par discipline.
mrr.test.ts epingle cette identite. C'est la correction d'un defaut reel :
la home admin excluait bien les plans offerts de son MRR, et affichait
juste en dessous des tuiles « Plan mix » calculees en
prix_catalogue × effectif, plans offerts compris. Deux nombres, la meme
page, les memes lignes, qui se contredisaient.
La cadence : la regle existe, la donnee manque
Un Pro annuel paie $750/an, soit $62.50 de revenu mensuel recurrent, pas $79. Le compter au prix mensuel surestime de ~26 % par client annuel, dans le sens qui flatte — c'est la raison de la mention « excluding annual prepay » sur la tuile ARR.
La conversion est ecrite une fois, dans monthlyEquivalentUsd()
(src/services/billing/mrr.ts) :
monthly -> PLAN_PRICING[plan].monthlyPrice
yearly -> PLAN_PRICING[plan].yearlyPrice / 12
custom -> null (sales-led, le montant vit dans Stripe)
null n'est pas zero. Zero dit « cette ligne ne rapporte rien » ; null
dit « cette ligne rapporte quelque chose que personne ici ne sait nommer ».
Les lignes null sont comptees dans byPlan.count et remontees dans
MrrSummary.unpricedSubs, pour qu'une surface puisse ecrire « MRR hors N
contrats sales-led » au lieu d'arrondir silencieusement a zero — ce qui est
exactement ce qui a rendu custom invisible pendant un an.
Ce qui manque est la donnee, pas la regle : Subscription ne porte
aucune colonne de cadence, donc chaque ligne arrive sans, retombe sur
"monthly", et l'ecart de 25 % est toujours la. La colonne, son
remplissage depuis le price id du webhook (cadenceForPriceId(), aucun
nouveau champ Stripe requis) et le branchement des deux appelants sont
backlog/data-platform/0529.
Deux nombres a ne pas confondre en lisant ce paragraphe : un abonne annuel n'est pas douze fois un abonne mensuel dans le MRR, il vaut un peu moins qu'un mensuel (la remise annuelle est de -20 %). Le MRR est normalise par definition ; c'est l'encaissement qui est annuel, pas le revenu reconnu.
custom a zero n'est pas un oubli
Son prix de liste est null par choix (sales-led, cf. listedPricing()),
et PLAN_PRICING.custom ne porte que des zeros qui signifient « rien a
accorder », pas « gratuit ». Lire ces zeros comme du revenu serait un defaut
plus grave que le trou. Le montant reel vit dans Stripe, et rien dans ce
depot ne peut l'interroger depuis la home admin.
La sortie n'est donc pas « inventer un prix custom » mais compter ces lignes
comme non chiffrees (unpricedSubs) et lire Stripe le jour ou le volume le
justifie.
Deux cadences, et seulement deux
Le catalogue cablait aussi priceIdQuarterly depuis l'environnement, sans
aucun montant trimestriel dans PLAN_PRICING : un abonnement trimestriel
aurait ete entitled et chiffrable nulle part. Le proprietaire a tranche le
2026-10-02 (billing/0373) : le trimestriel est supprime, et avec lui les
price ids, les variables d'environnement, la remise et l'onglet soon.
src/test/cadence-ladder.test.ts refuse qu'il revienne dans le catalogue
sans prix.
Suivi historique dans
backlog/_archive/billing/0074.
Comment la repartition par palier est derivee
Meme forme que le MRR, et pour la meme raison : une seule regle,
summarizePlanDistribution() dans
src/services/billing/plan-distribution.ts.
Les deux crons qui comptent les orgs par palier la lisent —
calculate-kpis et kpi-snapshot.
palier(org) = entitlementFromSubscription(sub).unlocked ? plan : "free"
payant = isRevenueSubscription(sub) // le meme predicat que le MRR
total = Σ paliers == prisma.organization.count()
Trois consequences a retenir :
- Le palier est celui dont l'org dispose, pas celui que la ligne
nomme. Un grant perime et un abonnement resilie tombent en
free: ils portent encoremax_20x, ils n'y donnent plus droit. - Une org sans ligne
Subscriptionreste comptee, enfree. Sans ca la repartition serait plus petite que la plateforme. grantedPaiddit ce que la colonne ne disait pas : le nombre d'orgs sur un palier payant que personne ne paie. Un Max 20x offert epaissit sa colonne et ne pese rien dans le MRR affiche a cote ; aucun chiffre ne le signalait.
calculate-kpis derivait cette repartition d'un groupBy sur
Organization.plan — le cache dont le commentaire de colonne dit
justement de ne pas decider. Les deux s'accordaient, le cache ayant un
ecrivain unique depuis #632, mais un chiffre derive d'un miroir cesse de
s'accorder des que le miroir retarde.
Subscription.source
| Valeur | Signification | Compte en MRR |
|---|---|---|
stripe | vrai abonnement Stripe (sub_…) | oui — le seul |
internal | free tier auto-créé à l'inscription (free_<orgId>) | non |
comped | plan accordé depuis le back-office admin | non |
partner | accès partenaire / NFR accordé hors back-office | non |
manual | ligne écrite à la main, hors des flux ci-dessus | non |
La colonne est nullable volontairement : une valeur par défaut aurait mal
étiqueté toute une classe de lignes existantes (défaut stripe → MRR gonflé,
défaut autre → MRR effacé). resolveSubscriptionSource() déduit la source du
préfixe de stripeSubscriptionId quand elle est nulle, la convention sur
laquelle le cron reconcile-stripe s'appuyait déjà. Aucun backfill, aucune
ligne historique mal classée.
Un grant porte aussi grantedByUserId, grantReason et grantExpiresAt.
Un comp sans date d'expiration ne se périme jamais tout seul : c'est un choix
explicite de l'admin qui l'accorde, pas un défaut. Une fois la date passée,
resolveEntitlement() renvoie free immédiatement : sans attendre un cron.
Ne jamais accorder un plan en écrivant Organization.plan
Organization.plan est un cache dénormalisé de Subscription.plan,
écrit uniquement par syncOrgPlanCache() dans la transaction qui écrit la
ligne mirroir. Aucun gate ne le lit. Un UPDATE à la main dessus n'accorde
donc rien : l'org paraît upgradée dans deux ou trois listes admin et reste
free partout où ça compte. Pour accorder un plan : le back-office admin
(applySubscriptionPlan), qui écrit la Subscription, stampe la source et
laisse une trace dans l'AdminAuditLog.
Code de référence
- Définition des plans : src/types/billing-plans.ts (
PLAN_LIMITS,PLAN_PRICING) - Config plans UI : src/config/plans.ts
- Entitlement (règle pure) : src/services/billing/entitlement.ts (
entitlementFromSubscription,isRevenueSubscription) - MRR (règle partagée) : src/services/billing/mrr.ts (
summarizeMrr) - Répartition par palier (règle partagée) : src/services/billing/plan-distribution.ts (
summarizePlanDistribution) - Entitlement (accès DB) : src/services/billing/entitlement-service.ts (
resolveEntitlement,syncOrgPlanCache) - Gate paid plan : src/lib/security/billing-gate.ts (
requirePaidPlan) - Stripe Checkout : src/app/api/credits/purchase/route.ts
- Daily bonus cron : src/app/api/cron/daily-bonus/route.ts (calibration par plan à ajouter)
- Cost allocation par store/agent : à enrichir Phase 1 (
agentIdà ajouter dansCredit.metadata)
Modifications futures
Toute évolution de pricing, paliers ou plans doit :
- Être actée dans une PR
- Bumper la version dans README.md
- Définir une date d'effet explicite (typiquement next billing cycle)
- Grandfather les users existants si downgrade de valeur (sinon = casus belli)
- Préserver les rétroactivités positives (si on offre +10% credits, OK pour tous)
Le plancher de marge ne s'applique pas au palier custom
ADMIN_PRICING_CONFIG.minimumMargin (1 $ par million de tokens) protege les
paliers majores : sur un modele bon marche, 1.5x d'un petit nombre est
trop petit pour porter l'infrastructure autour de l'appel.
Il ne s'applique pas a un palier vendu au cout. calculateUserPrice le
saute des que le markup vaut 1.0 ou moins — la regle est donc portee par le
markup et non par le nom du plan : un palier qui n'a pas l'intention de
prendre de marge n'en recoit pas de plancher.
Jusqu'en aout 2026 le plancher s'appliquait a tout le monde, donc le contrat
custom n'etait jamais au cout : $10 de gros etaient factures $11 (1,10x), et
l'ecart grandissait quand le modele etait bon marche — $0,50 de gros
factures $1,50, soit 3x le cout. Le tableau ci-dessus disait deja « au
cout, pas de marge » : c'est le code qui a ete corrige pour rejoindre la
documentation, pas l'inverse (billing/0097).
Un propriétaire, N orgs payantes : gardé, et voici pourquoi
Le daily bonus est per-Org, et la porte d'activité du cron regarde
l'activité du propriétaire, pas celle de chaque Org. Un seul opérateur
actif déclenche donc le bonus de toutes ses orgs payantes. C'est écrit
dans le schéma depuis la migration du 2026-05-12
(DailyBonus @@unique([orgId, day]), ex-(userId, day)) et c'était jusqu'ici
la seule des « conséquences assumées » du modèle à n'avoir jamais été
tranchée par écrit. backlog/billing/0361 la nommait comme « le vecteur de
farming le plus direct ».
Décision : on garde, sans plafond et sans comptage par propriétaire.
La raison est arithmétique, pas philosophique. Chaque org supplémentaire est facturée plein tarif, et le bonus qu'un plan débloque vaut strictement moins que le plan :
| Plan | Prix/mois | Bonus max/mois (31 j) | Solde pour l'acheteur |
|---|---|---|---|
| Pro | $79 | $31 | −$48 |
| Max 5x | $199 | $93 | −$106 |
| Max 20x | $399 | $155 | −$244 |
Empiler N orgs pour récolter N bonus coûte donc plus cher que ce que ça rapporte, quel que soit N : ce n'est pas une attaque, c'est une remise consentie au client multi-org — exactement la population (agence, holding, opérateur de plusieurs marques) que ces paliers visent. Plafonner reviendrait à facturer un client N fois et à ne le servir qu'une.
Le repricing v4.0 (ADR 0012) élargit cette marge de sécurité sans y toucher directement : le bonus max (fonction du daily bonus, inchangé) reste identique, seul le prix a augmenté. Le solde négatif — la garantie anti-farming — passe de −$18 à −$48 sur Pro, un coussin ~2.7× plus large pour le même mécanisme.
Deux garde-fous rendent la décision révocable plutôt que définitive :
- L'invariant
prix du plan > bonus × 31est dérivé et testé (src/test/daily-bonus-window.test.ts). Le jour où un changement de catalogue le casserait, le farming deviendrait profitable et la suite échoue avant le merge — c'est la condition qui rend ce paragraphe vrai, pas une opinion sur les clients. - Le seul chemin qui casse l'arithmétique est de ne pas garder l'argent
de l'abonnement. Il est fermé depuis le 2026-09-06 : un chargeback dans
les 30 jours coupe le bonus de l'org, cf. la section Anti-abuse de
credits.md.