unsubscribed
v0.1.0
Published
Point it at a bank statement. It finds every recurring charge, tells you what free self-hosted software replaces it, and links to the page that cancels it.
Maintainers
Readme
Export a CSV from your bank. Point this at it. It finds every recurring charge, works out what each one costs you a year, tells you which free self-hosted software replaces it, and gives you the link that actually cancels it.
Your statement is read from disk and never sent anywhere. There is no network
code in this repository — no API calls, no accounts, no bank connection, nothing
to sign up for. Read src/; it is about 700 lines.
$ npx unsubscribed statement.csv
unsubscribed Monzo · 776 transactions · 2025-07-31 → 2026-08-17
────────────────────────────────────────────────────────────────────────────
£2,111 a year, on repeat
11 active subscriptions · £175.94 a month
£1,391 of that has a free, self-hosted replacement
What you are paying for
£719.88/yr Adobe Creative Cloud monthly · £59.99
→ GIMP for Photoshop https://www.gimp.org
£240.00/yr ChatGPT Plus monthly · £20.00
→ Open WebUI + Ollama https://openwebui.com (docker: ghcr.io/open-webui/open-webui)
£191.88/yr NordVPN monthly · £15.99
→ WireGuard https://www.wireguard.com
↑ went up 23% from £12.99 to £15.99
£143.88/yr Dropbox monthly · £11.99
→ Nextcloud https://nextcloud.com (docker: nextcloud)
Stopped charging (already cancelled, or the export ends too early)
£3,250/yr HelloFresh last seen 2026-04-29See it with no data of your own
npx unsubscribed --demoA generated sample statement is committed to the repository — noisy descriptors,
a price rise, a cancelled service, and 700 transactions of ordinary spending for
the detector to correctly ignore. It is also what the GIF above records, via
demo.tape, so the demo is reproducible rather than a screenshot of
somebody's worst month.
Getting your statement out
| Bank | Where | |---|---| | Monzo | Search → export → CSV | | Starling | Statements → download CSV | | Revolut | Statement → Excel/CSV | | Amex | Statements & Activity → Download → CSV | | Chase | Account activity → download | | Barclays | Statements → export | | Wise | Statements → CSV | | YNAB / Firefly III | register export |
Those eight are recognised from the header row. Anything else with a date, an
amount and a description column works too — the columns are inferred, and you
can force it with --bank or --date-order. If your bank needs a profile,
open an issue with the header row
and it takes about five lines to add.
The full report
unsubscribed statement.csv --html report.htmlOne self-contained HTML file: every subscription, the replacement options with Docker image names, the exact URL that cancels each service, and the warnings that matter (Adobe's early-termination fee, for one). No JavaScript, no remote requests, nothing to host.
unsubscribed statement.csv --card card.svg # the number, shaped for posting
unsubscribed statement.csv --json # for piping somewhere else
unsubscribed catalog # what the catalog coversThe catalog
data/catalog.json is the actual value here: a curated map
from paid services to the open-source software that replaces them, plus the
cancellation URL for each. Every entry carries the honest caveat too —
Spotify → Navidrome. This replaces the player, not the catalogue. You need to own the music, which is not cheaper.
NordVPN → WireGuard. A VPN you host yourself does not give you the anonymity a commercial VPN sells. Different jobs — be clear which one you were paying for.
Entries with no honest replacement say so rather than inventing one. LinkedIn Premium is on the list purely to record that nothing here substitutes for a network effect.
Adding a service is the most useful thing you can contribute. One JSON object, no code:
{
"id": "example",
"name": "Example SaaS",
"category": "Notes",
"match": ["example", "exampleapp"],
"cancel": "https://example.com/account/billing",
"replacements": [
{ "name": "Thing", "url": "https://thing.dev", "docker": "thing/thing", "selfHost": true }
]
}CI validates that every entry has a cancellation link, that every replacement URL is https, and that anything with no replacement explains why.
How the detection works
Grouping charges by merchant is easy. Separating a subscription from a habit is the interesting part, because a weekly supermarket shop is also regular.
The signal that actually separates them is exactness: a subscription bills the same amount to the penny, on a fixed cycle. So a candidate has to clear all three of:
- Regular gaps — the median interval lands in a known cadence (weekly through yearly) and individual gaps barely drift from it.
- A repeated amount — the modal amount to the penny accounts for at least half the run, with at most two distinct values (which allows exactly one price change).
- Enough charges — three for monthly, four for weekly, two for annual.
That last one is a deliberate trade. A twelve-month export physically cannot
contain three annual charges, and annual subscriptions are exactly the ones people
forget — so two is allowed for those, held to a stricter bar (identical to the
penny, essentially no drift) and labelled thin in the output so you know it is
the weaker call.
Two other things fall out of the same model:
- A price rise is one subscription, not two. Amount clusters that run
sequentially get merged, so a £12.99 → £15.99 change shows as
↑ 23%rather than double-counting £346 a year. - The headline excludes cancelled things. Something that stopped charging in March is not money you will spend next year. Inflating the number with it would be the same trick the subscription businesses use.
Run --min-confidence 0.5 to see the borderline cases. It finds more, and it is
wrong more.
Docker
docker compose up # the demo
docker compose run --rm unsubscribed statement.csv # your own fileThe image has no network client in it. The volume is mounted read-only.
The joke that started this
Rocket Money charges about $6 a month to find your subscriptions.
It is in the catalog. It finds itself.
Contributing
npm test45 tests, no dependencies. The most valuable contributions, in order:
- A catalog entry for a service that is missing.
- A bank profile for an export that is not recognised — paste the header row.
- A false positive. If this reports your supermarket as a subscription, that is a bug and the fixture should grow a case for it.
MIT. Fork it, rename it, ship it.
