# Chirp **C**ross-border **H**ousehold **I**nvestment & **R**etirement **P**lanner — cross-border personal-finance consolidation and **tax-aware reporting** for dual **Canada/US** citizens, built on top of [ledger-cli](https://ledger-cli.org). Chirp is inspired by [Owl](https://github.com/mdlacasse/Owl) (a US retirement optimizer), but aimed at the dual-citizen problem: you have accounts in both countries and two tax systems apply at once — the US taxes you as a *citizen* on worldwide income while Canada taxes you as a *resident*, reconciled through the Canada–US treaty and foreign tax credits. Your **ledger journal is the single source of truth.** Chirp shells out to the `ledger` binary to parse it and apply FX valuation, then adds a cross-border tax brain on top: it consolidates CAD + USD holdings into one net-worth view, classifies every account by its cross-border tax treatment, and flags the reporting regimes you may trip (FBAR, Form 8938, CRA T1135, PFIC, foreign-trust 3520 risk). > **Not tax advice.** The classifications and thresholds are informational > pointers to the relevant regimes, not professional guidance. ## Requirements - SBCL + [ocicl](https://github.com/ocicl/ocicl) (dependencies: `clingon`, `version-string`, `alexandria`, `cl-ppcre`, `cambl`) - `ledger` 3.x on your `PATH` - `lp_solve` on your `PATH` (only for `chirp plan`/`optimize`) ## Build & test ```sh make # builds ./chirp make test # runs the FiveAM suite (asdf:test-system :chirp/tests) ``` ## Usage Point chirp at a journal with `-f` or the `LEDGER_FILE` environment variable. If your commodity/FX prices live in a separate ledger price database rather than inline in the journal, pass it with `--price-db FILE` (or set `LEDGER_PRICE_DB`); chirp threads it into every ledger call so valuations use those prices. ```sh # Consolidated net worth + reporting-threshold flags (valued in USD) chirp networth -f examples/dual-citizen.ledger # Value everything in Canadian dollars instead chirp networth -X CAD -f examples/dual-citizen.ledger # Value each holding at the FX rate on its own date (cost-basis view) chirp networth --historical -f examples/dual-citizen.ledger # FBAR (FinCEN 114): maximum value of each foreign account during the year chirp fbar --year 2025 -f examples/dual-citizen.ledger # CRA T1135: specified foreign property on a cost basis (max cost over the year) chirp t1135 --year 2025 -f examples/dual-citizen.ledger # Inspect how an account is classified chirp classify Assets:CA:TFSA:XIU chirp classify -f examples/dual-citizen.ledger # every account in the journal # Dual-currency realized capital gains (CAD for CRA, USD for IRS) chirp gains -f main.ledger chirp gains --year 2026 -f main.ledger # Tax-year income summary by type and source, dual-currency (FTC groundwork) chirp income --year 2026 -f main.ledger # Income tax estimate, US (federal+state) and Canada (federal+provincial) chirp tax --year 2026 --filing mfj --province ON -f main.ledger # Foreign tax credit position (US return, Form 1116) with treaty re-sourcing chirp ftc --year 2026 -f main.ledger # US tax auto-estimated chirp ftc --year 2026 --us-tax 42000 --resource -f main.ledger chirp ftc -f main.ledger # multi-year carryover schedule # Tax-minimising withdrawal-source mix for an after-tax spending target chirp optimize --spend 80000 --province ON -f main.ledger # Multi-year drawdown plan: conversions, benefits, and claim-age optimization chirp plan --spend 60000 --years 15 --province ON -f main.ledger chirp plan --spend 60000 --province ON --current-age 60 \ --cpp 15000 --oas 8500 --ss 24000 --optimize-claiming -f main.ledger # Monte-Carlo stress test with per-account asset allocation chirp plan --spend 55000 --province ON --equity 0.6 --montecarlo 2000 -f main.ledger # Print a guide teaching a coding agent how to drive chirp chirp skill # Run as an MCP server (stdio) exposing the reports above as tools chirp mcp ``` ### For coding agents (`chirp skill` and `chirp mcp`) `chirp skill` prints a self-contained guide — the journal annotations (`class`/`country`/`owner`/`use`), the named-commodity convention for real estate, `Owner:` profile directives for mixed households, and what each command answers. Point an agent at it first. `chirp mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, exposing `skill`, `networth`, `fbar`, `t1135`, `income`, `gains`, `tax`, `ftc`, `optimize`, and `plan` as tools an agent can call against a journal. Register it with any MCP client, e.g. Claude Code: ``` claude mcp add chirp -- chirp mcp ``` ### Multi-year drawdown plan `chirp plan` extends the optimizer across N years: it chooses each year's withdrawals so spending is met every year and **discounted lifetime tax is minimised**, including a **terminal tax on ordinary (RRSP/401k) balance left at the horizon**. That terminal term is what produces the classic result — *drain the tax-deferred accounts at moderate brackets over time rather than leave them to a ~50% estate tax*, while preserving tax-free and capital-gains balances: ``` yr tax-free cap-gains ordinary tax ord-bal-end 0 0.00 0.00 78,291.62 18,291.61 1,000,859.60 5 0.00 0.00 78,291.62 18,291.62 732,139.75 14 0.00 0.00 78,291.62 18,291.62 136,042.17 ``` The plan works in **real (inflation-adjusted) dollars**, so with fully inflation-indexed brackets no per-year bracket adjustment is needed. To stress **under-indexation** (bracket freezes, chained-CPI lag), pass `--bracket-drift` (e.g. `0.01`): brackets then erode in real terms each year, so a fixed real drawdown pays progressively more tax. The plan also models the **OAS clawback** band so it avoids pushing income into the 15% recovery-tax zone. It also models **Roth/RRSP conversions** (ordinary → tax-free, taxed now — the optimizer fills cheap brackets to escape the estate tax) and **CPP / OAS / US Social Security** income streams, which the treaty taxes **only in Canada**, so they raise the Canadian bracket baseline and reduce the withdrawal need. With `--optimize-claiming` it enumerates claim ages and recommends the ones leaving the most after-tax terminal wealth (the pre-benefit **gap years become prime conversion years**): ``` Recommended claim ages: CPP at 65 · OAS at 65 · US Social Security at 67 yr benefit tax-free cap-gains ordinary convert tax 0 0.00 0.00 0.00 86,038.32 19,736.68 26,038.34 5 23,500.00 0.00 0.00 62,316.74 28,933.25 24,168.07 10 47,500.00 0.00 0.00 38,316.74 28,933.26 19,356.07 ``` It also models **required minimum distributions** (a RRIF-schedule floor forcing ordinary distributions from age 72 — with `--current-age` — whose forced excess is auto-converted to tax-free) and **mortality** (`--mortality` survival-weights the objective from a mortality table, so far-future consequences count less). Claim ages are chosen either by **enumeration** (`--optimize-claiming`, accurate) or a **single MILP** with binary claim variables (`--milp-claiming`, faster) — both agree on the sample. With **`--equity`** (per bucket via `--equity-tf/-cg/-ord`) it sets each bucket's expected return from a stock/bond blend, so the optimizer holds growth where it's tax-sheltered; and **`--montecarlo N`** solves the plan at expected returns, then replays that fixed strategy over N random return sequences to report the **probability spending is met every year** (sequence-of-returns risk) and the terminal-wealth distribution: ``` Monte-Carlo stress test (2000 sims, 60% equity) Probability spending is met all years 94.5 % after-tax terminal wealth (real): 10th pct 537,394.28 median 992,093.17 90th pct 1,740,971.40 ``` Modelled as a linear program the same way Owl models retirement, solved with **`lp_solve`** (a robust MILP solver) at full bracket resolution over a 30-year horizon. A directional planning aid, not a financial plan. ### Withdrawal-source optimizer `chirp optimize --spend N` answers *"which accounts should I draw from this year?"* It chooses how much to withdraw from each of three buckets — **tax-free** (TFSA/Roth), **capital-gains** (non-registered securities), and **ordinary** (RRSP/RRIF/401k/IRA) — to hit an after-tax target while **minimising combined CA+US tax**, solved as a linear program (the same LP/MILP approach Owl uses). The cross-border insight that makes it a clean LP: under the foreign tax credit, the combined *marginal* rate on an ordinary dollar is `max(CA, US-federal) + US state`, and on a realized capital-gains dollar it's `max(50%×CA, US-LTCG)` — both non-decreasing, so the tax is convex and the solver fills the cheapest sources first (tax-free → capital-gains → ordinary). The capital-gains bucket's embedded-gain fraction is computed from the ledger's cost basis (via chirp's ACB engine), or set with `--gain-fraction`. First slice: single year, fixed FX, capital-gains taxed at one blended rate. Planning aid, not advice. ### Tax estimate `chirp tax` estimates income tax for both countries from the income engine: **US federal + state** (`--state`, default none) and **Canada federal + provincial** (`--province`), 2025 brackets, standard deduction / basic personal amount, and Canada's 50% capital-gains inclusion. Sub-national brackets live in `src/tax-data.lisp` — an Owl-style data table you edit in one place (Canadian provinces plus a starter set of US states; no-tax states are zero brackets). A US citizen resident in Canada usually owes no state tax, so `--state` defaults to none; `--province` is the one that matters. US **long-term capital gains** (held >1yr) are taxed at the preferential 0/15/20% rates, stacked on ordinary income. Still simplified — no NIIT/AMT, no provincial surtaxes or Quebec abatement, approximate brackets — a planning estimate, not a return. Its other job is to **feed `chirp ftc`**: without `--us-tax`, the FTC limit uses the computed US *federal* tax before credits. ### Foreign tax credit `chirp ftc` assembles the US-return foreign-tax-credit position from the ledger: income by Form 1116 **basket** (passive / general) and **source**, and the **Canadian income tax actually paid**. Given `--us-tax` (US tax before credits) it computes the per-basket **limitation** and the creditable amount, with any excess as carryover. `--resource` applies the treaty (Article XXIV) re-sourcing of US-source income to Canada — which, for a US citizen resident in Canada, is often what rescues an otherwise-stranded credit: ``` Form 1116 limitation (statutory source): | (with treaty resourcing): General fgn tax 0.00 creditable 0.00 | General creditable 2,084.08 Passive fgn tax 2,520 creditable 2,076 | Passive creditable 435.92 carryover (excess) 444.19 | carryover 0.00 ``` Run `chirp ftc` **without `--year`** for a **multi-year carryover schedule**: each year's foreign tax is credited first up to the limit, and any excess carries forward (per basket) to fill unused limit in later years. ``` Multi-year foreign tax credit carryover (treaty resourcing) year fgn tax US tax creditable carryforward 2024 45,880.00 17,663.00 17,663.00 28,217.00 2025 2,920.00 18,047.00 18,047.00 13,090.00 2026 2,160.00 15,814.00 15,250.00 0.00 ``` The credit computation is a planning aid with stated simplifications (passive/general baskets only, tax apportioned by income share, 10-year carryforward with carryback not modeled), **not** Form 1116 itself. ### Income summary `chirp income` reads your `Income` accounts, classifies each item by **type** (employment / interest / dividend / pension / …) and **source country**, and values it in both currencies at the transaction-date FX. Capital gains come from the inventory engine (the correct dual-currency figure), not from `Income:CapitalGains` postings. It then splits income US-source vs foreign-source — the split that **foreign-tax-credit** reconciliation rests on (US taxes you as a citizen, Canada as a resident; each credits the other's tax on the overlapping income). ### Dual-currency capital gains A US citizen resident in Canada reports the *same* disposal to two authorities, in two currencies, using **two different cost methods**: - **CRA** — gain in **CAD**, cost as **Adjusted Cost Base** (a single running average across all units of identical property); - **IRS** — gain in **USD**, cost by **FIFO** lots, each struck at the **acquisition-date** exchange rate. So the same shares carry two different cost bases, and because FX moves between buy and sell, the two gains diverge — a trade can be a gain in one currency and a loss in the other. `chirp gains` computes both, and flags whether each side is currently taxable given the account type: ``` 2026-05-01 Assets:CA:TFSA:XIU sold 80 XIU (FIFO from 2021-03-01) [PFIC — Form 8621] CAD (CRA, ACB): proceeds 3,200.00 - cost 2,506.67 = gain 693.33 [sheltered] USD (IRS, FIFO): proceeds 2,336.00 - cost 1,896.00 = gain 440.00 [taxable] ``` Here the TFSA disposal is **sheltered for the CRA** (tax-free in Canada) but **taxable for the IRS** and a **PFIC** — the classic cross-border trap. chirp needs **no lot annotations**: it reads every buy and sell, asks ledger to value each at its transaction-date FX in both currencies, and matches lots automatically (ACB pool for CAD, FIFO queue for USD). A plain sale is enough: ```ledger 2026-05-01 * Sell 80 XIU Assets:CA:TFSA:XIU -80 XIU @ CAD 40.00 Assets:CA:TFSA:Cash CAD 3,200.00 Income:CapitalGains ``` Selling more units than were held is reported with a warning. The CAD/ACB basis **pools identical property across all your non-registered accounts** (per owner), as the CRA requires; registered plans are kept separate, and the USD/FIFO basis stays per account, as the IRS applies it. ### How valuation works Chirp asks ledger for each posting's raw quantity plus a value computed with `market(amount, DATE, 'BASE')`: - **current mode** (default) values every holding at *today's* FX/price — the right basis for net worth; - **`--historical`** values each posting at the rate on its own date — the right basis for cost basis, capital-gain/FX-gain, and FBAR maximums. Multi-currency and securities chain automatically (e.g. a TSX ETF priced in CAD is converted ETF → CAD → USD) using the `P` price directives in your journal. Monetary amounts are represented with [`cambl`](https://github.com/ocicl/cambl) (Commoditized Amounts and Balances, from the ledger ecosystem): every amount is a quantity bound to a commodity, with **exact rational arithmetic** and display rounding, so an averaged cost like `7520/3` stays exact internally and prints as `2,506.67`. `src/money.lisp` is chirp's thin layer over it. ## Account classification Ledger account names are user-defined, so chirp classifies each account two ways, **explicit annotation first, name convention as fallback**. ### Annotating accounts (recommended) Tag the account on its ledger `account` directive. These `chirp` tags live in your journal (single source of truth) and are **ignored by ledger's own reports**, so they don't affect anything else you do with the file: ```ledger account Assets:CA:Investing note Self-directed TFSA at Questrade ; class: tfsa ; overrides name convention ; country: ca ; ca | us ; owner: anthony ; free-form; for per-person reporting later ``` - `class:` — one of the class keys below (`rrsp`, `rrif`, `lira`, `tfsa`, `fhsa`, `resp`, `rdsp`, `ca-nonreg`, `ca-cash`, `401k`, `traditional-ira`, `roth-ira`, `hsa`, `us-brokerage`, `us-cash`, `ca-realestate`, `us-realestate`, `liability`). - `country:` — `ca` or `us` (otherwise inferred from the class or path). - `owner:` — free-form (e.g. `anthony`, `sam`, `joint`). When present, `networth` adds a **By owner** section: each owner's net worth plus their own foreign-financial total, since FBAR/8938 are filed per US person. Annotations follow `include` directives and are matched against the exact account name. Anything you don't annotate falls back to convention. > Note: ledger stores account-directive metadata but does **not** expose it to > its own query/format engine, so chirp scans the journal for these directives > itself. ### Name convention (fallback) When an account isn't annotated, chirp classifies by path segments (`:RRSP`, `:401k`, `:TFSA`, `:RothIRA`, `:House`…) via `*classification-rules*` in `src/classify.lisp`. Recognized classes include: | Canada | US | |--------|----| | RRSP, RRIF, LIRA (treaty pension deferral) | 401(k), Traditional IRA | | TFSA, FHSA, RESP, RDSP (foreign-trust / PFIC risk to the US) | Roth IRA (treaty election keeps it tax-free in Canada), HSA | | Non-registered brokerage (PFIC exposure), chequing/savings | Taxable brokerage, checking/savings | | Real estate (non-financial) | Real estate (non-financial) | ## Simplifications & known limitations chirp is a **planning estimator**, not a tax preparer. The engine is validated by worked examples hand-derived from the published 2025 schedules (see the "Reference cases" in `tests/tests.lisp`) — but those are not reconciled against a filed return or a commercial calculator, which would be the next validation step. **Modelled** (with cited tests): 2025 federal + provincial/state brackets (the Canadian **14.5%** blended low rate, Bill C-4); the **federal BPA phase-out**, **Quebec abatement**, and **Ontario surtax**; US long-term capital-gains rates; character-aware **capital-loss carryforward** (short- vs long-term, §1211/§1212 $3,000 limit only when there's income to absorb it); **NIIT**; the **§988** mortgage gain (personal-loss nondeductibility + $200 de minimis); **§121/PRE** real estate; the **OAS clawback** (estimate, single-year optimizer, and the multi-year plan); **treaty Art. XVIII** residence-sourcing of CPP/OAS/SS; the **FTC** (owner-scoped income and Canadian tax paid); and cost-basis **T1135** / max-value **FBAR**. **Remaining simplifications**, so you know where the estimate is still approximate: - **No AMT** — neither the US AMT nor the revamped (2024) Canadian AMT, which can bite large capital-gains realizations. - **US Social Security** is treaty-sourced to Canada but included in full — the 85%-inclusion rule (15% treaty-exempt in Canada) is not modelled. - The optimizer's marginal schedule models ordinary brackets, the surtax, and the OAS clawback; it does **not** include NIIT or the §121/PRE exemptions (those live in the estimate only). - **FTC** models the passive + general baskets only, with 10-year carryforward and no carryback; the capital-loss long-term slice isn't owner-filtered (a multi-owner edge case). - **§988** uses pooled-average basis (not per-tranche); the FBAR maximum for securities accounts is an estimate (year-end prices on each date's holdings — use your statement's high value). - 2025 brackets only; the plan is a **real-dollar** model (indexed brackets need no adjustment), and `--bracket-drift` stress-tests under-indexation. ## License Copyright (C) 2026 Anthony Green. chirp is free software: you can redistribute it and/or modify it under the terms of the **GNU General Public License** as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file. Distributed WITHOUT ANY WARRANTY.