How X (Twitter) Designed Its House Timeline API: Classes to Study | by Oleksii Trekhleb | Dec, 2024

A better take a look at X’s API: fetching knowledge, linking entities, and fixing under-fetching.

When designing a system’s API, software program engineers typically consider numerous approaches, similar to REST vs RPC vs GraphQL, or hybrid fashions, to find out the most effective match for a particular job or challenge. These approaches outline how knowledge flows between the backend and frontend, in addition to the construction of the response knowledge:

  • Ought to all knowledge be packed right into a single “batch” and returned in a single response?
  • Can the “batch” be configured to incorporate solely the required fields for a particular shopper (e.g., browser vs. cellular) to keep away from over-fetching?
  • What occurs if the shopper under-fetches knowledge and requires extra backend calls to retrieve lacking entities?
  • How ought to parent-child relationships be dealt with? Ought to youngster entities be embedded inside their father or mother, or ought to normalization be utilized, the place father or mother entities solely reference youngster entity IDs to enhance reusability and cut back response measurement?

On this article, we discover how the X (previously Twitter) residence timeline API (x.com/residence) addresses these challenges, together with:

  • Fetching the record of tweets
  • Returning hierarchical or linked knowledge (e.g., tweets, customers, media)
  • Sorting and paginating outcomes
  • Retrieving tweet particulars
  • Liking a tweet

Our focus might be on the API design and performance, treating the backend as a black field since its implementation is inaccessible.

Instance of X residence timeline

Exhibiting the precise requests and responses right here is perhaps cumbersome and laborious to observe for the reason that deeply nested and repetitive objects are laborious to learn. To make it simpler to see the request/response payload construction, I’ve made my try and “sort out” the house timeline API in TypeScript. So in terms of the request/response examples I’ll use the request and response sorts as a substitute of precise JSON objects. Additionally, keep in mind that the kinds are simplified and lots of properties are omitted for brevity.

You could discover every kind in sorts/x.ts file or on the backside of this text within the “Appendix: Every type at one place” part.

All pictures, except othewise famous, are by the writer.

Fetching the record of tweets for the house timeline begins with the POST request to the next endpoint:

POST https://x.com/i/api/graphql/{query-id}/HomeTimeline

Here’s a simplified request physique sort:

sort TimelineRequest = {
queryId: string; // 's6ERr1UxkxxBx4YundNsXw'
variables: {
depend: quantity; // 20
cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA'
seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530659']
};
options: Options;
};

sort Options = {
articles_preview_enabled: boolean;
view_counts_everywhere_api_enabled: boolean;
// ...
}

Here’s a simplified response physique sort (we’ll dive deeper into the response sub-types under):

sort TimelineResponse = {
knowledge: {
residence: {
home_timeline_urt: {
directions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[];
};
};
};
};
};

sort TimelineAddEntries = TimelineCursor ;

sort TimelineItem = {
entryId: string; // 'tweet-1867041249938530657'
sortIndex: string; // '1866561576636152411'
content material: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161']
};
};
};

sort TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
outcome: Tweet;
};
};

sort TimelineCursor = {
entryId: string; // 'cursor-top-1867041249938530657'
sortIndex: string; // '1866961576813152212'
content material: 'Backside';
;
};

sort ActionKey = string;

It’s attention-grabbing to notice right here, that “getting” the info is completed through “POSTing”, which isn’t widespread for the REST-like API however it is not uncommon for a GraphQL-like API. Additionally, the graphql a part of the URL signifies that X is utilizing the GraphQL taste for his or her API.

I’m utilizing the phrase “taste” right here as a result of the request physique itself doesn’t appear to be a pure GraphQL question, the place we might describe the required response construction, itemizing all of the properties we wish to fetch:

# An instance of a pure GraphQL request construction that's *not* getting used within the X API.
{
tweets {
id
description
created_at
medias {
type
url
# ...
}
writer {
id
identify
# ...
}
# ...
}
}

The belief right here is that the house timeline API is just not a pure GraphQL API, however is a mixture of a number of approaches. Passing the parameters in a POST request like this appears nearer to the “practical” RPC name. However on the identical time, it looks like the GraphQL options is perhaps used someplace on the backend behind the HomeTimeline endpoint handler/controller. A combination like this may additionally be brought on by a legacy code or some kind of ongoing migration. However once more, these are simply my speculations.

