02Produkt5 min

Credits, Guthaben, Ausgabenlimits

In fünf Produkten habe ich die nutzungsbasierte Abrechnung gleich gebaut: Ich fange mit Credits an und lande bei Guthaben in Cent und Limits pro Schlüssel. Ich weiß inzwischen, auf welcher Stufe man anfangen sollte.

Von Salva Castells

Titelbild: Federico Candoni, CC BY-SA 4.0.
In diesem Beitrag

Wenn ein Produkt KI verbraucht, baue ich als Erstes immer Credits. Ein Integer in der Profiltabelle, pro Generierung wird einer abgezogen, fertig. Ich habe das in einer Transkriptions-App gemacht, in einem SaaS zur Bildgenerierung und in dem Boilerplate, mit dem ich Projekte starte. Alle drei Male bin ich am selben Punkt gelandet: ein Guthaben in Cent mit Nachkommastellen und Ausgabenlimits pro API-Schlüssel.

Der Weg ist immer derselbe, also weiß ich inzwischen, auf welcher Stufe man sinnvollerweise anfängt.

Warum Credits am Anfang immer ausreichend wirken

Weil sie es sind, solange die Kosten pro Operation konstant sind. Ein Bild kostet ein Bild. Ein Credit pro Tag gratis, zwanzig mit dem Abo, und der check ist eine Zeile:

if (!profile || profile.subCredits < estimatedCost) {
  return NextResponse.json({ error: "Insufficient credits" }, { status: 402 });
}

Man versteht es auf der Landingpage ohne jede Erklärung. Und als ganze Zahl ist es bequem: keine Rundungen, der Nutzer sieht "dir bleiben noch 12" und fragt nicht weiter.

Das erste Anzeichen, dass das Modell zu kurz greift, ist nicht technisch, sondern eine Preisfrage. In einem Transkriptionsprojekt rechnete ich nach Zeichen ab, und zwei Wochen nach dem Start habe ich den Preis pro Zeichen auf das Sechsfache der Anbieterkosten angehoben, nachdem ich ausgerechnet hatte, wie viele Zeichen ein Satz im Schnitt auf Englisch hat (rund 75) und auf Spanisch (rund 90), und wie viele Sätze pro 2-Dollar-Paket herauskommen. Diese Rechnung machst du nicht am Anfang. Du machst sie, wenn du Rechnungen vom Anbieter hast und siehst, dass die Marge nicht die war, die du dachtest.

Wo es bricht, sobald die echten Kosten ins Spiel kommen

Es bricht in dem Moment, in dem ein Produkt mehr als ein Modell hat. Wenn der Nutzer zwischen einem billigen und einem präzisen Transkriptionsmodell wählen kann, bedeutet ein Credit nichts mehr. Entweder du berechnest das teure wie das billige und verlierst Geld, oder du berechnest alles zum Preis des teuren und das billige ergibt keinen Sinn.

Und es bricht noch einmal bei zwei Credit-Töpfen. Im Bilder-SaaS habe ich kostenlose tägliche Credits zusätzlich zu den Abo-Credits eingeführt, und noch am selben Tag musste ich reparieren, dass sich überhaupt nichts mehr generieren ließ: Die Prüfung schaute weiterhin nur auf subCredits. Der Fix war, die beiden Töpfe vor dem Vergleich zu addieren.

const totalAvailableCredits = (profile?.dailyCredits || 0) + (profile?.subCredits || 0);

Beim Lesen wirkt das offensichtlich. Ist es nicht, wenn der neue Topf in einem Commit dazukommt und die Prüfung in einer anderen Datei wohnt.

Was das Guthaben löst, was die Credits nicht lösten

Das Guthaben ist eine Zahl in Cent, von der die echten Kosten jeder Operation abgezogen werden. Damit lassen sich Modelle, Anbieter und verschiedene Einheiten (Audiominuten und Text-Tokens) auf derselben Rechnung mischen.

