The publishing model

Why X posting is browser automation instead of the official API, and how the three connection types differ.

ConnectionType.oauth_api exists in the schema as a placeholder and has no implementation. Everything that posts to X today does it by replaying a saved session (a Playwright storageState) through headless Chromium, restoring the cookies, typing into X's own compose box, and clicking tweet.

Why not the official API

X's pay-per-post API pricing makes the official route expensive for a platform footing the bill across many tenants and accounts. Browser-session automation has no per-post platform fee. The cost it carries instead is risk: X can detect and suspend an account it identifies as automating a session this way. That trade-off is made explicit to the operator, not hidden. See Channels for the acknowledgment flow this requires.

The three connection types

TypePlatformsHow it posts
browser_sessionXHeadless Playwright restores the account's storageState, types into the Draft.js compose box with page.keyboard.insertText() (not .fill(), since Draft.js needs real input/beforeinput events), and clicks tweet.
bot_tokenTelegram, DiscordA direct API call: sendMessage for Telegram's Bot API, an equivalent call for Discord's bot API. No browser involved.
manualLinkedIn, InstagramNothing automates posting at all. PublishingService.publish() takes an early-return branch for these accounts: length-check the draft, flip its status to ready_to_copy, and stop, skipping idempotency, the adapter registry, cost recording, and metrics entirely, because none of those apply to a post nobody's system actually sent.

What every real publish goes through

For browser_session and bot_token accounts, PublishingService.publish(postId) runs, in order:

  1. An idempotency-key check (post:<id>:publish) so a retried job can't double-post or double-charge.
  2. A platform-aware length guard (X: 280 weighted characters via twitter-text; Telegram: 4096 plain characters) that fails fast before touching the browser or bot API at all.
  3. AdapterRegistry.forAccount(account).post(): the actual send.

If the session has expired, the account's status flips to needs_reauth rather than failing silently on every retry.

Metrics only follow real sends

Reading engagement back (the pipeline behind Analytics) only fires for accounts that were really posted through by Pegacraft's own automation. It's X-only today, because the metrics reader depends on the same Playwright storageState the browser adapter already uses; there is no equivalent read-back path for a manual-posting account, since nothing here ever saw the post go live.

On this page