You may additionally discover that the identical TimelineRequest.queryId is used within the API URL in addition to within the API request physique. This queryId is most likely generated on the backend, then it will get embedded within the essential.js bundle, after which it’s used when fetching the info from the backend. It’s laborious for me to grasp how this queryId is used precisely since X’s backend is a black field in our case. However, once more, the hypothesis right here is perhaps that, it is perhaps wanted for some kind of efficiency optimization (re-using some pre-computed question outcomes?), caching (Apollo associated?), debugging (be a part of logs by queryId?), or monitoring/tracing functions.

It’s also attention-grabbing to notice, that the TimelineResponse incorporates not an inventory of tweets, however fairly an inventory of directions, like “add a tweet to the timeline” (see the TimelineAddEntries sort), or “terminate the timeline” (see the TimelineTerminateTimeline sort).

The TimelineAddEntries instruction itself might also comprise several types of entities:

  • Tweets — see the TimelineItem sort
  • Cursors — see the TimelineCursor sort
  • Conversations/feedback/threads — see the TimelineModule sort
sort TimelineResponse = {
knowledge: {
residence: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Right here
// ...
;
};
};
};

sort TimelineAddEntries = TimelineModule)[]; // <-- Right here
;

That is attention-grabbing from the extendability perspective because it permits a greater variety of what could be rendered within the residence timeline with out tweaking the API an excessive amount of.

The TimelineRequest.variables.depend property units what number of tweets we wish to fetch without delay (per web page). The default is 20. Nevertheless, greater than 20 tweets could be returned within the TimelineAddEntries.entries array. For instance, the array would possibly comprise 37 entries for the primary web page load, as a result of it consists of tweets (29), pinned tweets (1), promoted tweets (5), and pagination cursors (2). I am undecided why there are 29 common tweets with the requested depend of 20 although.

The TimelineRequest.variables.cursor is answerable for the cursor-based pagination.

“Cursor pagination is most frequently used for real-time knowledge as a result of frequency new information are added and since when studying knowledge you typically see the newest outcomes first. It eliminates the opportunity of skipping objects and displaying the identical merchandise greater than as soon as. In cursor-based pagination, a continuing pointer (or cursor) is used to maintain monitor of the place within the knowledge set the subsequent objects ought to be fetched from.” See the Offset pagination vs Cursor pagination thread for the context.

When fetching the record of tweets for the primary time the TimelineRequest.variables.cursor is empty, since we wish to fetch the highest tweets from the default (most likely pre-computed) record of personalised tweets.

Nevertheless, within the response, together with the tweet knowledge, the backend additionally returns the cursor entries. Right here is the response sort hierarchy: TimelineResponse → TimelineAddEntries → TimelineCursor:

sort TimelineResponse = {
knowledge: {
homet: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Right here
// ...
;
};
};
};

sort TimelineAddEntries = TimelineModule)[]; // <-- Right here (tweets + cursors)
;

sort TimelineCursor = {
entryId: string;
sortIndex: string;
content material: 'Backside';
;
};

Each web page incorporates the record of tweets together with “prime” and “backside” cursors:

Examples of how cursors are handed together with tweets

After the web page knowledge is loaded, we will go from the present web page in each instructions and fetch both the “earlier/older” tweets utilizing the “backside” cursor or the “subsequent/newer” tweets utilizing the “prime” cursor. My assumption is that fetching the “subsequent” tweets utilizing the “prime” cursor occurs in two instances: when the brand new tweets had been added whereas the consumer continues to be studying the present web page, or when the consumer begins scrolling the feed upwards (and there aren’t any cached entries or if the earlier entries had been deleted for the efficiency causes).

The X’s cursor itself would possibly appear to be this: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA. In some API designs, the cursor could also be a Base64 encoded string that incorporates the id of the final entry within the record, or the timestamp of the final seen entry. For instance: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"}, after which, this knowledge is used to question the database accordingly. Within the case of X API, it appears to be like just like the cursor is being Base64 decoded into some customized binary sequence that may require some additional decoding to get any which means out of it (i.e. through the Protobuf message definitions). Since we do not know if it’s a .proto encoding and in addition we do not know the .proto message definition we could assume that the backend is aware of question the subsequent batch of tweets primarily based on the cursor string.

