- Questions
- Tools/Libraries/Etc
- Tweets
NotificationsTimelineAPI Deep Dive and Pagination Cursor / ID Structure Decoding (Dec 2025)- Notification Deep Dive and React-Based Tweet Counting (May 2025)
- Ideas for Improving Twitter Notification UX (Dec 2024)
- Manual Notification Triage and Workflow Notes (May 2024 – Dec 2024)
- Gists for Workflow Optimization and Sorting (May 2024)
- Twitter API Limitations and Browser-Based Alternatives (Apr 2024)
- Twitter Blue vs Building a Custom ‘Top Articles’ Tool (Jul 2022)
- Unsorted
- See Also
- Can we get a 'peek notifications' userscript without triggering marking them read?
- https://developer.x.com
-
X Developer Platform
-
An all-new Developer Console with consumption-based billing. No fixed monthly costs, no monthly caps.
-
- https://docs.x.com/x-api/introduction
-
X API
-
Programmatic access to X with v2 REST endpoints for Posts, users, Spaces, lists, DMs, and trends, with pay-per-use pricing and modern SDKs.
-
- https://docs.x.com/x-api/getting-started/pricing
-
Pricing
-
Pay-per-usage pricing for the X API
-
The X API uses pay-per-usage pricing. No subscriptions—pay only for what you use.
- https://docs.x.com/x-api/getting-started/pricing#owned-reads
-
Owned Reads
-
Owned Reads are requests made by your own developer app for your own data (posts, bookmarks, followers, likes, lists, and more). These endpoints are priced at $0.001 per resource (1,000 resources for $1).
-
- https://docs.x.com/x-api/getting-started/pricing#deduplication
-
Deduplication
-
All resources are deduplicated within a 24-hour UTC day window. If you request and are charged for a resource (such as a Post), requesting the same resource again within that window will not incur an additional charge.
-
-
- https://docs.x.com/tools-and-libraries
-
Tools & Libraries
-
Official X API SDKs in Python and TypeScript, CLI tools like xurl, the playground server, MCP servers for AI, and community client libraries.
- https://docs.x.com/tools-and-libraries#official-sdks
-
Official SDKs
-
Python SDK: Async support, type hints, and automatic token refresh. Perfect for data analysis and automation.
- https://docs.x.com/xdks/python/overview
-
Python XDK
-
Official Python client library for the X API v2. Supports OAuth, automatic pagination, real-time streaming, and all v2 endpoints with a clean, high-level interface.
-
- https://docs.x.com/xdks/python/overview
-
TypeScript SDK: Full type safety and ESM support. Works in Node.js and modern bundlers.
- https://docs.x.com/xdks/typescript/overview
-
TypeScript XDK
-
Official TypeScript/JavaScript SDK for the X API v2 with full type safety, automatic pagination, event-driven streaming, and support for all authentication methods.
-
- https://docs.x.com/xdks/typescript/overview
-
- https://docs.x.com/tools-and-libraries#developer-tools
-
Developer tools
-
Postman Collection: Interactive API testing for all v2 endpoints.
-
xurl: curl-like CLI for the X API with built-in OAuth authentication. No manual token management.
- https://docs.x.com/tools/xurl
-
xurl
-
xurl is a curl-style CLI for the X API with built-in OAuth handling, making it easy to test endpoints, sign requests, and prototype from the terminal.
- https://github.com/xdevplatform/xurl
-
xurl - A curl-like CLI Tool for the X API
-
The official CLI for the X API
-
- https://docs.x.com/tools/xurl#using-xurl-with-ai-agents
-
Using xurl with AI agents
-
xurl includes a
SKILL.mdfile that describes its capabilities in a machine-readable format. AI agents can use this to understand how to invoke xurl commands on your behalf.
-
-
- https://docs.x.com/tools/xurl
- etc
-
- https://docs.x.com/tools-and-libraries#community-libraries
-
Community libraries
-
Community-maintained libraries with X API v2 support. Check each library’s documentation for current coverage.
-
-
- https://docs.x.com/tools/skill-md
-
skill.md
-
skill.md is a structured capability summary that describes what AI agents can do with the X API, used by tools like agentskills.io to enable workflows.
-
The
skill.mdfile follows the agentskills.io specification and describes what AI agents can do with the X API. Whilellms.txtis a directory of pages,skill.mdis a capability summary — it lists specific actions, required inputs, and constraints so agents can use the API more reliably.
-
- https://docs.x.com/tools/mcp
-
MCP Servers
-
Connect AI tools to the X API using Model Context Protocol servers for calling endpoints, searching the docs, and building agent workflows.
- https://docs.x.com/tools/mcp#xmcp-—-x-api-endpoints
-
XMCP — X API endpoints
-
XMCP is an official MCP server that exposes X API endpoints as callable tools. Run it locally and connect any MCP-compatible client — like Cursor, Windsurf, or your own agent — to read and write to X programmatically.
-
- https://docs.x.com/tools/mcp#docs-mcp-—-documentation-search
-
Docs MCP — documentation search
-
An MCP server for the X API documentation is hosted at https://docs.x.com/mcp. Connect it to your AI tool to search and read documentation pages without leaving your workflow.
-
-
- https://docs.x.com/fundamentals/x-ids
-
X IDs
-
Understand 64-bit Snowflake IDs in the X API for Posts and users, including format, ordering, ID inflation, and safe handling in JavaScript clients. Documentation Index
-
Every object in the X API—posts, users, lists, DMs, spaces—has a unique ID. Understanding how these IDs work helps you build reliable integrations.
- https://docs.x.com/fundamentals/x-ids#id-format
-
ID format X IDs are 64-bit unsigned integers generated using a system called “Snowflake.” Each ID encodes:
- Timestamp — When the object was created
- Worker number — Which server generated the ID
- Sequence number — Order within that millisecond
This means IDs are roughly time-ordered: higher IDs generally represent newer objects.
-
- https://docs.x.com/fundamentals/x-ids#common-id-types
-
Common ID types
-
- Post (Tweet):
1234567890123456789(Also called Tweet ID) - User:
2244994945(Older accounts have shorter IDs) - List:
1234567890 - Space:
1YqGodQbNXDxv(Alphanumeric, not Snowflake format) - DM Event:
1234567890123456789
- Post (Tweet):
-
-
- https://docs.x.com/x-api/fundamentals/data-dictionary
-
Data Dictionary
-
Definitive reference for every field, object, expansion, and metric returned by the X API v2. Covers Posts, Users, Media, Spaces, Lists, Polls, Places, Communities, Direct Messages, and all possible expansions.
The X API returns rich structured JSON. This dictionary documents every available field for each object type.
- https://docs.x.com/x-api/fundamentals/data-dictionary#post-tweet
-
Post (Tweet)
-
Posts are the core content unit on X. Each Post object includes text, metadata, and references to related objects like authors, media, and polls.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#user
-
User
-
The user object contains Twitter user account metadata describing the referenced user. The user object is the primary object returned in the users lookup endpoint. When requesting additional user fields on this endpoint, simply use the fields parameter
user.fields.The user object can also be found as a child object and expanded in the Tweet object. The object is available for expansion with
?expansions=author_idor?expansions=in_reply_to_user_idto get the condensed object with only default fields. Use the expansion with the field parameter:user.fieldswhen requesting additional fields to complete the object.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#space
-
Space
-
Spaces allow expression and interaction via live audio conversations. The Space data dictionary contains relevant metadata about a Space; all the details are updated in real time.
User objects can be found and expanded in the user resource. These objects are available for expansion by adding at least one of
host_ids,creator_id,speaker_ids,mentioned_user_idsto theexpansionsquery parameter.Unlike Tweets, Spaces are ephemeral and become unavailable after they end or when they are canceled by their creator. When your app handles Spaces data, you are responsible for returning the most up-to-date information and must remove data that is no longer available from the platform. The Spaces lookup endpoints can help you ensure you respect the users’ expectations and intent.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#list
-
List
-
The list object contains Twitter Lists metadata describing the referenced List. The List object is the primary object returned in the List lookup endpoint. When requesting additional List fields on this endpoint, simply use the fields parameter
list.fields.The List object is not found as a child of other data objects. However, user objects can be found and expanded in the user resource. These objects are available for expansion by adding
owner_idto theexpansionsquery parameter. Use this expansion with thelist.fieldsfield parameter when requesting additional fields to complete the primary List object anduser.fieldsto complete the expansion object.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#media
-
Media
-
Media refers to any image, GIF, or video attached to a Tweet. The media object is not a primary object on any endpoint, but can be found and expanded in the Tweet object. The object is available for expansion with
?expansions=attachments.media_keysto get the condensed object with only default fields. Use the expansion with the field parameter:media.fieldswhen requesting additional fields to complete the object.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#poll
-
Poll
-
A poll included in a Tweet is not a primary object on any endpoint, but can be found and expanded in the Tweet object. The object is available for expansion with
?expansions=attachments.poll_idsto get the condensed object with only default fields. Use the expansion with the field parameter:poll.fieldswhen requesting additional fields to complete the object.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#place
-
Place
-
The place tagged in a Tweet is not a primary object on any endpoint, but can be found and expanded in the Tweet resource. The object is available for expansion with
?expansions=geo.place_idto get the condensed object with only default fields. Use the expansion with the field parameter:place.fieldswhen requesting additional fields to complete the object.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#direct-message-events
-
Direct Message events
-
Direct Message (DM) conversations are made up of events. The X API v2 currently supports three event types: MessageCreate, ParticipantsJoin, and ParticipantsLeave.
DM event objects are returned by the Direct Message lookup endpoints, and a MessageCreate event is created when Direct Messages are successfully created with the Manage Direct Messages endpoints.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary#community
-
Community
-
Communities are dedicated places for X users to connect, share, and get closer to the discussions they care about most.
Posts in Communities can be seen by anyone on X, but only others within the Community itself can engage and participate in the discussion.
The Community object contains relevant metadata about a Community.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference
-
Data Dictionary Reference
-
Complete field-level reference and example payloads for every X API v2 object — Posts, Users, Media, Spaces, Lists, Polls, Places, Communities, and Direct Message events.
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#how-to-use-fields-and-expansions
-
How to use fields and expansions
-
By default, the X API v2 data objects include a small number of default fields when making a request without the use of the fields or expansions parameters. This guide will show you how to use the
fieldsandexpansionsquery parameters in your request to receive additional objects and fields in your response.
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#x-api-v2-example-payloads
-
X API v2 Example payloads
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#tweet
-
Tweet
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#tweet-reply
-
Tweet reply
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#extended-tweet
-
Extended Tweet
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#tweet-with-media
-
Tweet with media
-
- https://docs.x.com/x-api/fundamentals/data-dictionary/reference#retweeted-quote-tweet
-
Retweeted quote Tweet
-
-
-
-
- https://devcommunity.x.com/t/announcing-the-python-and-typescript-xdks-for-the-x-api-v2/250860
-
Announcing the Python and Typescript XDKs for the X API v2
- https://docs.x.com/xdks/overview
-
XDK Overview
- https://docs.x.com/xdks/typescript/overview
-
TypeScript XDK
-
A comprehensive TypeScript SDK for the X API (formerly Twitter API) with advanced features including smart pagination, multiple authentication methods, real-time streaming, and full type safety.
-
- https://docs.x.com/xdks/python/overview
-
Python XDK
-
The Python XDK (X Developer Kit) is our official client library for interacting with the X API v2 using Python. It allows developers to get started with our API quickly and build applications with it. It is generated based on our official OpenAPI specification. It abstracts away low-level HTTP details while providing fine-grained control when needed.
-
-
- https://github.com/xdevplatform
-
X Developer Platform Resources
- https://github.com/xdevplatform/xdk
-
XDK Project
-
SDK generator for the X APIs.
-
Welcome to the XDK project! This repository contains a collection of Rust crates designed for generating and managing SDKs (or XDKs) for the X APIs. The project focuses on creating high-quality, type-safe SDKs for various programming languages, starting with Python, from OpenAPI specifications.
-
-
-
- https://github.com/informigados/x-archive-explorer
-
X Archive Explorer 🚀
-
Local-first explorer for archived X (Twitter) posts and replies.
-
Import your official X export, index everything locally, search with filters, and export results. No scraping as the primary strategy.
-
- https://github.com/rollecode/tweets
-
🐦 tweets - Yet another Twitter archive viewer
-
A modern, responsive interface for viewing your Twitter archive data locally. This tool transforms your downloaded Twitter data into an elegant, Twitter-like interface that you can browse offline - with minimal effort!
-
The goal: Single index.html file with JS, CSS and HTML - No compiling, no frameworks, no dependencies.
-
- https://github.com/Dicklesworthstone/xf
-
xf
-
Search your X/Twitter data archive from the command line with sub-millisecond full-text queries via Tantivy and SQLite
-
Ultra-fast CLI for searching and querying your X data archive with sub-millisecond latency.
-
TL;DR
- The Problem: X lets you download all your data, but actually finding anything in that archive is painful. The built-in HTML viewer is slow and clunky, there's no real search, and your data is scattered across separate files.
- The Solution:
xfindexes your X (formerly Twitter) data export and provides blazingly fast full-text search across tweets, likes, DMs, and Grok conversations—all from the command line.
-
- https://github.com/mathowie/tweet-search-archive
-
Tweet Archive Search
-
a twitter search archive that works off JSON files, all vibe coded in Claude.
-
A self-contained, offline-friendly search page for your Twitter/X archive. No server, no database, no dependencies — just a Python build script and a single HTML file you open in any browser.
-
- https://github.com/timhutton/twitter-archive-parser
-
twitter-archive-parser
-
Python code to parse a Twitter archive and output in various ways
-
What does it do?
The Twitter archive gives you a bunch of data and an HTML file (
Your archive.html). Open that file to take a look! It lets you view your tweets in a nice interface. It has some flaws but maybe that's all you need. If so then stop here, you don't need our script.Flaws of the Twitter archive:
- It shows you tweets you posted with images, but if you click on one of the images to expand it then it takes you to the Twitter website. If you are offline or have deleted your account or twitter.com is down then that won't work.
- The tweets are stored in a complex JSON structure so you can't just copy them into your blog for example.
- The images they give you are smaller than the ones you uploaded. I don't know why they would do this to us.
- DMs are included but don't show you who they are from - many of the user handles aren't included in the archive.
- The links are all obfuscated in a short form using t.co, which hides their origin and redirects traffic to Twitter, giving them analytics. Also they will stop working if t.co goes down.
Our script does the following:
- Converts the tweets to markdown and also HTML, with embedded images, videos and links.
- Replaces t.co URLs with their original versions (the ones that can be found in the archive).
- Copies used images to an output folder, to allow them to be moved to a new home.
- Will query Twitter for the missing user handles (checks with you first).
- Converts DMs (including group DMs) to markdown with embedded media and links, including the handles that we retrieved.
- Outputs lists of followers and following.
- Downloads the original size images (checks with you first).
- https://github.com/timhutton/twitter-archive-parser#related-tools
-
Related tools
-
If our script doesn't do what you want then maybe a different tool will help
-
-
- https://github.com/tweetback/tweetback
-
tweetback Twitter Archive
-
Take ownership of your Twitter data. First talked about at Jamstack Conf 2019 and in this blog post.
-
- https://github.com/steipete/birdclaw
-
birdclaw 🪶 — Local Twitter memory in SQLite: archives, DMs, likes, bookmarks
-
Stores all your tweets nicely claw-able for agents.
-
birdclawis a local-first Twitter workspace: archive import, cached live reads, focused triage, and reply flows in one local web app + CLI. Built by @steipete. -
Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and rough edges while the core settles.
- steipete/birdclaw#93
-
Issue #93: Clarify status of optional bird transport
-
- steipete/birdclaw#99
-
Issue #99: Add an FxTwitter read transport for public lookups, threads, profiles, and search
-
- steipete/birdclaw#98
-
Issue #98: Import Scrollmark and twitter-web-exporter captures into the canonical local store
-
-
- https://bird.fast/
-
bird.fast
-
Fast X CLI. Cookie auth. Zero fuss.
-
Tweet, reply, read threads, search, mentions — powered by X/Twitter GraphQL and your existing browser session.
-
How it works
- Use your existing session: Cookie auth. No password prompt. Reads from Safari/Chrome/Firefox (configurable).
- GraphQL first: Talks to X’s internal endpoints. Query IDs rotate — bird auto-refreshes and caches them.
- Fast, scriptable output: Human output by default. Flip to
--jsonor--plainwhen you need stability.
-
bird.fast is the landing page; the project lives on GitHub.
-
- https://github.com/steipete/bird
- Note: Was made private, no longer available officially
- https://github.com/steipete/steipete#cli-utilities--home-ops
-
🐦 bird - Fast X CLI for tweeting, replying, and reading (had to make it private)
- https://github.com/steipete/steipete/commit/fe87f3c406234ca7b3e5036897d9231c4665641a
-
Feb 18, 2026
-
Update 'bird' project status to private Updated the description for the 'bird' project to indicate it is now private.
-
- https://github.com/steipete/steipete/commit/fe87f3c406234ca7b3e5036897d9231c4665641a
-
- https://github.com/steipete/steipete#cli-utilities--home-ops
- Forks / Re-Uploads:
- https://github.com/search?q=%22%5C%22build%3Abinary%5C%22%3A+%5C%22BIRD_VERSION%22&type=code
- https://github.com/forkoooor/bird (330 commits, Latest Commit: 4635108 (Jan 25, 2026),
v0.8.0)- https://github.com/leavingme/bird (342 commits, Latest Commit: b26a081 (Apr 9, 2026),
v0.8.3)
- https://github.com/leavingme/bird (342 commits, Latest Commit: b26a081 (Apr 9, 2026),
- https://github.com/jawond/bird (52 commits, Latest Commit: c0d08f3 (Dec 6, 2025),
v0.1.0)- https://github.com/jawond/bird/forks
- https://useful-forks.github.io/?repo=jawond/bird
- https://github.com/rymalia/bird (333 commits, Latest Commit: d530105 (Feb 24, 2026),
v0.8.1)- https://github.com/cloud-algodyn/bird (338 commits, Latest Commit: 2743797 (Jun 5, 2026),
v0.8.2)
- https://github.com/cloud-algodyn/bird (338 commits, Latest Commit: 2743797 (Jun 5, 2026),
- https://github.com/jscraik/xKit (264 commits, Latest Commit: 70b9e28 (Feb 1, 2026))
- https://github.com/dannote/bird-premium (332 commits, Latest Commit: f87497e (Jan 23, 2026))
-
bird-premium 🐦💰
-
X/Twitter CLI using the official paid API v2. A fork of steipete/bird that uses the documented API instead of undocumented GraphQL endpoints.
- https://www.npmjs.com/package/@dannote/bird-premium
-
- https://github.com/jawond/bird/forks
- https://github.com/devskale/peep
-
peep 👀 - fast X CLI for tweeting, replying, and reading
-
a branch out of steipete's bird project
-
- Package Managers:
- https://www.npmjs.com/package/@steipete/bird
-
@steipete/bird
-
0.8.0 • Public • Published 6 months ago
-
This package has been deprecated
- https://www.npmjs.com/package/@steipete/bird?activeTab=versions
- To download a snapshot of all the versions locally, we can use
npm view <package> versions+npm pack <package@version>in a loop:pkg='@steipete/bird'; npm view "$pkg" versions --json | jq -r '.[]' | while IFS= read -r version; do npm pack "${pkg}@${version}"; done
- To download a snapshot of all the versions locally, we can use
-
- https://www.jsdelivr.com/package/npm/@steipete/bird
- https://www.npmjs.com/package/@steipete/bird
- Wayback Machine:
- Calendar: https://web.archive.org/web/20260000000000*/https://github.com/steipete/bird
- Seems like the last available snapshot was Feb 6, 2026:
- https://web.archive.org/web/20260206041856/https://github.com/steipete/bird
-
bird 🐦 — fast X CLI for tweeting, replying, and reading
-
birdis a fast X CLI for tweeting, replying, and reading via X/Twitter GraphQL (cookie auth). -
Disclaimer
This project uses X/Twitter’s undocumented web GraphQL API (and cookie auth). X can change endpoints, query IDs, and anti-bot behavior at any time — expect this to break without notice.
- https://web.archive.org/web/20260206041856/https://github.com/steipete/bird?tab=readme-ov-file#install
-
Install
-
npm install -g @steipete/bird -
pnpm add -g @steipete/bird -
bun add -g @steipete/bird -
bunx @steipete/bird whoami -
brew install steipete/tap/bird- https://github.com/steipete/homebrew-tap
- steipete/homebrew-tap#13
-
Issue #13: bird is gone
-
Fixed by
2f5466a: the stale bird formula was removed from this tap after the release asset disappeared.
-
- https://github.com/search?q=repo%3Asteipete%2Fhomebrew-tap+bird&type=commits
-
chore: remove bird formula
- https://github.com/steipete/homebrew-tap/commit/2f5466a63b44d048688ae15c5298be921f8849db
-
bird 0.8.0
- https://github.com/steipete/homebrew-tap/commit/1c8432f415933d33372ef3d10392d3f3c4fb68c7
-
url "https://github.com/steipete/bird/releases/download/v0.8.0/bird-macos-universal-v0.8.0.tar.gz" sha256 "3d89bb404e8b0ed4ef331f0dc62d873852634ca2a814ae7a4ac7effc114320cf" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.8.0/bird-macos-universal-v0.8.0.tar.gz
-
chore: bump bird to v0.7.0
- https://github.com/steipete/homebrew-tap/commit/a0fe061f9ef74a98b87f86376e4dd0b610ab6f43
-
url "https://github.com/steipete/bird/releases/download/v0.7.0/bird-macos-universal-v0.7.0.tar.gz" sha256 "5e8d0bd15cc5e4438d3fd025998cdce7da91d7193d1d268b15f031ada9d2d30d" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.7.0/bird-macos-universal-v0.7.0.tar.gz
-
bird 0.6.0
- https://github.com/steipete/homebrew-tap/commit/1867ff7ef10d53fb3d14295f53c47d20b5a7d82c
-
url "https://github.com/steipete/bird/releases/download/v0.6.0/bird-macos-universal-v0.6.0.tar.gz" sha256 "1ddbf234192af7a454afbbcdb431efa9b2afe7bc3f4952d5dc05a26a231439da" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.6.0/bird-macos-universal-v0.6.0.tar.gz
-
bird 0.5.1
- https://github.com/steipete/homebrew-tap/commit/d83fdc0a6185fa1e206fadf050e02c2113a98eca
-
url "https://github.com/steipete/bird/releases/download/v0.5.1/bird-macos-universal-v0.5.1.tar.gz" sha256 "7e95b1bd41bc9d9bbd55848dbe4cd9e72e6794b64ae8ef7b6b95160cf80d923d" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.5.1/bird-macos-universal-v0.5.1.tar.gz
-
chore(brew): bird 0.5.0
- https://github.com/steipete/homebrew-tap/commit/c8ed8e56e76d41de1f59bd13b731e5733fad6b5c
-
url "https://github.com/steipete/bird/releases/download/v0.5.0/bird-macos-universal-v0.5.0.tar.gz" sha256 "dbfe85420f57a3fcfc05c397d55461fec64ebb794b230dd805bed3826c737fbe" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.5.0/bird-macos-universal-v0.5.0.tar.gz
-
bird 0.4.0
- https://github.com/steipete/homebrew-tap/commit/b0e23db9342a65ef2d35900002636f8e1bab9678
-
url "https://github.com/steipete/bird/releases/download/v0.4.0/bird-macos-universal-v0.4.0.tar.gz" sha256 "51f82698414b14cc2237ec3fcdcfa4f39df3e1ff4476f6b2190bc073c6bc6365" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.4.0/bird-macos-universal-v0.4.0.tar.gz
-
bird 0.3.0
- https://github.com/steipete/homebrew-tap/commit/a7dd97c48a0049ba0f27cdcec289a60becd680fd
-
url "https://github.com/steipete/bird/releases/download/v0.3.0/bird-macos-universal-v0.3.0.tar.gz" sha256 "8b64ce93f32562ce823cef0f906d8104c00a39655766ebc441d2a0da7830b7b1" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.3.0/bird-macos-universal-v0.3.0.tar.gz
-
chore(bird): bump to 0.2.0
- https://github.com/steipete/homebrew-tap/commit/0ecf6259285decdf298d49dbd3f8f5c4ed834fd1
-
url "https://github.com/steipete/bird/releases/download/v0.2.0/bird-macos-universal-v0.2.0.tar.gz" sha256 "7a10066b6c3c30507920e2461ccf55b6046bdfb051e9baf69039f19c5f3c24ea" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.2.0/bird-macos-universal-v0.2.0.tar.gz
-
bird 0.1.0
- https://github.com/steipete/homebrew-tap/commit/d52774904697441b7434b4c1c817433eb63ead4c
-
url "https://github.com/steipete/bird/releases/download/v0.1.0/bird-macos-universal-v0.1.0.tar.gz" sha256 "55b68e41f004e3ade4cdafaa5e650af600f30a37ad1995ee0d91a96823b58a95" - Seemingly not archived on Wayback Machine: https://web.archive.org/web/*/https://github.com/steipete/bird/releases/download/v0.1.0/bird-macos-universal-v0.1.0.tar.gz
-
- steipete/homebrew-tap#13
- https://github.com/steipete/homebrew-tap
-
- This seems to show:
- Latest Commit:
d8c2181("2 months ago" at time of snapshot) - Latest Release:
v0.8.0("3 weeks ago" at time of snapshot)
- Latest Commit:
-
- https://web.archive.org/web/20260206041856/https://github.com/steipete/bird
- Seems like the last available snapshot was Feb 6, 2026:
- URLs: https://web.archive.org/web/*/https://github.com/steipete/bird*
- https://web.archive.org/web/*/https://github.com/steipete/bird/archive*
- Listing for the redirects: https://web.archive.org/web/*/https://codeload.github.com/steipete/bird*
- https://web.archive.org/web/20260107164926/https://github.com/steipete/bird/archive/refs/tags/v0.2.0.tar.gz
- https://web.archive.org/web/20260107135322/https://github.com/steipete/bird/archive/refs/tags/v0.2.0.zip
- https://web.archive.org/web/20260105091159/https://github.com/steipete/bird/archive/refs/tags/v0.3.0.zip
- https://web.archive.org/web/20260115091907/https://github.com/steipete/bird/archive/refs/tags/v0.6.0.tar.gz
- https://web.archive.org/web/20260107205726/https://github.com/steipete/bird/archive/refs/tags/v0.6.0.zip
- https://web.archive.org/web/*/https://github.com/steipete/bird/archive*
- Calendar: https://web.archive.org/web/20260000000000*/https://github.com/steipete/bird
- Note: Was made private, no longer available officially
- https://github.com/kmccleary3301/scrollmark
-
Scrollmark
-
Local-first X/Twitter research archive, search workbench, media viewer, and portable bundle system.
-
Built for researchers, designers, engineers, and high-signal collectors who need to preserve, search, inspect, and share what flows through the X web app without sending private archives to a cloud service.
-
Scrollmark is maintained by Kyle McCleary. It began as an MIT-licensed fork of prinsss/twitter-web-exporter, but has since been rebuilt and overhauled across capture, search, storage, UI, bundle import/export, diagnostics, performance, branding, and release workflows. Original copyright and license notices are preserved.
- https://github.com/prinsss/twitter-web-exporter
-
twitter-web-exporter
-
Export tweets, bookmarks, lists and much more from Twitter(X) web app.
-
Once the script is installed, you can find a floating panel on the left side of the page. Click the 🐈 Cat button to close the panel or open it again. You can also open the control panel by clicking the Tampermonkey/Violentmonkey icon in the browser menu bar and then selecting it from the script menu.
- https://greasyfork.org/en/scripts/492218-twitter-web-exporter
-
- https://github.com/prinsss/twitter-web-exporter
-
What Scrollmark does
Scrollmark runs as a userscript on
x.com,twitter.com, andmobile.x.com. It observes the same GraphQL/API responses that the X web app loads while you browse, parses useful structures out of those responses, stores them locally in IndexedDB, and gives you a fast explorer for search, review, export, and sharing.It is intentionally not a cloud product, not a bot, and not a Twitter/X developer API client. The core idea is simple: if the web app loads useful research material into your browser, Scrollmark can help you preserve and query it locally.
-
- https://github.com/FxEmbed/FxEmbed
-
FxEmbed
-
Home of FxTwitter, FixupX, and FxBluesky
-
Fix X/Twitter and Bluesky embeds! Use multiple images, videos, polls, translations and more on Discord, Telegram and others
- https://docs.fxembed.com
-
FxEmbed
-
What is FxEmbed?
FxEmbed powers FxTwitter, FixupX, and FxBluesky — services that turn social media links into rich embeds with videos, polls, quotes, translations, and more. Just change a URL prefix and share it in Discord, Telegram, or anywhere that supports link previews.
-
Public API: Use the public FxTwitter and FxBluesky APIs to power your own projects.
- https://docs.fxembed.com/api/introduction/
-
API Overview
-
FxEmbed provides a public API for X/Twitter and Bluesky. These are the same APIs that power the embed service itself and more.
-
We’ve built abstractions between different platforms to make it easier to support multiple at once and adapt your own work between them.
- https://docs.fxembed.com/api/introduction/#fxtwitter-api
-
FxTwitter API
-
The most feature-complete API, covering X/Twitter data.
- https://docs.fxembed.com/api/twitter/
-
FxTwitter API
-
-
-
- https://docs.fxembed.com/api/introduction/
- https://docs.fxembed.com/deployment/
-
Self-Hosting
-
FxEmbed is designed to run on Cloudflare Workers, which are free for up to 100,000 requests per day per account. Because Workers run on Cloudflare’s global edge network, your instance will automatically be distributed worldwide for low latency.
- https://docs.fxembed.com/deployment/credentials/
-
Credentials
-
FxEmbed works best with X/Twitter account credentials to fetch post data from the Twitter API. Without them, you will have lower rate limits and not be able to fetch NSFW posts.
-
-
-
-
- https://github.com/fa0311/TwitterFrontendFlow
-
Unofficial Client for Twitter Internal API
-
- https://github.com/tweepy/tweepy
-
Tweepy: Twitter for Python!
-
- https://github.com/tsukumijima/tweepy-authlib
-
A library for authenticating with a screen name and password on Tweepy using the internal API of the Twitter Web App (official web client).
-
- https://github.com/fa0311/twitter-openapi-typescript
-
Implementation of Twitter internal API (Twitter graphql API) in TypeScript
-
- https://github.com/fa0311/twitter_openapi_python
-
Implementation of Twitter internal API (Twitter graphql API) in Python with data validation by pydantic
-
- https://github.com/fa0311/twitter-openapi
-
OpenAPI(Swagger) specification of Twitter Internal API (Twitter graphql API)
-
- https://github.com/fa0311/TwitterInternalAPIDocument
-
Reverse engineering of the web version of Twitter.
- https://github.com/fa0311/TwitterInternalAPIDocument#reverse-engineering-note
- https://github.com/fa0311/TwitterInternalAPIDocument/blob/master/docs/markdown/RE.md
-
Twitter Reverse Engineering Document
-
- https://github.com/fa0311/TwitterInternalAPIDocument/blob/master/docs/markdown/RE.md
- https://deepwiki.com/fa0311/TwitterInternalAPIDocument/1-overview
- https://github.com/fa0311/antibot_blog_archives
-
antibot_blog_archive
-
Dear obfio archives. About Twitter X-Client-Transaction-Id.
-
Blog
- https://fa0311.github.io/antibot_blog_archives/web/twitter-header-part-1.html
-
Twitter Header: Part 1, Deobfuscation (10/03/2025)
- https://web.archive.org/web/20260409204359mp_/https://antibot.blog/posts/1741552025433
-
- https://fa0311.github.io/antibot_blog_archives/web/twitter-header-part-2.html
-
Twitter Header: Part 2, Reverse Engineering (10/03/2025)
- https://web.archive.org/web/20260306063150/https://antibot.blog/posts/1741552092462
-
- https://fa0311.github.io/antibot_blog_archives/web/twitter-header-part-3.html
-
Twitter Header: Part 3, Generating the header (10/03/2025)
- https://web.archive.org/web/20260311114430/https://antibot.blog/posts/1741552163416
-
- https://fa0311.github.io/antibot_blog_archives/web/twitter-header-part-1.html
-
Source
- https://github.com/fa0311/twitter-tid-deobf
-
twitter-tid-deobf
-
Deobfuscation for the Twitter X-Client-Transaction-Id header. For part 1 of my blog at https://web.archive.org/web/20241205164726/https://antibot.blog/twitter/
-
- https://github.com/fa0311/twitter-tid-script-cleaned
-
twitter-tid-script-cleaned
-
Cleaned script and POC for the
X-Client-Transaction-Idheader used on twitter. Made for part 2 of my blog reverse engineering the header on https://web.archive.org/web/20260306063150/https://antibot.blog/posts/1741552092462
-
- https://github.com/fa0311/twitter-tid-generator
-
twitter-tid-generator
-
This project is a fully working generator for the
X-Client-Transaction-Idheader on Twitter, used in a majority of API requests. -
This was made for my blog at https://web.archive.org/web/20260311114430/https://antibot.blog/posts/1741552163416 and was a three part series! I hope someone can get some use out of this, or maybe just learn from how I went about porting the code, the issues I ran into, etc.
-
- https://github.com/fa0311/twitter-tid-deobf
-
Other
-
Clean up by fa0311
- https://github.com/fa0311/twitter-tid-deobf-fork
-
twitter-tid-deobf-fork
-
This repository performs deobfuscation of the
X-Client-Transaction-Idheader used on Twitter. It is automatically updated daily at 22:00 UTC. -
It is a fork of https://github.com/obfio/twitter-tid-deobf.
-
Output: https://github.com/fa0311/twitter-tid-deobf-fork/blob/main/output/additional2.js
-
- https://github.com/fa0311/twitter-tid-deobf-fork
-
Python implementation by iSarabjitDhiman
- https://github.com/iSarabjitDhiman/XClientTransaction
-
X-Client-Transaction-ID
-
Twitter X-Client-Transaction-Id generator written in python.
- https://github.com/swyxio/XClientTransactionJS
-
X-Client-Transaction-ID
-
JavaScript implementation of Twitter/X's Client-Transaction-ID generator for API requests.
-
Note: This is a JavaScript port of the original Python implementation. The Python version may be more actively maintained and include additional features.
-
-
- https://github.com/iSarabjitDhiman/TweeterPy/commits/master/tweeterpy/tid
- added in
3e2616d - replaced with
XClientTransactionin9b87eef
- added in
- https://github.com/iSarabjitDhiman/XClientTransaction
-
Python implementation by d60
-
JavaScript implementation by BANKA2017
- https://blog.nest.moe/posts/twitter-header-part-4
-
Twitter header: part 4
-
This is a supplementary article to update the information on changes one year later. Please see the first three parts at https://antibot.blog/ (Note: I am not the author of the first three parts).
-
- https://github.com/BANKA2017/twitter-monitor/blob/node/libs/core/Core.xClientTransactionID.mjs
- https://blog.nest.moe/posts/twitter-header-part-4
-
-
-
- https://github.com/glizzykingdreko/twitter-generator
-
Twitter / X Headers Generator
-
Python module with an attached reverse-engineering study of the X (former Twitter)
x-client-transaction-idandx-xp-forwarded-forrequest headers. -
Be sure to check out LEARN.md in order to understand how this has been made
- https://github.com/glizzykingdreko/twitter-generator/blob/main/LEARN.md
-
How did we get here?
-
This is the first of a 3 episodes series about X (former Twitter) login flow protections.
Be sure to check the related Medium Article and subscribe to my newsletter (from Medium) in order to get notified when the next episode will be out (a follow on Github would make me happy too).
Next one is going to be about the first antibot challenge.
- https://medium.com/@glizzykingdreko/x-former-twitter-login-flow-reconstruction-episode-1-headers-generation-68a23add39e1
-
X (former Twitter) Login flow reconstruction. Episode 1 — Headers generation (Jan 14, 2026)
- https://glizzykingdreko.com/x-former-twitter-login-flow-reconstruction-episode-1-headers-generation-68a23add39e1
-
-
- https://github.com/glizzykingdreko/twitter-generator/blob/main/LEARN.md
-
- https://github.com/glizzykingdreko/x-twitter-ui-metrics
-
X Twitter UI Metrics Solver
-
Multi-language solver for the X (former Twitter)
ui_metricschallenge with reverse-engineering study. Implementations in Python, Go and Node.js. All based on the same regex-based logic. -
This repository is the Episode 2 of a multi-part analysis of Twitter/X login-flow protections.
- https://glizzykingdreko.com/x-former-twitter-login-flow-reconstruction-episode-2-ui-metrics-549eab0c0f9c
-
X (former Twitter) Login flow reconstruction. Episode 2 — UI Metrics (Feb 21, 2026)
-
-
- https://github.com/she-llac/twitter-graphql-scraper
-
twitter-graphql-scraper
-
Extracts GraphQL endpoint IDs (query hashes) from Twitter/X's web client.
-
Twitter's internal GraphQL API requires these IDs to make requests. They rotate periodically, so this tool scrapes the current values from the JavaScript bundles.
- https://github.com/she-llac/twitter-graphql-scraper#extra-headers-for-some-endpoints
-
Extra Headers for Some Endpoints
-
Some endpoints require two additional headers or they return 404:
x-client-transaction-id: ~70-100 char base64 string, single-use per requestx-xp-forwarded-for: 512 hex chars, session-based with expiry
-
Known endpoints that require these headers:
FollowersSearchTimeline/i/api/1.1/saved_searches/list.json/i/api/1.1/account/settings.json- (likely others)
-
These values are validated server-side (random values won't work). Both have been reverse-engineered:
- Transaction ID: XClientTransaction (Python) / XClientTransactionJS (JS)
- XP-Forwarded-For: twitter-x-xp-forwarded-for-header — AES-GCM encrypted JSON using SHA-256(static_key + guest_id), valid for 5 minutes
- https://github.com/dsekz/twitter-x-xp-forwarded-for-header
-
Twitter X-Xp-Forwarded-For Header Reverse Engineering & Generator
-
A few days ago, Twitter quietly added a new header: "X-Xp-Forwarded-For".
As soon as I noticed it, I started digging in, turns out it's being generated inside a WASM. I poked around the WASM and some of the JS, and saw it was pulling in some basic fingerprint data and grabbing stuff from cookies.
Hold up... did Twitter just build their own anti-bot system?
- https://github.com/dsekz/twitter-x-xp-forwarded-for-header#reverse-engineering
-
Reverse Engineering
-
This setup manages Twitter’s XPForwardedFor SDK, which is used to generate a encrypted header (XPFF). You can find the files I mentioned in the reverse engineering folder.
- https://github.com/dsekz/twitter-x-xp-forwarded-for-header/tree/main/reverse%20engineering
-
-
-
-
- https://github.com/Thytu/XRSS
-
🌟 XRSS - Your Twitter Feed, RSS-ified and Supercharged!
-
Transform tweets into an elegant RSS feed: custom filters, user tracking, and real-time updates.
-
Transform your Twitter experience into a delightful, distraction-free RSS feed!
-
XRSS is your passport to a cleaner, more organized Twitter experience. It transforms the chaotic Twitter timeline into a structured, filterable RSS feed that puts YOU in control. Perfect for researchers, developers, and anyone who loves their content well-organized!
-
- https://github.com/zedeus/nitter
-
Nitter
-
Alternative Twitter front-end
-
A free and open source alternative Twitter front-end focused on privacy and performance.
-
- https://x.com/search?q=%22twitter%22%20-%22twitter.com%22%20(from%3A_devalias)&src=typed_query&f=top
Analysis of X's NotificationsTimeline GraphQL API, including request parameters, instruction types, entry and cursor structures, and pagination behaviour. Covers cursor/ID encoding (Base64 / Apache Thrift) and shows that older notifications appear to be unretrievable via the API beyond a cutoff, with notes on unread-state mutation instructions and limited recovery via advanced search.
- https://x.com/_devalias/status/2003233313339900130
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
I didn't realise there was a cap on how far back twitter notifications go...
Guess I shouldn't have ignored looking through / triaging them for so long 😅
October 10th seems to be my oldest.
- https://x.com/_devalias/status/2003237160418410999
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
I wonder if this is just another UI limitation thing, and the raw requests would actually let me keep 'scrolling'.
@sucralose__ I feel like you might have explored something adjacent to this at one point; did you know off hand if it's possible by chance?
-
- https://x.com/_devalias/status/2003240898059223269
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Looks like the main GraphQL API request is to:
https://x.com/i/api/graphql/Ev6UMJRROInk_RMH2oVbBg/NotificationsTimeline?variable...&features=...With the
variablesparam set to:{ "timeline_type": "All", "count": 20 }(The
featuresparam is also a large encoded JSON object, but less directly relevant here.)
-
- https://x.com/_devalias/status/2003244816260645126
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
That returns a deep JSON object structure; where the most interesting parts (for this use case at least) seem to be at:
.data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]
-
- https://x.com/_devalias/status/2003245626012893338
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Within that array, there seem to be a bunch of different object types, such as:
TimelineClearCacheTimelineRemoveEntries(entry_ids: [])TimelineAddEntries(entries: [])TimelineClearEntriesUnreadStateTimelineMarkEntriesUnreadGreaterThanSortIndex(sort_index: "")
-
- https://x.com/_devalias/status/2003247614649860534
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
In
TimelineAddEntries's.entries, we can see that each entry is an object with the shape:{ "entryId": "", "sortIndex": "", "content": {} }Where
entryIdhas shapes like:cursor-top-[sortIndex-timestamp]notification-[id]cursor-bottom-[sortIndex-timestamp]
-
- https://x.com/_devalias/status/2003251212230246907
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
For the
cursor-[top/bottom]-[sortIndex]entries, theircontentobject looks like:{ "entryType": "TimelineTimelineCursor", "__typename": "TimelineTimelineCursor", "value": "", "cursorType": "" }Where
cursorTypeis one ofTop/Bottom, andvalueis the cursor ID.
-
- https://x.com/_devalias/status/2003275081490075953
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
While it's probably not super relevant, the cursor value doesn't seem to be fully opaque; it appears to be Base64 encoded bytes that seem to be Apache Thrift struct format.
-
- https://x.com/_devalias/status/2003275987283509537
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
If you want to know more about decoding Apache Thrift byte format into something more readable, check out this ~2018 post / tool (
readable-thrift) from @jamchamb_ / @NCCGroupInfosec
-
- https://x.com/_devalias/status/2003276696259383746
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
For our needs here, we probably don't need to bother going much deeper at this stage; though I will note that the i64 in field 1 appears to match the
.data.viewer_v2.user_results.result.rest_idvalue.
-
- https://x.com/_devalias/status/2003281283544416517
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Also the
.data.viewer_v2.user_results.result.notification_timeline.idappears to be a Base64 encoded string prefix `Timeline:``, followed by Apache Thrift struct bytes that contains 2 fields:- i64 (8 bytes) matching my user
rest_id - i32 (4 bytes) containing the number
1
- i64 (8 bytes) matching my user
-
- https://x.com/_devalias/status/2003287594646528158
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Climbing out of that rabbit hole.. back to trying the most basic thing, which is to just take the bottom cursor and query for more notification entries with that; we unfortunately seem to get an essentially empty result, which only includes a
cursor-top-[sortIndex]entry.
-
- https://x.com/_devalias/status/2003287893884948891
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
So it looks like, at least based on the above research, as far as accessing via this API goes; those older notifications are likely lost forever :(
Guess that is incentive for me to check more often... 😅
-
- https://x.com/_devalias/status/2003289523879575825
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Though something else I did notice is that the 'bottom' cursor I used, and the 'top' cursor I received in that empty response only differed by the Apache Thrift field ID.
- 1 (
DAAB...) is a 'Top' cursor - 2 (
DAAC...) is a 'Bottom' cursor
- 1 (
-
- https://x.com/_devalias/status/2003290867835285632
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Also worth noting for future is the
.data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]types:TimelineClearEntriesUnreadStateTimelineMarkEntriesUnreadGreaterThanSortIndexAs everything getting marked read when I open notifications is annoying.
-
- https://x.com/_devalias/status/2003291231129072099
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
There'll no doubt be corresponding API calls triggered based on that that actually updates the 'unread' status on the backend; but that can be a future distraction to explore.
-
- https://x.com/_devalias/status/2003295675518447966
-
Glenn ‘devalias’ Grant @_devalias (Dec 23, 2025)
Back to the 'lost' notifications, I can use advanced search to attempt to figure out which where replies I never got back to at least:
(to:_devalias) (-from:_devalias) since:2025-01-01 until:2025-12-31And then click onto the 'Latest' tab to get chronological ordering.
-
-
A spontaneous attempt to catch up on two weeks of notifications leads into a DOM and React fibre deep-dive to extract and count tweet data, quantify the backlog, and lay groundwork for future tooling.
- https://x.com/_devalias/status/1924368431979335819
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
That feel when you ignore your 'notify on post' notifications for like 2 weeks, and then you finally go to procrastinate by catching up on them, and scroll and scroll, and then instead of actually reading them you start to wonder how you can count how many need triaging...
- https://x.com/_devalias/status/1924368434261012764
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...so you accidentally find yourself in the familiar rabbit hole of skimming through the DOM looking for relevant bits to build selectors off, and then remembering that the timeline only renders a slice of the list to the DOM so you can't just count rendered elements...
-
- https://x.com/_devalias/status/1924368436383326540
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...but then you remember about React fibres and start looking through those to see if you can find where the full list of notification tweets are, and you find it but it's just a list of IDs, so you crawl deeper into the rabbit hole until you get a reference to the datastore...
-
- https://x.com/_devalias/status/1924368440992932182
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...and then with all of that, you build up some little helper functions and gadgets to stitch the data together, then a few more to process that hydrated array and count the tweets per day/user/etc, and then finally end up knowing that you have ~560 tweets to look through...
-
- https://x.com/_devalias/status/1924368443606028586
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
...but by that stage you've run out of time in the day to actually look through them all... 😅
At least I can quantify how much effort it will take now; which should help remove the 'do I have enough time for this' paralysis and maybe actually process them more often.
-
- https://x.com/_devalias/status/1924368445711515736
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Today's deepdive was probably also the most productive one yet for finding the bits and pieces I need to pull together a user script / Chrome extension to help make this 'keep up to date' / 'triage' process much less painful / time consuming...
But that's a story for another day
-
- https://x.com/_devalias/status/1924369583751987497
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Also, shoutout to the top 3 by post count over this period — forever keeping me busy trying to keep up with your interesting tidbits:
- @testingcatalog : 109 posts (19.5%)
- @btibor91 : 92 posts (16.4%)
- @EMostaque : 53 posts (9.5%)
-
- https://x.com/_devalias/status/1924656001812455848
-
Glenn 'devalias' Grant @_devalias (May 19, 2025)
Crossposted:
-
-
Suggestions for features like unread tracking, author grouping, and timeline recall for accounts marked “notify on post.” Includes related gists and prior threads.
- https://x.com/_devalias/status/1870292640320892955
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 Anyone know of any good browser userscripts or similar tools to make twitter notifications nicer to work with? I’d love to be able to easily see how many I have unread + jump back to the last one I saw.
- https://x.com/_devalias/status/1870292643231785338
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 Maybe also to group/count by author or similar too.
And potentially be able to mark some as ‘seen’ while leaving others ‘unread’ or otherwise ‘saved’ to review closer later.
-
- https://x.com/_devalias/status/1870292646373044590
-
Glenn 'devalias' Grant @_devalias Dec 21, 2024 I guess I should say that this is mostly for accounts I have on ‘notify on post’ specifically.
-
- https://x.com/_devalias/status/1870731056464429308
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 Got bored this afternoon and decided to update my "Notes on API/userscript to improve Twitter 'Notifications Timeline'" research gist related to this: https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#notes-on-apiuserscript-to-improve-twitter-notifications-timeline
Specifically this section: https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#timeline-notes-2
-
- https://x.com/_devalias/status/1870736469989654544
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 And here's one of my older threads on how painful this manual process is, including some stats about time taken, and musings on things that would be helpful along the way:
-
-
Walkthrough of the manual backlog review process, challenges of staying on top of notifications, and efforts to batch/sort tweet URLs for focused review.
- https://x.com/_devalias/status/1788441848274731052
-
Glenn 'devalias' Grant @_devalias May 9, 2024 That feel when you ignore twitter notifications for a week or two, and then have ??? of backlog posts to skim through and review.
Starting at 15:32, lets see how long it takes..
- https://x.com/_devalias/status/1788442402514203013
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Instantly distracted sidenote: does anyone have any better methods for keep up with Twitter updates/etc these days rather than the official site/client?
I would love to be able to see metadata like X new posts, from Y users since Z time.
-
- https://x.com/_devalias/status/1788442563382505764
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Bonus points if I could then drill down into that to see how many tweets per user, and be able to review those in batches/etc.
-
- https://x.com/_devalias/status/1788442973539377537
-
Glenn 'devalias' Grant @_devalias May 9, 2024 I tend to have a completionist mindset and so hate opening (and thus clearing) notifications when I can’t process them all.
But then when I leave it for a few days/weekend it becomes a big overwhelming unknown as I don’t know how many posts are there; so I put it off further
-
- https://x.com/_devalias/status/1788443083648233966
-
Glenn 'devalias' Grant @_devalias May 9, 2024 This isn't for all tweets/people I follow by the way; just the ones I have put on 'notify for all posts'
-
- https://x.com/_devalias/status/1788457390268109087
-
Glenn 'devalias' Grant @_devalias May 9, 2024 16:32, up to date, ~28 tweets/threads open for deeper review..
I guess ~1hr to catch up (or at least, filter down) ~2 weeks of not paying attention isn't the worst thing ever.. maybe that'll motivate me to not put it off so much in future..
-
- https://x.com/_devalias/status/1788459969857351705
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Then I extract all the tweet URLs to be processed, and sort them based on account then ID to vaguely contextually batch them (and avoid brain context switching):
pbpaste | sort -t '/' -k 4,4 -k 6n | awk -F'/' '{if (last && $4 != last) print ""; last=$4; print $0}' | subl
-
- https://x.com/_devalias/status/1788464333976191404
-
Glenn 'devalias' Grant @_devalias May 9, 2024 Captured these sorting notes on a gist for easier future reference:
-
- https://x.com/_devalias/status/1791303467728028019
-
Glenn 'devalias' Grant @_devalias May 17, 2024 So it took me ~3-4min of scrolling + some arbitrary guessing to even get back to where I was last viewing from.. wish I could just jump straight to it.
-
- https://x.com/_devalias/status/1791353323687383296
-
Glenn 'devalias' Grant @_devalias May 17, 2024 4:20; finally caught up... for now.. 😮💨
Really need a more efficient way to keep on top of things, while also not being constantly distracted by it all..
-
- https://x.com/_devalias/status/1870736089050480770
-
Glenn 'devalias' Grant @_devalias Dec 22, 2024 And then... queue me more or less burning out on trying to keep up with things and half ignoring twitter for months on end except for the most basic of direct notifications...
Then eventually looking for a better solution again:
-
-
Links and commentary on scripts for sorting tweet URLs and improving the triage workflow.
- https://x.com/_devalias/status/1788753444494561542
-
Glenn 'devalias' Grant @_devalias May 10, 2024 Or I might be musing about how I can improve my efficiency/workflow in other ways, such as:
"Notes on API/userscript to improve Twitter 'Notifications Timeline'": https://gist.github.com/0xdevalias/8885b10795eb3267b703ed5943087953#notes-on-apiuserscript-to-improve-twitter-notifications-timeline
"Sorting Twitter URLs": https://gist.github.com/0xdevalias/0b56aef69e20bfcbf0f1eed3960f4db8#sorting-twitter-urls
-
Discussion with @sucralose_ on API restrictions and fallback approaches using browser scripting and frontend scraping._
- https://x.com/sucralose__/status/1783665885305061783
-
Michael Skyba @sucralose__ Apr 26, 2024 Wait can you not fetch Tweets at all in the Free tier? Elon why
- https://x.com/_devalias/status/1783675392488034540
-
Glenn 'devalias' Grant @_devalias Apr 26, 2024 Yeah, the Twitter API got butchered.. it's sad; so much potential, but for a random independent dev hacking on a personal project, there's just no way that it's worth that kind of $$
-
- https://x.com/sucralose__/status/1783678183092392316
-
Michael Skyba @sucralose__ Apr 26, 2024 Fortunately the frontend seems pretty simple in how it gets its posts. Not sure if I want to waste an account getting banned by trying it though
-
- https://x.com/_devalias/status/1783678936712400983
-
Glenn 'devalias' Grant @_devalias Apr 26, 2024 Yeah, that was kind of my view as well. Depends on the project idea, some I can probably achieve with browser userscripts (so that'll probably be the approach I take); but the fact that the API isn't available just sort of puts a damper on my mind coming up with other cool ideas
-
-
Considering whether to pay for Twitter Blue or recreate its link aggregation feature using the Twitter API and custom tooling.
- https://x.com/_devalias/status/1551033467924004864
-
Glenn 'devalias' Grant @_devalias Jul 24, 2022 Hrmm.. AU$4.49/mo for Twitter Blue to get the 'Top Articles' feature that I used to get for free from @nuzzel.. or spend ?? hours diving into the Twitter API docs and building something that extracts + orders all the links shared by people I follow..
https://help.twitter.com/en/using-twitter/twitter-blue-features#top-articles
-
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
As part of this page, an API call is made to fetch the tweets to be shown:
This returns JSON data, from which we can extract the Tweets, and sort them by post time with something like jq:
pbpaste | jq '.globalObjects.tweets | to_entries | sort_by(.value.created_at | strptime("%a %b %d %H:%M:%S %z %Y")) | reverse | from_entries' | sublThis data will then look something like this:
{
"1734010714510049396": {
"created_at": "Mon Dec 11 00:42:25 +0000 2023",
"id": 1734010714510049300,
"id_str": "1734010714510049396",
"full_text": "Don’t be S-A-D! https://t.co/fkHsvfwuGG",
"truncated": false,
"display_text_range": [
0,
15
],
"entities": { /* ..snip.. */ },
"extended_entities": { /* ..snip.. */ },
"source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user_id": 44196397,
"user_id_str": "44196397",
"geo": null,
"coordinates": null,
"place": null,
"contributors": null,
"is_quote_status": false,
"retweet_count": 5203,
"favorite_count": 56940,
"reply_count": 3883,
"quote_count": 410,
"conversation_id": 1734010714510049300,
"conversation_id_str": "1734010714510049396",
"conversation_muted": false,
"favorited": false,
"retweeted": false,
"possibly_sensitive": false,
"lang": "en",
"ext": {
"superFollowMetadata": {
"r": {
"ok": {}
},
"ttl": -1
}
}
},
// ..snip..
}From that info (and matching against the DOM itself), it should be possible to implement a userscript/similar that can:
- mark where the last tweet I saw was (both automatically, and manually)
- hide/dim tweets older than that
- tell me how many tweets there are till I am 'caught up'
- etc
(ChatGPT Ref (private): https://chatgpt.com/c/67679a82-16fc-8008-8cbe-9c932f4cdfab)
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
In the source of that page there is a script tag with a window.__INITIAL_STATE__ object containing lots of settings config type things. Among these are some keys mentioning graphql, including:
{
// ..snip..
graphql_is_translatable_rweb_tweet_is_translatable_enabled: {
value: true,
},
graphql_mutation_retweet_mode: { value: 'rest_only' },
graphql_mutation_update_mode: { value: 'graphql_only' },
graphql_timeline_v2_bookmark_timeline: { value: true },
graphql_timeline_v2_user_favorites_by_time_timeline: {
value: true,
},
graphql_timeline_v2_user_media_timeline: { value: true },
graphql_timeline_v2_user_profile_timeline: { value: true },
graphql_timeline_v2_user_profile_with_replies_timeline: {
value: true,
},
// ..snip..
}There are also additional script tags loading various bundles:
- https://abs.twimg.com/responsive-web/client-web/vendor.c14be00a.js
- https://abs.twimg.com/responsive-web/client-web/i18n/en.1e92731a.js
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
In Chrome DevTools, using the 'Search' panel, we can search across the source files for graphql_timeline_v2_user_profile_timeline, which gives an interesting looking hit in the 'main' bundle:
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
-
fetchUserTweets: ({count: d, cursor: t, userId: a}) => e.graphQL(M(), { userId: a, count: d, cursor: t, includePromotedContent: !0, withQuickPromoteEligibilityTweetFields: !0, ...(0, r.d)(n), withVoice: n.isTrue("voice_consumption_enabled"), withV2Timeline: n.isTrue("graphql_timeline_v2_user_profile_timeline") }, f, k(n)).then((e => { let n = y.cY; return e?.user?.result && "User" === e.user.result.__typename && (n = e.user.result.timeline?.timeline || e.user.result.timeline_v2?.timeline || y.cY), n } )),
- Also within that same module are similar functions:
fetchUserTweetsfetchUserTweetsAndRepliesfetchUserSuperFollowsTweetsfetchUserHighlightsTweetsfetchUserArticlesTweetsfetchUserPromotableTweetsfetchLikesfetchUserMediafetchBusinessProfileTeam
-
This could be an interesting place to look deeper. Also, of particular note is e.graphQL, which is likely the main GraphQL client, and could lead to us being able to find and/or monitor other interesting points of data fetching. In that same module, we can see that e is destructured from a named parameter apiClient:
, v = ({apiClient: e, featureSwitches: n}) => ({If we search for apiClient across the code, we get a LOT of hits across many different modules including:
- https://abs.twimg.com/responsive-web/client-web/main.e541178a.js
requestGuestToken:e.post("guest/activate"requestSsoInitToken:e.post("onboarding/sso_init"logout:e.post("account/logout"cleanupMulti:e.post("account/multi/clean"list:e.get("account/multi/list"add:e.post("account/multi/add"switch:e.post("account/multi/switch"logoutAll:e.post("account/multi/logout_all"authenticatePeriscopefetchBadgeCount:e.getURT("badge_count/badge_count"fetchTweetDetailacceptConversation:e.post("dm/conversation/addParticipants:e.post("dm/conversation/addWelcomeMessageToConversation:e.post("dm/welcome_messages/add_to_conversation"deleteConversations:e.post("dm/conversation/bulk_delete"editMessage:e.post("dm/edit"fetchConversation:e.get("dm/conversation/fetchConversationFromParticipants:e.get("dm/conversation"fetchPermissions:e.get("dm/permissions"fetchSecretPermissions:e.get("dm/permissions/secret"fetchUserInbox:e.get("dm/inbox_initial_state"fetchInboxHistory:e.get("dm/inbox_timeline/fetchUserUpdates:e.get("dm/user_updates"leaveConversation:e.post("dm/conversation/search:e.post("dm/search/query"sendMessage:e.post(kmarkRead:e.post("dm/conversation/reportSpam:e.post("direct_messages/report_spam"reportDSA:e.post("dm/report"updateConversationAvatar:e.post("dm/conversation/updateLastSeenEventId:e.post("dm/update_last_seen_event_id"updateConversationName:e.post("dm/conversation/disableNotifications:e.post("dm/conversation/enableNotifications:e.post("dm/conversation/updateMentionNotificationsSetting:e.post("dm/conversation/updateTyping:e.post("dm/conversation/muteDMUser:e.post("/dm/user/update_relationship_state"fetch:e.get("help/settings"fetchLanguages:e.get("help/languages"fetchFollowersYouFollow:e.get("friends/following/list"fetchPendingFollowers:e.get("friendships/incoming"fetchFollowing:e.get("friends/list"acceptPendingFollower:e.post("friendships/accept"declinePendingFollower:e.post("friendships/deny"updateFriendship:e.post("friendships/update"createAllFriendships:e.post("friendships/create_all"destroyAllFriendships:e.post("friendships/destroy_all"fetchBlockedAccountsImportedGraphqlfetchBlockedAccountsAllGraphqlfetchFollowersfetchFollowersYouKnowfetchFollowingfetchSuperFollowersfetchCreatorSubscriptionsfetchCreatorSubscribersfetchVerifiedFollowersfetchModeratedTimelinefetchMutedAccountsfetchGenericTimelineByIdfetchHomefetchHomeLatestfetchHomeCreatorSubscriptionsclientEvent:e.post("jot/client_event"errorLog:e.post("jot/error_log"externalReferer:e.post("jot/external_referer"fetchLiveEventMetadata:e.get("live_event/1updateRemindMeSubscription:e.post("live_event/1task:e.post("onboarding/task"syncContacts:e.post("onboarding/contacts_authorize"getContactsImportStatus:e.get("onboarding/contacts_import_status"getVerificationStatus:e.get("onboarding/verification_status"callInteractiveSpinnerPath:e.getcallOnboardingPath:e.post("onboarding/referer:e.post("onboarding/referrer"removeContacts:e.post("contacts/destroy/all"setUserPwaLaunched:e.put("strato/column/User/verifyUserIdentifier:e.post("onboarding/begin_verification"verificationLink:e.post("onboarding/verify"getBrowsableNuxRecommendations:e.post("onboarding/fetch_user_recommendations"fetchUserTweetsfetchUserTweetsAndRepliesfetchUserSuperFollowsTweetsfetchUserHighlightsTweetsfetchUserArticlesTweetsfetchUserPromotableTweetsfetchLikesfetchUserMediafetchBusinessProfileTeamlog:e.post("promoted_content/log"fetch:e.get("account/settings"fetchRateLimits:e.get("application/rate_limit_status"fetchHashflags:e.get("hashflags"update:e.post("account/settings"deleteSSOConnection:e.post("sso/delete_connection"deleteLocationData:e.post("geo/delete_location_data"deleteContacts:e.post("contacts/destroy/all"fetchNotificationFilters:e.get("mutes/advanced_filters"updateNotificationFilters:e.post("mutes/advanced_filters"updateProfile:e.post("account/update_profile"removeProfileBanner:e.post("account/remove_profile_banner"updateProfileAvatar:e.post("account/update_profile_image"updateProfileBanner:e.post("account/update_profile_banner"fetchPlaceTrendSettings:e.getURT("guide/get_explore_settings"updatePlaceTrendSettings:e.postURT("guide/set_explore_settings"usernameAvailable:e.dispatch({ path: "/i/users/username_available.json"fetchApplications:e.get("oauth/list"revokeApplication:e.post("oauth/revoke"revokeOauth2Token:e.postUnversioned("/2/oauth2/revoke_token_hash"changePassword:e.postI("account/change_password"deactivate:e.post("account/deactivate"fetchWoeTrendsLocations:e.get("trends/available"fetchPlaceTrendsLocations:e.getURT("guide/explore_locations_with_auto_complete"fetchLoginVerificationSettings:e.get("strato/column/User/{t}/account-security/twoFactorAuthSettings2"fetchBackupCode:e.get("account/backup_code"fetchNewBackupCode:e.post("account/backup_code"fetchTemporaryPassword:e.post("account/login_verification/temporary_password"fetchEncryptedDMsPublicKeysAndDevices:e.get("keyregistry/extract_public_keys/{d}"deregisterDevice:e.delete("keyregistry/delete/{n.registrationToken}"fetchSessionsfetchUserPreferencesenableVerifiedPhoneLabeldisableVerifiedPhoneLabelfetchUserProfilePhoneStaterevokeSession:e.postUnversioned("/account/sessions/revoke"revokeAllSessions:e.postUnversioned("/account/sessions/revoke_all"enrollIn2FA:e.post("bouncer/opt_in"disable2FA:e.delete("account/login_verification_enrollment"disable2FAMethod:e.post("account/login_verification/remove_method"rename2FASecurityKey:e.post("account/login_verification/rename_security_key_method"verifyPassword:e.post("account/verify_password"fetchAltTextPromptPreferenceupdateAltTextPromptPreferencefetchDataUsageSettingsupdateDataUsageSettingsupdateDmNsfwMediaFilterupdateSharingAudiospacesListeningDataWithFollowersfetchTopicLandingPagefetchTopicsManagementPagefetchOneTopicfetchTopicsPickerPagefetchViewingOtherUsersTopicsPagePagefollowunfollownotInterestedundoNotInterestedfetchTopicsToFollowSidebarn.post("statuses/update"fetchfetchMultipleshowfetchTranslationmuteunmutelikeunlikeretweetunretweetbookmarkunbookmarkhighlightundoHighlightpinunpinhideReplyGraphQLunhideReplyGraphQLremoveTagsendTweetdestroychangeConversationControlsremoveConversationControlsunmentionUserFromConversationfetchfetchCommunityInviteUsersfetchCommunityMembersfetchPaymentsUsersprefetchUsersfetchExplorefetchExploreGraphQLfetchTrendHistoryfetchExploreSidebarGraphQLfetchTrendRelevantUsersGraphQLfetchGlobalCommunitiesLatestPostSearchfetchGlobalCommunitiesPostSearchfetchExploreTopicfetchGenericfetchBookmarkSearchfetchListSearchfetchLiveEventTimelinefetchNotificationsfetchNotificationsUnreadCountfetchNUXUserRecommendationsfetchRichConnectTimelinefetchRichSuggestedTimelinefetchSearchfetchSearchGraphQLfetchSimilarPostsfetchReactiveInstructionsfetchTestFixturesfetchTestGraphqlFixturesfetchUserMomentspostCustomEndpointsubmitTimelinesFeedbackupdateNotificationsLastSeenCursorfetchUsersfollowunfollowcancelPendingFollowblockunblockdmBlockdmUnblockmuteunmutefetchProfileTranslationfetchViewerfetchOneUserByScreenNamefetchOneUserfetchUsersremoveFollowercreateLocalId:e.postUnversioned("/12/measurement/dcm_local_id"postfetchfetchDevicePermissionsState:e.get("strato/column/None/${r}"fetchInfo:e.get("users/email_phone_info"resendConfirmationEmail:e.post("account/resend_confirmation_email"removeDevice:e.post("device/unregister"updateDevicePermissionsState:e.put("strato/column/None/${r}"getNotificationSettingsLogin:e.post("notifications/settings/login"getNotificationSettings:e.post("notifications/settings/checkin"updateNotificationSettings:e.post("notifications/settings/save"removePushDevices:e.post("notifications/settings/logout"putClientEducationFlagfetchPreferences:e.get("account/personalization/p13n_preferences")updatePreferences:e.post("account/personalization/p13n_preferences")fetchData:e.get("account/personalization/p13n_data")fetchTwitterInterests:e.get("account/personalization/twitter_interests")fetchPartnerInterests:e.get("account/personalization/partner_interests")createAudienceDownload:e.post("account/personalization/email_advertiser_list")createDataDownload:e.post("account/personalization/email_your_data")updateCookies:e.get("account/personalization/set_optout_cookies")syncSettings:e.post("account/personalization/sync_optout_settings")fetchPinnedTimelinespinTimelineunpinTimelinefetchUserClaims
- https://abs.twimg.com/responsive-web/client-web/bundle.Grok.2fe94cba.js
- TODO: add summary of specific API names/endpoints here
- https://abs.twimg.com/responsive-web/client-web/loader.AppModules.9e1a436a.js
updateSubscriptions:e.post("live_pipeline/update_subscriptions", o, {}, a, "")metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/loader.DMDrawer.bb6db51a.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.HWCard.1d1419aa.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.SideNav.ed72f65a.js
metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/loader.TimelineCardHandler.f5e9e45a.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSlice
- https://abs.twimg.com/responsive-web/client-web/loader.Typeahead.4504647a.js
metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/modules.audio.44d4466a.js
spacebar:e.getUnversioned("/fleets/v1/fleetline"byIdsubscribeToScheduledSpaceByIdunsubscribeFromScheduledSpaceByIdfetchTopicssearchaddSharingdeleteSharingfetchPresence:e.getUnversioned("/fleets/v1/avatar_content"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.Articles~bundle.AudioSpaceDetail~bundle.AudioSpaceDiscovery~bundle.AudioSpacebarScreen~bundle.B.6db0364a.js
addToListcreateListeditBannerImagedeleteListdeleteBannerImagefetchListfetchCombinedListsfetchListsManagementPageTimelinefetchTweetsGraphQLfetchMembersGraphQLfetchRecommendedUsersGraphQLfetchSubscribersGraphQLfetchSuggestedListsfetchOwnershipsGraphQLfetchMemberships:e.get("lists/memberships"fetchMembershipsGraphQLremoveFromListcreateSubscribersdestroySubscriberstoggleMuteeditListpinunpinfetch:e.post("videoads/v2/prerolls"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.AudioSpaceDetail~bundle.AudioSpaceDiscovery~bundle.AudioSpacebarScreen~bundle.Birdwatch~bundle..e24934ea.js
fetchImmersiveMediafetchImmersiveProfilefetchImmersiveProfileByUserIdfetchTVHomeMixerGraphQLfetchTVUserProfileGraphQLfetchTVTrendGraphQLfetchTweetRelatedVideosGraphQLgeneratePinCodeGraphQLdeviceIsVerifiedGraphQL
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.Grok~bundle.ReaderMode~bundle.Birdwatch~bundle.TwitterArticles~bundle.Compose~bundle.Settings~b.03592eba.js
bookmarkTweetToFoldercreateBookmarkFolderdeleteAlldeleteBookmarkFoldereditBookmarkFolderremoveTweetFromBookmarkFolderfetchBookmarksTimelinefetchBookmarkFolderTimelinefetchBookmarkFoldersSliceclearConversationssetPreferencesfetchConversationfetchGrokShareGraphQLfetchGrokHomefetchHistoryfetchMediaHistorysearchConversationsdeleteMessageuploadFile:e.postForm("grok/attachment"getQuickPromoteEligibilitygetCouponsgetBudgetsgetAudienceEstimategetBoostAudienceEstimategetPaymentMethodsdeletePaymentMethodsetDefaultPaymentMethodcreatePromotionenrollCoupongetAdAccountsfetchArticleDomainsGraphQLfromSharegetTargetableLocations:e.getUnversioned("/12/targeting_criteria/locations"getQueriedTargetableLocations:e.getUnversioned("/12/targeting_criteria/locations"
- https://abs.twimg.com/responsive-web/client-web/shared~bundle.TwitterArticles~loader.TweetCurationActionMenu.956f177a.js
createDraftArticlefetchArticleEntitydeleteArticleEntityupdateArticleEntityContentupdateArticleEntityCoverMediaupdateArticleEntityTitlepublishArticleEntityunpublishArticleEntityfetchArticleEntitiesSlice
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AppModules~loader.LoggedOutNotifications.94c3e97a.js
enableLoggedOutWebNotifications
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AppModules~ondemand.SettingsRevamp~bundle.NotABot~bundle.TwitterBlue.a61dad2a.js
fetchSubscriptionProductDetailsfetchSubscriptionProductCheckoutUrlfetchNotABotCheckoutUrlfetchProductSubscriptionsswitchTier
- https://abs.twimg.com/responsive-web/client-web/shared~loader.AudioDock~loader.DashMenu~loader.DashModal~loader.DMDrawer~bundle.Grok~bundle.Account~bundle.Re.635a6b9a.js
fetchLiveVideoStreamStatus:e.get("live_video_stream/status/"metadataCreate:e.post("media/metadata/create"attachSubtitles:e.post("media/subtitles/create"
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DMDrawer~bundle.LiveEvent~bundle.Compose~bundle.DirectMessages~bundle.DMRichTextCompose~bundle..d0f9ae9a.js
fetchBroadcastfetchLatestBroadcast
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DMDrawer~ondemand.SettingsInternals~bundle.DirectMessages.6bf37dca.js
fetchDMAllSearchfetchDMGroupSearchfetchDMPeopleSearchfetchDMMutedUsers
- https://abs.twimg.com/responsive-web/client-web/shared~loader.DashMenu~loader.SideNav~loader.AppModules~loader.DMDrawer~bundle.Grok~bundle.MultiAccount~bundl.2aa3c46a.js
createCommunityfetchCommunityfetchCommunityWithoutRelayfetchAboutTimelinefetchCommunityLoggedOutTweetsfetchCommunityTweetsfetchCommunityMediaLoggedOutTweetsfetchCommunityMediaTweetsfetchCommunityRankedLoggedOutTweetsfetchCommunityMembershipsfetchRecentCommunityMembershipsfetchCommunitiesMembershipsSlicefetchModerationCasesSlicefetchTweetModerationLogSlicefetchCommunitiesMainDiscoveryModulefetchCommunitiesMainTimelinefetchCommunitiesRankedTimelinefetchCommunitiesExploreTimelinefetchCommunityHashtagsTimelinefetchCommunityDiscoveryTimelinekeepCommunityTweetjoinCommunityrequestToJoinCommunityleaveCommunityinviteToCommunityupdateCommunityRolecreateDraftTweetdeleteDraftTweeteditDraftTweetfetchDraftTweetsfetchPlace:e.get("geo/id/search:e.get("geo/places"scheduleTweetfetchScheduledTweetsdeleteScheduledTweeteditScheduledTweeteditCommunityNameeditCommunityPurposeeditCommunityQuestioneditCommunityRulecreateCommunityRuleremoveCommunityRulereorderCommunityRuleseditCommunityBannerMediaremoveCommunityBannerMediafetchAuthenticatedUserTFListscreateTrustedFriendsList
- https://abs.twimg.com/responsive-web/client-web/shared~loader.Typeahead~bundle.Search.24bc9bda.js
fetch:e.get("saved_searches/list"create:e.post("saved_searches/create"destroy:e.post("saved_searches/destroy/
- https://abs.twimg.com/responsive-web/client-web/shared~loader.WideLayout~loader.ProfileClusterFollow.d1ed818a.js
fetch:e.get("users/recommendations"fetchSidebarUserRecommendations
- https://abs.twimg.com/responsive-web/client-web/shared~ondemand.DirectMessagesCrypto~ondemand.SettingsRevamp.bcca9e0a.js
register:e.post("keyregistry/register"extractPublicKeys:e.get("keyregistry/extract_public_keys/
From that, we can see that some of the apiClient features seem to relate to REST (get, post, postForm, postUnversioned, delete) endpoints, and others to graphQL.
For the graphQL parts of the apiClient, we can see that the structure tends to look something like this:
// ..snip..
{
// ..snip..
705929: (e, t, o) => {
"use strict";
// ..snip..
var s =
// ..snip..
, m = o(403807)
, f = o.n(m)
// ..snip..
F = ({apiClient: e, featureSwitches: t}) => ({
bookmarkTweetToFolder: t => e.graphQL(f(), {
...t
}, (0, i.kj)((e => !e.bookmark_collection_tweet_put), "GQL Bookmark Folders: failed to Add Tweet to Bookmark Folder")),
// ..snip..
})
// ..snip..
}
// ..snip..
}
// ..snip..Here, we can see that the first param to graphQL is f(), which is the query/mutation. Tracing that backwards, we can find the module where it was imported from:
(From ChatGPT convo: https://chatgpt.com/c/6767a484-feb8-8008-ba03-5f802a5dd3ad)
In the
bookmarkTweetToFolderfunction:bookmarkTweetToFolder: t => e.graphQL(f(), { ...t }, (0, i.kj)((e => !e.bookmark_collection_tweet_put), "GQL Bookmark Folders: failed to Add Tweet to Bookmark Folder"))
f()provides the query/mutation details for this call.{ ...t }supplies the variables (likely includingtweet_idandbookmark_folder_id).- The third argument handles errors or conditions for failure.
f()is imported as:var f = o.n(m);Where
mrefers to:var m = o(403807);
In this case, that GraphQL query/mutation module definition looks like this:
403807: e => {
e.exports = {
queryId: "4KHZvvNbHNf07bsgnL9gWA",
operationName: "bookmarkTweetToFolder",
operationType: "mutation",
metadata: {
featureSwitches: [],
fieldToggles: []
}
}
}(From ChatGPT convo: https://chatgpt.com/c/6767a484-feb8-8008-ba03-5f802a5dd3ad)
Module
403807exports the following:e.exports = { queryId: "4KHZvvNbHNf07bsgnL9gWA", operationName: "bookmarkTweetToFolder", operationType: "mutation", metadata: { featureSwitches: [], fieldToggles: [] } };Key parts:
queryId:"4KHZvvNbHNf07bsgnL9gWA", uniquely identifying this mutation.operationName:"bookmarkTweetToFolder", the logical name of the mutation.operationType:"mutation", indicating that it modifies data.metadata: Additional metadata such as feature toggles, which are currently empty.
- The
queryId("4KHZvvNbHNf07bsgnL9gWA") is sent to the server via thegraphQLmethod.- On the server:
- The
queryIdis mapped to a stored GraphQL mutation string.- This mapping is typically maintained in:
- A server-side database or configuration file.
- A GraphQL operation registry.
- The server uses the mutation string mapped to the
queryIdto execute the operation with the provided variables.
- Efficiency: The client only sends the
queryIdinstead of the full mutation string, reducing payload size.- Flexibility: The server can update or optimize the mutation without requiring changes to the client code.
- Security: Prevents exposing the full query or mutation details in the client.
On Twitter, from my notifications page:
When I click on the 'New post notifications for...' section, I am taken to a new page:
A few potentially interesting data-testid / aria-label / etc type attributes we could potentially use for selectors:
<div data-testid="primaryColumn"
<div aria-label="Home timeline"
<section aria-labelledby="accessible-list-6" role="region" class="css-175oi2r">
<h1 dir="auto" aria-level="1" role="heading" class="css-146c3p1 r-4iw3lz r-1xk2f4g r-109y4c4 r-1udh08x r-wwvuq4 r-u8s1d r-92ng3h" id="accessible-list-6">Posts</h1>
<div aria-label="Timeline: Posts"
<div data-testid="cellInnerDiv"
<article data-testid="tweet"
<div data-testid="User-Name"
<time datetime="2025-05-06T07:20:52.000Z"
<a href="/REDACTED/status/REDACTED" dir="ltr" aria-label="May 6" role="link" class="css-146c3p1 r-bcqeeo r-1ttztb7 r-qvutc0 r-37j5jr r-a023e6 r-rjixqe r-16dba41 r-xoduu5 r-1q142lx r-1w6e6rj r-9aw3ui r-3s2u2q r-1loqt21" style="color: rgb(113, 118, 123);">
<time datetime="2025-05-06T07:20:52.000Z">May 6</time>
</a>
<div data-testid="tweetText"
<div data-testid="tweetPhoto"
<div aria-label="Embedded video"
<div data-testid="previewInterstitial"From there, I decided to start diving a little deeper into the React Internals, specifically traversing through the fibers and props to get access the internal data stores/etc. The following were my snippets of exploratory code as I was investigating things, so it's more hacky prototype than clean final implementation:
const timelinePosts = $('div[aria-label="Timeline: Posts"]')
const reactPropsKey = Object.keys(timelinePosts).find(k => k.startsWith('__reactProps$'));
const timelinePostsProps = timelinePosts[reactPropsKey]
const seeminglyInterestingChild = timelinePostsProps.children[2]
const cacheKey = seeminglyInterestingChild.props.cacheKey
// Items seems to contain a slice of the tweets that are currently visible; or at least, their ID and some related metadata/etc
const items = seeminglyInterestingChild.props.items
console.log({ cacheKey, items, itemsLength: items.length })
// Other potentially interesting props/functions here:
// const { onAtEnd, onAtStart, onItemsRendered, onKeyboardRefresh, onNearEnd, onNearStart, onPositionRestored, renderer, sortIndexFunction, identityFunction, /* etc */ } = seeminglyInterestingChild.props
const interestingChildOwner = seeminglyInterestingChild._owner
const interestingChildOwnerElementTypeContextType = interestingChildOwner.elementType.contextType.$$typeof // Symbol(react.context)
const interestingChildOwnerElementTypeDefaultProps = interestingChildOwner.elementType.defaultProps // impressionCache, lingerCache, refreshControl, scroller, etc
const tweetNotificationsIDs = interestingChildOwnerElementTypeDefaultProps.impressionCache.get('tweet_notifications')
console.log({ tweetNotificationsIDs, tweetNotificationsIDsSize: tweetNotificationsIDs.size })
const interestingChildOwnerDependenciesFirstContextMemoizedValue = interestingChildOwner.dependencies.firstContext.memoizedValue // featureSwitches, history, isRestrictedSession, loggedInUserId, scrollManager, store, userAgent, userClaims, verifiedCrawlerName, viewerUserId
const store = interestingChildOwnerDependenciesFirstContextMemoizedValue.store // @@observable, dispatch, getState, replaceReducer, subscribe
const state = store.getState()
const stateKeys = Object.keys(state) // Some potentially interesting ones include: developer, directMessages, draftTweets, editTweet, entities, featureSwitch, grok, liveTweetCounts, session, settings, tweetComposer, urt, userClaim, etc
console.log({ state, stateKeys, stateKeysLength: stateKeys.length })
const stateEntities = state.entities
const stateEntitiesKeys = Object.keys(stateEntities) // Some potentially interesting ones include: cards, communities, conversations, genericNotifications, publishedArticles, tweets, users
console.log({ stateEntities, stateEntitiesKeys, stateEntitiesKeysLength: stateEntitiesKeys.length })
const stateEntitiesTweets = stateEntities.tweets
const stateEntitiesUsers = stateEntities.users
console.log({
stateEntitiesTweets,
stateEntitiesTweetsLength: Object.keys(stateEntitiesTweets.entities).length,
stateEntitiesUsers,
stateEntitiesUsersLength: Object.keys(stateEntitiesUsers.entities).length,
})
const tweetNotificationIDsArray = Array.from(tweetNotificationsIDs)
// const [entityType, entityKey] = tweetNotificationIDsArray[0].split('-')
const getEntity = (entityType, entityKey) => state.entities?.[`${entityType}s`]?.entities?.[entityKey]
const getUser = (userId) => {
const user = getEntity('user', userId)
const userLabel = !!user ? `${user.name} (@${user.screen_name})` : `Unknown (userId: ${userId})`
return {
userId,
user,
userLabel,
}
}
const getNotificationTweetByFullEntityId = (fullEntityId) => {
const [tweetEntityType, tweetEntityKey] = fullEntityId.split('-')
return getEntity(tweetEntityType, tweetEntityKey)
}
const getNotificationTweetByIndex = (notificationIndex) => getNotificationTweetByFullEntityId(tweetNotificationIDsArray[notificationIndex])
const tweetNotifications = tweetNotificationIDsArray.map(id => getNotificationTweetByFullEntityId(id))
console.log({
tweetNotifications,
tweetNotificationsLength: tweetNotifications.length,
tweetNotificationsNewestLoaded: tweetNotifications[0].created_at,
tweetNotificationsOldestLoaded: tweetNotifications[tweetNotifications.length - 1].created_at,
})
function getTweetCounts(tweets) {
const byUser = Object.create(null);
const byDay = Object.create(null);
const byUserDay = Object.create(null); // userLabel → { day → count, _total }
const byDayUser = Object.create(null); // day → { userLabel → count }
for (let i = 0; i < tweets.length; i++) {
const tweet = tweets[i];
const userId = tweet.user_id || tweet.user;
const { userLabel } = getUser(userId);
const day = tweet.created_at.slice(0, 10);
// Count by user
byUser[userLabel] = (byUser[userLabel] || 0) + 1;
// Count by day
byDay[day] = (byDay[day] || 0) + 1;
// Count by user → day, plus track total
if (!byUserDay[userLabel]) {
byUserDay[userLabel] = Object.create(null);
byUserDay[userLabel]._total = 0;
}
byUserDay[userLabel][day] = (byUserDay[userLabel][day] || 0) + 1;
byUserDay[userLabel]._total++;
// Count by day → user
if (!byDayUser[day]) byDayUser[day] = Object.create(null);
byDayUser[day][userLabel] = (byDayUser[day][userLabel] || 0) + 1;
}
return { total: tweets.length, byUser, byDay, byUserDay, byDayUser };
}
const {
total: tweetNotificationsTotal,
byUser: tweetNotificationsByUser,
byDay: tweetNotificationsByDay,
byUserDay: tweetNotificationsByUserDay,
byDayUser: tweetNotificationsByDayUser
} = getTweetCounts(tweetNotifications);
console.log({
tweetNotificationsTotal,
tweetNotificationsByUser,
tweetNotificationsByDay,
tweetNotificationsByUserDay,
tweetNotificationsByDayUser
});But since that's all a bit messy, here is a bit of a quick ChatGPT refactor of the code gadgets/etc into more logical groupings:
I will no doubt clean up / refine / enhance these more specifically myself in future; and then build them into a toolkit that I can use to further build out into relevant user scripts and/or a Chrome extension, etc.
- Twitter API Responses / etc (0xdevalias' secret gist)
- Twitter JavaScript scripts / etc (0xdevalias' secret gist)
- Sorting Twitter URLs (0xdevalias' gist)
- AI Agent Toolkit for Automated Browser Userscript Development (0xdevalias' gist)
- Deobfuscating / Unminifying Obfuscated Web App Code (0xdevalias gist)
- Reverse Engineering Webpack Apps (0xdevalias gist)
- React Server Components, Next.js v13+, and Webpack: Notes on Streaming Wire Format (
__next_f, etc) (0xdevalias' gist)) - Fingerprinting Minified JavaScript Libraries / AST Fingerprinting / Source Code Similarity / Etc (0xdevalias gist)
- Bypassing Cloudflare, Akamai, etc (0xdevalias gist)