Waiting on one busy account added about half a second to every page while other accounts were finishing. Co-authored-by: Cursor <[email protected]>
teapawt
A privacy-focused Twitter/X frontend written in Rust.
Public instances: nitter.cf, nitter.gq, xitter.cf, xitter.gq, teapaw.org, teapawt.net, and teapawt.org.
Live status for those instances: status.d420.net. Scraping is allowed on them. Ask the operator before scraping any other Nitter instance.
Features
- Privacy-focused: No third-party JavaScript, tracking, ads, or remote fonts
- RSS feeds: Subscribe to any user's tweets
- SOCKS5 exits for GraphQL: Pin each cookie session to one proxy port so its apparent IP stays stable
- In-process cache: First-page search, profiles, and stale-while-revalidate so repeats do not wait on X
- FxEmbed-style Discord embeds:
- Multiple images displayed in carousel on mobile
- Videos play directly in the embed
- Proper image dimensions for better layout
- ActivityPub JSON endpoint for rich embeds
Requirements
- Rust 1.96.1+
- Twitter/X session tokens for API access
- Optional: SOCKS5 endpoints if X blocks the server's own IP
Building
cargo build --release
The crate and binary stay named teapot so existing configs and process
managers keep working.
Configuration
- Copy the example config:
cp config/teapot.example.toml config/teapot.toml
-
Edit
config/teapot.toml. Settitle = "teapawt"(or whatever you want shown in the navbar). -
Behind nginx or another TLS terminator, set
https = trueandpublicPort = 443so prefs bookmarks, RSS, andog:imagelinks omit the listen port (8471or whateverportis). -
Create a sessions file with your Twitter/X credentials:
cp sessions.example.jsonl sessions.jsonl
# Edit sessions.jsonl with your auth_token and ct0 from browser cookies
Cookie sessions use auth_token and ct0. OAuth sessions use
oauth_token and oauth_secret. See sessions.example.jsonl for the JSONL
shape.
Never commit a real sessions.jsonl, config/teapot.toml, proxies.txt,
or session-proxies.json. Those paths are gitignored.
Proxies
X GraphQL (search, profiles, tweets) can go out through SOCKS5. Media (/pic,
/video, /gif) stays on the optional HTTP proxy and does not use the
SOCKS list.
cp proxies.example.txt proxies.txt
# one host:port:user:pass per line
# config/teapot.toml
socksProxiesFile = "proxies.txt"
sessionProxiesFile = "session-proxies.json"
All lines must share one hostname (different ports are the rotating exits).
On startup teapawt DNS-probes that host, keeps the lowest-ping A record, and
pins each session id to one port. Pins are rewritten to sessionProxiesFile
as { "sessionId": port } only — credentials stay in proxies.txt.
apiProxy is an HTTP/HTTPS proxy for GraphQL when you are not using SOCKS.
It is ignored when socksProxiesFile is set.
Tunnels to x.com are HTTP/1.1 over TLS with keep-alive, TCP_NODELAY, and
rustls session resumption. Idle tunnels are reused for a short window so a
second search does not pay a new SOCKS + TLS handshake.
Rate limits and cache
maxConcurrentReqsis per cookie session, not global. At2, an account that already has two calls in flight is skipped and the next account is used immediately. A crawl stays at two in flight per account across the whole pool. Raising it into the millions lets one burst pile onto the few accounts already known to work, which is what produces a cliff of 429s.clientBudgetcaps how fast one visitor IP can spend uncached X calls. Cache hits, media, and static files are free. SearchTimeline costs 2 tokens.- First-page search is cached for 60 seconds, then served stale for 4 more minutes while a refresh runs.
- Profiles are cached for 5 minutes, then served stale for 20 more minutes.
- Search result pages seed user ids and prefetch a few profiles so a click often hits cache.
Some otherwise valid cookie accounts are not allowed to search: X answers
their SearchTimeline calls with an empty HTTP 404. teapawt retries the same
search on another session. When that session returns results, the first
account is recorded in session-limits.json and permanently left out of
search rotation; it can still serve other operations. If both sessions return
nothing, teapawt treats it as an upstream/query-id problem instead of blaming
either account. Dead cookies are 401, or 403 with code 326 — not those empty
404s.
Some queries fail with X error 214 (Query is denylisted in Search Content Control tool). That is X's platform denylist, not a teapawt setting.
Running
# Development
cargo run
# Production
./target/release/teapot
The server listens on http://localhost:8080 by default (address / port
in the config).
Getting Twitter Sessions
To use the Twitter API, you need session tokens from a logged-in Twitter account:
- Log into Twitter/X in your browser
- Open Developer Tools (F12) → Application → Cookies
- Copy the values of
auth_tokenandct0 - Add them to
sessions.jsonl
Project Structure
teapawt/
├── src/
│ ├── main.rs # Entry point
│ ├── config.rs # Configuration
│ ├── error.rs # Error types
│ ├── types/ # Data structures (User, Tweet, Timeline, etc.)
│ ├── api/ # Twitter API client, SOCKS5 pool, OAuth, parsing
│ ├── cache/ # In-process cache (TTL + stale-while-revalidate)
│ ├── routes/ # HTTP route handlers
│ ├── views/ # Maud HTML templates
│ └── utils/ # Utilities (HMAC, formatters)
├── public/ # Static assets (CSS, JS, fonts, logo)
├── config/ # teapot.example.toml only
├── proxies.example.txt # Dummy SOCKS5 list
├── sessions.example.jsonl
└── Cargo.toml
Embed Improvements
This rewrite includes FxEmbed-style improvements for Discord embeds:
Multiple Images
Discord can display all images from a tweet in a carousel. This works via an ActivityPub JSON endpoint at /users/{username}/statuses/{id} that returns media attachments.
Video Playback
Videos can be played directly in Discord embeds using twitter:player meta tags:
twitter:player- Embed URLtwitter:player:stream- Direct MP4 URLtwitter:player:width/height- Dimensions
Image Dimensions
All images include og:image:width and og:image:height meta tags for proper layout.
Donate
Running the public instance costs money for servers, bandwidth and the accounts and exits that keep it working. If nitter.cf is useful to you, donations help keep it online. Nothing is required.
| Coin | Address |
|---|---|
| BTC | bc1qtm4rep7kyp2p28hlkfktpnd0j7uew47vnt2mev |
| LTC | ltc1q9xlaysse0mpfwf025esd69kt9zu3md22gh23du |
| ETH | 0x6abA18D203520c8F90ca63eE7426a7bC345D98e6 |
| SOL | E5ekoiauCR8yiNp451dmuPzhUxo84P5x3m9WJBK5kxZe |
| XRP | r3jYXNdMk2Vz5hXhKCKE9re8uz93JQFG1r |
| DOGE | DSBq5jRWkiwmEh1G7zBwmQDgRkvRr61fmb |
| XMR | 85CTwq5wGjWZw5GgzgwZPrHyZxpLXMK5oZSSSk5j4x9w1bpqzisaWAUZYaAPfh6rP2MZJcaFqF1XmEzkw6YCqJSS2Txu6Z8 |