ADR-0010 — Un seul markup, porte par le plan : l'origine d'un credit ne change pas son taux
Le catalogue v3.0 declare le markup applique au cout brut de l'AI Gateway dans ADMIN_PRICING_CONFIG.markupByPlan (src/types/billing-plans.ts) : 1.5x sur Pro / Max 5x / Max 20x, 1.0x sur Custom, 2.0x en repli quand aucun…
Statut
Accepté · 2026-09-06
Piliers : billing
Contexte
Le catalogue v3.0 declare le markup applique au cout brut de l'AI Gateway
dans ADMIN_PRICING_CONFIG.markupByPlan
(src/types/billing-plans.ts) : 1.5x sur
Pro / Max 5x / Max 20x, 1.0x sur Custom, 2.0x en repli quand aucun plan
n'est resolu.
A cote vivait une SECONDE declaration, CREDIT_PURCHASE_MARKUP = 2.0,
censee s'appliquer aux credits achetes sur le slider $10-$5000. Elle etait
accompagnee d'un commentaire nommant un « codepath separe »,
calculateUserPriceForPurchase. Etat verifie le 2026-09-06, avant toute
modification :
git grep CREDIT_PURCHASE_MARKUPrendait sa declaration et trois commentaires. Zero lecteur executable.calculateUserPriceForPurchasen'existait nulle part dans le depot.- La seule fonction qui facture un tour,
calculateUserPrice(..., { plan }), resoutresolveMarkup(plan)et ecrit-userPriceau ledger (src/features/ai/orchestrator/runtime/billing.ts). Elle ne recoit aucune information sur l'origine des credits.
Trois surfaces publiaient donc un taux que rien ne pouvait facturer :
billing-credits.ts, docs/business-model/credits.md, et surtout
docs/business-model/financial-model.md, qui calculait sur ce 2.0x une
marge d'achat de ~46-47% et la reportait dans le mix de revenu et la
projection $130k MRR.
Une contrainte structurelle ferme la porte a l'autre bout : le solde est
un scalaire FIFO unique
(src/services/billing/credit-balance.ts,
balance = max(0, N - max(0, D - E))). Un tour de chat ne consomme pas un
lot identifiable, il decremente une somme. Facturer selon l'origine du
credit demanderait une comptabilite par lot qui n'existe pas, et un tour
qui traverse deux origines devrait etre facture a deux taux.
Décision
Le markup est une propriete du plan, jamais de l'origine du credit.
ADMIN_PRICING_CONFIG.markupByPlan est le seul endroit ou un taux est
ecrit. CREDIT_PURCHASE_MARKUP est supprime, et les documents qui en
derivaient une marge sont recalcules sur 1.5x.
Consequence pour le client : aucune. C'est ce qui etait deja facture. Cette ADR aligne les chiffres publies sur le code, elle ne change pas un seul debit.
Alternatives écartées
| Option | Pourquoi non |
|---|---|
Appliquer reellement 2x aux credits achetes (option A de billing/0362) | C'est une hausse de prix : le meme dollar financerait moitie moins d'inference. Une hausse de prix se decide et s'annonce, elle ne se livre pas dans une PR de nettoyage. Techniquement, elle exige en plus une comptabilite par lot que le solde FIFO scalaire ne porte pas. Reste ouvert comme decision commerciale : billing/0411. |
| Vendre le pack a un ratio < 1:1, par exemple 0.75:1 (option C) | Lisible sur le slider et sans changement du moteur de consommation, mais c'est la meme hausse de prix vue de l'autre cote, donc la meme decision commerciale. Egalement billing/0411. |
| Garder la constante « pour plus tard », avec un commentaire disant qu'elle ne sert pas | C'est exactement l'etat qui a produit le defaut : trois fichiers ont recopie le 2.0 et un modele financier a calcule dessus. Une constante non lue finit par etre lue par quelqu'un qui la croit vraie. |
Deplacer le markup dans PLAN_PRICING | PLAN_PRICING est le catalogue commercial (prix, credits inclus) ; le markup est un parametre de cout d'execution, edite dans l'admin pricing. Les fusionner obligerait le client a importer le markup avec les prix. |
Conséquences
- La marge reelle sur un achat de credits est celle du markup de plan
moins Stripe : ~27% sur $10, ~30% au-dela.
financial-model.mdannoncait ~46-47%. Les projections qui en decoulaient sont revues a la baisse dans le meme mouvement : c'est le prix de la correction, et il est comptable, pas technique. - Le levier « marge sur les achats » n'existe plus tant que
billing/0411n'est pas tranche. Le signal qui imposerait de le rouvrir : la part des achats de credits dans le revenu qui depasse la part des abonnements, parce qu'alors la marge consolidee suit la marge d'achat. - Free reste a 2.0x dans
markupByPlan. Ce n'est pas un tarif : c'est le repli quand aucun plan n'est resolu, et un compte free ne consomme pas de credits metres.
Comment c'est appliqué
Trois gardes, chacune verifiee par mutation avant d'etre commitee :
src/test/one-markup-source.test.tsechoue si une seconde constante de markup reapparait danssrc/, et si une fonction de prix accepte l'origine d'un credit comme parametre.src/services/billing/purchase-credit-markup.test.tsfacture le meme tour sur un solde constitue uniquement d'achats puis uniquement de credits mensuels et exige le meme debit, au taux du plan.src/test/financial-model-margins.test.tsrecalcule le tableau de marge definancial-model.mddepuismarkupByPlan: changer le markup sans recalculer le document echoue.