02 Chrome Extension Live

Bulk Unsaver for Insta

Chrome Extension

Clear out a Saved collection you have been meaning to clear out for two years — without opening four thousand posts one at a time.

Role Solo — build, productisation, licensing, store release
Stack JavaScript · Chrome Extensions / Manifest V3 · DOM automation
  • JavaScript
  • Chrome Extensions / Manifest V3
  • DOM automation
  • Chrome extension APIs
  • chrome.storage
  • Licensing & payments
  • Web Store publishing
01

The problem

Instagram gives you a "Save" button on every post and no way to bulk undo it. A Saved collection with a few thousand posts in it is a genuine problem with no product solution — so the only available method is opening each post and unsaving it, one click at a time.

The naive fix is a script that fires requests as fast as possible. That is the version that gets rate-limited, flagged, or quietly stops half way, and it leaves the user with a partially-cleared collection and no idea where it stopped. Speed is not the hard part here. Knowing when to act, when the page has actually changed, and when to stop is.

The other half of the problem is trust. An extension that acts on a page you are looking at can do irreversible things to an account you care about. It should do nothing at all until you tell it to, and it should never be the reason your account gets restricted.

02

What I built

  • A Manifest V3 Chrome extension that drives Instagram's own Saved-collection interface once you explicitly start the run — no injected API calls, just the interface a person would use.
  • A sequential run loop: unsave the current post, advance, observe that the collection actually changed, then unsave the next. Each step is confirmed before the next begins.
  • Start-from-anywhere logic, so you can begin at post 2,400 instead of being forced to clear from the top.
  • Progress persistence, so closing the tab, losing connection, or a crash does not lose your place — and does not re-process posts already handled.
  • Randomized delays between actions instead of rapid bursts, which is both safer for the account and gentler on the page.
  • Language-independent selectors, so the extension behaves identically on an interface in any locale.
  • Start-state validation with actionable feedback when the page is not in a state where the run can begin.
  • A license-key system gating the paid tier, and the packaging, listing, and update cycle to keep it working as Instagram changes.
03

Key features

The run

  • Automatically unsaves posts sequentially through the Saved collection
  • Start from any post in the collection, not just the top
  • Continue through every subsequent post automatically
  • Stop at any point — already-processed posts stay processed
  • Progress is persisted, so an interrupted run resumes rather than restarts

Safety & trust

  • Randomized delays between actions rather than rapid bursts
  • Runs only after the user explicitly starts it — never on page load
  • Detects invalid starting states and tells the user exactly what is wrong
  • Only acts on posts that are currently saved, so nothing is double-processed

Compatibility

  • Works with Instagram in different interface languages
  • Keys off DOM structure rather than English text labels
  • Survives the interface re-rendering between steps

Product

  • Published on the Chrome Web Store
  • License-key licensing for the paid version
  • Free tier, so the value can be evaluated before paying
04

Technical architecture

A content script on the Saved pages owns the run loop, a background service worker owns lifecycle and persistence, and a small popup gives the user control. The important architectural decision is that there is no state held in the page: everything the run needs survives a tab crash because it is written to extension storage on every step.

  1. Interface

    The popup: start, pause, stop, and progress. Deliberately thin — the user should never have to configure anything to use the thing.

  2. Run loop (content script)

    A small explicit state machine — idle, running, stopping — that advances one post at a time. Each transition is driven by an observed DOM change rather than a timer, so a slow page slows the loop instead of desynchronising it.

  3. Selector layer

    All knowledge of Instagram's DOM lives in one module, isolated from the run logic. That separation is what makes the extension maintainable: when the markup changes, there is exactly one file to fix, and the loop's correctness is testable against fixtures without a live page.

  4. State & persistence

    Extension storage holds the session, the last processed post, and the counters. Written on every step, so the worst case after any interruption is repeating one post — which is harmless — rather than losing the whole run.

  5. Licensing

    Key validation and entitlement state, checked at the boundary that matters, with the failure mode being "free tier" rather than "extension broken".

  • No state is held in the page — everything durable is in extension storage.
  • All Instagram-specific knowledge is isolated in one selector module, so DOM changes have a single blast radius.
05

Visuals

06

What was technically hard

01

There is no API to build this on

The Saved collection is a client-rendered app, not a page. There is no "next post" element and no bulk endpoint to call, so advancing means inferring from how the DOM changes after an action. The loop cannot just "click and move on" — it has to wait for confirmation that the click did what it was supposed to, or it will happily unsave the same post twice or stall forever.

02

Randomized delays are a correctness feature, not a politeness one

Firing as fast as possible is the obvious implementation and the wrong one: it looks like automation, it saturates a single-threaded page, and it is exactly the pattern most likely to get an account flagged. The randomized delay makes the run survivable, and it is the reason this works on real accounts at all.

03

Language independence without a single English string

The obvious selectors are text-based, which means the extension works in English and quietly does nothing anywhere else. Keying entirely off structure, roles, and URL patterns — no readable text in the selector path at all — means one build works across every locale, which is most of the addressable audience.

04

Resumability without double-processing

A run can be interrupted at any instant: tab closed, network dropped, page navigated, extension reloaded mid-step. The session has to resume from roughly the right place, and the worst acceptable outcome is re-processing one post — not restarting the collection, and not silently skipping thirty of them.

05

Telling the user when it cannot start

Half the possible states are simply "this page is not a runnable Saved collection" — logged out, not on the Saved page, a modal open, a post already unsaved. Without explicit start-state validation the failure is a button that does nothing, which is indistinguishable from a broken extension. Detecting the invalid state and naming it is most of what makes this feel like a product.

06

Instagram ships changes, and you do not get a changelog

Shipping a real product means the platform underneath you keeps moving and users notice immediately. That turned the isolated selector layer from a tidiness preference into the feature that makes the extension maintainable — the run logic never had to be touched when the markup did.

07

Testing & reliability

Because the platform is a live React app that changes under you, the tests are built around a state machine with fixed inputs and a fake page, not around a live session. The state machine is the part I can assert on exhaustively; the selectors are the part I regression-test against saved DOM fixtures.

End-to-end assertion chain

  1. 01 Explicit start
  2. 02 DOM state validated
  3. 03 Post unsaved via interface
  4. 04 Collection change observed
  5. 05 Progress persisted
  6. 06 Next post
  7. 07 Final result for the user

A test passes only when every link above is observed. When one breaks, the failure names the link instead of saying the feature is broken.

Run-loop state machine

The loop is factored out as an explicit state machine with the DOM behind an interface, so every transition — including the awkward ones — is directly testable: start, pause, stop mid-run, resume after interruption, and complete.

DOM fixtures across locales

Saved snapshots of the Saved-collection markup in several interface languages drive the selector tests, which is how language independence is verified rather than assumed.

Invalid start-state detection

Logged out, wrong page, empty collection, post already unsaved, modal in the way — each verified to produce a clear message instead of a silent no-op.

Interruption and idempotency

Killing the tab and reloading mid-run, and asserting the run resumes from the right place and does not re-process what it already handled.

Service worker lifecycle

Manifest V3 workers are killed aggressively; the run has to survive that and reload its state from storage rather than assuming it is still in memory.

Store release loop

Packaging, review feedback, and shipping updates when Instagram changes the interface — the real-world regression suite is a user reporting a button that stopped working.

08

My contribution

Solo — build, productisation, licensing, store release

Solo. I built the extension, reverse-engineered the interface into an isolated selector layer, designed the run state machine, implemented progress persistence and the resume path, and built the license-key system for the paid version.

Productisation was a real part of this one: packaging it, writing the store listing, and maintaining it against a platform that changes its markup without notice.