The TimelineResponse.variables.seenTweetIds parameter is used to tell the server about which tweets from the at present energetic web page of the infinite scrolling the shopper has already seen. This most likely helps be sure that the server doesn’t embody duplicate tweets in subsequent pages of outcomes.

One of many challenges to be solved within the APIs like residence timeline (or House Feed) is to determine return the linked or hierarchical entities (i.e. tweet → consumer, tweet → media, media → writer, and so on):

  • Ought to we solely return the record of tweets first after which fetch the dependent entities (like consumer particulars) in a bunch of separate queries on-demand?
  • Or ought to we return all the info without delay, rising the time and the dimensions of the primary load, however saving the time for all subsequent calls?
  • Do we have to normalize the info on this case to cut back the payload measurement (i.e. when the identical consumer is an writer of many tweets and we wish to keep away from repeating the consumer knowledge again and again in every tweet entity)?
  • Or ought to or not it’s a mixture of the approaches above?

Let’s see how X handles it.

Earlier within the TimelineTweet sort the Tweet sub-type was used. Let’s have a look at the way it appears to be like:

export sort TimelineResponse = {
knowledge: {
residence: {
home_timeline_urt: TimelineTerminateTimeline)[]; // <-- Right here
// ...
;
};
};
};

sort TimelineAddEntries = TimelineModule)[]; // <-- Right here
;

sort TimelineItem = {
entryId: string;
sortIndex: string;
content material: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet; // <-- Right here
// ...
};
};

sort TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
outcome: Tweet; // <-- Right here
};
};

// A Tweet entity
sort Tweet = {
__typename: 'Tweet';
core: {
user_results: {
outcome: Consumer; // <-- Right here (a dependent Consumer entity)
};
};
legacy: {
full_text: string;
// ...
entities: { // <-- Right here (a dependent Media entities)
media: Media[];
hashtags: Hashtag[];
urls: Url[];
user_mentions: UserMention[];
};
};
};

// A Consumer entity
sort Consumer = {
__typename: 'Consumer';
id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2'
// ...
legacy: {
location: string; // 'San Francisco'
identify: string; // 'John Doe'
// ...
};
};

// A Media entity
sort Media = {
// ...
source_user_id_str: string; // '1867041249938530657' <-- Right here (the dependant consumer is being talked about by its ID)
url: string; // 'https://t.co/X78dBgtrsNU'
options: {
massive: { faces: FaceGeometry[] };
medium: { faces: FaceGeometry[] };
small: { faces: FaceGeometry[] };
orig: { faces: FaceGeometry[] };
};
sizes: {
massive: MediaSize;
medium: MediaSize;
small: MediaSize;
thumb: MediaSize;
};
video_info: VideoInfo[];
};

What’s attention-grabbing right here is that a lot of the dependent knowledge like tweet → media and tweet → writer is embedded into the response on the primary name (no subsequent queries).

Additionally, the Consumer and Media connections with Tweet entities usually are not normalized (if two tweets have the identical writer, their knowledge might be repeated in every tweet object). Nevertheless it looks like it ought to be okay, since within the scope of the house timeline for a particular consumer the tweets might be authored by many authors and repetitions are attainable however sparse.

My assumption was that the UserTweets API (that we do not cowl right here), which is answerable for fetching the tweets of one specific consumer will deal with it in a different way, however, apparently, it’s not the case. The UserTweets returns the record of tweets of the identical consumer and embeds the identical consumer knowledge again and again for every tweet. It is attention-grabbing. Possibly the simplicity of the method beats some knowledge measurement overhead (perhaps consumer knowledge is taken into account fairly small in measurement). I am undecided.

One other statement in regards to the entities’ relationship is that the Media entity additionally has a hyperlink to the Consumer (the writer). Nevertheless it does it not through direct entity embedding because the Tweet entity does, however fairly it hyperlinks through the Media.source_user_id_str property.

The “feedback” (that are additionally the “tweets” by their nature) for every “tweet” within the residence timeline usually are not fetched in any respect. To see the tweet thread the consumer should click on on the tweet to see its detailed view. The tweet thread might be fetched by calling the TweetDetail endpoint (extra about it within the “Tweet element web page” part under).

