{"id":51447109,"url":"https://github.com/yshmarov/x2md","last_synced_at":"2026-07-05T17:01:07.664Z","repository":{"id":360206378,"uuid":"1249095394","full_name":"yshmarov/x2md","owner":"yshmarov","description":"Convert an X (Twitter) URL into clean markdown — tweets, photos, videos, long-form X Articles, and top replies. No auth, no API key.","archived":false,"fork":false,"pushed_at":"2026-05-25T12:01:46.000Z","size":9,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-16T08:33:07.932Z","etag":null,"topics":["agent-tools","claude-code","cli","fxtwitter","llm-tools","markdown","nitter","python","scraper","twitter","x-twitter"],"latest_commit_sha":null,"homepage":"https://github.com/yshmarov/x2md","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yshmarov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T10:50:13.000Z","updated_at":"2026-05-25T13:47:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yshmarov/x2md","commit_stats":null,"previous_names":["yshmarov/x2md"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yshmarov/x2md","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yshmarov%2Fx2md","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yshmarov%2Fx2md/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yshmarov%2Fx2md/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yshmarov%2Fx2md/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yshmarov","download_url":"https://codeload.github.com/yshmarov/x2md/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yshmarov%2Fx2md/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35162071,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-tools","claude-code","cli","fxtwitter","llm-tools","markdown","nitter","python","scraper","twitter","x-twitter"],"created_at":"2026-07-05T17:01:06.927Z","updated_at":"2026-07-05T17:01:07.654Z","avatar_url":"https://github.com/yshmarov.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# x2md\n\n**Convert an X (Twitter) URL into clean markdown — including tweets, threads, photos, videos, long-form X Articles, and the top replies.**\n\n```\n$ x2md https://x.com/yarotheslav/status/2015892349688111187\n---\ntitle: \"Tweet by Yaroslav Shmarov\"\nauthor: \"Yaroslav Shmarov (@yarotheslav)\"\nsource_tweet: \"https://x.com/yarotheslav/status/2015892349688111187\"\npublished: \"Mon Jan 26 20:59:24 +0000 2026\"\nsaved_via: \"x2md (api.fxtwitter.com)\"\nsaved_on: \"2026-05-25\"\n---\n\n# Tweet by Yaroslav Shmarov\n\n\u003e Saved from [@yarotheslav](https://x.com/yarotheslav/status/2015892349688111187).\n\n💩 nginx\n🥲 localtunnel\n🤩 Cloudflare tunnels\n\n...\n```\n\nNo login, no API key, no rate limits to worry about. One file. Python stdlib plus `curl`.\n\n## Why this exists\n\nX content is increasingly hard to read without an account, hard to archive, and hard to feed into LLMs. The official API is paid and restrictive. Mirrors come and go. `x2md` quietly stitches together two unauthenticated sources — [FxTwitter](https://github.com/FixTweet/FxTwitter) for the tweet body / media / X Article content, and Nitter for the conversation — and produces a single self-contained markdown blob you (or your LLM) can read, search, and store.\n\n## Install\n\n**With [pipx](https://pipx.pypa.io) (recommended):**\n\n```bash\npipx install git+https://github.com/yshmarov/x2md\n```\n\n**No-install (single file, just clone or curl it down):**\n\n```bash\ncurl -O https://raw.githubusercontent.com/yshmarov/x2md/main/x2md.py\nchmod +x x2md.py\n./x2md.py \u003curl\u003e\n```\n\nRequirements: Python 3.9+ and `curl` on PATH (curl is shelled out for the replies fetch because Nitter sits behind Anubis proof-of-work bot challenges that block Python's urllib but pass curl's TLS fingerprint).\n\n## Usage\n\n```bash\nx2md \u003curl\u003e                    # markdown → stdout\nx2md \u003curl\u003e -o notes/          # writes notes/YYYY-MM-DD-\u003chandle\u003e-\u003cslug\u003e.md\nx2md \u003curl\u003e -o tweet.md        # writes exactly tweet.md\nx2md \u003curl\u003e --top 5            # include top 5 replies (default: 2)\nx2md \u003curl\u003e --no-replies       # skip the replies section\nx2md \u003curl\u003e --quiet            # suppress stderr warnings\n```\n\n`\u003curl\u003e` can be from `x.com`, `twitter.com`, `fxtwitter.com`, `vxtwitter.com`, or `fixupx.com` — they all resolve to the same status ID.\n\n## What you get\n\n- **YAML frontmatter** — title, author, source URL, canonical URL (for X Articles), publish date, cover image URL.\n- **The body** — tweet text for plain tweets; the full draft-js → markdown render for X Articles, with inline images spliced in at their original positions.\n- **Attached media** — photo URLs as `![]()`, video URLs as direct MP4 links (downloadable with `curl`).\n- **Top replies** — the top N third-party replies ranked by like count, each with author, like count, body (as a blockquote), and permalink. Replies from the original poster are filtered out so what you see is genuine commentary.\n\n## Use it as an LLM tool\n\n`x2md` is built to be discoverable by coding agents. The output is markdown so it slots straight into any prompt; the CLI is small and predictable so any agent with shell access can use it.\n\n**Claude Code / Cursor / aider / etc.** Tell the agent:\n\n\u003e Use `x2md \u003curl\u003e` to read tweets. It returns markdown including the tweet body, any attached media, X Article content if applicable, and the top replies.\n\n**Pipe directly into a chat:**\n\n```bash\nx2md \u003curl\u003e | pbcopy           # macOS: copy to clipboard\nx2md \u003curl\u003e | xclip -sel clip  # Linux\nx2md \u003curl\u003e \u003e /tmp/t.md \u0026\u0026 claude /tmp/t.md\n```\n\n**Add as a Claude Code permission** (so it never prompts):\n\n```jsonc\n// ~/.claude/settings.json\n{\n  \"permissions\": {\n    \"allow\": [\"Bash(x2md:*)\"]\n  }\n}\n```\n\n**Use from a script / agent loop:**\n\n```python\nimport subprocess\nmd = subprocess.check_output([\"x2md\", url], text=True)\n# md is ready to feed into a model\n```\n\n## How it works\n\n1. The status URL is parsed into `(handle, status_id)`.\n2. **Tweet body \u0026 media** come from `https://api.fxtwitter.com/\u003chandle\u003e/status/\u003cid\u003e`. This returns JSON with the tweet text, attached photos/videos (including raw MP4 URLs), and — crucially — the full structured content of long-form X Articles, which the standard X embed APIs don't expose.\n3. **X Article rendering** converts the draft-js block array into markdown: `header-two` → `##`, `unordered-list-item` → `-`, `atomic` blocks look up an image URL in `entityMap` → `media_entities` and emit `![]()`. Inline images land at their original positions in the prose.\n4. **Top replies** are scraped from a working Nitter mirror's conversation HTML. Authors, body text, like counts, and permalinks are paired in document order; the OP's own replies are filtered out; the rest are sorted by like count.\n5. The whole thing is assembled with YAML frontmatter on top.\n\n## Limitations\n\n- **No auth** — protected accounts, age-gated content, and deleted tweets won't work.\n- **Nitter is flaky.** When all mirrors are down, the tweet body still saves fine — only the replies section is skipped (with a stderr warning). The `NITTER_INSTANCES` list at the top of `x2md.py` is what to update if all current mirrors die.\n- **Single tweet, not threads.** If the URL is a thread root, only the root post is captured (plus its top replies). Capturing whole threads is a future feature.\n- **Best-effort reply scraping.** Nitter's HTML structure changes occasionally; if reply parsing ever returns garbage, the regexes in `fetch_top_replies` are where to adjust.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyshmarov%2Fx2md","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyshmarov%2Fx2md","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyshmarov%2Fx2md/lists"}