Turn your X archive into a private, local X network graph and a Top 200 circle screenshot.
This is a standalone extraction of the X Network graph from the local Obsidian Portal. It runs fully in the browser after you generate public/generated/x-network.json from your own X archive.
Top 200 circle export:
- Force-style X interaction graph with small avatar nodes.
- Top 200 circle view with your profile centered.
- Save-to-filesystem PNG export.
- Copy-to-clipboard PNG export.
- Searchable right rail with ranks, DM/reply/mention/retweet stats.
- Virtualized interaction table and basic stats view.
- Sample data fallback so the app works before you import an archive.
bun install
bun run devOpen the Vite URL and you should see the sample X Circle.
X's current archive flow is:
- On x.com, open More in the left navigation.
- Go to Settings and privacy.
- Choose Your account.
- Select Download an archive of your data.
- Confirm your password or verification code.
- Click Request archive.
- Wait for X to email or notify you that the archive is ready.
- Download the
.zipwhile logged into the same X account. - Save the original
.zipsomewhere durable before extracting it.
Recommended local layout:
mkdir -p ~/Documents/exports/X
ARCHIVE_DATE=$(date +%Y-%m-%d)
mv ~/Downloads/twitter-*.zip ~/Documents/exports/X/x-archive-$ARCHIVE_DATE.zip
ditto -x -k ~/Documents/exports/X/x-archive-$ARCHIVE_DATE.zip ~/Documents/exports/X/archiveAfter extraction, you should have a data folder with files like:
~/Documents/exports/X/archive/data/account.js
~/Documents/exports/X/archive/data/tweets.js
~/Documents/exports/X/archive/data/follower.js
~/Documents/exports/X/archive/data/following.js
~/Documents/exports/X/archive/data/direct-messages.jsX says archive preparation can take a few days, and the archive includes machine-readable HTML and JSON-style files for posts, DMs, followers, following, profile data, media, address book data, ads data, and more. See X's official docs: download your X archive and access your X data.
Run the included extraction script against your archive data/ directory:
bun run import:archive -- --archive-dir ~/Documents/exports/X/archive/dataThis writes:
public/generated/x-network.json, used by the app.data/x-network/interactions.jsonl, the normalized interaction table.data/x-network/followers.jsonl, a normalized followers table.data/x-network/following.jsonl, a normalized following table.data/x-network/top-interactions.md, a readable top-200 summary.
Generated data is ignored by Git.
The interaction table is JSONL: one person per line, with fields like:
{
"userId": "123",
"handle": "alice",
"dm_total": 12,
"dm_sent": 7,
"dm_received": 5,
"mentions_sent": 40,
"replies_sent": 9,
"retweets_sent": 3,
"interaction_score": 130
}The graph JSON is derived from that table and follows the TypeScript model in src/types.ts.
python3 scripts/build-x-network.py \
--archive-dir ~/Documents/exports/X/archive/data \
--out public/generated/x-network.json \
--normalized-dir data/x-networkOptional flags:
--tagged-following path/to/tagged.jsonl: merge handle metadata such asname,bio,verified, andtags.--me-id 123456: override your account id ifaccount.jsis missing or malformed.
The archive does not include profile images for everyone. You can optionally fetch public X profile images for the top handles:
bun run fetch:avatarsUseful environment variables:
X_AVATAR_LIMIT=0 bun run fetch:avatars
X_AVATAR_LIMIT=200 bun run fetch:avatars
X_AVATAR_HANDLES=alice,bob,charlie bun run fetch:avatars
X_AVATAR_CONCURRENCY=3 bun run fetch:avatarsUse X_AVATAR_LIMIT=0 to try every handle in your generated graph. The default limit is 500 so first runs finish in a reasonable amount of time.
Fetched avatars are saved under public/generated/x-avatars/ and the generated JSON is updated with local avatar URLs.
The easiest path is to ask Codex to do the avatar and tag enrichment from this repo root after you have imported your archive:
I have an X archive extracted at ~/Documents/exports/X/archive/data.
In this X Circle repo, import the archive, then fetch profile images for every
handle in public/generated/x-network.json so the graph has pfps. Use the existing
scripts where possible. Set X_AVATAR_LIMIT=0 if you use the included avatar
fetch script.
Also infer useful role/tags for the top interactors from public profile names,
bios, handles, and obvious context. Write the result as JSONL at
data/x-network/tagged-following.jsonl with fields like handle, name, bio,
verified, and tags. Then regenerate public/generated/x-network.json with
--tagged-following data/x-network/tagged-following.jsonl.
Keep generated private archive data out of Git.Codex should be able to infer most role and tag data from public profile metadata. You can edit data/x-network/tagged-following.jsonl afterward if you want more personal labels.
If a profile no longer exists or should not appear in Top 200 screenshots, mark it inactive in tagged-following.jsonl:
{"handle":"old_handle","inactive":true,"inactiveReason":"no longer active","tags":["status:inactive"]}Inactive profiles stay greyed out behind the No longer active filter in the list, but they are excluded from graph counts and screenshot exports.
The app expects:
interface XNetwork {
account?: XNetworkAccount;
counts?: Record<string, number>;
nodes: XNetworkNode[];
edges: XNetworkEdge[];
followersOnlyIds: string[];
followingOnlyIds: string[];
dmMonthly: XNetworkDmMonthly[];
}Interaction score is:
DM * 5 + reply * 3 + mention + retweet + groupDM * 0.5All imported archive data stays local. Do not commit public/generated/ or data/x-network/ if it contains your private network. The app works from local files and does not need a server-side database.
This repository is designed to be public and ships with sample data only. Keep private archive exports out of Git.

