arizukocomponents › twitd

twitd

What it is

twitd is the X / Twitter channel adapter. It wraps agent-twitter-client (the ai16z fork of @the-convocation/twitter-scraper) to drive X via browser emulation — cookies, not an API key. TypeScript / Bun, JID prefix twitter:.

Why it exists

X’s official v2 API is paywalled past the point of being useful for a hobbyist bot: writing tweets, mentioning users, posting DMs — none of it is available on the free tier. agent-twitter-client drives the same web surface a logged-in user sees, which is the only path that works for arizuko deployments. Account suspensions are common; the README documents an account-loss runbook.

How it fits

X (twitter.com web surface)
        |  agent-twitter-client poll (mentions, DM convs)
        v
      twitd     (LISTEN_ADDR=:8080, Bun runtime)
        |  POST /v1/messages    (signed by CHANNEL_SECRET)
        v
      gated
        |  POST /v1/send         (callback to twitd)
        v
      twitd     (sendTweet | sendDirectMessage | favorite | ...)
        |
        v
      X

JID forms: twitter:home for the timeline / mentions, twitter:tweet/<id> for a specific tweet, twitter:dm/<conv_id> for a DM thread, twitter:user/<handle> for a profile.

Wired verbs (twitd/src/verbs.ts): send (DM via sendDirectMessage), post (tweet), reply (sendTweet(text, replyToId)), repost (retweet), quote (quote-tweet; empty body returns hint to repost), like (favorite — binary, no emoji), delete (own tweets), send_file (image / video for tweets; DM attachments degrade to text-only). forward, dislike, edit are 501-with-hint — X has no downvote and X Premium edit is not exposed by the library.

No streaming. twitd polls mentions on TWITTER_POLL_INTERVAL (default 90 s).

Auth (3 paths, priority order)

  1. Cookie file at $TWITTER_AUTH_DIR/cookies.json. Export from a logged-in browser. Recommended — skips 2FA.
  2. Username + password via TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_EMAIL, TWITTER_2FA_SECRET. Used only when cookies are missing or invalid; on success cookies are persisted.
  3. Pair mode: bun dist/main.js --pair performs login with env creds, persists cookies, exits. Useful for warm-up.

Cookies are atomically rotated to cookies.json.bak on every save.

Standalone usage

export ROUTER_URL=http://gated:8080
export CHANNEL_SECRET=$(grep ^CHANNEL_SECRET .env | cut -d= -f2)
export LISTEN_ADDR=:8080
export TWITTER_AUTH_DIR=/srv/data/store/twitter-auth
export TWITTER_POLL_INTERVAL=90
# either: drop a cookies.json into TWITTER_AUTH_DIR, or:
export TWITTER_USERNAME=...  TWITTER_PASSWORD=...  TWITTER_EMAIL=...
bun dist/main.js

GET /health returns 503 with {status:"disconnected"} while unauthenticated, and 503 {status:"stale"} if no inbound has flowed in 5 minutes. Triage: cookies expired → drop a new cookies.json, restart twitd. 2FA challenge during password login → switch to the cookie path.

The library (agent-twitter-client@0.0.18) is solo-maintained; pin the version so X-side breakage stays out of the dep graph until the operator chooses to upgrade.

Go deeper