- Rust 92%
- Shell 7.1%
- PLpgSQL 0.5%
- Dockerfile 0.4%
|
All checks were successful
build / image (push) Successful in 4s
The GitHub repo is deliberately kept as a mirror, but its main is frozen at b08f1f0 (pre-thor-deployment) with a .gitmodules that still points every submodule back at GitHub — the same trap finding 3 caught in install.md. An agent following this link lands on stale code, not the current tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FGisjtKSjUcVGw1jX1Hf8M |
||
|---|---|---|
| .forgejo/workflows | ||
| migrations | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| compose.yaml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| script-auth.sh | ||
| seed.sh | ||
| smoke.sh | ||
openbooks-api
Rust API for a family or club to track a bank account, cash, and expenses on a real double-entry ledger — without anyone needing to know what double-entry means.
Very early POC: no auth, no RBAC, CORS wide open. Don't put real money data in it.
Run it
docker compose up -d # postgres on host port 38083
cargo run # migrations run on startup; listens on :38081
./smoke.sh # end-to-end check (needs jq)
Config via env: DATABASE_URL, BIND_ADDR.
How the ledger works
One table of entries, each with a signed amount_cents: debit is positive,
credit is negative. A transaction is balanced when its entries sum to zero, and
a deferred constraint trigger in Postgres enforces that — so no API bug can write
crooked books.
Money is stored as integer cents. No floats anywhere.
Reports flip the sign on income, liability, and equity accounts so every statement line reads as a positive number.
Endpoints
| Method | Path | Notes |
|---|---|---|
| GET | /health |
|
| GET | /accounts |
|
| POST | /accounts |
{name, kind}, kind is asset/liability/equity/income/expense |
| GET | /transactions?from=&to= |
entries nested per transaction |
| POST | /transactions |
see below |
| DELETE | /transactions/{id} |
removes both legs |
| GET | /reports/balances?from=&to= |
balance per account |
| GET | /reports/income-statement?from=&to= |
the meeting report: money in, money out, net |
| GET | /reports/balance-sheet?as_of= |
what we own vs. owe |
| POST | /mcp |
MCP endpoint, see below |
Recording $45 of pizza paid from checking:
curl -X POST localhost:38081/transactions -H 'content-type: application/json' -d '{
"occurred_on": "2026-03-05",
"description": "Meeting pizza",
"entries": [
{"account_id": 7, "amount_cents": 4500},
{"account_id": 1, "amount_cents": -4500}
]
}'
Unbalanced or single-legged transactions come back 400.
MCP
POST /mcp speaks MCP over Streamable HTTP, so an assistant can read the books and
record transactions. Six tools: list_accounts, account_balances, income_statement,
balance_sheet, list_transactions, and record_transaction.
Point a client at http://localhost:38081/mcp — the repo's .mcp.json already does.
Request/response only; there's no SSE stream, so GET /mcp returns 405.
Each tool calls the same function as the matching HTTP handler, so the two interfaces
can't disagree about the ledger — smoke.sh asserts they return identical numbers.
record_transaction goes through the same validation, so unbalanced entries come back
as JSON-RPC -32602 rather than being written.
curl -s localhost:38081/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq -r '.result.tools[].name'
Same no-auth caveat as the rest of the API: anything that can reach the port can write to the books.
Chart of accounts
Migration 0002 seeds Checking Account, Cash Box, Dues, Donations, Fundraising,
Supplies, Food, Rent, Fees, and Opening Balance. Add your own via POST /accounts.