Skip to main content
Glama
aroesec

moneybags

by aroesec

Moneybags

A self-hosted personal finance ledger that you can talk to.

Import statements or sync your banks. Transactions get categorized by rules first and a model second, and every correction you make teaches a rule so the same merchant is never miscategorized twice. Then ask about your money in plain language — Moneybags runs an MCP server, so Claude reads your ledger directly.

"How much did I spend on groceries in August?"
"I just bought coffee, about six dollars"
"That Venmo payment was for tree work, not uncategorized"

One deployment, one owner. Your transactions live in your database, your API keys are yours, and there is no service in the middle.


What this is, and what it isn't

It is a ledger for someone who wants their financial data in a database they control, with a categorizer that can be corrected and a conversational interface that isn't a chatbot bolted onto a dashboard.

It isn't a budgeting app with a mobile client and a support team. There is no signup, no multi-tenancy, no hosted version. If you want an app your family can log into on their phones, use Monarch or YNAB — genuinely, they are good at that and this is not trying to be.

Running it costs whatever your database and API keys cost. For a personal ledger on Neon's free tier with statement upload only, that is nothing.

Related MCP server: OpenCoffer

Why the design is the way it is

Nearly every hard decision in this codebase is about not silently losing money. Not crashing — losing. A ledger that drops a category is annoying; a ledger that drops $6,000 and still balances is dangerous, because it looks correct.

These are the rules that follow from that:

Money is integer cents. bigint in the schema, number in TypeScript. Floats appear only at the formatting boundary. Nothing sums a float, ever.

Negative means money left. Applied in parsing, storage, ledger math and UI, so a period's net cashflow is a plain SUM(amount_cents) with no per-row branching. An import adapter that gets this backwards produces a ledger that is internally consistent and entirely wrong, which is why it is the one thing adapters are told twice.

is_transfer is not a category. It means this exact dollar is already counted somewhere else in this ledger — an internal transfer that names the other account, or a credit card payment whose purchases are also imported. It is emphatically not for Venmo, Zelle, Cash App, ATM withdrawals, or savings contributions. Money that left is spending, whichever rail carried it.

A payment rail is not a merchant. "Venmo" tells you how money moved and nothing about what it bought. Those rows are charged as spending immediately — so an unanswered question never quietly reduces the month's total — and queued for you to label. One answer teaches a rule keyed to the counterparty.

Manual classifications are never overwritten. Every automated pass filters on classification_source <> 'manual'. Your answer outranks any rule and any model.

Dedupe is by fingerprint, not by statement. sha256(account, date, amount, normalized description) with a unique index. Upload overlapping statements in any order; rows already present are skipped. The account is part of that fingerprint, which is why an unfiled import is refused once you have more than one account.

Income can only be lost one way. Totals split by sign, so a positive amount counts as income whatever category it lands in — an imperfect category still counts, and a classification failure costs nothing. is_transfer is the single point of failure, so a rule may only set it on an inflow when the pattern names a payment outright or names the other account. pnpm db:audit-income lists every inflow and every rule currently able to exclude one.

The classifier refuses to guess. Rules run first. Whatever is left goes to a model. Anything still unresolved lands in a review queue, not a confident wrong answer — and descriptions that structurally cannot carry a purpose skip the model entirely, because it would answer "unknown" every time at a cost.

Setup

Node 20+, pnpm, and a Postgres database.

git clone https://github.com/YOUR-USERNAME/moneybags && cd moneybags
pnpm install
cp .env.example .env.local

Fill in three things:

# 1. Your database
DATABASE_URL="postgresql://..."

# 2. A session secret
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

# 3. A password
pnpm auth:hash 'the password you want'      # prints APP_PASSWORD_HASH=...

Then:

pnpm db:migrate
pnpm db:seed        # idempotent; seeds the category taxonomy
pnpm dev

That is a working ledger with CSV statement import. Everything below is optional and the app is honest about what each one adds.

Optional: a model

Set AI_API_KEY. You get model-assisted categorization for merchants no rule recognizes, written-up insights, and PDF/image statement reading.

Without it, rules still categorize, unmatched rows go to the review queue, and CSV import is unaffected. That is a supported way to run this, not a broken one.

Any provider works — Anthropic, OpenAI, OpenRouter, Groq, or a local Ollama or LM Studio. See docs/ai.md. PDF reading needs Anthropic; every other feature works anywhere.

Optional: bank syncing

Connect accounts through Plaid instead of uploading statements. Plaid's free tier covers 10 connections and includes transactions.

