···11+# circle-filter
22+33+Detect and remove Twitter Circle tweets from your Twitter/X archive export.
44+55+## The Problem
66+77+Twitter Circle was a feature (May 2022 - October 2023) that let users share tweets with a limited audience. When you download your Twitter archive, **Circle tweets are included but not marked as such** - there's no field indicating a tweet was Circle-only.
88+99+This is a privacy issue: if you share or upload your archive, your Circle tweets become public.
1010+1111+## The Solution
1212+1313+This tool uses Twitter's public syndication API to detect Circle tweets:
1414+1515+1. **Public tweets** return `__typename: "Tweet"` with full data
1616+2. **Circle tweets** return `__typename: "TweetTombstone"` with "This Post is unavailable"
1717+1818+The syndication API (`cdn.syndication.twimg.com/tweet-result`) is the same endpoint used for embedded tweets on websites - no API key or authentication required.
1919+2020+## Usage
2121+2222+### Step 1: Detect Circle Tweets
2323+2424+```bash
2525+cd circle-filter
2626+bun run src/detect.ts --archive ../data
2727+```
2828+2929+This will:
3030+- Load all tweets from the Circle era (May 2022 - Nov 2023)
3131+- Skip retweets (can't be Circle tweets)
3232+- Check each tweet against the syndication API
3333+- Save progress every 50 tweets (resumable if interrupted)
3434+3535+**Runtime:** ~2-3 hours for a typical archive (32k tweets to check at ~4 req/s)
3636+3737+**Output:**
3838+- `output/circle-tweets.json` - Array of Circle tweet IDs
3939+- `output/detection-log.json` - Full log of every API check
4040+- `output/progress.json` - Resume checkpoint
4141+4242+### Step 2: Generate Cleaned Archive
4343+4444+```bash
4545+bun run src/clean.ts --archive ../data --circles ./output/circle-tweets.json
4646+```
4747+4848+This generates cleaned `tweets*.js` files in `output/` with Circle tweets removed.
4949+5050+**Output:**
5151+- `output/tweets.js` - Cleaned tweets (same format as original)
5252+- `output/tweets-part1.js`
5353+- `output/tweets-part2.js`
5454+- `output/clean-summary.json` - Summary of removal
5555+5656+### For Future Archives
5757+5858+Once you have `circle-tweets.json`, reuse it for new archive exports:
5959+6060+```bash
6161+bun run src/clean.ts --archive /path/to/new/archive/data --circles ./output/circle-tweets.json
6262+```
6363+6464+No need to re-run detection - your Circle tweet IDs won't change.
6565+6666+## How Detection Works
6767+6868+```
6969+For each tweet in Circle era (May 2022 - Nov 2023):
7070+ 1. Skip if retweet (can't be Circle)
7171+ 2. Skip if in deleted-tweets.js (deleted, not Circle)
7272+ 3. Query: cdn.syndication.twimg.com/tweet-result?id={id}&token={token}
7373+ 4. If response.__typename === "TweetTombstone" → Circle tweet
7474+ 5. Save to circle-tweets.json
7575+```
7676+7777+The token is calculated as `(id / 1e15) * π` in base-36. Tweet IDs exceed `Number.MAX_SAFE_INTEGER`, so we use BigInt with hi/lo split to preserve precision:
7878+```typescript
7979+const bigId = BigInt(id)
8080+const hi = Number(bigId / 1_000_000_000_000_000n)
8181+const lo = Number(bigId % 1_000_000_000_000_000n) / 1e15
8282+token = ((hi + lo) * Math.PI).toString(36).replace(/(0+|\.)/g, "")
8383+```
8484+8585+## Rate Limiting
8686+8787+- Base delay: 250ms between requests (~4 req/s)
8888+- On HTTP 429: Exponential backoff (1s, 2s, 4s, 8s...)
8989+- Max retries per tweet: 5
9090+- Request timeout: 30s (prevents hung connections)
9191+- Progress saved every 50 tweets for resume after interruption
9292+- Transient errors (429, 5xx, network) are retried on resume
9393+9494+## Verification
9595+9696+The `detection-log.json` file contains every API call made:
9797+9898+```json
9999+{
100100+ "startedAt": "2025-12-07T...",
101101+ "completedAt": "2025-12-07T...",
102102+ "totalCandidates": 32150,
103103+ "circleCount": 847,
104104+ "results": {
105105+ "1718307594148651356": {
106106+ "id": "1718307594148651356",
107107+ "typename": "TweetTombstone",
108108+ "tombstoneText": "This Post is unavailable. Learn more",
109109+ "retries": 0,
110110+ "timestamp": "2025-12-07T..."
111111+ }
112112+ }
113113+}
114114+```
115115+116116+## File Structure
117117+118118+```
119119+circle-filter/
120120+├── src/
121121+│ ├── types.ts # TypeScript interfaces
122122+│ ├── syndication.ts # API client with retry logic
123123+│ ├── utils.ts # Archive loading helpers
124124+│ ├── detect.ts # Main detection script
125125+│ └── clean.ts # Archive cleaner script
126126+├── output/ # Generated files go here
127127+├── package.json
128128+├── tsconfig.json
129129+└── README.md
130130+```
131131+132132+## Credits
133133+134134+Detection method based on research into the Twitter syndication API. See also:
135135+- [community-archive circle-mitigation scripts](https://github.com/TheExGenesis/community-archive/tree/main/scripts/circle-mitigation)
136136+- [twittxr - Twitter Syndication API wrapper](https://github.com/Owen3H/twittxr)
137137+138138+## License
139139+140140+MIT