One other entity that every Tweet has is FeedbackActions (i.e. “Advocate much less typically” or “See fewer”). The way in which the FeedbackActions are saved within the response object is totally different from the best way the Consumer and Media objects are saved. Whereas the Consumer and Media entities are a part of the Tweet, the FeedbackActions are saved individually in TimelineItem.content material.feedbackInfo.feedbackKeys array and are linked through the ActionKey. That was a slight shock for me because it does not appear to be the case that any motion is re-usable. It appears to be like like one motion is used for one specific tweet solely. So it looks like the FeedbackActions could possibly be embedded into every tweet in the identical means as Media entities. However I is perhaps lacking some hidden complexity right here (like the truth that every motion can have kids actions).

Extra particulars in regards to the actions are within the “Tweet actions” part under.

The sorting order of the timeline entries is outlined by the backend through the sortIndex properties:

sort TimelineCursor = {
entryId: string;
sortIndex: string; // '1866961576813152212' <-- Right here
content material: 'Backside';
;
};

sort TimelineItem = {
entryId: string;
sortIndex: string; // '1866561576636152411' <-- Right here
content material: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[];
};
};
};

sort TimelineModule = {
entryId: string;
sortIndex: string; // '73343543020642838441' <-- Right here
content material: {
__typename: 'TimelineTimelineModule';
objects: {
entryId: string,
merchandise: TimelineTweet,
}[],
displayType: 'VerticalConversation',
};
};

The sortIndex itself would possibly look one thing like this '1867231621095096312'. It possible corresponds on to or is derived from a Snowflake ID.

Really a lot of the IDs you see within the response (tweet IDs) observe the “Snowflake ID” conference and appear to be '1867231621095096312'.

If that is used to kind entities like tweets, the system leverages the inherent chronological sorting of Snowflake IDs. Tweets or objects with a better sortIndex worth (a more moderen timestamp) seem larger within the feed, whereas these with decrease values (an older timestamp) seem decrease within the feed.

Right here’s the step-by-step decoding of the Snowflake ID (in our case the sortIndex) 1867231621095096312:

  • Extract the Timestamp:
  • The timestamp is derived by right-shifting the Snowflake ID by 22 bits (to take away the decrease 22 bits for knowledge heart, employee ID, and sequence): 1867231621095096312 → 445182709954
  • Add Twitter’s Epoch:
  • Including Twitter’s customized epoch (1288834974657) to this timestamp provides the UNIX timestamp in milliseconds: 445182709954 + 1288834974657 → 1734017684611ms
  • Convert to a human-readable date:
  • Changing the UNIX timestamp to a UTC datetime provides: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC)

So we will assume right here that the tweets within the residence timeline are sorted chronologically.

Every tweet has an “Actions” menu.

Instance of tweet actions

The actions for every tweet are coming from the backend in a TimelineItem.content material.feedbackInfo.feedbackKeys array and are linked with the tweets through the ActionKey:

sort TimelineResponse = {
knowledge: {
residence: {
home_timeline_urt: {
directions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[]; // <-- Right here
};
};
};
};
};

sort TimelineItem = {
entryId: string;
sortIndex: string;
content material: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161'] <-- Right here
};
};
};

sort TimelineAction = {
key: ActionKey; // '-609233128'
worth: 'SeeFewer'; // ...
immediate: string; // 'This publish isn’t related' ;
};

It’s attention-grabbing right here that this flat array of actions is definitely a tree (or a graph? I didn’t examine), since every motion might have youngster actions (see the TimelineAction.worth.childKeys array). This is smart, for instance, when after the consumer clicks on the “Do not Like” motion, the follow-up is perhaps to point out the “This publish isn’t related” motion, as a means of explaining why the consumer does not just like the tweet.

As soon as the consumer wish to see the tweet element web page (i.e. to see the thread of feedback/tweets), the consumer clicks on the tweet and the GET request to the next endpoint is carried out:

GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867231621095096312","referrer":"residence","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&options={"articles_preview_enabled":true}

I used to be curious right here why the record of tweets is being fetched through the POST name, however every tweet element is fetched through the GET name. Appears inconsistent. Particularly preserving in thoughts that related question parameters like query-id, options, and others this time are handed within the URL and never within the request physique. The response format can also be related and is re-using the kinds from the record name. I am undecided why is that. However once more, I am positive I is perhaps is perhaps lacking some background complexity right here.

Listed here are the simplified response physique sorts:

sort TweetDetailResponse = {
knowledge: {
threaded_conversation_with_injections_v2: TimelineTerminateTimeline)[],
,
},
}

sort TimelineAddEntries = TimelineCursor ;