Das Detail, mit dem ich nicht gerechnet hatte: ganze Cent reichen nicht. Eine kurze Transkription mit einem billigen Modell kostet den Bruchteil eines Cents, und mit integer rundete jeder Aufruf auf null und das Guthaben sank nie. Ich musste die Spalten auf Fließkomma migrieren:

ALTER TABLE "usage_logs"   ALTER COLUMN "cost_cents"    SET DATA TYPE real;
ALTER TABLE "user_billing" ALTER COLUMN "balance_cents" SET DATA TYPE real;
ALTER TABLE "api_keys"     ALTER COLUMN "spend_limit_cents" SET DATA TYPE real;

Ja, Geld in real. Für ein Prepaid-Guthaben, das nur abgezogen und aufgeladen wird, lohnt sich das für mich: Der Rundungsfehler ist irrelevant im Vergleich dazu, den gesamten Betrag durch Abschneiden zu verlieren. Würde ich nachträglich abrechnen, nähme ich Integer in der kleinsten existierenden Einheit.

Das Guthaben verändert auch das Pricing: von "einem Plan" zu dreien (4,99, 14,99 und 49,99 im Monat, ausgedrückt in Transkriptionsstunden und Tokens) plus einzelne Aufladungen von 5, 10 und 25 Euro. Innen ist alles dasselbe: Cent addieren.

Der Extremfall: drei Abrechnungsmodelle im selben Produkt

In der Transkriptions-App hatte ich irgendwann drei Dinge gleichzeitig, und nicht alle haben überlebt:

  • Guthaben. Was übrig ist. Ist es null oder weniger, wird nichts ausgeliefert.
  • Monatslimit des Nutzers. Ein Feld monthly_limit_cents mit 4,99 als Default, im Konto editierbar.
  • Ausgabenlimit pro API-Schlüssel. spend_limit_cents plus ein Zeitraum (daily, monthly, total), gegen die Summe der Nutzungslogs dieses Schlüssels.

Das Monatslimit des Nutzers habe ich gelöscht. ALTER TABLE "user_billing" DROP COLUMN "monthly_limit_cents", und raus mit dem Selektor, der Server Action und den Übersetzungen in fünf Sprachen. Es brachte nichts: Das Prepaid-Guthaben ist bereits das Monatslimit. Ein Nutzer, der 20 Euro einzahlt, muss nicht zusätzlich sagen, dass er nicht mehr als 20 Euro ausgeben will. Es war ein Bildschirm mehr zum Erklären und eine Bedingung mehr zum Prüfen.

Was bleibt, ist das Limit pro Schlüssel, denn da gibt es einen echten Fall: ein Schlüssel in einem Skript, das dir entgleitet. Und es gibt ein Detail, das ich auf die harte Tour gelernt habe: Man muss den Schlüssel vor dem Vergleich anhand seiner ID aus der Datenbank neu lesen, auch wenn man das Objekt aus der Authentifizierung schon hat. Sonst bedienst du Anfragen gegen ein veraltetes spendLimitCents.

Der Guard muss in alle Endpoints, nicht nur in die, die Geld kosten. Die zum Auflisten von Modellen und Presets haben ihn auch und geben 429 zurück. Wenn jemand bei null ist, soll ihm die ganze API Nein sagen, nicht nur der teure Teil.

Wie ich heute an Tag eins entscheide

Wenn das Produkt genau eine Operation mit festen Kosten hat: Credits. Wenn es mehr als ein Modell gibt oder die Kosten von der Größe der Eingabe abhängen: Guthaben in Cent mit Nachkommastellen ab dem ersten Commit. Später zu migrieren kostet einen Nachmittag Schema und einen Monat E-Mails, in denen du den Nutzern erklärst, dass ihre Credits jetzt Euro sind.

Und noch etwas, unabhängig vom Modell: Der Rabattgutschein im Checkout gehört in eine Konstante und will getestet werden. Ich habe eines Morgens die couponId von "-100 Euro" auf "-100 %" geändert und es am Nachmittag rückgängig gemacht. Zwei Commits, dieselbe Datei, eine Zeile.

Credits sind eine Marketingentscheidung im Kostüm einer technischen Entscheidung. Beim Guthaben ist es umgekehrt.