auto-x
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.
Why this project
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.
Requirements
- 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
Install
One command with pipx (recommended):
pipx install auto-tt
auto-tt init --account demo
Or with uv:
uv tool install auto-tt
auto-tt init --account demo
auto-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.
From source (contributors)
git clone https://github.com/xtea/auto-x
cd auto-x
uv sync
uv run auto-tt init --account demo
Configure an account
Edit 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.
Authenticate
Two paths; pick whichever you prefer.
Option A — Headed manual login (simplest)
auto-tt login --account demo
A 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.
Option B — Import cookies from your real browser
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.
Verify
auto-tt doctor --account demo
Should print OK: <handle> session is valid.
Publish content
Layout
content/
└── my-post/
├── post.yaml
└── media/
├── 1.jpg
└── 2.jpg
post.yaml schema (single tweet)
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 offset
post.yaml schema (thread)
type: 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 |
One-shot publish
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.
Scheduled / queued publish
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>&1
The queue stores state in sessions/queue.db (SQLite) with statuses: queued | running | succeeded | failed | paused. Use auto-tt list to inspect.
How the Playwright flow works
- 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.
Pacing & safety
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.
Known limitations
- 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.
Non-goals (for now)
- Web UI / dashboard (CLI + YAML only)
- Long-running daemon (cron-friendly invocation instead)
- Engagement automation (likes, follows, DMs, retweets-without-quote)
- Account registration
License
MIT.
Metadata
Release files for auto-tt 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| auto_tt-0.1.0.tar.gz | 31.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| auto_tt-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.8 kB
Release files / auto_tt-0.1.0.tar.gz
| Download URL | auto_tt-0.1.0.tar.gz |
|---|---|
| Size | 31.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
92f6b7d6decd92b6ac8d6b0655dd638232523abd2cf7dff800686ea79be75af0
|
|
BLAKE2b-256 checksum How to use checksums |
e10231080f363b96221449bb7b395177806bbc85feb2f0f6b724c4ad2e6c4819
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / auto_tt-0.1.0-py3-none-any.whl
| Download URL | auto_tt-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e337e0f18d8e49c03413c9748bbfa16f267c6eae5f6c4f4866763d57a788a383
|
|
BLAKE2b-256 checksum How to use checksums |
9adc2c7882f814654a8ec8b35bf1e82bfd27f622b7b765f0c8153b85b586a6bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|