A two-part toolkit for analyzing your LinkedIn ZIP head-to-head against a friend.
- A Chrome extension that pulls a conversation's full message history from LinkedIn's GraphQL endpoint and saves it as JSON.
- A Python analyzer that parses every ZIP result (English and Polish message formats) and produces a designed PDF report with KPIs, narrative analysis, and charts for wins, speed, and backtracks.
If you and a friend share ZIP results in a LinkedIn chat every day, the chat thread becomes a years-long dataset of solve times, flawless rates, backtrack counts, and streaks. This tool surfaces the lifetime head-to-head — who's faster, who's cleaner, who's on a hot streak — as a single shareable PDF.
- Direct GraphQL extraction — pulls the full conversation via LinkedIn's
voyagerMessagingGraphQLendpoint with proper cursor pagination. No DOM scraping, no manual scrolling. - Bilingual parsing — handles both English (
flawless,with N backtracks,N-day win streak) and Polish (bezbłędnie,Z N cofnięciami,N-dniową serię zwycięstw) LinkedIn message templates. - Designed PDF report (5 pages): cover with verdict and KPIs, head-to-head wins race, speed analysis with rolling averages, quality/backtracks breakdown, highlights (closest games, biggest blowouts, recent form).
- Auto-generated narrative — paragraph-form analysis that adapts to your data ("X is the faster solver by 8s on average", "Y backtracks half as often", "Form check: both have gotten faster…").
- Resilient extraction — auto-saves partial progress to
chrome.storage.localso a mid-flight error doesn't lose pages; popup offers a one-click Restore of the last partial fetch. - Auto-captured queryId — a background service worker watches for LinkedIn's GraphQL request and stashes the current
queryId(which LinkedIn rotates on every web deploy), so you never have to copy/paste it from devtools.
| Cover (KPIs + narrative) | Head-to-head race | Speed (with rolling avg) |
|---|---|---|
![]() |
![]() |
![]() |
| Quality (backtracks) | Highlights |
|---|---|
![]() |
![]() |
The PDF version with all five pages: docs/screenshots/report.pdf.
┌──────────────────────────────────────────────────────────────────┐
│ Chrome (logged into LinkedIn) │
│ │
│ extension/ background.js ◄── webRequest hook │
│ ┌───────────────┐ popup.js ─── scripting.executeScript │
│ │ ZIP Extractor │ │ │
│ │ popup │ ▼ │
│ └───────────────┘ pageFetchAll() ── GraphQL paginated fetch │
│ │ │
│ ▼ │
│ zip-messages.json ◄─ saved via downloads API │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Terminal │
│ │
│ analyze.py ── parse(text) ── matches EN + PL regexes │
│ │ │
│ ├─ compute_insights() ── stats per player │
│ ├─ narrate(insights) ── paragraph generator │
│ ├─ page_cover/... ── matplotlib pages │
│ └─ PdfPages ─► out/report.pdf │
└──────────────────────────────────────────────────────────────────┘
- Chrome (or any Chromium-based browser that loads MV3 unpacked extensions)
- Python 3.10+
- Clone the repo.
- Open
chrome://extensions→ enable Developer mode (top-right toggle) → Load unpacked → select theextension/directory. - The ZIP Stats Extractor icon appears in your toolbar.
git clone https://github.com/YOUR-USERNAME/zip-stats.git
cd zip-stats
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt- Open the LinkedIn conversation with your friend in a tab. The URL should look like
https://www.linkedin.com/messaging/thread/2-…/. - Scroll up in the conversation once so older messages load. This triggers LinkedIn's cursor-pagination GraphQL request, which the extension's background listener captures (it needs the current
queryId, which LinkedIn rotates on each web deploy). - Click the ZIP Stats Extractor icon. The popup shows the auto-detected
queryIdand a save path (default:zip-messages.jsonin your Downloads folder). - Click Fetch all messages. The extension paginates through the full history, persisting partial state after each page. When done, the JSON saves to disk.
If something goes wrong, reopen the popup and click Restore previous fetch — the last successful partial state is downloadable.
.venv/bin/python analyze.py ~/Downloads/zip-messages.jsonThis prints a terminal summary and writes to out/:
report.pdf— the 5-page designed report (the main artifact)01_cover.png…05_highlights.png— each page as a standalone image
Useful flags:
--you "Your Name"/--friend "Their Name"— explicitly pick which sender is which (otherwise auto-detected as whichever has more ZIP results)--out path/to/dir— write to a different output directory--debug— print per-message parse trace, useful if you suspect a new message format the regex doesn't cover
.venv/bin/python analyze.py sample.json
open out/report.pdfThis generates the same screenshots you see above — 30 days of fictional head-to-head between Alex Rivers (English messages) and Ola Nowak (Polish messages).
LinkedIn ToS. This tool reads messages via LinkedIn's internal GraphQL endpoint using your own logged-in session cookies. It does not log in for you, scrape other users' data, or use credentials. That said, automated access to LinkedIn data — even of your own conversations — sits in a gray area of LinkedIn's User Agreement. Use at your own risk; this is provided for personal-data-export use cases only. Don't run it at high volume.
Rotating queryId. LinkedIn rebuilds the messaging GraphQL hash (messengerMessages.<hash>) on every web deploy, sometimes multiple times a week. The extension auto-captures the current hash by listening for it in network traffic. If extraction fails with HTTP 400, scroll up in the conversation once to fire a fresh request, then retry — the listener will have re-captured the new hash.
Message format changes. LinkedIn occasionally re-localizes their ZIP result strings. The current regexes cover all formats observed in English and Polish. If your dataset includes other languages, run --debug and the parser will log every match — add new patterns to FLAWLESS_RE, NO_BACKTRACK_RE, WITH_BACKTRACK_RE, and STREAK_RE near the top of analyze.py.
zip-stats/
├── extension/
│ ├── manifest.json # MV3 manifest, host_permissions for linkedin.com
│ ├── background.js # webRequest listener that captures the GraphQL queryId
│ ├── popup.html # Popup UI (path input, fetch / restore buttons)
│ └── popup.js # GraphQL pagination, partial-state persistence
├── analyze.py # Single-file analyzer (parse → insights → charts → PDF)
├── scripts/
│ └── gen_sample.py # Generates the synthetic sample.json
├── sample.json # Anonymized 30-day demo conversation
├── requirements.txt # matplotlib, numpy
├── docs/screenshots/ # README images regenerated from sample.json
├── LICENSE # MIT
└── README.md
Issues and PRs welcome. Some directions if you're looking for somewhere to start:
- Additional language support (Spanish, German, French — same message structure, just translated strings)
- Other LinkedIn games — Queens, Tango, Pinpoint — share the same messaging plumbing
- HTML report variant (single self-contained HTML alongside the PDF)
- Time-of-day analysis (use
deliveredAttimestamps to bucket by hour)
MIT — see LICENSE.




