Skip to main content
Glama
super-turbo-engineer

goblin-mny

README.md
# goblin-mny

**Ask Claude about your money.** A local MCP server over your New Zealand bank
accounts, synced from [Akahu](https://www.akahu.nz).

Everything runs on your machine. Your transactions live in a SQLite file in your home
directory, your Akahu tokens never leave it, and there is no hosted component — nothing
to sign up for beyond Akahu itself.

The successor to [`bobtronic/mny`](https://github.com/bobtronic/mny), which did this with
fish, curl and jq.

---

## What you can ask

> *"What did I spend on groceries last month, and how does that compare to my average?"*
>
> *"Find every subscription I'm paying for and what they cost me a year."*
>
> *"I think I'm still being charged for a gym I cancelled — check?"*
>
> *"How much did eating out cost me in 2025 vs 2026 so far?"*
>
> *"Show me every transaction over $500 in the last six months."*
>
> *"Am I spending more than I earn? Break it down by month."*

Answers come out of the local cache, so they're instant, work offline, and can span your
entire history without paging an API.

---

## How it works

```
   ┌──────────┐   sync    ┌──────────────────┐   MCP tools   ┌────────┐
   │  Akahu   │ ────────▶ │  local SQLite    │ ────────────▶ │ Claude │
   │   API    │           │  + FTS5 search   │               │        │
   └──────────┘           └──────────────────┘               └────────┘
        │                    ~/.goblin-mny/
        ▼
   your bank
```

Akahu is the only thing that talks to your bank. `goblin-mny` pulls from Akahu into a
local cache and answers questions from there.

---

## Setup

**Full instructions: [SETUP.md](SETUP.md)** — written so a coding agent can do the
install for you. The short version:

```bash
# Download a prebuilt binary (or build from source with `bun run build`)
curl -fsSL -o goblin-mny https://github.com/super-turbo-engineer/goblin-mny/releases/latest/download/goblin-mny-linux-x64
install -m 755 goblin-mny ~/.local/bin/

goblin-mny init                       # then paste your Akahu tokens into the config
goblin-mny doctor                     # verify credentials
goblin-mny sync                       # pull your history
goblin-mny diagnose --write           # learn your bank's transfer patterns
claude mcp add goblin-mny -s user -- ~/.local/bin/goblin-mny serve
goblin-mny install-skill              # the skill is embedded in the binary
```

**You need a New Zealand bank account.** Akahu supports ANZ, ASB, BNZ, Kiwibank,
Westpac, TSB, The Co-operative Bank, Heartland and Rabobank. Sign up at
<https://my.akahu.nz> (identity verification and 2FA required), **connect the accounts
you want counted**, then create a free personal app at
<https://my.akahu.nz/developers> for the two tokens.

Connecting the accounts is the step people miss — the personal app grants API access,
it doesn't connect anything by itself. Connect all of them, not just your everyday
account: transfers between your own accounts can only be recognised when both ends are
present.

The tokens grant full read access to your bank data — put them in
`~/.goblin-mny/config.json` yourself, never into a chat window.

> Not OAuth. Akahu's OAuth flow is for full commercial apps, which need a commercial
> agreement and would route your bank data through somebody else's app. Personal apps
> keep each person's data entirely their own — which is why everyone runs their own
> copy of this.

Prebuilt binaries for Linux, macOS and Windows (x64 + arm64) are on the
[releases page](https://github.com/super-turbo-engineer/goblin-mny/releases/latest).
macOS builds are unsigned — clear the quarantine flag with
`xattr -d com.apple.quarantine ~/.local/bin/goblin-mny`.

### Try it without a bank account

```bash
goblin-mny seed-demo    # 18 months of synthetic data
```

---

## Tools

| Tool | What it answers |
|---|---|
| `list_accounts` | What accounts exist, balances, and their IDs |
| `search_transactions` | Full-text + structured search; capped rows, totals over the **full** match set |
| `spending_summary` | Totals bucketed by category, merchant, month, account or type |
| `merchant_history` | Everything at one payee, with a per-month trend |
| `cashflow` | Income vs expenses vs net, by month or week |
| `recurring_charges` | Subscription and standing-payment detection |
| `sync_status` | How fresh the cache is |
| `sync_now` | Pull latest from Akahu |
| `query_sql` | Read-only `SELECT` escape hatch for anything else |

`list_accounts` reports balances **grouped by type** rather than as one total —
`spendable` (checking + savings), `restricted` (KiwiSaver, locked until retirement) and
`owed` (cards and loans, negative). Summing those into a single "net worth" produces a
number that reads as available money while including funds you can't touch for decades.

---

## Why the tools don't mirror the Akahu API

The obvious design is to proxy Akahu's REST shape and cache the responses. It's the wrong
shape for a model.

Ask *"how much did I spend on coffee last month"* through a cursor-paginated
`/transactions` endpoint and the model pages tens of thousands of tokens of JSON into its
context, then does the arithmetic itself — slowly, and sometimes wrongly.

The entire reason for a local database is that aggregation can happen **before** the
answer reaches the context window. So the tools are task-shaped, not endpoint-shaped:
they return totals, trends and small capped result sets. `query_sql` covers the long tail
— call it with no arguments and it returns the schema.

Storage, on the other hand, **is** a faithful mirror: same `_id`s, same field names, and
the untouched API response in `raw_json`, so new columns can be backfilled without
re-syncing.

---

## Design notes

**Sign convention** is Akahu's throughout: `amount` is negative for money out, positive
for money in.

**Incremental sync re-fetches a rolling window** (`lookbackDays`, default 30) rather than
resuming from the newest row it holds. Akahu revises transactions in place after they
settle, and merchant enrichment can arrive days late — so "everything newer than my
newest row" would permanently miss those revisions. Upserting by `_id` makes the overlap
free.

**Pending transactions get their own table**, replaced wholesale each sync. They mutate
and then vanish when they settle, reappearing in `transactions` under a different `_id`;
keeping them separate means they can never double-count.

**Full-text search, not embeddings.** Bank descriptions are short, abbreviated and mostly
structured, and Akahu already supplies merchant and category enrichment — which is
precisely the labelling a vector store would be straining to recover from
`COUNTDOWN 1234 AUCKLAND`. FTS5 over description, merchant and the
particulars/code/reference fields covers it at zero cost. A vector index can be added
later as an additive migration if fuzzy grouping proves genuinely necessary.

**Internal transfers and credit-card repayments are not spending**, and are excluded from
`spending_summary`, `cashflow` and `recurring_charges` by default. Moving $500 into
savings isn't $500 spent, and paying off a card double-counts the purchases it settles —
those were already counted on the card account. On the demo fixtures, counting them
overstated spending by **48%**. Cashflow was worse: income and expenses inflate by the
same amount, so `net` stays correct while both components are wrong, which is exactly the
kind of bug a spot-check misses. The excluded amount is always reported in
`excludedInternalTransfers`; pass `include_transfers` to reconcile against a raw
statement.

**Credit-card repayments are excluded only where the card's own history covers the
date.** The exclusion exists to stop double-counting purchases already recorded on the
card — but banks commonly expose only a few months of card history, and plenty of people
repay a card they never connected. In both cases the repayment is the *only* record of
that spending, and dropping it understates real outgoings. So the guard is date-bounded,
not a simple "is a card connected" check.

**Bank-specific patterns live in the user's rules file, never in the codebase.** The
detectors here work off Akahu's own type vocabulary and off structure — paired legs
across connected accounts. Nothing keys off description formats, which differ per
institution. `goblin-mny diagnose` bridges the gap per user.

**Recurring detection needs two signals**, not one: a regular cadence *and* a stable
amount. Regular-but-variable is a habit (weekly groceries), not a subscription. Payees
Akahu didn't enrich are grouped by a normalised description, which strips the receipt
numbers and store codes that would otherwise split one payee into forty.

**Cashflow averages exclude the current period**, because a part-month otherwise reads as
a spending drop.

**`query_sql` is guarded twice**: it runs on a separately-opened read-only connection,
*and* rejects anything that isn't a single `SELECT`/`WITH`.

**All SQL goes through `src/sqlite.ts`**, a thin adapter over `bun:sqlite`. It exists
because bun:sqlite binds named parameters *only* when the object keys carry the
placeholder sigil: given `@id` in the statement, a key of `id` doesn't throw — it
silently binds NULL and reports success. With a NULL primary key an `ON CONFLICT` clause
never fires either, so upserts quietly accumulate duplicate empty rows. The adapter
normalises keys and throws on any placeholder the caller didn't supply, turning an
invisible corruption into a loud error. `bun test` covers it.

---

## Development

```bash
bun install
bun test                     # adapter + environment assumptions
bun run typecheck
bun run dev -- doctor        # run the CLI from source
bun run serve                # run the MCP server from source
bun run build                # single executable -> dist/goblin-mny
bun run build:all            # cross-compile every platform
```

Runtime is Bun (`bun:sqlite` is built in, which is what makes the single-file executable
possible — a native module like `better-sqlite3` can't be embedded this way). Nothing is
required at *run* time: the binary is self-contained.

---

## CLI

```
goblin-mny init            Create the config file
goblin-mny doctor          Verify config, credentials and database
goblin-mny sync [--full]   Pull from Akahu into the cache
goblin-mny status          Cache freshness and coverage
goblin-mny accounts        Accounts and balances
goblin-mny top [N]         Top N spending categories, last 90 days
goblin-mny serve           Run the MCP server on stdio
goblin-mny seed-demo       Synthetic data for trying it out
```

---

## Limitations

- **New Zealand only** — Akahu covers NZ institutions.
- **Read-only.** Personal apps can't initiate payments, and this server exposes no write
  path to your accounts by design.
- **No webhooks** on personal apps, so freshness is sync-driven.
- **Akahu personal apps refresh from the bank once daily** (manual refresh has a 1-hour
  cooldown). Syncing more often returns the same data — the cache isn't stale because
  sync is broken.
- **One user per install.** A personal app is scoped to its owner's Akahu account. Each
  person runs their own copy with their own tokens — which is the point.

## Licence

MIT