Jede semantische Suche in einem meiner Projekte kostet Geld: ein Embedding, ein Reranking-Aufruf und eine Textsynthese. Cents pro Anfrage. Multipliziert mit einem Bot in einer Schleife sind das keine Cents mehr. Die erste Version verlangte für nichts ein Konto, denn ein Konto zu verlangen, um Anime zu suchen, wirkte absurd. Es wirkt weiterhin so. Ich mache es trotzdem.
Der Grund ist nicht Marketing und nicht der Newsletter. Es ist, dass es ohne Identität nichts zu zählen gibt.
Was es kostet, Leute unidentifiziert hereinzulassen
Ich habe ein Fake-Door-Validierungs-Harness mit fünf Landingpages gleichzeitig gebaut, um Interesse zu messen, bevor ich irgendetwas baue. Das Einzige, was geschützt werden musste, war das Admin-Panel, also habe ich nach mehreren fehlgeschlagenen Logins eine IP-Sperre eingebaut. Reicht, dachte ich.
Tat es nicht. Die Client-IP kam aus x-forwarded-for, und dieser Header ist eine Liste client, proxy1, proxy2…, in der jeder Proxy rechts anhängt, aber niemand aufräumt, was schon drinsteht. Den ersten Wert zu nehmen heißt, das zu nehmen, was der Client hineinschreiben will. Jeder schickt x-forwarded-for: 1.2.3.4, umgeht seine eigene Sperre und kann nebenbei die IP eines anderen sperren, indem er dessen IP hineinschreibt.
Ich habe es behoben, indem ich Hops zähle, statt der linken Seite zu vertrauen:
const TRUSTED_PROXY_HOPS = Number(process.env.TRUSTED_PROXY_HOPS || 1);
const list = String(xff).split(",").map((s) => s.trim()).filter(Boolean);
const idx = list.length - TRUSTED_PROXY_HOPS;
return list[idx >= 0 ? idx : 0];
Mit einem Proxy davor ist der echte Client der letzte Wert: der, den der Proxy schreibt und den der Client nicht anfassen kann. Funktioniert. Aber schau, was ich gerade getan habe: Ich habe eine Umgebungsvariable eingeführt, die von der Netzwerktopologie des Deployments abhängt, um jemanden näherungsweise identifizieren zu können. Wenn ich morgen ein CDN davorsetze, ändert sich die Zahl und das Sperrsystem irrt sich stillschweigend. Es fällt nicht aus: Es irrt sich.
Warum die IP nicht als Identität taugt
Abgesehen davon, dass sie hinter dem falschen Proxy fälschbar ist, ist die IP aus Gründen, die sich nicht beheben lassen, ein schlechter Schlüssel. Vier Leute aus demselben Büro teilen sich eine. Eine Person am Handy wechselt die IP, wenn sie aus dem Haus geht. Mobilfunknetze machen NAT über riesige Blöcke: Eine Provider-IP zu sperren heißt, ein ganzes Viertel zu sperren.
Was du von einem Identifikator willst, ist, dass er für dieselbe Person stabil und für verschiedene Personen verschieden ist. Die IP erfüllt keines von beiden. Eine user_id erfüllt beides von Natur aus.
Was du nur tun kannst, wenn du weißt, wer wer ist
In der Suchmaschine bin ich bei drei Stufen gelandet, mit einem endlichen Limit auf allen dreien:
visitor: { semanticSearchLimit: 10, historyLimit: 0 }
registered: { semanticSearchLimit: 30, historyLimit: 10 }
premium: { semanticSearchLimit: 200, historyLimit: null }
Der Kommentar, den ich im Typ hinterlassen habe, ist das ganze Kriterium: immer endlich; keine Stufe ist vom Suchkontingent ausgenommen. Premium hat die höchste Zahl, nicht unendlich. historyLimit lässt sehr wohl null für "unbegrenzt" zu. Das Suchlimit nicht, denn jede Suche ist eine Rechnung.
Den Besucher gibt es weiterhin, mit einem Cookie, das ihm einen anonymen Schlüssel gibt. Aber das Erneuerungsfenster ist je nachdem, wer du bist, ein anderes: Der Registrierte erneuert zum Kalendermonat in UTC, der Premium-Nutzer am Jahrestag seiner Anmeldung. Das erfordert, ein premiumAnchorAt zu speichern, und irgendetwas zu speichern erfordert eine Zeile mit Besitzer. Ohne Konto gibt es keinen Jahrestag zum Verankern.
Der Fall von Apps mit Kosten pro Anfrage
Wenn jede Aktion echtes Geld kostet, ist das Kontingent keine Verteidigung mehr, sondern Buchhaltung. Und Buchhaltung geht auf langweilige Arten schief.
In der Suchmaschine wurden zwei identische Anfragen doppelt gezählt. Die Lösung war eine Reservierung mit einem Hash über den Inhalt der Anfrage, nicht über ihren Text: die normalisierte Query, das Limit, das Explicit-Flag und die sortierten Filter. Der Kommentar am Typ sagt, warum die Reihenfolge zählt — dieselbe Menge in anderer Reihenfolge gewählt ist dieselbe Suche — und warum die Filter mit hineingehören: derselbe Text mit einem anderen Genre ist eine andere Anfrage, und sie zusammenzulegen würde falsche Ergebnisse liefern. Die Reservierung liefert reserved, duplicate, in_progress oder exhausted. Alle vier Zustände existieren, weil alle vier eingetreten sind.
In einer Transkriptions-API begann die Abrechnung nach Zeichen als Transkription plus verarbeiteter Text geteilt durch zehn. Als ich die echten Kosten gemessen habe, wurde aus der Zeile das hier:
transcriptionText.length + Math.floor(processedText.length * 0.25)
Von /10 zu * 0.25: zweieinhalbmal teurer. Der Preis lag um den Faktor 2,5 daneben, und monatelang habe ich die Differenz bezahlt, ohne es zu wissen, weil mir die Messung fehlte, um es zu merken.
In einem SaaS zur Bildgenerierung war der Bug der umgekehrte: Die Tagescredits existierten, aber die Vorabprüfung schaute nur auf subCredits. Nutzer mit Tagescredit bekamen ein 402, obwohl Guthaben auf dem Konto war. Ein dailyCredits + subCredits hat es behoben. All das sind Spalten einer Nutzerzeile. Ohne Zeile gibt es keinen Bug zu beheben, weil es nichts zu zählen gibt.
Wenn Identität auch nicht reicht
Ein Konto zu haben heißt nicht, eine gültige Identität zu haben. In einer API mit Keys überlebte die Zeile des Keys den Zustand des Kontos seines Besitzers: Beim Offboarding wurde der Abrechnungseintrag gelöscht, ohne dass jemand das isActive des Keys angefasst hätte. Der Key authentifizierte weiter. Die Korrektur war, direkt in authenticateApiKey zu revalidieren, dass der Besitzer weiterhin einen Abrechnungseintrag hat, statt sich auf das Flag zu verlassen:
if (!ownerBilling) {
return { authenticated: false, error: "API key owner is no longer active" };
}
Das steht genau dort und nicht in jedem Endpoint, damit sich eben kein Endpoint daran erinnern muss.
Wann ich doch ohne Konto hereinlasse
Wenn die Aktion nichts kostet und keinen Zustand ansammelt: einen Eintrag lesen, einen Katalog ansehen, eine Landingpage laden. Und eine Besucherstufe mit knappem Kontingent, damit jemand vor der Entscheidung ausprobieren kann. Zehn Suchen vor der Registrierung sind ehrliche Reibung; null ist eine offene Rechnung.
Die Frage ist nicht, ob der Login nervt. Er nervt. Die Frage ist, was du in das WHERE schreiben willst, wenn jemand über die Stränge schlägt.
