Open-source X (Twitter) publisher that drives x.com with Patchright and your own imported cookies. Prepare content in a folder, configure your account once, publish.
Status: Alpha. Personal-account use only. Browser-driven automation of X is restricted by X's ToS — use a scratch account, conservative pacing, and a residential IP that matches where you got the cookies.
X's official developer API gates writes behind a paid tier and an app-review process. For one operator who already has a real logged-in browser tab, the most durable workflow is to drive that same browser. auto-x does exactly that.
Supported post types in v1:
tweet— single post, up to 4 images or 1 video, with optional per-image alt textthread— a chain of 2+ tweets, each segment can carry its own media
X Premium accounts unlock 25,000-character posts; toggle premium: true in the account YAML to lift the 280-char cap.
Not supported in v1: replies, quote-tweets, polls, Spaces, long-form Articles. They will follow once the v1 surface is stable.
- Python 3.11+
- macOS or Linux (tested on macOS)
- A real Chrome/Chromium you can log in from on the same network you'll run the bot on
- (Recommended) a residential proxy if you plan to run this on a different machine from where you logged in
One command with pipx (recommended):
pipx install auto-tt
auto-tt init --account demoOr with uv:
uv tool install auto-tt
auto-tt init --account demoauto-tt init installs the patched Chrome channel Patchright needs and scaffolds a working directory in .:
./config/demo.yaml # account config from the shipped template
./content/example-post/ # sample post descriptor
./sessions/ # session files (gitignored)
./.gitignore # appended with auto-x entries
It is safe to re-run — existing files are preserved.
git clone https://github.com/xtea/auto-x
cd auto-x
uv sync
uv run auto-tt init --account demoEdit config/<account>.yaml. The important fields:
handle— your X username without@(display only)premium— true if your account has X Premium (raises the per-tweet cap to 25,000 chars)user_agent,viewport,locale,timezone— match the browser you'll log in from. Drift between these and the cookie's origin is the #1 cause of re-challenges on x.com.pacing.max_posts_per_day— start at 25 or lower. X's rolling-window soft caps fire well before the documented daily limit.
Two paths; pick whichever you prefer.
auto-tt login --account demoA Chrome window opens at https://x.com/i/flow/login regardless of your headless setting. Log in by hand (handle Arkose / 2FA yourself). When the home timeline appears, the session is saved to sessions/demo.json.
X's most important cookies (auth_token, ct0, kdt) are HttpOnly — they will not appear in document.cookie. Use a tool that reads the browser's cookie store directly:
-
Install Cookie-Editor (browser extension).
-
Open
x.comin your real browser, click the extension, Export → Export as JSON, save tox-cookies.json. -
Run:
auto-tt import-cookies ./x-cookies.json --account demo
The tool rejects the import if auth_token or ct0 is missing. Cookies on the legacy .twitter.com domain are auto-mirrored to .x.com, so a session migrated mid-rebrand still works.
auto-tt doctor --account demoShould print OK: <handle> session is valid.
content/
└── my-post/
├── post.yaml
└── media/
├── 1.jpg
└── 2.jpg
type: tweet
caption: |
Your caption. 280 chars on free, 25,000 on Premium.
#opensource
media:
- ./media/1.jpg # 1–4 images, OR exactly one video
alt_texts: # optional, aligned by index, ≤1000 chars each
- "Description of 1.jpg"
schedule: 2026-04-25T14:00:00Z # optional, UTC or with offsettype: thread
segments:
- caption: "First tweet in the chain."
media: [./media/1.jpg]
- caption: "Second tweet — every segment can have its own media."
media: [./media/2.mp4]Validation runs before any browser work:
| Surface | Rule |
|---|---|
| Caption (free) | ≤ 280 chars |
| Caption (Premium) | ≤ 25,000 chars |
| Images per segment | 1–4, .jpg/.jpeg/.png/.webp, ≤5 MB (15 MB for .gif) |
| Video per segment | 1 only, .mp4/.mov, ≤512 MB and ≤140 s on free tier |
| Mixing media | images xor video — never both |
| Alt text | ≤ 1000 chars per image |
| Thread length | 2–25 segments |
auto-tt publish content/my-post --account demo --dry-run # safe first run
auto-tt publish content/my-post --account demo # actually posts--dry-run walks the full upload flow and stops before clicking Post — useful when patching selectors.
Drop posts with schedule: set in the future, then run auto-tt queue from cron / launchd / systemd-timer:
*/5 * * * * cd /path/to/auto-x && auto-tt queue --account demo >> sessions/queue.log 2>&1The queue stores state in sessions/queue.db (SQLite) with statuses: queued | running | succeeded | failed | paused. Use auto-tt list to inspect.
- Launch Chrome via Patchright (Chromium with CDP/webdriver leaks patched at the binary level). Vanilla
playwrightis detectable by X's fingerprinting stack — don't use it. - Load
sessions/<account>.jsonas the Playwrightstorage_state. - Navigate to
https://x.com/home, confirm the Post side-nav button is visible (signal of authenticated state). - Click Post →
setInputFilesinto the hidden<input data-testid="fileInput">. - Type the caption into
[data-testid="tweetTextarea_0"](contenteditable, must usekeyboard.typenotfill). - For threads, click the
addButtonand repeat fortweetTextarea_1..N. - Click
tweetButtonInline. Watch for the toast[data-testid="toast"]and read itsViewlink to capture the new tweet's numeric ID.
All selectors are in src/auto_x/publisher/selectors.py — when X changes the UI, that is the file to patch.
Built-in guardrails, tunable in config/<account>.yaml:
max_posts_per_daydaily cap (enforced by the queue)min/max_step_delay_secondsrandomized delays between UI actions (30–180 s recommended)pre_run_idle_seconds_*scroll/dwell on the home timeline before the first click
On any redirect to /i/flow/login, /account/access, /i/flow/consent_flow, /i/flow/login/check, or /account/suspended, the runner pauses the job and records the reason. Re-authenticate with auto-tt login and retry — there is no auto-retry.
headless: true is the shipped default. The auto-tt login command always forces a headed window regardless, so cookie capture and 2FA still work.
- v1 surface only. Reply, quote, poll, Spaces, and long-form Article composition are not supported yet.
- Selectors rot. X ships UI changes frequently. Expect periodic patches to
selectors.py. - Arkose / Cloudflare-style challenges. If X drops a FunCaptcha mid-run, the tool pauses; manual re-login is required.
- Shared IP. Using cookies captured from residence A while running the bot on residence B's IP is the single most reliable way to get challenged.
- ToS risk. Browser-driven automation of personal X accounts is against X's terms. Ban risk is real. Use a scratch account.
- Web UI / dashboard (CLI + YAML only)
- Long-running daemon (cron-friendly invocation instead)
- Engagement automation (likes, follows, DMs, retweets-without-quote)
- Account registration
MIT.