Open-source X (Twitter) publisher that drives x.com with Patchright and imported cookies.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file auto_tt-0.1.0.tar.gz.
File metadata
- Download URL: auto_tt-0.1.0.tar.gz
- Upload date:
- Size: 31.6 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92f6b7d6decd92b6ac8d6b0655dd638232523abd2cf7dff800686ea79be75af0
|
|
| MD5 |
0d4ea96fd7c8a3f7e687117790d5d65f
|
|
| BLAKE2b-256 |
e10231080f363b96221449bb7b395177806bbc85feb2f0f6b724c4ad2e6c4819
|
File details
Details for the file auto_tt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: auto_tt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e337e0f18d8e49c03413c9748bbfa16f267c6eae5f6c4f4866763d57a788a383
|
|
| MD5 |
1a50e816bb15ce9932ea53cbae1a2b72
|
|
| BLAKE2b-256 |
9adc2c7882f814654a8ec8b35bf1e82bfd27f622b7b765f0c8153b85b586a6bf
|