Skip to main content
Glama
notsuhas

moneylover

by notsuhas
README.md
# moneylover-kit

[![npm](https://img.shields.io/npm/v/@notsuhas/moneylover-kit)](https://www.npmjs.com/package/@notsuhas/moneylover-kit)

Unofficial [Money Lover](https://moneylover.me) client, CLI and MCP server. Full
transaction CRUD, lending, and a documented API — from your email and password,
with nothing else to set up.

Not affiliated with Money Lover or Finsify. Money Lover has no public API; this
talks to the same endpoints its own apps use.

## Why this exists

Other Money Lover clients can create and read transactions. None of them can
**update** or **delete** one, because the wire format is a full replace — a
naive edit silently wipes the people, the event, the exclude-from-report flag
and the reminder attached to the row. This rebuilds every write from the live
row, so an edit changes only what you asked it to.

It also documents the parts that cost real time to work out: why a wrong
category id hangs for two minutes instead of erroring, why an empty result isn't
an empty account, and why logging in twice can sign you out of your own phone.
See [docs/traps.md](docs/traps.md).

## Install

```bash
npm install -g @notsuhas/moneylover-kit    # or npx @notsuhas/moneylover-kit
```

To run it from source instead, clone and build —
`npm install -g github:notsuhas/moneylover-kit` does _not_ work, because npm
skips devDependencies when preparing a git dependency and leaves no compiler to
build with:

```bash
git clone https://github.com/notsuhas/moneylover-kit && cd moneylover-kit
npm ci && npm run build && npm install -g .
```

Then set your credentials:

```bash
export MONEYLOVER_EMAIL="you@example.com"
export MONEYLOVER_PASSWORD="…"
```

## CLI

```bash
moneylover login                 # once — mints and caches a token
moneylover wallets               # names and balances
moneylover categories            # what you can write to
moneylover list --note coffee --from 2026-01-01 --limit 10

moneylover add  --wallet Cash --category Groceries --amount -480 --note "DMart"
moneylover edit <id> --note "DMart, cleaning supplies"
moneylover rm   <id>
```

Wallets and categories too:

```bash
moneylover add-wallet   --name Travel --currency 11
moneylover add-category --name Supplements --all-wallets --parent "Health"
moneylover edit-category --category Supplements --name Vitamins
moneylover rm-category  --category Vitamins
```

`--all-wallets` and `--parent` need `--backend mobile`: a category that spans
every wallet, or nests under another, lives in a layer the web API doesn't
model.

Amounts are **signed**: `-480` is spent, `4500` is earned. Money Lover stores
the direction in the category rather than the amount, so a sign that disagrees
with the category is rejected instead of quietly corrected.

`--json` on any command gives machine-readable output.

### Lending

Money Lover tracks money between people in four of its own system categories.
This wraps them, so the app's debt view sees what you record:

```bash
moneylover lend    --person Sam --amount 5000 --wallet Savings
moneylover collect --person Sam --amount 3000 --wallet Current --parent-id ORIGINAL_LOAN_ID
moneylover lending
```

```
person                          lent   collected   outstanding     you owe
Sam                          5000.00     3000.00       2000.00        0.00
Jordan                        800.00      800.00          0.00      450.00
```

The wallet is per leg, so lending from one account and being paid back into a
different one is normal — the balance is tracked against the **person**, not the
account. `borrow` and `repay` are the mirror for money you owe. Collections and
repayments require `--parent-id` pointing to the original loan or debt. Split a
payment across originals when necessary; standalone offsets are rejected because
they leave the app’s original records open.

## MCP server

Two transports. Both expose the same tools: `list_wallets`, `list_categories`,
`list_events`, `search_transactions`, `add_transaction`, `edit_transaction`,
`delete_transaction`, `record_lending`, `lending_summary`.

Managing wallets, categories and events is opt-in, behind two flags:
`MONEYLOVER_MCP_ALLOW_STRUCTURE=1` for creating and editing them, and
`MONEYLOVER_MCP_ALLOW_DELETE=1` for `delete_wallet` and `delete_category`. A
rename can be typed back; deleting a wallet takes every transaction in it and
neither API has an undo, which is why it is its own switch.

**Local, for Claude Desktop / Cursor** — add to your MCP config:

```json
{
  "mcpServers": {
    "moneylover": {
      "command": "npx",
      "args": ["-y", "-p", "@notsuhas/moneylover-kit", "moneylover-mcp"],
      "env": {
        "MONEYLOVER_EMAIL": "you@example.com",
        "MONEYLOVER_PASSWORD": "…"
      }
    }
  }
}
```

**Remote, over HTTP** — for running it somewhere and pointing a client at it:

```bash
MCP_TOKEN=$(openssl rand -hex 32) moneylover-mcp-http   # POST /mcp, GET /health
```

It refuses to start without `MCP_TOKEN`. This endpoint can create and delete
transactions in a real account; an unauthenticated port is never the right
default.

**In Docker** — a `Dockerfile` and `compose.yaml` are in the repo:

```bash
cp .env.example .env      # email, password, and an MCP_TOKEN
docker compose up -d      # POST localhost:8790/mcp
```

Mount something persistent at `/config`, as the compose file does. That is where
the token cache lives, and without it every restart spends one of the account's
device slots.

Full setup notes, including how to keep an agent from doing something
irreversible: [docs/mcp.md](docs/mcp.md).

## Library

```ts
import { createClient } from "@notsuhas/moneylover-kit";

const ml = createClient();

await ml.wallets();
await ml.transactions({ note: "coffee", from: "2026-01-01" });
await ml.addTransaction({
  wallet: "Cash",
  category: "Groceries",
  amount: -480,
});
await ml.lending("Sam");
```

The CLI and the MCP server are both thin layers over this, so they cannot do
anything the library can't.

## One mobile session

When the Android OAuth client is configured, every normal read and transaction
or category write uses the mobile API. Wallet balances are derived exactly as
the app derives them: sum the synced rows in integer minor units and exclude
future-dated transactions. The result matched the web API for every wallet on a
live 11,239-row account.

Wallet-filtered fresh searches and ordinary receipt duplicate checks/readback
fetch a fresh complete snapshot of only the affected wallets, using the Android
app's `account_id` request filter. Receipt balances reuse the verified wallet
snapshot. No cross-call TTL or incremental cursor is used for this fast path, so
external edits and deletions remain visible on the next operation.

Whole-account audits and linked loan/debt validation retain a full
reconciliation. Interrupted pulls restart from the beginning; partial pages
never become a complete snapshot. Wallet-scoped reads do not replace other
wallets.

To enable mobile:

```bash
export MONEYLOVER_MOBILE_CLIENT="…"
export MONEYLOVER_MOBILE_SECRET="…"
```

Those are the Android app's own credentials, hardcoded in every copy of it. They
aren't a secret of yours, but they're not published here — a searchable public
copy is what gets them rotated, which would break every unofficial client at
once. [docs/api.md](docs/api.md) explains how to get them.

Without those two values the client fails clearly; it never falls back to a web
login. `--backend web` remains an explicit diagnostic escape hatch. Wallet
creation, rename and deletion use the mobile sync protocol too.

Use `MONEYLOVER_MOBILE_TOKEN` if you supply a token directly. Normally the
cached rotating refresh token is safer because it self-renews.

## The one thing to know before you start

**A Money Lover account allows only a handful of devices to hold a token at once
— five, currently — and every login registers another one.** Nothing gets kicked
off when you reach the limit; instead the _next_ login is refused, with "Maximum
device limit reached. Please log out to continue." So you don't lose data — you
lose the ability to sign in at all, including on a replacement phone, until you
log out from a device you still have.

So this caches your token under `~/.config/moneylover-kit/` and renews it
without registering another device, so a service maintains itself with no
further logins. The mobile route is non-standard: the Android app sends an empty
body to `oauth.moneylover.me/refresh-token` with the refresh token as Bearer
auth.

Set `MONEYLOVER_ACCESS_TOKEN` to supply a token directly and never log in.

## Environment

|                                                         |                                   |
| ------------------------------------------------------- | --------------------------------- |
| `MONEYLOVER_EMAIL` · `MONEYLOVER_PASSWORD`              | credentials                       |
| `MONEYLOVER_ACCESS_TOKEN`                               | use this token, never log in      |
| `MONEYLOVER_BACKEND`                                    | force `web` or `mobile`           |
| `MONEYLOVER_MOBILE_CLIENT` · `MONEYLOVER_MOBILE_SECRET` | required by the mobile backend    |
| `MONEYLOVER_CONFIG_DIR`                                 | where the token cache lives       |
| `MCP_TOKEN` · `MCP_PORT` · `MCP_HOST`                   | HTTP MCP transport                |
| `MONEYLOVER_MCP_ALLOW_STRUCTURE`                        | MCP: wallet/category/event writes |
| `MONEYLOVER_MCP_ALLOW_DELETE`                           | MCP: wallet/category deletes      |

## Docs

- [docs/api.md](docs/api.md) — both APIs: auth, endpoints, item shapes
- [docs/traps.md](docs/traps.md) — everything that cost a day to work out
- [docs/mcp.md](docs/mcp.md) — wiring the MCP server into a client
- [spec/](spec/) — OpenAPI spec for the web API, plus observed schemas

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). `npm run verify` runs typecheck, lint,
formatting and the codebase-health gate.

## Licence

MIT. Use it against your own account.

## Transaction times

Write tools accept optional `dateTime` instead of `date`, for example
`"2026-10-19T11:10:00+09:00"` for a receipt printed at 11:10 in Japan. Use a
full ISO timestamp with seconds and either `Z` or an explicit UTC offset. Do not
send both fields. Date-only calls keep their existing behavior; omitted fields
on edits preserve the saved time. The CLI equivalent is
`--date-time '2026-10-19T11:10:00+09:00'`. MCP transaction results include
`dateTime` when a timestamp is available.

### npm trusted publishing setup

The release workflow uses GitHub OIDC rather than `NPM_TOKEN`. Configure a
trusted publisher on the npm package for this exact GitHub repository and
`release.yml`, with direct publishing allowed and no environment restriction.
Keep `repository.url` aligned with the repository. A newly configured publisher
must complete a successful publish within two days or npm expires it.

If publishing returns E404 while package reads work, check the package's trusted
publisher configuration and repository metadata before replacing tokens. Passing
CI does not verify npm publish authorization; verify the release job itself.

## Persistent MCP reads

MCP read tools now return local SQLite snapshots with explicit `sync` metadata.
Pass `fresh: true` for an audit or latest upstream state. A stale read starts
background reconciliation; receipt writes and verification always use fresh API
reads. This reduces repeated provider calls without promising network latency.
Node 22.13+ is required. See [MCP caching](docs/mcp.md) for migration, response
shape changes, recovery and private-volume requirements.