You do not need this. Statement upload is a complete way to use the app, and skipping Plaid means one less third party holding credentials to your bank. See docs/plaid.md, which also explains the free-tier trap worth knowing before you start.

Optional: talking to it

Settings → issue an MCP token, then point any MCP client at https://your-host/api/mcp with that bearer token. Fourteen tools for reading, logging and correcting. There is deliberately no delete tool — a misheard instruction must not be able to destroy a record.

Deploying

Two documented paths, neither privileged over the other:

Docker Compose — app plus Postgres, nothing else needed:

cp .env.example .env    # set APP_PASSWORD_HASH and SESSION_SECRET
docker compose up -d

Vercel + Neon — free tier, no server to run.

Both are in docs/deploy.md, including the reverse-proxy setup if you want it behind Authelia or Tailscale.

Authentication

Three methods; configure at least one or the app refuses to start rather than serving your finances to anyone who finds the URL.

Method

For

Password

The default. Store a scrypt hash via pnpm auth:hash, not plaintext.

OIDC

Any standards-compliant provider — Google, Authentik, Keycloak, Zitadel, Okta. An allowlist is required; an empty one denies everyone.

Trusted header

Already behind Authelia, oauth2-proxy, Cloudflare Access or Tailscale. Only safe when the app is unreachable except through the proxy.

Login is rate-limited, passwords are compared in constant time, sessions are signed JWTs revocable in bulk via SESSION_VERSION, and Plaid access tokens are AES-256-GCM encrypted at rest. See docs/security.md for the threat model and what it does not protect against.

Composability

Transactions enter through one boundary: src/lib/sources. Everything downstream — dedupe, reconciliation, classification, the ledger — sees only ParsedTransaction[] and cannot tell whether a row arrived by upload or sync.

Adding a bank, an aggregator, or an awkward CSV dialect is an adapter and nothing else:

registerFileSource({
  id: "my-bank",
  label: "My Bank CSV",
  accepts: ({ filename }) => filename.startsWith("mybank-"),
  parse: ({ bytes }) => ({ transactions: parseMyBank(bytes), warnings: [] }),
});

The model provider is behind the same kind of seam — no vendor SDK is imported outside src/lib/ai. docs/extending.md covers the taxonomy, rules, sources, sync providers and MCP tools.

Commands

pnpm dev · build · test · typecheck

the usual

pnpm auth:hash '<password>'

generate APP_PASSWORD_HASH

pnpm db:migratedb:seed

schema, then taxonomy

pnpm db:reclassify

re-run the pipeline over the ledger, skipping manual rows

pnpm db:audit-income

verify no inflow is being excluded from income

pnpm db:plaid-status

what is linked, and each account's sync boundary

Stack

Next.js 15 (App Router), Postgres via Drizzle, Tailwind. The Anthropic SDK and Plaid SDK are both optional at runtime and isolated behind interfaces.

Contributing

CLAUDE.md documents why things are the way they are, usually naming the bug that caused them. Read it before touching src/lib/classify or src/lib/reconcile — several regressions are pinned by tests, and the comments say what breaks if you undo them.

The general rule when changing the classifier: prefer under-matching to over-matching. A rule that never fires again costs one re-correction. A rule that over-matches silently rewrites history you already checked.

License

MIT — see LICENSE.

This handles real financial data. It comes with no warranty, and your deployment, keys and backups are your responsibility.

A
license - permissive license
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables users to track personal expenses through natural language interactions with comprehensive category support and financial summaries. Provides both local and remote MCP server options with SQLite storage for fast expense management operations.
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables querying personal finance data including accounts, transactions, spending, holdings, net worth, and budgets from your self-hosted OpenCoffer instance. Supports natural language queries through any MCP-compatible client.
    14
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Personal expense tracker MCP server that enables tracking expenses, income, budgets, and savings goals through natural language.
    10
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Exposes personal-finance tools like accounts, transactions, spending analysis, budgets, bills, reminders, portfolio, and goals via MCP, enabling any MCP client to query financial data.

View all related MCP servers

Related MCP Connectors

  • Personal finance by conversation: expenses, receipts, statement import, budgets, net worth.

  • Log, query, and edit expenses, budgets, and accounts in Ledgy from any MCP-compatible AI assistant.

  • Ask your AI about bank accounts, spending, debts, holdings, and investment activity.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aroesec/moneybags'

If you have feedback or need assistance with the MCP directory API, please join our Discord server