finfry
finfry
A command-line budget & expense tracker written in Crystal, built on the Jargon CLI shard. It keeps a proper double-entry ledger stored as plain JSON — every transaction moves money between accounts and always balances to zero.
Installation
shards install
shards build --release
The finfry binary is produced in ./bin.
Concepts
Accounts are hierarchical, colon-separated names. By convention:
Assets:*— what you own (Assets:Checking,Assets:Savings)Liabilities:*— what you owe (Liabilities:CreditCards:ChasePlatinum)Income:*— where money comes from (Income:Salary)Expenses:*— where it goes (Expenses:Food:Coffee)
Every transaction is a set of postings (an account + a signed amount) that sum to zero. You rarely write that by hand — the spend/earn/transfer commands build the balanced pair for you.
Chart of accounts
An account is known if it's been declared in the chart or already used by a posting. A new ledger is seeded with a small starter chart (Assets:Checking, Expenses:Food, …). How finfry reacts to an unknown account is set per ledger:
strict(default) — recording to an unknown account is rejected; declare it first withfinfry accounts add. The error suggests close matches, so a typo (Expenses:Foood) is caught instead of silently becoming a new account.guard— prompts ("Expenses:Foood— did you meanExpenses:Food? Create it? [y/N]") and declares it on confirmation.off— any referenced account is created silently.
finfry accounts # list known accounts (declared + used)
finfry accounts add Expenses:Food:Coffee Assets:Brokerage
finfry accounts rename Expenses:Foood Expenses:Food # rewrites postings; merges if target exists
finfry accounts rm Assets:Brokerage # remove from the chart
finfry accounts policy guard # strict | guard | off (no arg prints current)
The AI path respects the policy too: a proposal that introduces a new account shows it as New account: …, and your confirmation is the deliberate "yes" that declares it — even in strict mode.
Undo & corrections
There are two ways to take something back, matching how bookkeeping actually works:
finfry undoremoves the most recent change outright — as if it never happened. Safe because nothing has been recorded on top of it yet (like backspacing before it's part of the record). Repeatedundopops back through recent changes.finfry undo <id>corrects an older change (find the id withfinfry history) by posting a reversing entry — a mirror-image transaction (Reversal of #N) so the two net to zero. The original is kept and the audit trail is preserved, because you can't un-happen history that later entries sit on top of.
finfry redo brings back the change undo just removed (single level; any new change invalidates it). finfry history lists changes and marks which have been reversed.
Sign convention
Amounts are signed cents internally. Assets/Expenses are debit-normal (positive), Income/Liabilities/Equity are credit-normal. Reports flip the sign on credit-normal accounts so they read as positive numbers.
Usage
AI assistant
finfry ai lets you ask questions or make changes in plain English. Under the hood every finfry command is exposed to Claude as a tool, so it can read the ledger to answer you and propose changes for you to approve.
export ANTHROPIC_API_KEY=sk-ant-...
# Ask — read-only, answered directly
finfry ai "what did I spend on food last month?"
# Record — one or many changes, gathered into a plan you approve
finfry ai "spent $50 at Starbucks yesterday on my Chase Platinum CC"
finfry ai "set next month's food budget 10% under what I spent in May"
echo "netflix 15.49 monthly" | finfry ai --yes
How it works:
- Read tools (
list,balance,report,daily,accounts,history) run immediately so the AI can answer and gather context. - Write tools (
spend,earn,transfer,budget,accounts add/rename) don't take effect right away — they're collected into a plan that finfry shows you and applies only once you approve it (--yesskips the prompt). The whole plan applies as one undoable change. - It reuses your existing accounts for consistency and resolves relative dates. finfry assembles the balanced postings and enforces the account policy, so the AI can't throw off the books.
deleteandaccounts policyare withheld from the AI on purpose.
Set FINFRY_MODEL to override the model (default claude-opus-4-8).
Use finfry inside an agent harness (MCP)
finfry mcp runs finfry as an MCP server over stdio, exposing the same safe command surface as tools so any MCP client (Claude Code, Claude Desktop, …) can read and update the ledger — with the harness providing the chat, history, and approval UI.
Register it with an MCP client (standard config shape):
{
"mcpServers": {
"finfry": { "command": "finfry", "args": ["mcp"] }
}
}
Or with the Claude Code CLI: claude mcp add finfry -- finfry mcp.
Notes:
- The client (not finfry) gates each tool call, so writes execute when invoked — each still records its own undoable change, and the account policy + balance guards still apply.
deleteandaccounts policyare not exposed over MCP (same withholding as the built-in agent).- Set
FINFRY_DATAin the server's env if you want it pointed at a specific ledger.
Manual entry
# Spend (default funding account is Assets:Checking)
finfry spend 50 Expenses:Food:Coffee -m "Starbucks" -d 2026-06-15
# Spend on a credit card (any account works as the source)
finfry spend 89.99 Expenses:Shopping -f Liabilities:CreditCards:ChasePlatinum
# Income
finfry earn 3000 Income:Salary
# Tag recurring items with a cadence (daily/weekly/biweekly/monthly/quarterly/yearly)
finfry spend 15.49 Expenses:Subscriptions:Netflix -m Netflix -r monthly
finfry earn 4000 Income:Salary -m Salary -r monthly
# Move money between accounts
finfry transfer 500 --from Assets:Checking --to Assets:Savings
# Split one purchase across several accounts. A trailing lone account is
# inferred so the transaction balances:
finfry add -m "market run" \
Expenses:Food:Groceries 42.00 \
Expenses:Food:Snacks 8.00 \
Assets:Checking # amount inferred: -50.00
# Reports
finfry list [-a Expenses:Food] [-m 2026-06] [-n 10] # transactions
finfry balance [Assets] # account balances
finfry report [-m 2026-06] # income statement
finfry daily # per-day cost of recurring items
finfry accounts # accounts in use
finfry history [-n 10] # change history
finfry undo # remove the most recent change
finfry undo 4 # reverse an older change (correcting entry)
finfry redo # bring back the change undo just removed
finfry init [dir] # create a per-directory book
finfry path # print the active ledger file
# Budgets (per account, rolled up over the subtree)
finfry budget set Expenses:Food 400
finfry budget list
finfry budget rm Expenses:Food
# Delete a transaction by id (shown in `list`)
finfry delete 3
Run finfry --help, or finfry <command> --help, for full options.
Recurring items & daily cost
Tag a transaction with -r <cadence> to mark it recurring. finfry daily then shows what each commitment costs per day, derived from its cadence (e.g. a $15.49/month subscription is 15.49 ÷ (365.25/12) ≈ $0.51/day), plus a total burn rate projected to monthly and yearly equivalents:
Recurring expenses
Rent monthly $1,200.00 → $39.43/day
Netflix monthly $15.49 → $0.51/day
Prime yearly $119.88 → $0.33/day
Total $41.58/day ($1,265.48/mo, $15,185.76/yr)
A recurring stream is identified by its description (or, if blank, the accounts it touches), and only its most recent occurrence counts — so recording the same monthly bill repeatedly doesn't inflate the daily figure.
Amounts
Amounts accept a $ and thousands separators ($1,234.56) and are stored as integer cents, so there is no floating-point rounding error.
Data storage & books
A ledger is a single JSON file. finfry finds the active one by, in order:
FINFRY_DATA— an explicit path override.- The nearest
finfry.jsonfound by walking up from the current directory (like git's.git) — this is a per-directory book. - The global ledger at
$XDG_DATA_HOME/finfry/data.json(typically~/.local/share/finfry/data.json).
finfry path prints whichever is active.
Create a per-directory book with finfry init (defaults to the current directory; pass a path to use another):
mkdir ~/finances && cd ~/finances && finfry init
# Initialized finfry book at /home/you/finances/finfry.json
Running finfry anywhere under that directory then uses that book. This pairs well with a directory-scoped MCP registration (--scope local) so only sessions started there can touch those books. Without a book in scope, finfry uses the global ledger, so casual single-book use still works anywhere.
Writes are atomic (temp file + rename), and an older single-entry ledger is migrated to double-entry on first load (the original is kept as a .bak).
Development
crystal spec # run the test suite
shards build # build a debug binary
crystal tool format # format the source
Layout:
src/finfry/money.cr— parse/format money as integer centssrc/finfry/models.cr—Posting,Transaction,Databaserecordssrc/finfry/recurrence.cr— cadence→per-day amortization and recurring-item rollupsrc/finfry/store.cr— JSON persistence, queries, legacy migrationsrc/finfry/ai.cr— theFinfry::AIseam: a tool-use conversation loop over the Claude API (raw HTTP, no SDK); swappable behind one modulesrc/finfry/mcp.cr— a stdio MCP server exposing the safe command surface as tools, reusing the same registry and executor as the built-in agentsrc/finfry/app.cr— Jargon CLI definition and command handlers; the sharedcommit/render/postings_forcore that the manual and AI entry paths sharesrc/cli.cr— executable entry point
Roadmap
- A lightweight built-in chat fallback (a thin REPL over
finfry ai) for when no external harness is available - AI support for split transactions (multiple categories in one entry)
Contributing
- Fork it (https://github.com/transfire/finfry/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Thomas Sawyer - creator and maintainer
finfry
- 0
- 0
- 0
- 0
- 1
- about 1 hour ago
- June 18, 2026
MIT License
Thu, 18 Jun 2026 18:54:14 GMT