Reading X posts
Two methods depending on what you need.
Single tweet (oEmbed, preferred for one post)
Twitter's oEmbed endpoint returns the full text as JSON. No auth, no JS rendering, no rate limits.
curl -s 'https://publish.twitter.com/oembed?url=https://x.com/USER/status/ID' | jq -r '.html' | sed 's/<[^>]*>//g'
Or use WebFetch:
https://publish.twitter.com/oembed?url=https://x.com/USER/status/ID
The response JSON has html (blockquote with full tweet text), author_name, and author_url.
Limitation: oEmbed only returns the single tweet you request. It cannot fetch replies or thread continuations. You would need to know every reply's status ID upfront.
Full thread (Playwriter)
Use Playwriter when you need the full thread, replies, or any content beyond a single post. X is JS-heavy so webfetch/curl on the page itself returns an empty shell.
# 1. Create a session
playwriter session new
# 2. Navigate to the tweet
playwriter -s <ID> -e '
state.page = context.pages().find(p => p.url() === "about:blank") ?? await context.newPage();
await state.page.goto("https://x.com/USER/status/STATUSID", { waitUntil: "domcontentloaded" });
await waitForPageLoad({ page: state.page, timeout: 8000 });
'
# 3. Extract the full thread as markdown
playwriter -s <ID> -e '
const content = await getPageMarkdown({ page: state.page, showDiffSinceLastCall: false });
console.log(content);
'
getPageMarkdown uses Mozilla Readability (same as Firefox Reader View). It strips nav, sidebar, ads, and returns just the thread content. Much cleaner than querying individual [data-testid="tweetText"] elements.
Decision rule
- One tweet, just need the text -> oEmbed. Instant, zero setup.
- Full thread, replies, or visual context -> Playwriter. Requires a session but gets everything.
Never use webfetch or curl directly on x.com page URLs. The HTML is an empty JS shell.