Playwright statt API — Browser-Automatisierung fuer X.com-Posts
Nach dem Einbau der KI-Teaser-Generierung (OpenAI GPT-4o-mini erzeugt aus jedem Blogbeitrag einen Twitter-Thread) stellte sich die Frage: Wie kommen die Teaser auf X.com? Die offizielle API verlangt inzwischen 5 Cent pro Write-Aktion — bei regelmaessigem Posten summiert sich das schnell. Also haben wir einen anderen Weg gewaehlt: Browser-Automatisierung mit Playwright.
Das Problem
Die X.com API (v2) hat ein Preismodell, das fuer kleine Projekte unattraktiv ist. Ein einzelner Tweet kostet 5 Cent, ein Thread (Haupt-Tweet + Reply) also 10 Cent. Klingt wenig, aber bei Tests, Dry Runs und regelmaessigem Posten laeutert sich das schnell. Ausserdem aendert X regelmaessig die API-Bedingungen.
Die KI-Teaser-Generierung via OpenAI war bereits implementiert und speichert die generierten Texte in der Datenbank (x_main_tweet und x_reply_tweet). Bisher musste man die Teaser manuell kopieren und auf X.com einfuegen. Das wollten wir automatisieren — ohne API-Kosten.
Die Architektur
Das System hat drei unabhaengige Ausfuehrungspfade, die einen gemeinsamen Browser-Core teilen:
-
Teaser generieren — Die bestehende Route
/api/blog/[id]/share-xnutzt OpenAI, um aus Titel, Excerpt und Content einen Twitter-Thread zu generieren. Die Texte werden in der Datenbank gespeichert. -
Jetzt posten / In Warteschlange — Die neue Route
/api/blog/[id]/publish-x-browserstartet Playwright, oeffnet X.com mit einer gespeicherten Session, tippt den Text mit menschenaehnlichen Delays ein und klickt auf "Posten". -
Dry Run — Gleiche Route mit
?dry=true: Der Composer wird gefuellt und ein Screenshot gemacht, aber nicht abgeschickt. Perfekt zum Testen.
Anti-Bot-Strategie
X.com erkennt Bots zuverlaessig. Deshalb setzt die Integration auf mehrere Massnahmen, um wie ein normaler User auszusehen:
- Persistente Session — Login-Cookies werden gespeichert und bei jedem Start wiederverwendet. Kein "neues Geraet" bei jedem Aufruf.
- Menschenaehnliches Timing — Variable Tipp-Geschwindigkeiten (statt konstanter Delays), natuerliche Pausen zwischen Aktionen und laengere Wartezeiten vor dem Absenden.
- Browser-Fingerprint — Verschiedene Einstellungen sorgen dafuer, dass der Browser nicht als automatisiert erkannt wird.
- Rate-Limiting — Maximal 3 Posts pro Tag, mindestens 30 Minuten Abstand.
- Locale und Timezone —
de-DEundEurope/Berlin, passend zum Datacenter-Standort.
Das Bootstrap-Script
Bevor automatisch gepostet werden kann, muss einmalig eine Session eingerichtet werden:
npm run x:bootstrapDas Script oeffnet ein sichtbares Chrome-Fenster auf x.com/login. Man loggt sich manuell ein, drueckt ENTER im Terminal, und die Session-Cookies werden gespeichert. Das Script validiert, ob die relevanten Session-Cookies vorhanden sind.
Das muss nur einmal gemacht werden — die gespeicherte Session wird bei jedem Aufruf geladen und am Ende aktualisiert (falls Cookies refresht wurden).
Der PM2-Worker
Fuer automatisches Posten laeuft ein separater Node.js-Prozess via PM2:
// ecosystem.config.cjs
{
name: "x-publisher",
script: "node_modules/.bin/tsx",
args: "scripts/x/publish-worker.ts",
restart_delay: 60000,
max_restarts: 3,
}Der Worker pollt alle 5 Minuten die Datenbank nach Posts mit x_publish_status = "queued" und verarbeitet sie der Reihe nach. Bei Challenge-Erkennung (CAPTCHA, Login-Redirect) beendet sich der Worker — PM2 startet ihn nach 60 Sekunden neu, maximal 3 Mal.
Challenge-Detection
Nach jeder Navigation und nach jedem Senden prueft das System auf:
- Arkose CAPTCHA — iframe mit
src*="arkose" - Login-Redirect — URL enthaelt
/loginoder/i/flow/login - Identitaetspruefung — Text "Verify your identity"
- Rate-Limit — Text "Rate limit exceeded"
Bei Erkennung wird ein Screenshot gespeichert und der Status auf challenge_detected oder review_required gesetzt. Im Dashboard sieht man sofort, was passiert ist.
Dashboard-Integration
Der Blog-Editor zeigt jetzt einen neuen Abschnitt unter der Teaser-Vorschau:

- Status-Badge — Farbcodiert: Blau fuer "In Warteschlange", Teal fuer "Gepostet", Lila fuer "Dry Run OK", Rot fuer Fehler.
- Drei Buttons — "Dry Run" (testen ohne Posten), "Jetzt posten" (sofort ausfuehren), "In Warteschlange" (Worker uebernimmt).
- Fehler-Anzeige — Bei Problemen erscheint die Fehlermeldung in einer roten Box.
- Tweet-Link — Nach erfolgreichem Posten erscheint ein klickbarer Link zum Tweet.
Datenbank-Erweiterung
Sechs neue Spalten auf blog_posts tracken den Publishing-Status:
x_publish_status— Der aktuelle Status (queued, in_progress, published, failed, etc.)x_publish_error— Die letzte Fehlermeldungx_attempt_count— Anzahl der Versuche (max. 3)x_last_attempt_at— Zeitstempel des letzten Versuchsx_reply_post_id— Die Post-ID des Reply-Tweetsx_post_url— Die vollstaendige URL zum geposteten Tweet
Ein partieller Index auf x_publish_status beschleunigt das Worker-Polling.
Proxmox LXC: Headed Chrome ohne Display
Der Produktionsserver ist ein Proxmox LXC-Container — kein Desktop, kein Display. Playwright mit headless: false braucht aber einen Bildschirm. Die Loesung: Xvfb (X Virtual Framebuffer):
apt-get install -y xvfb
Xvfb :99 -screen 0 1280x800x24 -nolisten tcp &
export DISPLAY=:99Xvfb stellt einen virtuellen Bildschirm bereit, auf dem Chrome normal rendert. Der Overhead ist minimal — ein paar MB RAM.
Rollout in drei Stufen
- Dry Run (1-2 Wochen) — Nur testen: Session laden, Composer fuellen, Screenshot machen. Kein Tweet wird gepostet.
- Test-Account (1 Woche) — Mit einem Zweit-Account real posten. Einen Post pro Tag, engmaschig monitoren.
- Produktion — Mit dem echten Account. Worker starten, Dashboard nutzen.
Der manuelle Fallback bleibt: Die generierten Teaser koennen jederzeit per Copy-Paste manuell auf X.com gepostet werden.
Fazit
Playwright-basierte Browser-Automatisierung ist kein Ersatz fuer eine offizielle API — aber bei den aktuellen Preisen eine pragmatische Alternative. Mit persistenten Sessions, menschenaehnlichem Timing und einem robusten Error-Handling-System laesst sich das Risiko minimieren. Und wenn X.com die Erkennung verschaerft? Dann bleibt immer noch das manuelle Copy-Paste der KI-generierten Teaser.