Coffer (in development)
Omniscio's first-party personal-finance tracker: a fully local Mint-style ledger — accounts, transactions, budgets, recurring bills, goals, trends and alerts — with bank sync as an opt-in phase and no cloud backend. Covers what the panel contains today, how to turn it on, and the wiring and remaining phases behind it.
Status: in-development — default-hidden. Gated via the coffer unreleased-feature
entry (cofferEnabled toggle in Settings → Lab, or launch with AMC_SHOW_COFFER=1).
Ships dark until flipped to shipped in src/shared/unreleased-features.ts.
What it is
Coffer is the first-party personal-finance tracker — a Mint.com-style feature built
fully local (no cloud backend, no bank credentials): link nothing, import everything.
It adds a "Coffer" sidebar virtual project (sentinel __coffer__, nested in the
Productivity group) whose panel renders a native full-screen finance UI
(panelOwnsLayout, arij-style — no vendored bundle, AMC design system throughout).
The product and architecture spec is the research library at docs/research/mint/ — Mint's anatomy rebuilt on the successors' architecture (Actual Budget's local-first rules engine, Maybe Finance's account taxonomy, Firefly III's budget semantics).
Feature surface (Phase 1 local core + Phase 2 intelligence + Phase 3 connectivity + Phase 4 polish/v2)
Fourteen sections in the panel's internal nav: Overview · Transactions · Budgets · Recurring · Goals · Trends · Insights · Accounts · Investments · Rules · Alerts · Bank sync · Import · Assistant (the Import view also carries Export and the demo seeder; Insights and Investments are the two sections Phase 4.2/4.3 added, and Assistant is the read-only AI money assistant Phase 4.6 added — see below).
- Accounts — typed (checking/savings, credit card, investment, loan, property, vehicle, other), on/off-budget, with balances and a daily balance-snapshot series.
- Transactions — a ledger with search/filters, split transactions, renameable
payees, notes, a duplicate flag, and bulk edit; integer minor-unit amounts
throughout;
imported_iddedup on every imported row. Dedup is backed at the DB layer by a partial UNIQUE index on(account_id, imported_id)over live rows (is_deleted = 0), so even a cross-process re-import race (two connections importing the same row) is a no-op counted asdeduped, never a double count. - Categorization — a fixed two-level default category tree + custom subcategories, auto-categorization via a staged rules engine (pre/default/post) that learns from corrections: recategorizing a merchant offers a rule for future (and optionally past) transactions. Uncategorized rows land in a review queue.
- Budgets — per category per month with Every Month / Every Few Months / Once frequencies, an optional per-budget rollover (carries surplus AND deficit), the "Everything Else" catch-all row, and an auto-starter budget proposed from imported spending history.
- Transactions ledger extras — a right edit sidebar (not a modal): category with
the "remember this merchant" rule checkbox (+ apply-to-past), payee rename
(renames the merchant everywhere), notes, duplicate flag, guarded delete, and a
split editor with exact integer-cent sum validation. Manual/cash entry via the
"+ Add" form (unsigned amount + Income flip). Every budget's frequency is one of
monthly/every-n-months/once(displayed as Every Month / Every Few Months / Once), with the auto-starter proposing ~85% of trailing average spend and add/edit/close plus credit limits and manual balance updates on the typed account roster feeding the daily snapshot series behind net worth. - Rules & Categories — every rule in plain language, pause/guarded-delete; user subcategories under the fixed default groups, hideable.
- Import — Mint-format CSV, generic bank CSV with an in-view column mapper (signed amount or debit/credit split), and OFX/QFX (FITID → dedup key). No screen scraping, ever; bank sync (SimpleFIN) is a later phase.
- Export — the data-longevity escape hatch: one-click Mint-format CSV of every transaction (re-importable into Coffer itself — the machine-migration path) and a versioned full JSON backup (accounts, custom categories, payees, rules, month-keyed budgets, balance snapshots, embedded CSV).
- AMC backups carry the ledger — twenty of the durable
coffer_*tables (the fourteen Phase 1–3 tables — ten Phase 1 tables pluscoffer_recurring_series,coffer_goals,coffer_alert_rules,coffer_alert_events— plus six of the Phase 4 tables:coffer_bucket_budgets,coffer_securities,coffer_trades,coffer_holdings,coffer_insightsandcoffer_attachments) are classifiedmergein the portable-backup coverage map (a self-contained uuid graph) and listed in the DSAR/data-transfer table set, so Backup Mirror / Portable Backup restore Coffer data like any other user data. The two exceptions arecoffer_metaandcoffer_security_prices, both deliberatelyexclude-ephemeral:coffer_metaholds only per-install maintenance watermarks (the intelligence-pass throttle cursor, digest cursors, and the quotes-refresh watermark), andcoffer_security_pricesis a re-fetchable Yahoo Finance quote cache — both are meaningless on another machine and rebuild themselves after a restore. - Overview dashboard — summary cards: net worth (with a 90-day sparkline), cash flow, spending, budget bars, coming-up bills, goal progress, recent transactions, and the uncategorized review-queue nudge.
Phase 2 — intelligence
- Recurring & bills — subscriptions and bills detected automatically from history (same payee + similar amount + regular cadence; weekly through yearly), each series projecting its next due date and re-projecting as charges match. Price increases are flagged on the row and raised as alerts. "Not recurring" is a tombstone the detector never resurrects; manual bills cover what the ledger can't see. Per-series reminder lead-time override.
- Transfer auto-matching — equal-and-opposite amounts across two accounts
within 3 days link structurally via
transfer_id(and drop out of spending reports). A false pair can be undone through thecoffer:unlink-transferchannel; the ledger-side unlink button is future work (the handler ships, the UI affordance does not yet).transfer_idis a raw self-pointer with no FK, so link integrity is enforced by convention:softDeleteCofferTransactionunlinks the survivor when one side is deleted (it never stays a silently-dropped transfer), andlistBrokenCofferTransferLinks(queries-coffer-transactions.ts) reports every live row whose partner is missing, soft-deleted, or does not point back — the diagnostic that makes a desynced pair detectable. - Goals — Mint's nine presets + custom, balance-linked (progress = the linked account's balance, optionally minus a creation-time baseline; debt goals measure pay-down). Two-way solve: a target date derives the required monthly, a monthly plan derives the projected finish. Manual accounts allowed.
- Trends — ONE parameterized report engine: measure (spending / income / net income / assets / debts / net worth) × group-by (category / group / merchant / account / tag / over time) × period (3M / 6M / 1Y / YTD), with the signature two-click drill-down from any bar to its transactions. Balance measures read the daily snapshot series.
- Alerts — seven rule types (low balance, bill reminders, unusual spending,
over budget, fees ≥ $1, large purchases, price increases) + the weekly/monthly
digest. The default-on set ships enabled ("an alert system that starts silent
never gets configured"); unusual-spending starts off. Events are dedup-keyed
per rule+entity+period (idempotent), stored as history in the Alerts view, and
forwarded to AMC's inbox as dismissible cards via
raiseAgentAlert. - Weekly digest — the re-engagement card: balances, budget pacing vs month
position, where the money went, notable transactions, coming-up bills, goal
progress — one inbox card per period (
coffer-digest:<period>dedup key). - The intelligence pass —
runCofferMaintenance(insrc/main/services/coffer/coffer-intelligence.ts) runs snapshots → transfer matching → series matching/detection → holdings materialization (Phase 4.2 extension step, anchorpost-series) → alert evaluation → insight generation (Phase 4.3 extension step, anchorpost-alerts) → digest. The two extension steps self-register viaregisterCofferMaintenanceSteprather than being hardcoded into the core sequence, so a Coffer build with neither feature touched runs the original five-step pass byte-identically. It triggers after every import commit (unthrottled) and behind Overview loads (throttled to 6h); deliberately NO startup task or cron, so an unused feature costs nothing. The Alerts view's "Check now" forces a pass.
Phase 3 — Bank sync (SimpleFIN)
- The model — the user buys a SimpleFIN Bridge subscription themselves (about $15/year, paid to SimpleFIN), links their banks at the bridge, and pastes the bridge's ONE-TIME setup token into the panel's Bank sync section. AMC never sees a bank credential; screen scraping is banned forever (research doc 30). Teller remains a possible later second channel; file import stays the universal fallback.
- The credential — the claim exchanges the token for an access URL whose
userinfo embeds the Basic-Auth pair. It is stored as the encrypted
simplefinAccessUrlsettings field (safeStorageenc:at rest, inENCRYPTED_APP_SETTINGS_KEYS+CLI_SETTINGS_SENSITIVE_KEYS), read ONLY throughservices/coffer/sync/simplefin-credential-store.ts, and never appears in any IPC response, log, or renderer state — the UI learns onlyconnectedplus the bridge hostname. - Linking — the provider roster (a cheap balances-only probe) is linked
account-by-account: create a typed new Coffer account or ADOPT an existing
manual one. The link key is
coffer_accounts.provider_account_id(partial unique index: one provider account feeds at most one live ledger). - Sync runs — manual ("Sync now"; deliberately no cron while unreleased).
One bridge fetch covers all accounts (90-day first backfill, then
watermark-minus-7-days incremental windows, idempotent via native-id dedup).
Per account: same-id pending settles → dedup → pending→posted
reconciliation (exact amount, ±5 days, single-candidate-only — never
guesses between two identical pending charges) →
importCofferRows(the one import path: payees, rules, review queue; pending rows enter aspending) → the provider's balance applied LAST as authoritative (the daily snapshot carries it) →last_synced_at+coffer_metawatermark stamps. Then the intelligence pass runs unthrottled, exactly like an import commit. - Failure honesty — sync is treated as unreliable-by-nature: per-account
failures isolate into that account's row; a whole-run failure records
sync:simplefin:last-errorfor the health table and surfaces humanized (never a raw HTTP status or provider JSON). A failed run never advances the watermark, so its window is re-covered next time. - Disconnect — confirm-guarded; forgets ONLY the credential. Accounts, history, and provider links survive, which is what makes a later re-connect relink cleanly instead of duplicating.
Where to find it
Coffer is a sidebar row, not a settings page: once it is enabled, a Coffer entry appears in the sidebar's Productivity group and clicking it opens the finance panel full-screen. The switch that reveals it is in Settings → Lab, and while the feature is in development it ships hidden, so that toggle is the first thing to look for.
How it behaves
How to use (current state)
- Settings → Lab → enable Coffer (or
AMC_SHOW_COFFER=1). - The Coffer row appears in the sidebar's Productivity group; click it to open the panel.
- Import a bank CSV/OFX export, add accounts/transactions manually, or connect a SimpleFIN bridge in Bank sync to pull them automatically.
Per-record flags the desktop can set
Four flags are persisted on every Coffer record and were long settable only from the mobile client — the desktop rendered no control:
| Flag | Where | Effect |
|---|---|---|
| Off budget (account) | Accounts — add row + edit sidebar | Account stays in net worth; its spending never counts toward a budget. Pre-ticked for investment / loan / property / vehicle / other asset+debt. |
| Hide from reports (category) | Rules & Categories — per user subcategory | Mint's "Hide from Budgets & Trends" — category stays pickable, leaves every report and budget roll-up. Distinct from Hide, which only removes it from the pickers. |
| Stop after this (rule) | Rules & Categories — per rule row | Ends the rule chain on a match, so no later rule runs. This is what makes rule ORDER mean anything. |
| Cleared (transaction) | Transactions — edit sidebar, Save details | Reconciliation state: this row was matched against the bank statement. |
Only excludeFromReports needed a new channel
(coffer:set-category-exclude-from-reports) — the other three already had one.
Retirement paths that used to be dead ends
Four handlers were registered, Zod-validated and reachable but had ZERO renderer call sites, so the capability existed with no way to use it:
| Action | Where | Behaviour |
|---|---|---|
| Remove budget | Budgets — per budget row (tracking mode) | Was create-and-edit only; the nearest gesture was setting it to $0, which still renders a bar. Soft-delete keyed by category + month, so other months keep theirs; the spend falls into "Everything else". |
| Rename (category) | Rules & Categories — inline on a user subcategory | Categories could not be renamed anywhere. Seeded defaults stay fixed (Mint's fixed-tree rule) and the handler refuses them. |
| Delete (category) | Rules & Categories — per user subcategory | Hiding was the only retirement path. F040: live transactions are REASSIGNED to Uncategorized (never dropped) and the category's budgets are soft-deleted — the confirm copy says so. |
| Unlink transfer | Transactions — edit sidebar, transfer-linked rows only | A wrongly-paired transfer took BOTH rows out of spending reports with no way to undo it. Clears transfer_id on both halves; neither row is deleted. |
All three destructive ones route through useConfirmDialog; every one surfaces a
rejected write via surfaceIpcError rather than reporting a silent success.
The rule builder
Rules & Categories → + New rule (or Edit on a row) opens CofferRuleEditor. Before
it existed the ONLY rule producer was buildLearnedCategoryRule — the "always use this
category for this payee" loop — which emits exactly one shape (payee is X →
set-category Y, default stage, one condition). That left most of a live, tested engine
unreachable: 1 of 7 condition fields, 1 of 9 operators, 1 of 5 actions, 1 of 3 stages.
The builder exposes condition rows (field / operator / value), action rows, all/any
matching, the run stage, and stop-after-this. Two deliberate constraints, both enforced by
coffer-rule-editor-helpers.ts and its tests:
- A field only offers operators the engine can satisfy for it.
greater-than/less-thanare guarded bytypeof raw === 'number'incofferConditionMatches, and onlyamountprojects to a number — sodate(a'YYYY-MM-DD'STRING) gets the string operators instead, andcontains "2026-08"is the honest way to say "in August 2026". Offering the numeric ops there would build a rule that silently never fires. matches(regex),one-ofandnot-one-ofare omitted. Engine-supported, but a raw regex box is a footgun and one-of is covered by two conditions joined with "any".
Amounts are entered in signed dollars and sent as signed minor units — spending is negative, so the editor echoes what a pairing will actually catch ("Matches money OUT of more than $200.00") rather than leaving the user to infer it from the sign.
How rules are ordered
Rules run one after another and later matches OVERWRITE earlier ones, so order is the semantics. Three keys decide it, in priority:
- Stage —
pre→default→post. - Specificity, ASCENDING — broad rules run FIRST so the precise rule applies last
and wins. An exact match (
is) outscores a fuzzy one (contains); more conditions score higher still. So "bank description contains WHOLE → Groceries" runs before "merchant is Whole Foods → Organic Food", and the second corrects the first. sortOrder— the user's manual position, reached ONLY when stage and specificity both tie.
The Rules list renders through the same orderCofferRules the engine runs (shared, not
mirrored — see coffer-rule-order-contract.md),
numbers each row by execution position, and states the rule in prose above the list.
A drag handle appears only inside a tie group — a run of rules that stage and specificity cannot separate. Everywhere else the manual position is never consulted, so a drag there would change the stored value and change nothing about execution.
Phase 4.1 — flex budgeting
Monarch-style partition inside the Budgets view (Category ⇄ Flex toggle; no new
nav section). Every spending category classifies at READ TIME into
fixed / non-monthly-recurring / flexible from its trailing-6-month share of
spend linked to recurring series (Phase 2 linkage): ≥60% monthly-or-faster
cadence → fixed, ≥60% longer cadence → recurring, else flexible;
coffer_categories.flex_bucket_override beats the computation and "Auto"
clears it (NULL). Bucket-level budgets live in coffer_bucket_budgets
(month-keyed history rows, rollover semantics identical to category budgets
with an implied monthly frequency). Pure engine
services/coffer/flex-engine.ts, read model services/coffer/flex-month.ts
(buildCofferFlexMonth), channels coffer:get-flex-month /
coffer:set-flex-override / coffer:upsert-bucket-budget /
coffer:delete-bucket-budget (WS-blocked; mutations cli-parity-exempt as
feature-scoped-surface). Determinism is contract-locked:
coffer-flex-classifier-contract.md.
CLI routes and the full IPC surface are added as the feature builds out — see the cli-parity manifest for current coverage.
Phase 4.2 — Holdings, trades & benchmarks
Manual buy/sell trades on investment accounts (coffer_trades, symbol
autocomplete over coffer_securities, created on first use). Holdings are a
materialization (coffer_holdings) rebuilt by the holdings-materialization
intelligence step (anchor post-series): average-cost basis, oversells clamp
at zero and set coffer_trades.is_inconsistent (recomputed every pass).
Quantities are integer micro-shares (1e-6); valuation is
round_half_away_from_zero(quantity_micro × close_minor / 1e6) in BigInt —
see .claude/memory/contracts/coffer-holdings-integer-purity-contract.md.
Accounts with trades AND sync_source = 'manual' get their balance set to
holdings value at latest prices each pass; synced accounts stay authoritative.
Quotes: Yahoo Finance v8 chart JSON (quotes-client.ts wire facts; replaced the
Stooq CSV feed on 2026-08-10 after Stooq gated it behind a JS anti-bot
challenge), opt-in via cofferQuotesEnabled, manual refresh + 24h throttle
behind Investments loads, per-symbol isolation, coffer_meta watermark
(quotes:last-run-at) that a fully-failed run never advances. Benchmark = ^SPX
stored, mapped to Yahoo's ^GSPC at fetch time (is_benchmark = 1,
shared price table). Views: Investments section (portfolio value, allocation
donut, vs-benchmark, holdings table with stale badges, trades ledger, consent
card). Out of scope: broker-CSV import, FIFO/tax lots, any trading.
Channels: coffer:list-securities / coffer:list-trades /
coffer:create-trade / coffer:update-trade / coffer:delete-trade /
coffer:refresh-quotes (all reserved by the Phase
4.0 foundation) plus coffer:get-portfolio (the combined portfolio + series +
benchmark + allocation payload the Investments view reads, added fresh by
this feature — not part of the 4.0 reservation batch). The foundation also
reserved coffer:list-holdings, REMOVED in the §B pass: coffer:get-portfolio
returns the same CofferHoldingView[] from the same builder after the same
quote refresh, so it was a duplicate read whose only extra was an accountId
filter the renderer can do in one line — and it had no call site. All WS-blocked;
create-trade / update-trade / delete-trade / refresh-quotes are
cli-parity-exempt as feature-scoped-surface (the rest are reads, out of the
command surface). coffer:list-securities takes no query param — it lists
the whole registry and the trade form's autocomplete filters client-side.
Phase 4.3 — Insights
Six generated insight types (fees-paid, category-trend, spending-projection,
top-merchants, subscription-creep, savings-rate) computed from the ledger only
by src/main/services/coffer/insights-engine.ts — pure generators + ONE
intelligence step at anchor post-alerts (before the digest step, so the
weekly digest can embed the top 1–2 undismissed insights as "Spotlights").
Persistence is the alert-event pattern on coffer_insights: unique
dedup_key (coffer-insight:<type>:<entity|_>:<period>), INSERT OR IGNORE,
dismissal is a per-period tombstone. Thresholds live as named constants in the
engine (20% + $50 floor for category trends, $50 floor + day-7 minimum for the
spending projection, $10/$50 increase floors for subscription creep). The fee
identification is shared verbatim with the fee-charged alert via
isFeeLikeTransaction (alerts-engine.ts) so the two can never drift.
IPC: coffer:list-insights / coffer:dismiss-insight (both reserved by the
Phase 4.0 foundation, WS-blocked; dismiss-insight is cli-parity-exempt as
feature-scoped-surface, list-insights is a read out of the command surface).
UI: the Insights section is a card feed, severity DESC then created_at DESC,
with per-card dismiss (permanent for that period) and a deep link into the
view that explains it — category-trend auto-drills Trends via the
CofferTrendsIntent prop; fees-paid/top-merchants/savings-rate open Trends
pre-filtered; spending-projection opens Budgets; subscription-creep opens
Recurring.
Phase 4.4 — Receipt attachments
Image/PDF receipts attached to a transaction from the ledger edit sidebar's
Attachments grid (add via native picker, thumbnail tiles, safe open, guarded
delete). Storage is hash-addressed and reference-counted
(services/coffer/attachments-store.ts): bytes are written once per SHA-256
content hash to a flat dir (getCofferAttachmentsDir()), so attaching the
same file to two transactions stores it once, and a file is only physically
unlinked once no live coffer_attachments row still references its hash
(deleting a transaction cascades the same ref-count check across its split
children). The canonical on-disk extension comes from the content-VALIDATED
MIME, never from user input or the picker's reported name. A candidate is
gated by extension allow-list, the 20 MB size cap (checked via stat BEFORE
the file is read into memory, rejecting an oversized pick cheaply), and a
content-signature sniff against the declared MIME (fileBytesMatchDeclaredMime)
— PNG/JPEG/GIF/WebP/PDF are fingerprinted, HEIC has no signature and passes on
extension alone. The renderer never supplies bytes or a filesystem path for
ADD (the native picker runs in Main); OPEN resolves the on-disk path
server-side from the row id and routes through openPathSafely, never trusts
a renderer-controlled path. Attachment metadata (filename/mime/size/hash) also
rides the versioned JSON backup export alongside the rest of the ledger.
Channels: coffer:add-attachment / coffer:list-attachments /
coffer:open-attachment / coffer:delete-attachment (all reserved by the
Phase 4.0 foundation, WS-blocked; add/open/delete are cli-parity-exempt as
feature-scoped-surface, list is a read out of the command surface).
Phase 4.5 — Envelope mode
Opt-in YNAB-style allocation-first budgeting behind cofferBudgetMode
('tracking' default). coffer_budgets.mode partitions the table: tracking
rows and envelope rows coexist per month+category and neither mode's queries
ever read the other's rows (queries-coffer-budgets.ts is mode-scoped
throughout). Math lives in envelope-engine.ts (pure):
available = assigned − activity + prior available (carries BOTH directions);
To Be Budgeted = on-budget cash − Σ available — always computed, never
stored. The composition layer is envelope-month.ts.
Money-conservation invariant (same-PR invariant contract):
moveCofferEnvelopeMoney updates two mode='envelope' rows inside ONE
better-sqlite3 transaction; a move never creates or destroys cents (Σ assigned
unchanged) and a mid-move failure rolls the whole move back. Locked by
tests/unit/services/coffer/envelope-conservation-invariant.test.ts — if that
suite fails, fix the mutation path, never the test.
In envelope mode the over-budget alert rule evaluates available (negative envelope = over); tracking mode's alert inputs are byte-identical to Phase 2.
IPC: coffer:assign-envelope (direct edit, IPC.COFFER_ASSIGN_ENVELOPE) and
coffer:move-envelope-money (IPC.COFFER_MOVE_ENVELOPE_MONEY) were both
reserved by the Phase 4.0 foundation batch — note the assign channel's real
name differs from a naive "set-envelope-assigned" guess, so check
src/shared/ipc-channels/coffer.ts before assuming a channel name.
coffer:get-envelope-month (IPC.COFFER_GET_ENVELOPE_MONTH) was NOT part of
that batch (no composed-view GET was reserved) and was added by this feature
build itself, mirroring the COFFER_GET_PORTFOLIO / COFFER_GET_FLEX_MONTH
"GET returns a composed view" convention. All three are WS-blocked; the two
mutations are cli-parity-exempt as feature-scoped-surface (the get is a read,
out of the command surface).
Phase 4.6 — AI money assistant (read-only)
A 14th "Assistant" section: a read-only, grounded natural-language Q&A over
the user's own ledger ("where did my money go last month?", "am I over budget?",
"what do my subscriptions cost?"). Default OFF behind the cofferAssistantEnabled
consent setting (an in-panel CofferAssistantConsentCard names the cost + privacy
before enabling); an "Ask" card on the Overview deep-links into it.
- Grounded tool-use. The service (
src/main/services/coffer/assistant/) wires Coffer's AGGREGATE read engines in as read-only TOOLS for the sharedrunAnthropicToolLoopprimitive: the model calls a tool → the tool runs locally against SQLite → returns a small aggregate → the model composes the answer. Numbers come from the tools, never the model's arithmetic. Seven tools:query_trends,get_spending_by_category,get_budget_status,get_cashflow,get_net_worth,get_recurring_and_bills,get_goals. - Aggregates-only egress (privacy).
coffer-assistant-tools.tsimports NO raw-transaction reader (listCofferTransactions/getCofferTransaction/listCofferLargestOutflows/listCofferFeeCandidateOutflows, and NOTbuildCofferOverviewwhich embedsrecentTransactions), and every tool result is labels + formatted money + entity-grain rollups with noid/raw date — so raw ledger rows structurally cannot reach the model. Locked in code + a test, not by prompt wording → coffer-assistant-egress-contract.md (tests/unit/services/coffer/coffer-assistant-egress.test.ts). It is a prompt-egress surface indocs/PII_INVENTORY.md. - Credential + cost. The BYO Anthropic API-key path
(
resolveApiKeyForAiFeatures, same as Drive/Sheets/Calendar); no key configured → a friendlyno_api_keystate, no call. Default modelMODEL_HAIKU_LATEST; cost tracked inside the loop under the unique labelcoffer-ai(per-feature-billing §I3); reuses the sharedANTHROPIC_BREAKER_KEYbreaker + daily-$ cap. Errors humanized (humanizeAiCallError). - Wiring. One channel
coffer:assistant-ask(IPC.COFFER_ASSISTANT_ASK), Zod-validated +wrapHandler'd incoffer-handlers.ts, gated oncofferAssistantEnabled; single request/response (no streaming). WS-blocked (a paired phone can't reach a paid call over the ledger) and cli-parity-exempt asfeature-scoped-surface("ask" is a command verb, so an entry is required). The handler always returnssuccess: truewith a discriminatedstatus(ok/no_api_key/disabled/offline/unavailable) so the renderer branches without parsing an error. Gated under the existingcofferunreleased-feature (no new feature entry).
For agents
Wiring (for AI agents)
- Virtual project:
COFFER_PROJECT_ID = '__coffer__'(src/shared/virtual-project-ids.ts); NON-spawnable (no Claude sessions). - Settings slice:
src/shared/types/settings/coffer-settings.ts(cofferEnabled, default false). - Gate:
UNRELEASED_PROJECT_GATESinsrc/renderer/src/stores/project-visibility.ts(featureIdcoffer). - Manifest:
src/shared/integrations/coffer.ts(parentGroupId: 'productivity'). - Data: local SQLite tables (
coffer_*) in the app DB; money stored as integer minor units per the monetary-integer-twin contract. - Bank sync:
src/main/services/coffer/sync/(protocol client · credential store · pending-reconciliation policy · orchestrator); channelscoffer:sync-*(all WS-blocked; mutations cli-parity-exempt as feature-scoped-surface); the SimpleFIN wire facts are recorded at the top ofsimplefin-client.ts(verified against simplefin.org/protocol.html, 2026-07-30).
Related
Nothing here leaves the app or needs a bank login, so the questions a reader arrives with are usually about where the data is kept and how to get it out. The app's wider backup and restore story is on the portable backup page, and the encryption guarantees behind your local data are on the is my data encrypted page. What the app spends on AI features like the read-only assistant is covered on the cost control page.
Last verified 2026-10-06