Fetch an individual X post and return whether it was accessible, its text when available, attached image and video metadata, page signals, and visible restriction or error signals. When only an account and a content clue are available, resolve candidate status URLs from the account's /media or /with_replies route before opening the selected post. Preserve enough structured media information to support a detailed explanation without confusing replies or related posts with the requested post.
Use Cases
Use when the caller supplies an X status URL or opaque status ID. It also applies when a relevant post must be found among an account's media posts or surrounding posts using text, timestamp, attached-image, or neighboring-post clues. This recipe is for reading a single post and its directly attached media, including posts relevant to researching equipment, products, or other detailed subject information; it is not for broad timeline searches or collecting replies.
Automation Flow
- If a status URL or status ID is available, navigate directly to
https://x.com/{username}/status/{status-id}?s=20(or use the caller-provided status URL), waiting fordomcontentloaded. Do not visit the homepage or use the search box first. - If no status ID is available, navigate directly to
https://x.com/{username}/mediafor image/media clues, orhttps://x.com/{username}/with_replieswhen surrounding posts or neighboring context is needed. Run the candidate evaluator below, select the candidate whose text, timestamp, image alt text, or neighboring context matches the clue, and read its opaque/status/{status-id}URL fromstatusUrl; never guess the ID. Navigate directly to that status URL and run the post evaluator. - On the loaded status page, run this
evaluate()in the same browser call/session:
(() => {
const bodyText = (document.body?.innerText || '').trim();
const articles = [...document.querySelectorAll('article')];
const posts = articles.map((article, index) => {
const tweetText = [...article.querySelectorAll('[data-testid="tweetText"]')]
.map(node => (node.innerText || node.textContent || '').trim())
.filter(Boolean);
const links = [...article.querySelectorAll('a')]
.map(a => ({ text: (a.innerText || '').trim(), href: a.href || '' }))
.filter(link => link.text || link.href.includes('t.co'))
.slice(0, 30);
const imgs = [...article.querySelectorAll('img')]
.map(img => ({ alt: img.alt || '', src: img.src || '', width: img.naturalWidth || null, height: img.naturalHeight || null }))
.filter(image => image.src)
.slice(0, 20);
const vids = [...article.querySelectorAll('video')]
.map(video => ({ src: video.currentSrc || video.src || '', poster: video.poster || '', width: video.videoWidth || null, height: video.videoHeight || null, duration: Number.isFinite(video.duration) ? video.duration : null, currentTime: video.currentTime || 0, paused: video.paused }));
return { index, text: (article.innerText || '').trim(), tweetText, links, imgs, vids };
});
const restrictionPatterns = [
/log in/i, /sign up/i, /create an account/i, /something went wrong/i,
/this page doesn.?t exist/i, /post isn.?t available/i, /content warning/i,
/account suspended/i, /rate limit/i, /unusual activity/i, /challenge/i, /robot/i
];
const restrictions = restrictionPatterns.filter(pattern => pattern.test(bodyText)).map(pattern => pattern.source);
const primary = posts[0] || null;
return {
url: location.href,
success: Boolean(primary?.tweetText?.length),
extractedPostText: primary?.tweetText?.join('\n') || null,
posts,
media: primary ? { images: primary.imgs, videos: primary.vids } : { images: [], videos: [] },
restrictions,
pageTitle: document.title || null
};
})()- For resolving candidates from
/mediaor/with_replies, use this evaluator on that page:
(() => [...document.querySelectorAll('article')].map((article, index) => {
const statusUrl = [...article.querySelectorAll('a[href*="/status/"]')]
.map(a => a.href).find(href => /\/status\/\d+/.test(href)) || null;
const text = (article.innerText || '').trim();
const tweetText = [...article.querySelectorAll('[data-testid="tweetText"]')]
.map(n => (n.innerText || n.textContent || '').trim()).filter(Boolean);
const images = [...article.querySelectorAll('img')].map(img => ({ alt: img.alt || '', src: img.src || '', width: img.naturalWidth || null, height: img.naturalHeight || null })).filter(img => img.src);
const videos = [...article.querySelectorAll('video')].map(video => ({ src: video.currentSrc || video.src || '', poster: video.poster || '', width: video.videoWidth || null, height: video.videoHeight || null, duration: Number.isFinite(video.duration) ? video.duration : null }));
return { index, statusUrl, text, tweetText, images, videos };
}))()- Use the first
articleon a status page as the requested post and treat later articles as replies or related content. If media metadata is incomplete, optionally opena[aria-label="View media"], wait briefly, and rerun the post evaluator. Attached photo routes may also be inspected directly athttps://x.com/{username}/status/{status-id}/photo/{n}. - For a video whose meaning is not represented by text or metadata, inspect a small set of temporal positions (for example 0%, 25%, 50%, 75%, and near the end): pause the first
article video, setcurrentTime, wait for the frame to render, and inspect the resulting frame or screenshot. Do not infer visual claims from duration or poster metadata alone. Avoid exhaustive frame-by-frame sampling. - Combine the evaluator result with browser navigation status. Report tool-level failures separately from page-level
restrictions; successful navigation without a matching article is not successful extraction.
Possible Friction Points
- The primary post is represented by an
article; its text is normally in[data-testid="tweetText"]. Additional articles may be replies or related content, so use the first article for a requested status page. - Account media posts are reachable at
https://x.com/{username}/media, while surrounding posts and replies are reachable athttps://x.com/{username}/with_replies. Status links embedded in article cards expose the opaque ID needed for a direct status URL; resolve and read that link rather than deriving or guessing the ID. - Images attached to a post are exposed as
article imgelements. Preserve theiralt,src, and natural dimensions; photo-specific routes use/photo/{n}when direct inspection is necessary. - Videos may not expose a usable
videoelement until playback or the media viewer is opened. PreservecurrentSrc/src, poster, intrinsic dimensions, duration, and playback state. For detailed explanations, sample a few meaningful timestamps rather than repeatedly seeking through the entire video. - For equipment or other detailed subject research, preserve the full post text, links, and image/video metadata; images and video may contain information not represented in the text.
- X may render a login wall, unavailable-post message, consent screen, rate-limit page, or anti-bot challenge without exposing tweet text. Return
success: falseand the visible signals in that case. - The status ID is opaque and must come from the caller's URL or be resolved from a status link in an account timeline; never guess it from the username.
- The
s=20query parameter is optional for direct status navigation and should be preserved when supplied. - A browser navigation status is a tool-level result, while
successindicates that post text was actually extracted from the DOM.