Skip to content

About

analyzes bank statements, auto-maps messy csvs, forecasts cash flow and reconciles recurring charges

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ExpenseEye - Personal Finance Analytics

Privacy-first bank statement analyzer with intelligent subscription detection and overspending alerts

Copyright © 2026 Shantanu (shan3520). Licensed under the MIT License.
Original Author: Shantanu
Repository: github.com/shan3520/expenseeye

Python Flask React Vite scikit-learn statsmodels License

🌐 Live demo: expenseeye.pages.dev (frontend) · API at https://smartspend-v975.onrender.com

No bank statement to hand? "Try a sample statement" on the landing page loads viewer/public/sample-statement.csv and analyses it in one click — 8 months, 533 transactions, entirely synthetic. It is shaped like a real Indian bank export (banner preamble, DD/MM/YY, split Withdrawal/Deposit columns, trailing footer) so loading it exercises the auto-mapper, and it contains one genuinely missing subscription charge and one large one-off purchase so the exception list and the forecast's one-off handling have real work to show. Regenerate with python scripts/make_sample_statement.py. First request after idle can take up to ~60s while the free-tier backend wakes from sleep; the app shows a waking state and waits it out.

Overview

ExpenseEye is a privacy-conscious financial analytics platform that helps you understand your spending patterns. It is open-source and self-hostable; on the public demo your bank-statement CSV is parsed by ExpenseEye's own API (on infrastructure the operator controls) — session-scoped and auto-deleted — never sold or handed to a third-party data broker. Upload your CSV and get instant insights into recurring subscriptions, overspending months, and — powered by real machine learning — a cash-flow forecast, automatic transaction categorization, and anomaly detection.

Key Features:

  • 🔒 Privacy-conscious: parsed by ExpenseEye's own API (self-hostable), session-scoped and auto-deleted — never sold or shared with data brokers
  • 📊 Smart CSV Auto-Mapper: handles common real-world layouts — comma / semicolon / tab / pipe delimiters, ISO / DD-MM / MM-DD dates, DrCr+Amount, Debit/Credit and signed-amount columns, thousands separators (incl. European decimal commas), currency symbols and codes, and CR/DR markers. Ragged rows and implausible dates are skipped and reported, never silently dropped.
  • 💳 Subscription Detection: Identifies recurring payments with confidence scores
  • 📈 Overspending Analysis: Statistical detection of unusual spending months
  • 🔮 Cash-Flow Forecast (ML): Holt-Winters time-series forecast of the next 30 days / next month, with MAE/RMSE/MAPE accuracy reporting
  • 🏷️ Smart Categorization (ML): TF-IDF + LogisticRegression classifier labels each transaction, with a rule-based fallback for low-confidence cases
  • 🚨 Anomaly Detection (ML): Robust per-category z-scores flag unusual transactions with plain-English explanations
  • 🌍 Global Support: Auto-detects DD/MM/YYYY and MM/DD/YYYY date formats
  • ⚡ Session-Based: Ephemeral SQLite databases - data deleted after session
  • 🖥️ "Vault Terminal" UI: themeable dark/light instrument console - a 2-column landing with a live board preview, a parse/classify boot sequence on upload, and terminal-style readouts and error alerts throughout (WCAG 2.1 AA, reduced-motion aware)

Architecture

