Demo video coming soon...
Chrome extension that finds who doesn't follow you back · no backend, no credential handoff · rides the user's own Instagram session through an injected content script
Architecture
- Popup UI, Frontend · action popup, popup · popup.js
- Profile categorizer, Analysis · influencer scoring, popup.js · categorizeUsers
- Content script, Core · injected API client, content · content.js, cursor pagination, 429 kill switch, 300 ms pacing, CSRF from cookies, follow / unfollow POST, profile hydration
- Session detector, Identity · login detection, content.js · isUserLoggedIn
- chrome.storage.local, Storage · local, instagramAnalysisResults_<id>
- Instagram web API, External · private API, external service, friendships · web_profile_info
- Popup UI to Profile categorizer: SET DIFF
- Popup UI to Content script: MESSAGES · SCAN + UNFOLLOW
- Profile categorizer to Content script: PROFILE FETCH · BATCHES OF 5
- Content script to Session detector: RESOLVE USER ID
- Content script to Instagram web API: API CALLS · PAGINATED + PACED
- Popup UI to chrome.storage.local: SAVE SCAN · 24 H STAMP
- chrome.storage.local to Popup UI: RESTORE ON OPEN
You open the popup on any instagram.com tab. Popup UI first restores the previous scan from chrome.storage.local and checks the 24 h cooldown. On Analyze it messages the Content script, which asks the Session detector for the logged-in user id (cookies first, profile fetch as fallback). The content script then pages through /friendships/<id>/followers (25 per page) and /following (200 per page) against the Instagram web API with 300 ms pacing between requests. Back in the popup, the diff yields the not-following-back set, the Profile categorizer hydrates each account in batches of 5 to separate influencers from regular users, and results plus caches are written back to storage.
Deployed: Extension → Chrome · Manifest V3, loaded unpacked or via Web Store
Outcomes
- 0 · servers · everything runs inside the user's browser
- 40+ · UI languages the login detection survives
- 1st 429 · trips a kill switch, scan degrades instead of failing
- 24 h · scan cooldown per account, results restored meanwhile
Skills demonstrated: private API reverse-engineering · rate-limit-aware pagination · cache invalidation (7-day TTL) · Chrome message passing · CSRF token handling
Problems and solutions
One constraint drives everything: Instagram has no public API for follower lists, so the extension must reuse the user's own session without ever holding credentials or tripping rate limits.
█ Ride the user's session, not a backend
Problem: There is no official API to list followers. A server-side scraper would need the user's password or stolen cookies, plus proxies to survive blocks. Both are dealbreakers for trust and cost.
A content script injected into instagram.com calls the same private endpoints the web app uses. Requests go out with credentials: include, the browser's own user agent, and the web app id header, so to Instagram they look like the user browsing.
- Rejected a scraping backend: zero credential handling beats full control of the pipeline
- Rejected DOM-scraping the followers modal: brittle against markup changes and far slower than JSON endpoints
- Accepted trade-off: undocumented endpoints can change without notice, so the code treats every response as optional
▒ Rate limits: pace, detect, degrade
Problem: Follower pagination plus one profile fetch per account can mean hundreds of requests. Hammering private endpoints earns a 429 and can flag the user's real account.
300 ms pauses between pages, profile hydration in batches of 5, and a global kill switch: the first 429 stops all further profile fetches for the session and the scan continues with basic data only.
- Rejected retry-with-backoff: retrying against an anti-abuse system digs the hole deeper
- Degrading beats failing: users still get the full not-following-back list, just without influencer scores
- The kill switch is a one-way flag, deliberately conservative to protect the user's account
▚ 24-hour scan cooldown
Problem: The full scan is the heaviest operation. Users clicking Analyze repeatedly multiply the block risk for zero new information.
Each scan is stamped per account in chrome.storage.local. Within 24 h the button is disabled with a live countdown and the saved results are shown instead.
- Rejected unlimited scans: protecting the user's account outranks freshness
- Accepted trade-off: data can be up to a day stale
- Follow / unfollow actions update the saved results in place, so the cached view stays correct between scans
▞ Login detection in 40+ languages
Problem: The first version detected login by the English alt text 'profile picture'. Any non-English Instagram UI made the extension think the user was logged out.
Five layered fallbacks: a localized alt-text table covering 40+ languages, a geometry heuristic (small square image inside the nav), Instagram's own data objects, the logout link, and session cookies.
- Rejected a single 'reliable' selector: Instagram's obfuscated class names change too often to pin
- Heuristics can false-positive individually, so five independent signals cross-check each other
- Cookie check is the last resort because cookies can outlive a real session
▓ Cache aggressively, key by account
Problem: Profile hydration costs one request per analyzed user. Rescans and account switches would re-pay that cost and re-risk rate limits.
Per-username analysis results live in a 7-day cache; scan results are keyed by account id, so people managing several accounts keep separate histories that survive switching.
- Rejected no-cache simplicity: repeat requests are the scarcest resource here
- 7 days balances staleness against request budget, follower counts move slowly
- When the 429 kill switch fires, cached profiles still fill in scores for free
▤ Unfollow without losing history
Problem: Unfollowing someone while reviewing the list mutates the data underneath: the user disappears, and a rescan to reflect it would burn the daily scan.
Unfollowed accounts move to a persisted recently-unfollowed list instead of vanishing. Refollowing restores them to the regular list, and every change edits the saved results in place.
- Rejected rescan-after-action: one unfollow should not cost a full scan
- Rejected silent removal: users want an undo path and a record of who they dropped
- State transitions live in storage, so closing the popup mid-cleanup loses nothing