sort TimelineTerminateTimeline = {
sort: 'TimelineTerminateTimeline',
route: 'High',
}

sort TimelineModule = {
entryId: string; // 'conversationthread-58668734545929871193'
sortIndex: string; // '1867231621095096312'
content material: {
__typename: 'TimelineTimelineModule';
objects: {
entryId: string, // 'conversationthread-1866876425669871193-tweet-1866876038930951193'
merchandise: TimelineTweet,
}[], // Feedback to the tweets are additionally tweets
displayType: 'VerticalConversation',
};
};

The response is fairly related (in its sorts) to the record response, so we gained’t for too lengthy right here.

One attention-grabbing nuance is that the “feedback” (or conversations) of every tweet are literally different tweets (see the TimelineModule sort). So the tweet thread appears to be like similar to the house timeline feed by exhibiting the record of TimelineTweet entries. This appears to be like elegant. A great instance of a common and re-usable method to the API design.

When a consumer likes the tweet, the POST request to the next endpoint is being carried out:

POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet

Right here is the request physique sorts:

sort FavoriteTweetRequest = {
variables: {
tweet_id: string; // '1867041249938530657'
};
queryId: string; // 'lI07N61twFgted2EgXILM7A'
};

Right here is the response physique sorts:

sort FavoriteTweetResponse = {
knowledge: {
favorite_tweet: 'Performed',
}
}

Seems simple and in addition resembles the RPC-like method to the API design.

We’ve got touched on some fundamental elements of the house timeline API design by X’s API instance. I made some assumptions alongside the best way to the most effective of my information. I consider some issues I may need interpreted incorrectly and I may need missed some complicated nuances. However even with that in thoughts, I hope you bought some helpful insights from this high-level overview, one thing that you can apply in your subsequent API Design session.

Initially, I had a plan to undergo related top-tech web sites to get some insights from Fb, Reddit, YouTube, and others and to gather battle-tested greatest practices and options. I’m undecided if I’ll discover the time to do this. Will see. Nevertheless it could possibly be an attention-grabbing train.

For the reference, I’m including every kind in a single go right here. You may additionally discover every kind in sorts/x.ts file.

/**
* This file incorporates the simplified sorts for X's (Twitter's) residence timeline API.
*
* These sorts are created for exploratory functions, to see the present implementation
* of the X's API, to see how they fetch House Feed, how they do a pagination and sorting,
* and the way they move the hierarchical entities (posts, media, consumer information, and so on).
*
* Many properties and kinds are omitted for simplicity.
*/

// POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
export sort TimelineRequest = {
queryId: string; // 's6ERr1UxkxxBx4YundNsXw'
variables: {
depend: quantity; // 20
cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA'
seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530658']
};
options: Options;
};

// POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
export sort TimelineResponse = {
knowledge: {
residence: {
home_timeline_urt: {
directions: (TimelineAddEntries | TimelineTerminateTimeline)[];
responseObjects: {
feedbackActions: TimelineAction[];
};
};
};
};
};

// POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
export sort FavoriteTweetRequest = {
variables: {
tweet_id: string; // '1867041249938530657'
};
queryId: string; // 'lI07N6OtwFgted2EgXILM7A'
};

// POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
export sort FavoriteTweetResponse = {
knowledge: {
favorite_tweet: 'Performed',
}
}

// GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867041249938530657","referrer":"residence","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&options={"articles_preview_enabled":true}
export sort TweetDetailResponse = {
knowledge: {
threaded_conversation_with_injections_v2: TimelineTerminateTimeline)[],
,
},
}

sort Options = {
articles_preview_enabled: boolean;
view_counts_everywhere_api_enabled: boolean;
// ...
}

sort TimelineAction = {
key: ActionKey; // '-609233128'
worth: 'SeeFewer'; // ...
immediate: string; // 'This publish isn’t related' ;
};

sort TimelineAddEntries = TimelineCursor ;

sort TimelineTerminateTimeline = {
sort: 'TimelineTerminateTimeline',
route: 'High',
}

sort TimelineCursor = {
entryId: string; // 'cursor-top-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content material: 'Backside';
;
};

sort TimelineItem = {
entryId: string; // 'tweet-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content material: {
__typename: 'TimelineTimelineItem';
itemContent: TimelineTweet;
feedbackInfo: {
feedbackKeys: ActionKey[]; // ['-1378668161']
};
};
};