expenseeye/
├── api/              # Flask REST API backend
│   └── app.py        # API endpoints
├── core/             # Business logic modules
│   ├── loader.py         # CSV auto-mapper
│   ├── subscriptions.py  # Subscription detection (rule-based)
│   ├── overspending.py   # Overspending analysis (rule-based)
│   ├── forecast.py       # Cash-flow forecast (statsmodels Holt-Winters)
│   ├── categorizer.py    # ML transaction categorizer (TF-IDF + LogisticRegression)
│   ├── anomaly.py        # Anomaly detection (robust z-score)
│   └── reconcile.py      # Recurring reconciliation (expected vs actual, exceptions)
├── models/           # Persisted ML artifacts
│   ├── category_clf.joblib   # Trained categorizer (loaded once at startup)
│   └── model_card.json       # Held-out evaluation metrics
├── data/             # Seed + sample data (synthetic, safe to commit)
│   ├── seed_transactions.csv     # Labeled training data for the categorizer
│   ├── eval_transactions.csv     # 40 hand-labelled realistic merchants (held-out eval)
│   ├── sample_statement.csv      # Realistic 18-month statement for demos
│   └── sample_statement_iso.csv  # Clean ISO-dated fixture (reconciles 60/60)
├── scripts/          # Reproducible data generation + model training
│   ├── generate_data.py
│   ├── train_categorizer.py
│   └── make_sample_statement.py  # Builds viewer/public/sample-statement.csv
├── tests/            # pytest suite (run in CI)
├── viewer/           # React + Vite + Tailwind frontend
│   ├── src/          # Components, API client, types
│   ├── public/       # sample-statement.csv for "Try a sample statement"
│   └── package.json  # Frontend dependencies
├── render.yaml       # Render Blueprint for the API
├── .python-version   # Interpreter pin (3.11.9)
├── requirements.txt  # Python dependencies (pinned)
└── requirements-dev.txt  # Test dependencies (pytest)

Quick Start

Prerequisites

  • Python 3.11.9 (pinned by .python-version; the pinned pandas 2.1.3 has no wheels for 3.13+)
  • Node.js 20.19+ or 22.12+ (the frontend uses Vite 8)
  • pip and npm

Installation

  1. Clone the repository
git clone https://github.com/shan3520/expenseeye.git
cd expenseeye
  1. Install backend dependencies
pip install -r requirements.txt
  1. Run the backend API
python api/app.py
  1. Run the frontend (in a new terminal)
cd viewer
npm install
npm run dev
  1. Access the application

