8 Platforms, 2 Strategieën
Wanneer Gebruik je de API en Wanneer Open je een Browser
Lessen uit het bouwen van een cross-platform social inbox met JSON APIs, AT Protocol en Playwright.
Looking for self-hosted alternatives to Buffer that don't require a dozen OAuth tokens...
Thread: The real cost of building on platform APIs that change every quarter
Has anyone built cross-platform posting tools? Struggling with LinkedIn's API limitations
Het Probleem
Elk sociaal platform wil dat je hun app gebruikt. Sommige geven je een API. Sommige geven je een kapotte API. Sommige geven je niets.
Ik moest op acht platforms tegelijk monitoren, concepten schrijven en posten vanuit een enkele terminal. Geen SaaS-dashboard met OAuth-knoppen en maandelijkse kosten — een lokale CLI die op mijn machine draait, sessies in mijn homedirectory opslaat en niet naar huis belt.
Elk platform heeft een andere houding tegenover automatisering. Bluesky geeft je een volledig protocol (AT Protocol) en zegt "bouw wat je wilt." Reddit biedt OAuth en een JSON API. Dev.to heeft een REST API die voor sommige dingen werkt en bij andere stilletjes faalt. LinkedIn bestrijdt automatisering op elke laag. IndieHackers heeft helemaal geen API.
Twee slechte opties. Alles via de browser: een Playwright-instantie voor alles, knoppen klikken, tekstvelden invullen. Het werkt, maar het is traag, fragiel, en platforms worden steeds beter in het detecteren. Alles via API: schoon en snel, maar de helft van de platforms biedt er geen, en die dat wel doen zijn vaak incompleet.
De derde optie — die ik uiteindelijk bouwde — was per platform en per operatie beslissen. Sommige reads gaan via JSON APIs. Sommige writes gaan via de browser. Sommige platforms ondersteunen beide strategieën tegelijk, geselecteerd at runtime.
De Beslismatrix
Elke cel in deze tabel is een beslissing. Groen betekent dat een stabiele API het werk doet. Amber betekent dat Playwright een browser opent. Rood betekent dat de API op papier bestaat maar niet werkt.
| Platform | Monitor | Post | Comment |
|---|---|---|---|
| JSON API | API / Browser | API / Browser | |
| Hacker News | Firebase API | — | Browser |
| Dev.to | REST API | REST API | Browser * |
| Bluesky | AT Protocol | AT Protocol | AT Protocol |
| Mastodon | REST API | REST API | REST API |
| Browser | OAuth / Browser | Browser | |
| IndieHackers | Browser | Browser | Browser |
| Discord | Browser + GQL | Browser | Browser |
De besliscriteria zijn simpel:
- 1Bestaat er een stabiele, gedocumenteerde API? Gebruik die.
- 2Bestaat de API maar werkt die niet? Browser als fallback.
- 3Is er helemaal geen API? De browser is de enige optie.
- 4Detecteert het platform actief automatisering? Voeg evasion en vertragingen toe.
Eén interface, twee implementaties
De orchestrator weet niet en hoeft niet te weten of hij een API of een browser aanspreekt. Elke adapter implementeert hetzelfde contract:
export interface PlatformAdapter {
readonly name: string;
readonly capabilities: readonly Capability[];
readonly authType: AuthType;
post?(content: PostContent): Promise<PostResult>;
comment?(target: ThreadTarget, content: string): Promise<CommentResult>;
monitor?(input: MonitorInput): Promise<Opportunity[]>;
healthCheck?(): Promise<HealthResult>;
}Uit de Praktijk: Reddit's Dubbele Modus
Reddit is het duidelijkste voorbeeld omdat het beide strategieën tegelijk ondersteunt.
De adapter heeft een mode property: "api" of "browser". In API-modus gebruikt hij Snoowrap (een Reddit OAuth-wrapper) voor posten en reageren. In browser-modus navigeert hij naar old.reddit.com via Playwright.
Waarom old.reddit.com? Het nieuwe design heeft versluierde CSS-klassenamen die veranderen tussen deploys. Succes met het schrijven van een stabiele selector voor het reactie-tekstveld. Old Reddit? textarea[name="title"]. Die selector is al vijftien jaar stabiel.
public async post(content: PostContent): Promise<PostResult> {
if (this.mode === "browser") {
return this.postViaBrowser(content, subreddit, title);
}
return this.postViaApi(content, subreddit, title);
}Vijf regels die de hele posting-flow routeren.
Veiligheidscontroles die de API niet nodig heeft
Het browser-pad heeft vangrails die het API-pad niet nodig heeft. Twee in het bijzonder.
De zelf-antwoord-guard. Voordat de adapter een reactie plaatst, controleert hij of de ingelogde gebruiker ook de auteur van het bericht is. Zonder dit antwoord je vrolijk op jezelf. Het klinkt logisch achteraf. Het was niet logisch de eerste keer dat het gebeurde.
De duplicaat-guard scant bestaande reacties in de thread op je eigen gebruikersnaam. Sta je er al? Overslaan. Deze guards bestaan omdat browserautomatisering niet de impliciete veiligheidsrails heeft die goed ontworpen APIs bieden. Als je op knoppen klikt, sta je er alleen voor.
const loggedInUser = await page.locator(".user a").first().textContent();
const targetAuthor = await page.locator(".comment .author").first().textContent();
if (loggedInUser?.toLowerCase() === targetAuthor?.toLowerCase()) {
return { success: false, error: "Self-reply blocked" };
}Uit de Praktijk: LinkedIn's Vijandige DOM
LinkedIn is het moeilijkste platform en de beste leraar.
De eerste ontdekking kostte uren: de "Bericht"-knop op een LinkedIn-profiel is geen <button>. Het is een <a>-tag. Als je zoekt naar page.getByRole('button', { name: 'Bericht' }), zoek je eeuwig. De DOM trekt zich niets aan van je aannames.
Het tweede probleem: lokalisatie. Mijn LinkedIn draait in het Nederlands. De "Post"-knop zegt "Plaatsen". "Next" zegt "Volgende". "Delete" zegt "Verwijderen". Je kunt geen Engelse labels hardcoden.
const POST_BUTTON = /Plaatsen|Post|Delen|Share/i;
const NEXT_BUTTON = /Volgende|Next/i;
const DELETE_BTN = /Verwijderen|Delete/i;
const EDITOR_SELECTOR =
'[role="dialog"] [contenteditable="true"], .ql-editor';Regex-patronen die taalinstellingen overleven.
De regex-aanpak was niet ontworpen. Hij ontstond tijdens het debuggen om 2 uur 's nachts. Maar het is het juiste patroon — ik had er meteen mee moeten beginnen in plaats van het via falen te ontdekken.
LinkedIn gebruikt ook geen infinite scroll in zijn feed. Er is een "Meer laden"-knop. Je klikt erop, wacht tot nieuwe content verschijnt, controleert of het aantal posts daadwerkelijk is gestegen, en stopt als dat niet zo is. Handmatige paginering in 2026.
Drie verschillende authenticatiestrategieën: een persistent browser-sessie voor de meeste operaties, een OAuth-token voor bedrijfspaginaposts, en de hybride variant waarbij reageren altijd de browser vereist, zelfs als je een OAuth-token hebt.
Wanneer de API Liegt
Dev.to heeft een comment API-endpoint. Het is gedocumenteerd. Het accepteert de juiste parameters. En het retourneert 404. Elke keer. Dit is al het geval sinds ten minste 2025.
Dus monitoring en posten gaan via de REST API (die werken prima). Reacties gaan via de browser. De textarea-selector probeert vier varianten in cascade omdat zelfs de browser-DOM niet stabiel is tussen deploys.
Andere platforms, andere lessen
- IndieHackers draait op Ember.js. Playwright's fill() triggert Ember's data binding niet. Je hebt keyboard.type() nodig met een bewuste vertraging tussen toetsaanslagen, anders dropt het framework karakters.
- Discord voegt willekeurige vertragingen van 2-3,5 seconden toe tussen acties om botdetectie te vermijden. De vertragingen zijn niet voor de show — zonder ze limiteert Discord stilletjes je sessie.
- Upwork heeft helemaal geen publieke API. De adapter onderschept GraphQL-responses uit de netwerklaag — Playwright's page.on('response') vangt de JSON-payloads op die de frontend ophaalt, zonder ooit de DOM te hoeven parsen.
"Heeft een API" en "heeft een werkende API" zijn verschillende dingen. Je adapter moet overleven in de kloof tussen wat een platform belooft en wat het levert.
Betrouwbaar Maken
Dat het werkt is het makkelijke deel. Dat het om 3 uur 's nachts werkt, onbeheerd, dat is de engineering.
Health tracking
Elk platform heeft een staleness-timer. Als er geen succesvolle actie is geweest in 72 uur, wordt het als stale gemarkeerd en overgeslagen tijdens preflight-checks.
Exponential backoff
Gefaalde queue-items krijgen drie herhaalpogingen: 10 minuten, 20 minuten, 40 minuten. Na drie mislukkingen wordt de status permanently_failed en wacht het op menselijke interventie.
Capability checking
Voordat adapter.comment() wordt aangeroepen, verifieert de orchestrator hasCapability(adapter, 'comment'). Geen capability betekent een duidelijke foutmelding in plaats van een crash. Dit is het verschil tussen een systeem dat netjes faalt om 3 uur 's nachts en een systeem dat de hele run onderuit haalt.
const BACKOFF_BASE_MS = 5 * 60 * 1000; // 5 minutes
const newRetryCount = retryCount + 1;
const backoffMs = Math.pow(2, newRetryCount) * BACKOFF_BASE_MS;
const nextEligibleAt = new Date(
now.getTime() + backoffMs
).toISOString();
// Retry 1: 10 min
// Retry 2: 20 min
// Retry 3: 40 min
// Then: permanently_failedHet retry-schema: opschalen, dan eerlijk opgeven.
Wat Ik Anders Zou Doen
Begin API-first, browser als laatste redmiddel.
Ik bouwde sommige browser-adapters voordat ik controleerde of er een JSON API bestond. Reddit heeft een uitstekende JSON API voor lezen — voeg .json toe aan elke URL. Ik bouwde eerst de browser-monitoring en ontdekte de API later. Verspilde moeite.
Persistent sessions vanaf dag één.
Cookies opslaan in een lokale directory en hergebruiken tussen runs elimineert 90% van de authenticatie-hoofdpijn. Eenmalig handmatig inloggen, dan pakt de adapter het op waar je gebleven was.
Ontwerp voor meertalige DOMs vanaf het begin.
De regex-aanpak voor LinkedIn (Nederlandse en Engelse knoplabels tegelijk matchen) werkt goed. Maar ik ontdekte het via falen in plaats van het te ontwerpen. Als je browserautomatisering bouwt voor een meertalig publiek, ga er dan vanuit dat de DOM een taal spreekt die je niet controleert.
Maak health checks verplicht.
Alleen Bluesky en Mastodon implementeren healthCheck() in hun adapters. De rest leunt op staleness-heuristieken. Een platform kan "vers" zijn (recente succesvolle actie) en toch kapotte authenticatie hebben. Dat is een gat.
De Les
De hybride aanpak is niet elegant. Het is een pragmatisch antwoord op een wereld waarin elk platform een andere mening heeft over automatisering. Elk platform krijgt de integratiestrategie die het verdient — geen one-size-fits-all wrapper die overal slecht werkt.
Het engineering-oordeel zit niet in het kiezen van API of browser. Het zit in weten wanneer je wisselt, waartegen je bewaakt, en wat je doet als het platform de regels 's nachts verandert.
Dit is het type systems engineering dat we bij MAD IT doen — rommelige real-world integraties die betrouwbaar werken.