sort TimelineModule = {
entryId: string; // 'conversationthread-1867041249938530657'
sortIndex: string; // '1867231621095096312'
content material: {
__typename: 'TimelineTimelineModule';
objects: {
entryId: string, // 'conversationthread-1867041249938530657-tweet-1867041249938530657'
merchandise: TimelineTweet,
}[], // Feedback to the tweets are additionally tweets
displayType: 'VerticalConversation',
};
};

sort TimelineTweet = {
__typename: 'TimelineTweet';
tweet_results: {
outcome: Tweet;
};
};

sort Tweet = {
__typename: 'Tweet';
core: {
user_results: {
outcome: Consumer;
};
};
views: {
depend: string; // '13763'
};
legacy: {
bookmark_count: quantity; // 358
created_at: string; // 'Tue Dec 10 17:41:28 +0000 2024'
conversation_id_str: string; // '1867041249938530657'
display_text_range: quantity[]; // [0, 58]
favorite_count: quantity; // 151
full_text: string; // "How I might promote my startup, if I had 0 followers (Half 1)"
lang: string; // 'en'
quote_count: quantity;
reply_count: quantity;
retweet_count: quantity;
user_id_str: string; // '1867041249938530657'
id_str: string; // '1867041249938530657'
entities: {
media: Media[];
hashtags: Hashtag[];
urls: Url[];
user_mentions: UserMention[];
};
};
};

sort Consumer = {
__typename: 'Consumer';
id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2'
rest_id: string; // '1867041249938530657'
is_blue_verified: boolean;
profile_image_shape: 'Circle'; // ...
legacy: {
following: boolean;
created_at: string; // 'Thu Oct 21 09:30:37 +0000 2021'
description: string; // 'I assist startup founders double their MRR with outside-the-box advertising and marketing cheat sheets'
favourites_count: quantity; // 22195
followers_count: quantity; // 25658
friends_count: quantity;
location: string; // 'San Francisco'
media_count: quantity;
identify: string; // 'John Doe'
profile_banner_url: string; // 'https://pbs.twimg.com/profile_banners/4863509452891265813/4863509'
profile_image_url_https: string; // 'https://pbs.twimg.com/profile_images/4863509452891265813/4863509_normal.jpg'
screen_name: string; // 'johndoe'
url: string; // 'https://t.co/dgTEddFGDd'
verified: boolean;
};
};

sort Media = {
display_url: string; // 'pic.x.com/X7823zS3sNU'
expanded_url: string; // 'https://x.com/johndoe/standing/1867041249938530657/video/1'
ext_alt_text: string; // 'Picture of two bridges.'
id_str: string; // '1867041249938530657'
indices: quantity[]; // [93, 116]
media_key: string; // '13_2866509231399826944'
media_url_https: string; // 'https://pbs.twimg.com/profile_images/1867041249938530657/4863509_normal.jpg'
source_status_id_str: string; // '1867041249938530657'
source_user_id_str: string; // '1867041249938530657'
sort: string; // 'video'
url: string; // 'https://t.co/X78dBgtrsNU'
options: {
massive: { faces: FaceGeometry[] };
medium: { faces: FaceGeometry[] };
small: { faces: FaceGeometry[] };
orig: { faces: FaceGeometry[] };
};
sizes: {
massive: MediaSize;
medium: MediaSize;
small: MediaSize;
thumb: MediaSize;
};
video_info: VideoInfo[];
};

sort UserMention = {
id_str: string; // '98008038'
identify: string; // 'Yann LeCun'
screen_name: string; // 'ylecun'
indices: quantity[]; // [115, 122]
};

sort Hashtag = {
indices: quantity[]; // [257, 263]
textual content: string;
};

sort Url = {
display_url: string; // 'google.com'
expanded_url: string; // 'http://google.com'
url: string; // 'https://t.co/nZh3aF0Aw6'
indices: quantity[]; // [102, 125]
};

sort VideoInfo = {
aspect_ratio: quantity[]; // [427, 240]
duration_millis: quantity; // 20000
variants: ...
url: string; // 'https://video.twimg.com/amplify_video/18665094345456w6944/pl/-ItQau_LRWedR-W7.m3u8?tag=14'
;
};

sort FaceGeometry = { x: quantity; y: quantity; h: quantity; w: quantity };

sort MediaSize = 'crop' ;

sort ActionKey = string;