The frontend reads the API URL from the VITE_API_URL env var (defaults to http://localhost:5000). Create viewer/.env with VITE_API_URL=... to point at a deployed backend.

Deployment

Live deployment: API on Render (https://smartspend-v975.onrender.com), frontend on Cloudflare Pages (https://expenseeye.pages.dev). A render.yaml Blueprint is included for new deployments; the live service was created in the Render dashboard, which does not read it, so its settings are kept in step by hand.

Backend (Render)

  1. Create a new Web Service on Render (or use the included render.yaml Blueprint)

  2. Connect your GitHub repository

  3. Configure the service:

    • Build Command: pip install -r requirements.txt
    • Start Command:
      gunicorn api.app:app --workers 1 --threads 4 --worker-class gthread --timeout 120 --graceful-timeout 30 --bind 0.0.0.0:$PORT --access-logfile -
      
    • Environment: Python 3.11.9 (read from .python-version)

    python api/app.py still works and is what local development uses, but it runs Werkzeug's development server, which says so on boot. gunicorn adds request timeouts, worker supervision and graceful restarts.

    Sizing is measured, not guessed. A warmed worker holds ~168 MB (pandas + statsmodels + scikit-learn) against the free plan's 512 MB on 0.1 CPU, so a second worker would double memory while contending for the same CPU slice — concurrency comes from threads instead. --timeout 120 is not padding: the default is 30s and /forecast alone measured 17s on a 25k-row statement, so a large upload would be killed mid-request and surface as a 502.

    gunicorn does not run on Windows (it needs fcntl), which is why local development keeps using python api/app.py.

  4. Set environment variable CORS_ORIGINS to your frontend origin, e.g. https://expenseeye.pages.dev (comma-separated for multiple).

ℹ️ The ML stack (scikit-learn, statsmodels, scipy) makes the first build slower and more memory-hungry than a plain Flask app. The trained model is committed under models/, so no training happens at deploy time.

Frontend (Cloudflare Pages)

  1. Create a new Pages project on Cloudflare Pages and connect your GitHub repository.
  2. Configure the build:
    • Root directory: viewer
    • Build command: npm run build
    • Build output directory: dist
  3. Add environment variable:
    • VITE_API_URL = https://your-render-app.onrender.com
  4. Deploy. On every push to main, Cloudflare rebuilds automatically.

See docs/DEPLOYMENT.md for the full step-by-step guide.

Supported CSV Formats

ExpenseEye automatically detects and handles multiple CSV formats:

Date Columns

  • Date, Transaction Date, Txn Date, Posting Date, Value Date
  • Supports: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD

Description Columns

  • Description, Name, Narration, Merchant, Details, Particulars, Remarks
  • Optional - uses "TRANSACTION" placeholder if not found

Amount Patterns

Pattern 1: DrCr + Amount

Date,Description,DrCr,Amount
01/01/2024,Netflix,DR,15.99
05/01/2024,Salary,CR,3000.00

Pattern 2: Debit + Credit Columns

Date,Description,Debit,Credit
01/01/2024,Netflix,15.99,
05/01/2024,Salary,,3000.00

Pattern 3: Signed Amount

Date,Description,Amount
01/01/2024,Netflix,-15.99
05/01/2024,Salary,3000.00

Special Features

  • ✅ Skips metadata header rows automatically
  • ✅ Handles empty cells and whitespace
  • ✅ Supports files up to 10MB
  • ✅ Clear error messages showing detected vs expected columns

API Documentation

Endpoints

Every session-scoped endpoint takes the session id from POST /upload in the X-Session-Id header. A ?session_id= query parameter is still accepted as a legacy fallback, but a session id in a URL lands in browser history, proxy logs and Referer headers. See docs/API.md.

POST /upload

Upload a bank statement CSV file.

Request:

  • Content-Type: multipart/form-data
  • Field: file (CSV file)

Response:

{
  "success": true,
  "session_id": "uuid-here",
  "message": "File processed successfully",
  "transactions_loaded": 265,
  "mapping_info": {
    "date_column": "Value Date",
    "description_column": "None (using TRANSACTION placeholder)",
    "amount_pattern": "Debit/Credit (Debit + Credit)",
    "rows_skipped": 167
  }
}

GET /subscriptions (session id via X-Session-Id)

Get detected subscriptions for a session.

Response:

{
  "success": true,
  "subscriptions": [
    {
      "description": "Netflix",
      "amount": -15.99,
      "frequency": "MONTHLY",
      "avg_gap": 30,
      "occurrences": 12
    }
  ]
}

GET /overspending (session id via X-Session-Id)

Get overspending analysis for a session.

Response:

{
  "success": true,
  "overspending": [
    {
      "month": "2024-03",
      "total_spending": 5000.0,
      "avg_spending": 3500.0,
      "pct_deviation": 42.86,
      "status": "OVERSPENDING"
    }
  ]
}

GET /forecast (session id via X-Session-Id)

Cash-flow forecast (ML). Forecasts the next 30 days and next month of spending using Holt-Winters exponential smoothing, with a moving-average/linear-trend baseline fallback for sparse history. Reports holdout accuracy.

Response (abridged):

{
  "success": true,
  "method": "Holt-Winters (ExponentialSmoothing)",
  "history_months": 18,
  "next_30_day_total": 55345.89,
  "next_month_total": 66216.41,
  "accuracy": { "mae": 3274.09, "rmse": 3575.15, "mape": 5.21, "holdout_months": 4 },
  "monthly": { "history": [{ "month": "2023-01", "spend": 47000.0 }], "forecast": [{ "month": "2024-07", "spend": 66216.41 }] }
}

Limits: the analysis window is capped at the most recent 3 years and the returned daily series at 400 points (history_truncated reports when this applies). A statement carrying a mistyped year would otherwise build a continuous daily index spanning centuries — 213k points and a ~7.7 MB response from a 3-row file.

GET /reconcile (session id via X-Session-Id)

Reconciles the expected recurring ledger (detected series: merchant + cadence + amount band) against the actual charges in the statement. Returns a match rate over every expected occurrence plus a two-sided exception list — missing (expected charge never landed) and unscheduled (recurring-merchant charge no occurrence explains). Matched rows carry day-drift and amount-variance; a series that simply stopped is reported as lapsed rather than generating phantom missing rows.

Monthly and quarterly schedules are projected by calendar month (same day of month, clamped for short months), not by a fixed day count. A fixed count accumulates about half a day of error per cycle, so on a multi-year statement the projected date drifts outside the match tolerance and every later charge is reported both as a missing occurrence and as an unscheduled charge. On a 75-month series billed on the 3rd of every month that produced a 71.2% match rate with 21 missing and 23 unscheduled; the correct answer, now reported, is 100% with an empty exception list.

GET /categorize (session id via X-Session-Id)

ML transaction categorization. Uses a trained TF-IDF + LogisticRegression classifier, falling back to rule-based keyword matching only for low-confidence predictions.

Response (abridged):

{
  "success": true,
  "counts": { "total": 613, "model": 580, "rule_fallback": 33 },
  "breakdown": [{ "category": "rent", "total_spend": 396000.0 }],
  "transactions": [{ "description": "NETFLIX", "category": "subscriptions", "confidence": 0.94, "source": "model" }]
}

GET /model-card

Returns the categorizer's held-out evaluation metrics (no session required).

Response (abridged):

{
  "success": true,
  "model": "TF-IDF (word 1-2gram + char 3-5gram) + LogisticRegression",
  "metrics": { "accuracy": 0.9314, "macro_precision": 0.9379, "macro_recall": 0.9301, "macro_f1": 0.9306 }
}

GET /anomalies (session id via X-Session-Id)

Statistical anomaly detection. Flags unusual transactions using robust per-category z-scores (median + MAD) and explains why each was flagged.

Response (abridged):

{
  "success": true,
  "method": "Robust per-category z-score (median + MAD)",
  "anomaly_count": 26,
  "anomalies": [{ "description": "ADOBE CREATIVE CLOUD", "category": "subscriptions", "z_score": 4.8, "explanation": "1811 is 3.2x the typical subscriptions spend (~564); robust z-score 4.8 exceeds 3.5." }]
}

GET /health

Health check endpoint.

Response:

{
  "status": "ok"
}

Analytics Algorithms

Subscription Detection

Algorithm:

  1. Group by a NORMALIZED description (card/reference digits stripped) and an amount BUCKET — within 15% or ₹50 of the group median — so a subscription whose price drifts with a GST revision or a plan change stays one series
  2. Require minimum 3 occurrences in the amount band
  3. Reject a series that is a lucky slice of a variable-spend merchant: it must either be most of what that merchant ever charged (≥50% coverage) or be effectively fixed-price (amount MAD ≤ 1% or ₹5). Either bar alone is unsafe — coverage rejects a real Prime subscription buried in Amazon orders, and fixed-price rejects a real subscription after a price rise
  4. Take day gaps between consecutive charges; cadence comes from the MEDIAN gap, so one skipped or early-billed month does not disqualify the series
  5. Classify as WEEKLY (6–8 days), FORTNIGHTLY (12–16), MONTHLY (25–35) or QUARTERLY (84–100). MONTHLY needs 3 occurrences; WEEKLY and FORTNIGHTLY need 4, and so does QUARTERLY (a full year) — three points 90 days apart is just as easily three unrelated shopping trips
  6. Require regularity on BOTH a proportional and an absolute bound — gap MAD ≤ min(40% of the median gap, 7 days). The proportional bound alone allows ±36 days of drift on a quarterly series, which lets irregular repeat purchases pass as a subscription

Detection is a pure read — it never writes to the database.

Overspending Detection

Algorithm:

  1. Calculate monthly spending totals
  2. For each month (after a 3-month baseline):
    • Calculate the average and standard deviation of every prior month
    • Flag if spending > 120% of average OR > avg + 2σ
  3. Report percentage deviation and the z-score

The two arms catch different failures: the percentage arm catches a steady drift upward that never breaks 2σ on noisy data, and the sigma arm catches a genuine spike on a month whose baseline is unusually low. Requiring both conditions together missed each of those cases.

Statistical Approach:

  • No data leakage: only uses months strictly before the one being judged
  • Handles zero/undefined variance with a 10% floor on std_dev
  • Skips the first 3 months (insufficient history)

Anomaly Detection (ML)

Method: robust per-category z-score (median + MAD), so a handful of extreme outliers cannot inflate the spread and mask each other. A charge scoring at or above 3.5 is a candidate, and every flag carries a plain-English explanation.

Two things are deliberately not flagged:

  • Detected subscriptions. A predictable monthly charge is a subscription, not an unexplained outlier. They stay in the population (dropping them would gut the statistics on subscription-heavy statements) but are excluded from candidacy.
  • Charges that are ordinary for their own merchant. A category is a coarse bucket: petrol (₹900–2,600) shares transport with bike taxis (₹45–190), so every fill scores a huge category z-score. On the demo statement that flagged 28 charges of which 27 were the same petrol station — and something that happens 27 times is a pattern, not an anomaly. A charge must now be unusual for its category and for its merchant.

The merchant test is not an amnesty. It applies only to merchants seen at least 4 times, so a genuine one-off from a first-time merchant is still flagged; and a merchant you use constantly can still make one charge that does not belong — a ₹84,999 purchase at a merchant whose usual is ₹1,800 is flagged, with the merchant comparison stated in the explanation. Suppressed charges are counted in routine_for_merchant_suppressed, so "found nothing" and "ruled these out" stay distinguishable.

demo statement: 28 flagged -> 1 flagged, 27 reported as routine-for-merchant

Cash-Flow Forecast (ML)

Model: Holt-Winters exponential smoothing (statsmodels) on a daily and monthly spend time series, with a moving-average + linear-trend baseline fallback when history is sparse (< ~60 daily points or < 6 months). Never crashes on sparse data.

One-off charges are excluded from what the model learns. A laptop, a deposit, an annual premium is real money but is not a recurring pattern to project; left in, the trend chases it and the model predicts another one. On a six-month statement, one ₹84,999 purchase moved next-month from ₹65,830 to ₹94,163 and MAPE from 12.7% to 102%.

A charge qualifies only if it is (1) not part of a detected recurring series, (2) large against the statement (robust z on median + MAD), and (3) ≥20% of its own month — the test that separates large from distorting. Rent fails (1); a ₹2,651 shopping trip in a ₹70,000 month fails (3).

Damping is then a hypothesis that must win a back-test, not an assumption. Both candidate models are graded against the same target — the raw totals actually spent — and the damped one is used only if it predicts those totals better. This is not decoration: on sample_statement.csv, whose rent is written inconsistently (LANDLORD RENT / HOUSE RENT TRANSFER), the detector wrongly called 18 rent payments one-offs; the back-test caught it (MAE ₹3,274 → ₹25,004) and the raw model was kept. Nothing is hidden either way: the charges are named in the response, the spike stays in the charted history, they remain flagged under Anomalies, and the rejected candidate's accuracy is published beside the chosen one.

Both headline totals come from one model. next_30_day_total used to be the sum of an independent daily model while next_month_total came from the monthly one, so the two figures were different forecasts and openly contradicted each other on screen (₹1,02,366 beside ₹69,594 — a 47% gap). The 30-day figure is now the month forecast scaled by 30 / days-in-month.

Reproducing these figures — and the limit of that.

Every dependency and the interpreter are pinned (requirements.txt, .python-version), which is load-bearing rather than tidiness. Production had been silently running a different Python from the one render.yaml claimed, with numpy and pandas unpinned entirely.

That was worth fixing, but it did not make the back-tested MAPE identical everywhere, and the honest thing is to say why. Holt-Winters fits by iterative numerical optimisation. Once the same statement, the same library versions and the same Python still disagree, what is left is the platform: Linux and Windows use different C math libraries and different SIMD paths, so results differ in the last bits, and an optimiser compounds those bits into a different local optimum. On the demo statement the same code scores 14.6% on Linux and 17.9% on Windows — deterministic on each, not equal across them.

So a MAPE quoted to two decimals is more precise than the method supports across platforms. The live demo is the reference: open expenseeye.pages.dev, click Try a sample statement, and read the number off the screen — that is the figure anyone can reproduce by clicking, and it is what the numbers below are measured against. To reproduce locally, install with pip install -r requirements.txt -r requirements-dev.txt on Python 3.11.9; expect exact agreement on Linux and a few points of drift on Windows.

Accuracy — reported on two datasets, each labelled (these metrics are dataset-dependent):

  • Synthetic sample (data/sample_statement.csv): monthly rolling one-step-ahead holdout MAE ₹3,274 · RMSE ₹3,575 · MAPE 5.21%. Holt-Winters fits this generator (a linear trend + seasonality) almost by construction, so this characterises the data, not real-world skill.
  • Real statement (a 22-month bank export, removed from the repo for privacy — see P1-11): MAE ₹22,527 · RMSE ₹30,561 · MAPE 36.72% — the honest real-world figure. (It measured ~142% before the ISO-8601 date bug was fixed; correcting the timeline brought it down, and it does not approach the synthetic 5%.)
  • Daily MAE/RMSE is a secondary metric (per-day MAPE is unreliable on spiky transaction data, so the monthly aggregate is the headline). A statement too short to back-test returns accuracy: null, and the UI says so explicitly rather than showing a blank.

Transaction Categorizer (ML)

Model: TF-IDF features over word (1-2 gram) and character (3-5 gram) n-grams → LogisticRegression, persisted to models/category_clf.joblib and loaded once at startup. Low-confidence predictions fall back to keyword rules.

Split narration. Some exports put the transaction rail in one column (UPI/NEFT/ATM, always present) and the counterparty in another (blank for ATM withdrawals, interest and cheques). Taking only one stored 27% of a real 509-row statement as UNKNOWN and discarded the rail — the most categorizable field in the file. The loader now recombines them, but only when the chosen description column has gaps, so well-formed exports are untouched. A column named category/label/tag is never folded in: appending a label would hand the classifier the answer and make its accuracy meaningless.

Rule matching is prefix-anchored. Bare substring matching let atm fire inside BATMAN and TREATMENT; a boundary on both sides fixes that but breaks the common case, since banks truncate (DOMINOSP, AMAZONPA). A boundary on the left only keeps both. Merchant categories are tested before transfers, so UPI SWIGGY is dining while a bare UPI <person> is a transfer — otherwise ~73% of an Indian statement collapses into one bucket.

Cash withdrawals (cash) and bank charges (fees) have their own categories. Cash leaving the account is a limit of what a statement can show, not a failure to classify, and saying so is more useful than uncategorized.

On a real 509-row Indian statement: uncategorized 99.0% → 1.4%, model coverage 3% → 62%, UNKNOWN descriptions 139 → 1.

Accuracy — reported on two datasets, each labelled:

  • Synthetic seed holdout (25% stratified, 9 categories): Accuracy 93.1% · Macro-F1 93.1%. This holdout shares its merchant vocabulary with training, so it largely measures memorisation, not generalisation.
  • Held-out realistic set (data/eval_transactions.csv, 40 hand-labelled Indian merchants; model-only, independent of the keyword fallback): argmax accuracy 77.5% · confident-coverage 70% · accuracy-when-confident 89.3%. Below the confidence threshold the pipeline falls back to keyword rules. This curated set is indicative; a larger owner-supplied real labelled set is the definitive generalisation measure (owner-gated, P1-5).
  • The keyword fallback now covers common Indian merchants (quick-commerce, fintech, ride-hailing, streaming); the jio rule bug that mislabelled JIOMART as utilities is fixed.

Retrain anytime: python scripts/generate_data.py && python scripts/train_categorizer.py

Development

Project Structure

expenseeye/
├── api/
│   └── app.py                 # Flask API with session management + CORS
├── core/
│   ├── loader.py              # CSV auto-mapper with smart detection
│   ├── subscriptions.py       # Subscription detection algorithm
│   ├── overspending.py        # Overspending analysis algorithm
│   ├── forecast.py            # Cash-flow forecast (Holt-Winters / baseline)
│   ├── categorizer.py         # ML categorizer (TF-IDF + LogisticRegression)
│   ├── anomaly.py             # Anomaly detection (robust z-score)
│   └── reconcile.py           # Recurring reconciliation (match rate + exceptions)
├── models/                    # Trained model + model card (versioned)
├── data/                      # Synthetic seed, eval and sample data (versioned)
├── scripts/                   # generate_data.py, train_categorizer.py, make_sample_statement.py
├── tests/                     # pytest suite, run in CI on every push
├── viewer/
│   ├── src/
│   │   ├── App.tsx            # Root React component
│   │   ├── components/        # FileUpload, SubscriptionsTable, OverspendingAnalysis,
│   │   │                      #   CashFlowForecast, TransactionCategories, AnomalyDetection,
│   │   │                      #   Reconciliation, plus UI pieces (States, SessionTape, ...)
│   │   ├── lib/               # axios API client + utils
│   │   └── types/             # TypeScript interfaces
│   ├── package.json           # Frontend dependencies
│   └── vite.config.ts         # Vite config (@ alias → src)
├── render.yaml                # Render Blueprint for the API
├── .python-version            # Interpreter pin (3.11.9)
├── requirements.txt           # Backend dependencies, all pinned (incl. scikit-learn, statsmodels)
├── requirements-dev.txt       # Test dependencies (pytest)
├── .gitignore                 # Excludes test files and sensitive data
└── README.md                  # This file

Running Tests

# Install runtime + test dependencies (pytest), then run the full suite
pip install -r requirements.txt -r requirements-dev.txt
pytest -q

# Just the CSV auto-mapper tests
pytest -q tests/test_loader_patterns.py tests/test_dates.py tests/test_amounts.py

The same suite runs in CI on every push (.github/workflows/ci.yml).

To see how the loader reads a specific file, start the API and post it to /preview-csv, which returns the detected header row, columns and first rows without creating a session:

curl -F "file=@your_file.csv" http://localhost:5000/preview-csv

Adding Support for New CSV Formats

  1. Add column aliases in core/loader.py:

    # In detect_date_column(). Aliases are compared after normalize_column_name(),
    # which lowercases and drops every non-alphanumeric character, so write
    # "Txn Posted On" as 'txnpostedon'.
    aliases = ['date', 'transactiondate', 'txndate', 'postingdate', 'valuedate', 'yournewalias']
  2. Check it against a sample CSV with /preview-csv (see above) or by uploading it.

  3. Add a regression test in tests/test_loader_patterns.py using the load_csv fixture from tests/conftest.py, then run pytest -q.

Security & Privacy

  • Self-hostable: run the whole stack yourself so nothing leaves your machine. On the public demo, your statement is parsed by ExpenseEye's own API (on infrastructure the operator controls), not a third-party processor.
  • Session-based storage: each upload gets an isolated SQLite database in the server's temp directory, keyed by an unguessable session UUID
  • Automatic cleanup: session databases are deleted on exit (DELETE /session/<id>) and swept by a TTL reaper (default 30 min) — no manual cleanup needed
  • Minimal retention: nothing persists beyond the session TTL; no accounts, analytics, or profiles are kept
  • File size limits: 10MB maximum upload size
  • Input validation: UUID-validated session paths, strict CSV parsing, and a per-IP rate limit on the upload/compute endpoints

Troubleshooting

CSV Upload Fails

Error: "Could not identify date column"

  • Solution: Check if your CSV has a date column with one of the supported names
  • Debug: POST the file to /preview-csv to see the header row and columns the loader detected

Error: "No valid transactions found"

  • Solution: Verify your CSV has numeric values in amount columns
  • Debug: Check for whitespace or special characters in amount cells

Analytics Not Showing

Subscriptions: Requires at least 3 charges from the same merchant within one amount band (±15% or ₹50 of the median) for a monthly series, and 4 for weekly, fortnightly or quarterly Overspending: Requires minimum 4 months of data (3-month baseline + 1 to analyze)

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

Support

For issues, questions, or suggestions:

  • Open an issue on GitHub
  • Check existing documentation in /docs

Made for privacy-conscious individuals who want to understand their spending without compromising their data.

About

analyzes bank statements, auto-maps messy csvs, forecasts cash flow and reconciles recurring charges

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages