find-subscriptions

Scans bank and credit card CSV exports to surface recurring charges — subscriptions, memberships, and other repeat payments you may have forgotten about.

Requirements

  • Ruby 3.x

Installation

gem install find-subscriptions

Or clone the repo and run directly:

git clone https://github.com/jeffreybaird/find-subscriptions
cd find-subscriptions
bundle install
./bin/find-subscriptions --files EXPORT.csv

Usage

find-subscriptions --files EXPORT.csv [options]

Run with no arguments (or --help) to see the full help menu:

find-subscriptions
find-subscriptions --help

Get detailed help for a specific flag:

find-subscriptions --sort --help
find-subscriptions --format --help
find-subscriptions --set-config --help

Options

Flag Description
--files FILES Comma-separated list of CSV files to scan
--schema NAME Force a schema instead of auto-detecting from headers
--known-payees PATH Path to a known-payees YAML file; matching payees are filtered out of results
--sort ORDER Sort order for results (see below)
--inactive-for DURATION Hide subscriptions with no recent transactions (see below)
--min-amount AMOUNT Hide subscriptions with a recurring charge below AMOUNT (e.g. 5.00)
--from DATE Only include transactions on or after DATE (YYYY-MM-DD)
--to DATE Only include transactions on or before DATE (YYYY-MM-DD)
--format FORMAT Output format: text (default), json, csv
--set-config PATH Register a config YAML file to use as defaults on future runs

Output formats

Value Output
text Human-readable table (default)
json Pretty-printed JSON array — pipe into jq or save for further processing
csv CSV with header row — open in a spreadsheet or feed into other scripts
find-subscriptions --files export.csv --format json
find-subscriptions --files export.csv --format csv > subscriptions.csv

Sort orders

Value Meaning
first_desc First charge date, newest first (default)
first_asc First charge date, oldest first
last_desc Most-recent charge, newest first
last_asc Most-recent charge, oldest first
count_desc Number of transactions, highest first
count_asc Number of transactions, lowest first

--inactive-for duration format

A number followed by year, month, or week (plurals accepted):

--inactive-for 6months
--inactive-for 1year
--inactive-for 3weeks

Subscriptions whose last transaction is older than the duration are hidden. Useful for trimming results to only currently-active charges.

Examples

Scan a single Amex export, auto-detecting the schema:

find-subscriptions --files Amex-2025.csv

Scan multiple files and force a schema:

find-subscriptions --files jan.csv,feb.csv --schema american_express

Filter out known/expected subscriptions and show only recent ones:

find-subscriptions --files Amex-2025.csv \
  --known-payees data/known_payees.yml \
  --inactive-for 6months \
  --sort last_desc

Config file

Save your preferred defaults so you don't have to repeat flags every run.

1. Create a YAML config file:

# ~/.find-subscriptions.yml
sort: last_desc
format: text
min_amount: "5.00"
inactive_for: 6months
filter_known_payees: true
known_payees_path: ~/.config/find-subscriptions/known_payees.yml

2. Register it:

find-subscriptions --set-config ~/.find-subscriptions.yml

This writes the path to ~/.find-subscriptions-config-path. On every subsequent run, options from that file are loaded as defaults before any CLI flags are applied.

Option precedence (lowest to highest):

built-in defaults 

CLI flags always win. The config file fills in anything you don't pass on the command line.

Supported config keys:

Key Type Description
sort string Default sort order
format string Default output format
min_amount string Minimum recurring amount to show
inactive_for string Hide subscriptions older than this duration
filter_known_payees boolean Apply known_payees_path filter by default
known_payees_path path Default known-payees YAML path
schema string Default schema name
from_date string (YYYY-MM-DD) Default --from date
to_date string (YYYY-MM-DD) Default --to date
schemas mapping User-defined CSV schemas (see below)

files is intentionally not supported in the config — you must provide it on the command line each run.

User-defined schemas

Define custom CSV schemas in your config file without writing any Ruby code. This lets you analyze exports from banks not built into the tool.

Add a top-level schemas: key to your config YAML:

schemas:
  my_bank:
    required_headers:
      - Date
      - Description
      - Amount
    amount_key: Amount
    direction: negative_debit
    date_column: Date
    date_format: "%Y-%m-%d"
    payee_column: Description

Then use it by name:

find-subscriptions --files my_bank_export.csv --schema my_bank

Or let it auto-detect (your schema is checked after the built-ins, so unique headers help):

find-subscriptions --files my_bank_export.csv

Schema config keys

Key Required Description
required_headers yes Array of CSV column names that must be present
amount_key yes Column name containing the transaction amount
direction yes How to determine if a transaction is outgoing (see below)
date_column yes Column name containing the transaction date
date_format yes strptime-compatible format string (e.g. %Y-%m-%d, %m/%d/%Y)
payee_column yes Column name containing the payee/description
indicator_column if direction: indicator_column Column holding debit/credit labels
debit_value no Value in indicator_column that means "debit" (default: Debit)

Direction strategies

Strategy When to use
negative_debit Outgoing charges are negative numbers (most generic bank CSVs)
positive_debit Outgoing charges are positive numbers (e.g. American Express)
indicator_column A separate column labels rows as Debit or Credit (e.g. Navy Federal)

indicator_column example:

schemas:
  regional_bank:
    required_headers:
      - Posted Date
      - Description
      - Amount
      - Transaction Type
    amount_key: Amount
    direction: indicator_column
    indicator_column: Transaction Type
    debit_value: Debit
    date_column: Posted Date
    date_format: "%m/%d/%Y"
    payee_column: Description

Supported built-in schemas

Name Bank / Issuer Required CSV headers
american_express American Express Date, Description, Amount, Card Member
navy_federal Navy Federal Credit Union Transaction Date, Description, Amount, Credit Debit Indicator
generic Generic (YYYY-MM-DD dates) Date, Description, Amount

The schema is auto-detected from the CSV headers. Pass --schema NAME to override.

Known-payees file

The --known-payees flag points to a YAML file that maps canonical names to regex patterns. Any subscription whose payee matches a pattern is removed from output — useful for filtering charges you already know about.

- name: "Netflix"
  normalized: "netflix"
  patterns:
    - '/\bnetflix\b/i'
    - '/\bnflx\b/i'

- name: "Amazon Web Services"
  normalized: "amazon web services"
  patterns:
    - '/aws\.amazon\.com/i'

Each entry requires:

  • name — human-readable label shown in output when the payee matches
  • normalized — internal deduplication key (lowercase, used for grouping)
  • patterns — list of Ruby regex literals in /pattern/flags format

data/known_payees.yml is the default file and is always loaded for payee normalization (display names). Filtering only applies when --known-payees is explicitly passed (or filter_known_payees: true is set in the config).

Output format

Subscriptions:
  - SPOTIFY                                                               : $9.99 since January 2025 (14 transactions) until February 2026
  - NETFLIX.COM                                                           : $15.49 since March 2024 (12 transactions) until February 2026

License

PolyForm Noncommercial License 1.0.0 — free to use and fork for personal and open-source projects. Commercial use prohibited. See LICENSE.