Analyse eines 179KB Inline-Chat-Bundles deckt vier API-Bugs einer KI-Companion-App auf
Das Team von The Companion Lab durfte die App EroLove erstmals nicht nur als Blackbox ueber die Oberflaeche testen, sondern auch das interne Chat-Bundle pruefen. Dabei fanden die Tester vier API-Fehler, die die Benutzeroberflaeche verbarg.

Wenn wir bei The Companion Lab KI-Companion-Apps testen, behandeln wir sie normalerweise als Blackbox: Wir senden dieselben sechs standardisierten Prompts, zahlen mit unserer eigenen Bankkarte, dokumentieren den genauen Kreditkartenabrechnungstext und durchlaufen die Kündigungsseiten.
Die Lektüre dieses 179 KB großen Pakets und die Untersuchung der Endpunkte, die es aufruft, förderte vier Architekturfehler zutage, die ein normaler UI-Durchlauf entweder falsch diagnostizieren oder vollständig übersehen hätte. Hier ist, was der Client-Code und die Netzwerkantworten offenlegten, und warum jeder dieser Fehler relevant ist, wenn man eine webbasierte Anwendung mit LLM-Unterstützung entwickelt.
Wenn man EroLoves Marketingseiten, Kategorienseiten oder sogar deren /privacy-Datenschutzerklärung liest, wird dort wiederholt angegeben, dass Nutzer eigene Fotos in den Chat hochladen und senden können. Innerhalb der Chat-Oberfläche gibt es jedoch nirgendwo einen Anhang- oder Upload-Button.
Ein Blick auf das Inline-Skript von 179 KB erklärt, warum das so ist:
// Vereinfachte Darstellung aus dem Inline-Client-Paket if ( feat ( " userPhotos " )) { renderUploadButton (); } Enter fullscreen mode Exit fullscreen mode Der Client koppelt den Upload-Button an ein Laufzeit-Feature-Flag, das über GET /api/config abgerufen wird. Als wir die Live-Antwort von /api/config am 23. Oktober 2026 untersuchten, lautete das enabled-Array:
{ "features": [ "chatPhotos", "presence", "voice" ] } Enter fullscreen mode Exit fullscreen mode "userPhotos" fehlt, sodass die Benutzeroberfläche den Button sauber ausblendet. Das Client-Paket enthält dennoch die vollständige Upload-Implementierung: fileToJpegDataUrl(), sendPhoto(), den [userphoto]-Nachrichtenparser samt aller zugehörigen Fehlerbehandlungen.
Um herauszufinden, ob die Backend-Route lediglich verborgen oder tatsächlich deaktiviert war, sendeten wir in angemeldetem Zustand eine Anfrage direkt an den in sendPhoto() genannten Endpunkt:
POST /api/chat/upload HTTP / 2 Host: erolove.ai Enter fullscreen mode Exit fullscreen mode Der Server antwortete umgehend:
HTTP 404 Not Found { "error": "not_found" } Enter fullscreen mode Exit fullscreen mode Warum das relevant ist: Code hinter einem Feature-Flag auszuliefern, ist üblich. Der Fehler besteht darin, dass 30 statische Seiten, einschließlich der Datenschutzerklärung und der maschinenlesbaren.md-Pendants, die Datenverarbeitung für ein Feature beschreiben, dessen Backend-Route einen 404-Fehler zurückgibt. Wenn die statische SEO- und GEO-Schicht und die Laufzeit-Flags von /api/config keine gemeinsame Quelle der Wahrheit haben, macht die Dokumentation falsche Angaben zur eigenen API-Oberfläche.
Das herausragende Merkmal von EroLove und der Grund, warum es in unserem vollständigen API-Abgleich und Speichertest beim Punkt Konversation gut abschnitt, ist sein transparentes Speicherpanel. Anders als die meisten Companion-Apps, die das Langzeitgedächtnis in einem intransparenten Vektorspeicher verbergen, stellt EroLove GET /api/memory?companion=... bereit, sodass Nutzer jeden extrahierten Fakt als HTML lesen, bearbeiten, anpinnen, löschen oder exportieren können.
Wir testeten die Extraktion des Gedächtnisses über drei Companions in einer rein englischen Sitzung. Der default_language unseres Kontos war "en", jedes Companion-Metadatenobjekt hatte language: "en", und alle Prompts, die wir sendeten, waren auf Englisch verfasst.
Anschließend untersuchten wir das von der Speicher-API zurückgegebene Array mit den extrahierten Fakten:
Als wir manuell eine der estnischen Speicherzeichenketten von Anna über die Speicher-API ins Englische übersetzten, spiegelte die nächste Chatrunde unsere Änderung unmittelbar wider. Das grenzte den Fehler ein: Die Speicherinjektion und das Bearbeiten funktionieren, aber der Hintergrund-Extraktionsprompt legt die Ausgabesprache nicht auf das default_language der Sitzung fest. Stattdessen scheint das Extraktions-LLM die Ausgabesprache aus dem biografischen Fließtext des Charakters abzuleiten (beispielsweise löst "Tallinn" Estnisch aus) oder driftet vollständig in eine andere Sprache ab.
Wer einen asynchronen Faktextraktionslauf über den Chatverlauf laufen lässt, um einen für den Nutzer sichtbaren Speicher zu befüllen, sollte die Ausgabesprache explizit im Extraktionssystemprompt und im Schema einschränken (zum Beispiel "Schreibe jeden Fakt in der Sitzungssprache des Nutzers: en"). Niemals davon ausgehen, dass ein LLM englische Zusammenfassungen liefert, nur weil die Nachrichten des Nutzers auf Englisch sind, zumal wenn der Systemprompt fremdsprachige Eigennamen enthält.
Im Rahmen unseres Bildgenerierungstests forderten wir im Laufe unserer Sitzung fünf Fotos an (10 Sparks pro Foto). Drei gelangen in 16 bis 23 Sekunden bei einer Auflösung von 768x1024. Zwei schlugen fehl.
In der Browser-Oberfläche zeigten beide Fehlversuche exakt dieselbe Meldung an:
"Konnte gerade kein Foto aufnehmen. Versuche es etwas später noch einmal"
Ein normaler Nutzer oder ein Blackbox-Tester liest diese Nachricht als vorübergehende Server- oder GPU-Warteschlangenstörung und klickt auf Wiederholen.
Betrachtet man jedoch die tatsächliche JSON-Antwort des Polling-Endpunkts, sieht man etwas völlig anderes:
{ "status": "blocked", "charged": 0 } Enter fullscreen mode Exit fullscreen mode Und in der switch-Anweisung des Client-Pakets werden status === "blocked" (eine Ablehnung durch den Inhalts- beziehungsweise Moderationsfilter) und photo_queue- beziehungsweise Timeout-Fehler auf exakt denselben benutzerseitigen Fehlertext umgeleitet.
EroLove zugutezuhalten ist, dass das Backend bei beiden blockierten Versuchen die Sparks zurückerstattet hat ( charged: 0 ). Doch wenn eine deterministische Richtlinienablehnung ( "blocked" ) auf eine vorübergehende Wiederholungsmeldung ( "versuche es etwas später" ) zusammengefasst wird, trainiert das die Nutzer, die Generierungswarteschlange mit demselben Prompt weiter zu bombardieren.
Zwei kleinere Befunde aus derselben Paketuntersuchung rundeten die Prüfung ab:
Blackbox-Tests der Benutzeroberfläche zeigen, ob sich eine App gut anfühlt; das Lesen des Client-Pakets zeigt, warum sie versagt hat.
Unsere vollständigen Erkenntnisse aus dem Oktober 2026, die alle 10 getesteten KI-Companion-Apps, 136,89 US-Dollar an realen Kartenzahlungen, 7 erfasste Kontoauszugsreferenzen und unseren 60-Prompt-Benchmark abdecken, haben wir als offenes CC BY 4.0-Datenset veröffentlicht:
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
When we test AI companion apps at The Companion Lab, we normally treat them as black boxes: we send the same six standardized prompts, pay with our own bank card, record the exact credit card descriptor, and walk the cancellation screens.
Reading that 179 KB bundle—and probing the endpoints it calls—turned up four architectural bugs that a normal UI walkthrough would either misdiagnose or miss completely. Here is what the client code and network responses revealed, and why each failure mode matters if you are building an LLM-backed web application.
If you read EroLove's marketing pages, category landings, or even its /privacy policy, they repeatedly state that users can upload and send their own photos into the chat. Yet inside the chat UI, there is no attachment or upload button anywhere.
Looking at the 179 KB inline script explains why:
// Simplified from the inline client bundle if ( feat ( " userPhotos " )) { renderUploadButton (); } Enter fullscreen mode Exit fullscreen mode The client gates the upload button on a runtime feature flag fetched from GET /api/config. When we inspected the live response from /api/config on October 23, 2026, the enabled array was:
{ "features": [ "chatPhotos", "presence", "voice" ] } Enter fullscreen mode Exit fullscreen mode "userPhotos" is omitted, so the UI cleanly hides the button. Meanwhile, the client bundle still contains the complete upload implementation: fileToJpegDataUrl(), sendPhoto(), the [userphoto] message parser, and all its error handlers.
To see whether the backend route was merely hidden or actually disabled, we sent a signed-in request directly to the endpoint named in sendPhoto():
POST /api/chat/upload HTTP / 2 Host: erolove.ai Enter fullscreen mode Exit fullscreen mode The server responded immediately:
HTTP 404 Not Found { "error": "not_found" } Enter fullscreen mode Exit fullscreen mode Why this matters Shipping dark code behind a feature flag is normal. The bug is letting 30 static pages—including the privacy policy and machine-readable.md twins—describe data handling for a feature whose backend route returns 404. When your static SEO/GEO layer and your runtime /api/config flags have no shared source of truth, your documentation lies about your API surface.
The standout feature in EroLove—and the reason it scored well on conversation in our full API teardown and memory test —is its transparent memory panel. Unlike most companion apps that hide long-term memory in an opaque vector store, EroLove exposes GET /api/memory?companion=..., allowing users to read, edit, pin, delete, or export every extracted fact as HTML.
We tested memory extraction across three companions in an all-English session. Our account's default_language was "en", every companion metadata object had language: "en", and every prompt we sent was in English.
Then we inspected the extracted facts array returned by the memory API:
When we manually edited one of Anna's Estonian memory strings into English via the memory API, the next chat turn immediately reflected our edit. That isolated the bug: memory injection and editing work, but the background memory-extraction prompt does not pin the output language to the session's default_language. Instead, the extractor LLM appears to infer the output language from the character's biographical flavor text (e.g., "Tallinn" triggering Estonian) or drifts into another language entirely.
If you run an asynchronous fact-extraction pass over chat history to populate a user-visible memory store, explicitly constrain the output language in the extraction system prompt and schema (e.g., "Write every fact in the user's session language: en" ). Never assume an LLM will output English summaries just because the user's messages are in English—especially when the system prompt contains foreign proper nouns.
During our image-generation test, we requested 5 photos across our session (10 Sparks per photo). Three succeeded in 16–23 seconds at 768x1024. Two failed.
In the browser UI, both failures displayed the exact same toast:
"Couldn't take a photo right now — try a bit later"
A normal user—or a black-box reviewer—reads that message as a transient server or GPU queue outage and clicks retry.
Look at the actual JSON payload returned by the polling endpoint, however, and you see something completely different:
{ "status": "blocked", "charged": 0 } Enter fullscreen mode Exit fullscreen mode And in the client bundle's switch statement, status === "blocked" (a content/moderation filter rejection) and photo_queue / timeout errors are routed to the exact same user-facing error string.
To EroLove's credit, the backend refunded the Sparks on both blocked attempts ( charged: 0 ). But collapsing a deterministic policy refusal ( "blocked" ) into a transient retry message ( "try a bit later" ) trains users to keep hammering the generation queue with the same prompt.
Two smaller findings from the same bundle inspection rounded out the audit:
Black-box UI testing tells you whether an app feels good; reading the client bundle tells you why it broke.
We published our full October 2026 findings—covering all 10 tested AI companion apps, $136.89 in real card charges, 7 captured bank statement descriptors, and our 60-prompt benchmark—as an open CC BY 4.0 dataset: