/mktg-x — authenticated Twitter/X reader
Read tweets, threads, bookmarks, articles, and user timelines from X (formerly Twitter) using an authenticated flow. Hits X's web GraphQL API with session cookies so /cmo, source-digestion workflows, and any other mktg skill can read auth-walled X content without a browser.
Firecrawl and WebFetch can't reach this content — Twitter serves a degraded logged-out page to unauthenticated clients. mktg-x is the only path.
On Activation
-
Verify auth credentials are set. mktg-x needs two env vars — a session token and a CSRF token. If neither is set, fail loud — do NOT fall back to firecrawl or WebFetch:
Priority Session token env var CSRF token env var 1 (preferred) MKTG_X_AUTH_TOKENMKTG_X_CT02 (explicit legacy compat) TWITTER_AUTH_TOKENorAUTH_TOKENwithMKTG_X_ENABLE_LEGACY_ENV=1TWITTER_CT0orCT0withMKTG_X_ENABLE_LEGACY_ENV=1The script resolves credentials in priority order. If no session token or CSRF token is found via any name, it emits the frozen error shape and exits with code 2. See references/auth.md for how to obtain both values from your browser and export them.
Headless agent warning: do NOT rely on browser-cookie extraction in headless environments. On macOS, Chrome's cookie database is Keychain-encrypted — the extraction flow triggers a GUI password prompt that a headless agent cannot answer and will hang indefinitely. Always use env vars for agent workflows.
-
Detect the URL shape. mktg-x handles five distinct shapes. Each dispatches to a different endpoint via scripts/fetch-x.ts:
URL pattern Shape Script invocation x.com/<user>/status/<id>(no reply siblings)single tweet bun run ./scripts/fetch-x.ts tweet "<url>"x.com/<user>/status/<id>(has reply chain by same author)thread bun run ./scripts/fetch-x.ts thread "<url>"x.com/i/bookmarksorx.com/<user>/bookmarksbookmarks bun run ./scripts/fetch-x.ts bookmarks "<url>"x.com/<user>/article/<id>(long-form article)article bun run ./scripts/fetch-x.ts article "<url>"x.com/<user>(no status/bookmarks/article suffix)timeline bun run ./scripts/fetch-x.ts timeline "<user>"Also accepts
twitter.com,mobile.twitter.com, andwww.x.com— the script normalizes before routing. -
Run the fetch. The script resolves
MKTG_X_AUTH_TOKEN+MKTG_X_CT0, calls the appropriate X GraphQL endpoint, sanitizes tweet text (terminal-escape injection prevention viasanitize.ts), normalizes the response, and writes structured JSON to stdout. Write large responses to.mktg-x/via shell redirect; stream small ones directly. -
Honor the frozen output contract. The script returns one canonical per-item shape that source-digestion workflows consume directly — six fields, nothing more:
{ "tweet_text": "...", "thread_unroll": ["first tweet text", "reply by same author", "..."], "embedded_links": ["https://..."], "media_urls": ["https://pbs.twimg.com/..."], "author": "@handle", "timestamp": "2026-04-11T00:00:00Z" }For
bookmarksandtimelinemodes, the output is a JSON array of this per-item shape — one entry per post.thread_unrollis empty[]for non-thread posts. This surface is frozen — adding or renaming fields breaks source-extractor's contract. Any change requires bumping the contract version.Engagement metrics (likes, reposts, replies, views) are deliberately omitted. See Anti-Pattern #1 — this is not optional polish, it's a load-bearing decision about what mktg ideates on.
-
Brand integration (progressive enhancement).
- L0 (no brand context): works identically — fetch, return JSON, done. mktg-x is a read-only fetch skill; it writes nothing to
brand/. - L1+ (brand/voice-profile.md exists): if the user asks for voice analysis after the fetch, hand the output to
brand-voiceorpositioning-angles. mktg-x does not analyze voice itself — it just grabs the source.
- L0 (no brand context): works identically — fetch, return JSON, done. mktg-x is a read-only fetch skill; it writes nothing to
When to Use
Trigger this skill whenever ANY of the following is true:
- The user pastes a tweet, thread, bookmark, or article URL (
x.com,twitter.com,mobile.twitter.com) - The user says "read this tweet", "pull this thread", "unroll this", "grab my X bookmarks", "fetch this Twitter article", "X post", "x thread"
- A source-digestion workflow's source-type detection returns
tweet(see Integration section below) - Any other mktg skill needs auth-walled X content as input (e.g.,
content-atomizerreversing a viral thread)
Do NOT use mktg-x for:
- Public non-X web pages →
firecrawl - YouTube, TikTok, or podcast URLs →
mktg transcribe - GitHub repos, issues, PRs →
ghCLI
Usage Patterns
Single tweet
bun run ./scripts/fetch-x.ts tweet "https://x.com/levelsio/status/1234567890" > .mktg-x/tweet-1234567890.json
jq '.tweet_text, .author' .mktg-x/tweet-1234567890.json
Thread unroll (consecutive same-author replies)
bun run ./scripts/fetch-x.ts thread "https://x.com/levelsio/status/1234567890" > .mktg-x/thread-1234567890.json
jq '.thread_unroll[]' .mktg-x/thread-1234567890.json
My bookmarks
bun run ./scripts/fetch-x.ts bookmarks "https://x.com/i/bookmarks" > .mktg-x/bookmarks-$(date +%Y-%m-%d).json
jq '.[] | {author, tweet_text}' .mktg-x/bookmarks-*.json
Long-form article
bun run ./scripts/fetch-x.ts article "https://x.com/levelsio/article/1234" > .mktg-x/article-1234.json
User timeline (recent posts from one account)
bun run ./scripts/fetch-x.ts timeline "levelsio" > .mktg-x/timeline-levelsio.json
Output & Organization
Write fetch results to .mktg-x/ via shell redirect. Add .mktg-x/ to .gitignore — it contains auth-walled content from the user's account and must NOT be committed.
echo '.mktg-x/' >> .gitignore
Naming conventions:
.mktg-x/tweet-<id>.json
.mktg-x/thread-<id>.json
.mktg-x/bookmarks-<YYYY-MM-DD>.json
.mktg-x/article-<id>.json
.mktg-x/timeline-<handle>.json
Never read an entire large file into context. Use jq, grep, or head to pull only what you need.
Integration with Source Digestion
mktg-x is the only auth-walled source path in mktg's multimedia digestion system. Source-type detection should route tweet URLs here — you don't need to re-detect the URL shape.
How source digestion calls mktg-x (canonical flow):
- User asks mktg to read
https://x.com/levelsio/status/1234567890 scripts/detect-source-type.shreturns{"type":"tweet","normalized":"<url>"}- The
source-extractorsub-agent picks the tweet branch and invokes mktg-x via the Skill tool - Fallback path: if the Skill tool isn't available, the extractor runs
bun run "$MKTG_REPO_PATH/skills/mktg-x/scripts/fetch-x.ts" <mode> "<url>"directly — the script is the canonical engine, the SKILL.md just wraps it - mktg-x's JSON output becomes the "Copy / Prose" section of the grounding memo. Media URLs go under a "Media" subsection
- If mktg-x fails (expired token, deleted tweet, suspended account, rate limit), the extractor returns an error extract with a suggestion to run
mktg doctoror paste tweet text directly. It does NOT fall back to firecrawl.
What mktg-x deliberately does NOT return to source-digestion workflows:
- Engagement metrics (likes, reposts, replies, views) — they bias ideation toward viral-not-truthful
- Reply chains by different authors (only same-author threads unroll)
- Embedded quote-tweets' full content (just the link; fetch separately if needed)
Failure Modes
mktg-x fails loud with a machine-readable error shape. It never falls back to firecrawl or WebFetch — that would return low-fidelity logged-out content and the agent would guess at missin