OpenChart
Claude·Codex와 함께하는 무료 데스크톱 차트 도구. 알림 설정·차트 움직임 분석·데이터 제공자 선택
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A Bespoke Intelligent Solutions (BIS) product.
A standalone consumer budgeting and money app: link a bank, then see where money came from, where it went, what's coming next, and how far you've come. Every screen leads with a chart.
Prism is its own product. It shares no code, database, authentication or deployment with any other BIS software.
DESIGN.mddocs/research-2026-09-27.mdRequires Node 22 and pnpm 10.
pnpm install
pnpm dev # http://localhost:3100
With no configuration it runs on a demo household: thirteen months of one person's (fictional) money, generated deterministically, so every chart works before anyone shares real data.
cp .env.example .env.local and fill in PLAID_CLIENT_ID and PLAID_SECRET.pnpm dev, press Connect a bank, pick any test bank and sign in
with user_good / pass_good.With accounts set up (below), connecting needs a signed-in account: a bank or Coinbase is kept only where two-step sign-in protects it and "Delete account" removes it. Without accounts, only Plaid's sandbox links, into the device, so Prism can be built and tried without a database; anything real refuses.
For production set PLAID_ENV=production and a real PRISM_VAULT_KEY
(openssl rand -base64 32); the app refuses to store tokens without one.
Investment accounts at a broker (Robinhood, Webull and the like) connect
with "Connect an investment account", beside "Connect a bank" on
Connections. Plaid shows only the institutions and accounts that support every
product a link requires, and its Transactions covers bank and card accounts,
never investment accounts, so a bank link can't show a brokerage account at
all. The investment link asks for Investments first and transactions where a
cash account sits alongside; the same consent for loan details is asked
either way. Such a connection has no transactions to give, so Plaid refusing
them is not an outage (holdingsOnly in src/lib/plaid/sync.ts): its balance
and holdings show, while a bank whose transactions fail still says it
couldn't be reached. Plaid bills Investments per connection from the moment
it links, as it already does for a bank holding investments. Fidelity needs
its own approval in the Plaid Dashboard first; Schwab arrives a few days after
Plaid's production approval.
Home values from RentCast are off until you set RENTCAST_API_KEY
(Sensitive, in Vercel). Once set, a home on Net worth offers "Keep its value up
to date with RentCast": the person gives its address, told first that it goes
to RentCast once a month and nothing else about them does. Each request asks
RentCast not to log it (suppressLogging=true, RentCast's documented opt-out),
so the address isn't kept in RentCast's request logs. The address is
sealed in a column of its own (profiles.sealed_home_values), never in what a
household is sent. RentCast bills every request past the plan's allowance, so
the database decides (claim_home_value_lookup): at most
home_value_lookups_31_days lookups in any 31 days across everyone (45 to
start, of the free plan's 50: a rolling window, so no billing month can hold
more), three lookups a person in any 31 days, each home once a month, never for a connected app
or before the second step. Raise the cap only with the plan
(update app_limits set value = 950 where name = 'home_value_lookups_31_days'
for the $74 plan's 1,000). A value the person types for a month is theirs; the
estimate fills months they didn't.
Car values from VinAudit are off until you set VINAUDIT_API_KEY
(Sensitive, in Vercel). Once set, a vehicle on Net worth offers "Keep its
value up to date with VinAudit": the person gives its VIN and, if they like,
its mileage, told first that both go to VinAudit once a month and nothing else
about them does. VinAudit takes its key in the query string, so the request's
address is never logged. The VIN and mileage are sealed in a column of their
own (profiles.sealed_vehicle_values), never in what a household is sent. A
mileage reading grows by the US average (1,125 miles a month) until the person
gives a new one. VinAudit bills every request, so the database decides
(claim_vehicle_value_lookup): at most vehicle_value_lookups_31_days
lookups in any 31 days across everyone (90 to start, inside the free trial's
100), three a person, each vehicle once a month, never for a connected app or
before the second step. On the paid plan ($100 a month, $0.10 a lookup) raise
it with update app_limits set value = 900 where name = 'vehicle_value_lookups_31_days', never past what the plan covers. Kelley Blue
Book has no public API; its values are licensed through Cox Automotive under
contract.
Card and loan due dates (Plaid Liabilities) are off until you set
PLAID_LIABILITIES=on. People already consent to them when they link a bank
(Link asks for Liabilities as consent only), but Plaid bills per bank from the
FIRST read, so nothing is read until the operator decides. Once on, Prism reads
them at most once a day per bank, only for banks holding a card or a loan,
never for a connected app, and keeps them sealed inside the bank's sync copy.
They show as the due date, minimum, statement balance and rate on Net worth, a
"Card and loan payments" list on Future, one-day reminders in the calendar
download and feed, and lender_terms for connected apps. The forecast uses
them as well: a repeating payment that shows up on both sides (out of
checking, into the card, same amount, at least twice) takes the lender's due
date and statement balance, or minimum, whichever the person usually pays, in
place of the estimate. They are not shown to
the household. The privacy policy's wording switches with the same setting, and
turning the setting off hides any terms already kept. Changing it needs a
redeploy (Vercel applies settings to new deployments).
Banks that sign you in on their own website (Chase, Bank of America, Wells Fargo, Capital One and most large banks; Plaid calls this OAuth):
/connections/return. That page reopens
Link with the same Link token, which was kept in an httpOnly cookie for up
to an hour. It saves the bank and returns the person to the page they
started on.https://<your-domain>/connections/return to Plaid's
Allowed redirect URIs (Developers → API), then set
PLAID_REDIRECT_URI to exactly that address.How a signed-in person's banks stay current (roadmap item 2):
PRISM_VAULT_KEY, so the database holds ciphertext only, never a merchant
or an amount. A page view uses that copy while it's fresh and Plaid has been
quiet (no Plaid call at all); otherwise it asks Plaid only for what changed
since the cursor. The first visit after linking is the only full read.https://<your-domain>/api/plaid/webhook (override with
PLAID_WEBHOOK_URL). The endpoint believes only Plaid's ES256 signature
over the exact body, and then flags that bank, keeping any warning about
it (sign in again, consent ending, access withdrawn: see Alerts). Prism holds
no privileged key, so the person's own next visit — or their next question
to a connected app — does the sync, as them. With no webhook, a copy older
than 15 minutes is refreshed anyway./connections/return as usual; the return cookie then names
the bank being signed in to again, so "Try again" means that bank, never a
second connection to it.Prism says when something needs acting on, rather than waiting to be opened
(src/lib/finance/alerts.ts, pure, each alert with an id for its occasion and
a wording without dollar amounts):
plaid_bank_warning_signed, which
answers only to the jobs' CRON_SECRET and sets three plain columns and
nothing else): sign in again (ITEM_LOGIN_REQUIRED),
consent ending on a date (PENDING_DISCONNECT in the US and Canada,
PENDING_EXPIRATION in Europe, about a week ahead), or access withdrawn.
A sign-in or a withdrawal ends when Plaid answers again; a consent running
out ends only when its owner finishes "Sign in again".On Overview, the urgent ones show as a Heads up at the top, on the person's own view only.
Opt-in, per person, on the Account page: which of the five kinds (a bank
needs you, a bill may not be covered, a subscription went up, an estimated
tax payment is due within the week and not yet seen paid, summaries) and
whether amounts may show. Prism holds no service-role key, so
the daily job (/api/cron/alerts, Vercel Cron at 13:00 UTC) can't read
anyone's money as itself:
alert_snapshots, and only while their emails are on. Turning them off
deletes it (a trigger). Bank warnings come straight from plaid_items.src/lib/alerts/refresh.ts),
for someone who left "Check my banks each morning" on
(profiles.alert_refresh) and has no Coinbase linked, when their snapshot
is over 12 hours old. It is the one place Prism reads a bank without its
owner present, so it is narrow: alerts_sources hands the job that
person's SEALED sources (never a home's address), the job opens them with
the vault key and draws their money exactly as a visit does
(morningFinance), asking Plaid only for balances and new transactions
(no holdings, no card terms) and nobody about wallets, and writes back only
the bank's sealed copy, over the version it read (alerts_save_sync), and
a "morning" snapshot (alerts_save_snapshot). 15 seconds a person at most;
the email then says the figures are from that check.CRON_SECRET; the database keeps only its sha256 (job_keys, unreadable
over the API) and answers alerts_due, alerts_languages, alerts_sent
and alerts_stop to nothing else. With the secret alone, a caller learns
email addresses, choices, languages and which banks need a sign-in: every
figure stays sealed.profiles.language, English or Spanish). Their own visits keep it in
step with the page (their pick on EN | ES, else their browser's language),
writing it only when it moved, so there is no second setting. A snapshot
holds its bills and price rises already in words, so it records its
language; one in a language the person has since left is worded again by
the morning check where they allow it, and otherwise goes as worded, since
a warning in the other language beats none.Switching it on (owner, once):
bis-rgv.com, and add it in Vercel as RESEND_API_KEY (Production
only, Sensitive).S=$(openssl rand -base64 32); printf %s "$S" | pbcopy; printf %s "$S" | shasum -a 256 | cut -d' ' -f1.
Paste the copied secret into Vercel as CRON_SECRET (Production only,
Sensitive). The secret itself never goes in chat or a document.insert into job_keys (name, sha256) values ('alerts', '<fingerprint>') on conflict (name) do update set sha256 = excluded.sha256;Replacing the secret is the same three steps with a new one; the old one stops working the moment the fingerprint changes. The job sends to people one at a time and stops 45 seconds in; anyone left is first the next day.
On the Account page, a signed-in person downloads everything Prism shows them
(src/app/account/export/*, built by src/lib/finance/export.ts):
src/lib/server/zip.ts, node:zlib
only) of transactions.csv, accounts.csv, balances.csv (every month-end
balance), budgets.csv, goals.csv, holdings.csv, everything.json
(all of it plus wallets, homes and things added by hand) and a README.?year=2025). Prism's own Import history reads it back exactly.getPersonalFinance, never the household
view), never the demo household, never cached, and nothing that opens
anything: no bank or Coinbase token, no calendar link, no sealed value. A
text cell that starts like a formula gets a leading apostrophe, so a
merchant name can't run in a spreadsheet.Taxes (/taxes, built by src/lib/finance/taxes.ts) sorts one year into
what a US tax return asks about. Money in: pay, interest, dividends, benefits
and pensions, other money (clients, side work) and tax refunds. Money out:
gifts to charity, medical and dental, taxes paid, mortgage and student-loan
payments, childcare and tuition. Each section names the form that holds the
official figure (W-2, 1099-INT, 1098-E…), says in a sentence what it means for
a return, and lists the transactions behind it; the hero is the forms to watch
for. It defaults to last year until the April deadline has passed.
How a line is found, most certain first:
taxHint, mapped from Plaid's detailed
category in src/lib/plaid/map.ts: donations, tax payments, medical but
never a vet, childcare, education, mortgage, student loans), unless the
person has since moved that line to another category: their word wins.It finds; it never advises. Nothing adds up a deduction or guesses a tax, and
the page says plainly that a bank line is what landed, not what a form says.
Download for your tax preparer (/account/export/taxes.csv?year=2025)
writes every line under its section, each section closed by its total, with
the same protections as every other download. The AI connector answers the
same summary through get_tax_summary, and deep research reads it as the
document taxes:<year>.
.github/workflows/production-check.yml,
src/lib/ops/production-check.ts): the pages, the protective headers, the
doors that must stay shut (the alert job, downloads, the AI connector),
/api/health, and the certificate. When anything fails it opens an issue
labelled production-check, which GitHub emails to you; when everything
passes again it closes it. It holds no secret.node src/lib/ops/production-check.ts (Node 22; nothing to install)./api/health says which release is live, whether the database
answers, and when the daily alert job last ran and whether that run
finished (job_runs, written only by the job through job_ran). A
morning without the job (26 hours) fails the check.Prism is free to start and stays useful for good; Prism Plus is the paid
plan. Prices live in src/lib/billing/plans.ts, and the pricing page
(/pricing) shows them by the month or by the year:
| Plan | Monthly | Yearly |
|---|---|---|
| Prism Plus (one person) | $5.99 | $49 |
| Prism Plus for your household (up to four people) | $8.99 | $79 |
A first subscription starts with 14 days free. Launch prices are founding-member prices: Stripe keeps a subscription on the price it started at, and nothing here ever moves one.
How it's built:
billing
table (migration billing) holds a person's plan, status and dates, one row
per Stripe mode (test or live), and nobody can write it over the API:
only billing_record, which answers to the job secret (CRON_SECRET,
production only), and only with what the server just read from Stripe./api/stripe/webhook): it
checks Stripe's signature over the exact bytes, then reads the subscription
the event names back from Stripe and keeps that, so late or out-of-order
events can't leave the wrong state. Coming back from Checkout
(/api/stripe/return) records the subscription at once, for the person it
was made for only.billing_counts,
billing_plus): trialing, paid or being retried, until a week past the end
of the time paid for. The server asks it through my_plan (the person),
alerts_plus (the alert job) and calendar_feed_plus (a calendar app), and
every gate reads plusFor (src/lib/billing/plus.ts). If the database
can't be asked, the gates open: nobody who pays meets a paywall because of
an outage of ours.plans.ts, and refuses to start if Stripe's amount,
currency or interval differs from the page's.Switching it on (owner, in this order):
https://prism.bis-rgv.com/terms and Privacy policy
https://prism.bis-rgv.com/privacy. Checkout's "I agree to the Terms" box
needs the first.prism_plus_monthly; $49 yearly
prism_plus_yearly.prism_household_monthly; $79 yearly prism_household_yearly.https://prism.bis-rgv.com/api/stripe/webhook, with the events
checkout.session.completed, customer.subscription.created,
customer.subscription.updated and customer.subscription.deleted.STRIPE_SECRET_KEY (Developers → API keys; a
restricted key with Checkout Sessions, Customer portal and Customers set to
write and Subscriptions and Prices to read works too) and
STRIPE_WEBHOOK_SECRET (the endpoint's signing secret). Never paste
either into a chat or a document. CRON_SECRET is already there.Do steps 4–9 in Stripe's test mode first (test keys, a test webhook) and
buy Plus with the card 4242 4242 4242 4242: the Account page should say
Prism Plus, and cancelling in the portal should say when it ends. Test
subscriptions never count once the live keys go in. Then, before the live
keys: the lawyer approves the paid Terms and the privacy policy's Stripe
lines, and everyone with an account is told about the change before it takes
effect, as the Terms promise (next section). Live mode needs its own products,
portal and webhook (Stripe can copy products from test mode). Live keys sell
nothing before November 23, 2026, the day every account was told Prism Plus
starts (PLUS_STARTS): until then a live key leaves billing off, exactly as no
key does, so putting them in early charges no one. Test keys are never held back.
The Terms and the privacy policy promise that a change that matters is told
"in the app or by email before it takes effect". A change notice is data in
src/lib/legal-notice.ts (NOTICES): its id, the day it's published, the day
the change takes effect, and each change in a plain sentence, in English and
Spanish (i18n/es/notice.ts).
prism-notice cookie, declared in the privacy policy).NOTICE_EMAIL, value
the notice's id (plus-2026-11), Production only, plain (it isn't a secret).
Save, then redeploy (Deployments → the latest → ⋯ → Redeploy). The next daily
job (13:00 UTC) sends it first, for up to 15 seconds a day, in each person's
language, and records each one that went (notice_deliveries), so nobody
gets it twice; whoever it didn't reach is first the next day. It never goes
on or after the change's day: a notice after the change isn't one. The counts
(never addresses) are in the Vercel log line "Prism: change notice" and in
the job's run record (job_runs.report.notice). Remove the variable once
due reaches 0.A new notice is a new entry with a new id (the cookie and the delivery log are keyed by it), its Spanish, and, if it changes when something starts, the guard that holds it until that day.
Ending the founding offer later: new prices under new lookup keys (for
example prism_plus_monthly_2027) and the new amounts in plans.ts. Never
move an existing lookup key to another price: a subscription's plan is read
from its price's key.
Waiting on Coinbase (October 2026): Coinbase has paused new OAuth clients ("New OAuth client creation is temporarily disabled", limited to approved partners). Request access from a CDP Portal account first; the code needs no change once a client exists.
https://<your-domain>/api/coinbase/callback.COINBASE_CLIENT_ID, COINBASE_CLIENT_SECRET and PRISM_VAULT_KEY.wallet:accounts:read (plus offline_access to stay connected) and never
for anything that can send, buy or sell.Coinbase refresh tokens can be used once, so src/proxy.ts refreshes a link
shortly before its hour is up and stores the new pair in the same response.
It runs only for browsers holding a Coinbase link.
None of the three lets another app read an account, so a bank shows only
"Venmo −$45.00". On Connections → Venmo, PayPal and Cash App
(/connections/payments) a signed-in person adds each app's own activity
file (Venmo's monthly statement CSV, PayPal's activity download, Cash App's
Export CSV). Prism reads the files in the browser and never uploads them
(src/lib/finance/p2p.ts), matches each payment to the bank line it caused
(same app, an amount the bank would show for it, the bank dated a day before
to five days after, closest dates first), and keeps only the matches: the
bank transaction's id, who it was to or from, and the note.
validP2pMatch), and
the rest are dropped and counted (src/lib/server/p2p-actions.ts).profiles.sealed_p2p_notes (the profile's policies govern
it; no household function names it), applied where money is assembled
(moneyFor), so the ledger, its search, the downloads and connected apps
all show "To Alex Kim · pizza". The morning alert job never reads them.A bank shows "AMZN Mktp US −$86.40" and nothing more, and Amazon lets no
other app read an account. On Connections → Amazon orders
(/connections/amazon) a signed-in person adds their own order history:
Amazon → Account → Request your data → Your Orders sends a zip a day or two
later, and they choose Retail.OrderHistory.1.csv from inside it (one row
per item). Prism reads it in the browser and never uploads it
(src/lib/finance/orders.ts). Amazon charges a card as each shipment
leaves, so a charge is a shipment's items added up (or the whole order's,
where it was charged once), matched to an Amazon line for exactly that
amount, dated a day before the shipment to six days after, closest first; an
item is never counted in two charges. Only the matches are kept: the bank
transaction's id, the order number, the day it was placed, and each item's
name, quantity and cost.
validOrderMatch). Matches
arrive a batch of 150 at a time (src/lib/server/orders-actions.ts), so no
request nears the size a server action takes.profiles.sealed_order_notes (the profile's policies govern
it; no household function names it, and the morning job never reads it),
applied where money is assembled (moneyFor), so the ledger shows "Dog
food, USB-C cable and 2 more" under the charge, its search finds an item
by name, the downloads list the items, and connected apps get
amazon_order. A charge with two or more items offers "Split by its
items" in its own dialog: one part per item (the smallest added up past
eight), each named, in the line's category until the person picks.A signed-in person opens any of their own transactions on Spending
(src/components/transaction-dialog.tsx) and, besides its category, can:
checkDetail in src/lib/finance/details.ts). A split becomes its
parts (<id>~1, <id>~2…) before anything is counted, so every total,
budget, chart, the tax summary and Claude agree; what looks for repeating
bills joins them back first (wholeLines), so a split bill still forecasts
as one. If the bank's amount later changes (a tip added when it posted), the
split is set aside and the line shows whole until it's split again.normalizeMerchant
writes it, so every branch's spelling counts. Every purchase there, before
and after, is split by those shares to the cent (the cents rounding leaves
go to the parts that lost the most) unless the person split it themselves
or kept it whole. Split rules on Spending lists them, with Remove and
Undo.Kept sealed in profiles.sealed_txn_details by the bank's transaction id,
never shown to a household, and read by the morning check so an alert email
counts a split bill the way the app does (alerts_sources hands it over).
Exports carry the split, tags and debts as their own columns, after the ones
Prism's importer reads.
/subscriptions lists everything a person pays for again and again
(src/lib/finance/subscriptions.ts, analyze().subscriptions), and every
screen and connector reads the same list and total: the Future card, the
insight, the alerts and list_subscriptions.
detectRecurring calls a subscription, and one
that comes every three, six or twelve months that would read as one if it
came monthly (looksLikeSubscription; its stream stays a "bill", so the
set-aside never moves). A free trial's first charge of a dollar or less is
left out of the rhythm, so the subscription after it stays fixed.profiles.sealed_txn_details: subs keyed by
the shop as ruleKey writes it, own by own-<hex>), applied to the
person's own view, the morning check and connected apps, never the
household or the example household (withSubscriptionNotes).CANCEL_LINKS, each checked by hand; Apple's and Google's
for what they bill. Apple and Google bill other apps' subscriptions under
their own name, so the page counts what each billed in the last month and
says where on the phone the list is./cards (src/lib/finance/cards.ts, get_cards) shows each credit card:
what it owes against its limit, the share of the limit in use (overall too,
across cards whose bank reports a limit), its last and next statement dates,
what's due, its APR, the interest it charged over the last 12 months and its
annual fee. The limit comes from Plaid with the balance
(balances.limit → Account.creditLimit); the statement date comes from
Liabilities (last_statement_issue_date → Liability.statementDate, kept as
issued in the bank's sealed copy, absent in copies from before), so it shows
only with PLAID_LIABILITIES=on. The next close is a month after the last.
Before it, the page says what paying would bring the reported share down to
(30%, then 10%), from today's balance: a description, never advice.
Buy now, pay later (src/lib/finance/paylater.ts, on Cards and in
get_cards): plans with Affirm, Klarna, Afterpay, Sezzle and Zip, found from
the payments to them (no aggregator reports the plans). A run of payments of
the same amount from one account, every two weeks, is a pay-in-4 plan: four
less those seen are still to come, two weeks apart. Every month, it's an
ongoing payment whose length the payments don't show. A plan whose next
payment is more than 10 days late is over. What's owed is "at least": a plan
that began before Prism's history looks shorter than it is.
/fees (src/lib/finance/fees.ts, get_fees, an Overview heads-up) lists
every fee the person's banks and cards charged over the last 12 months:
overdraft, returned payment, ATM, foreign transaction, late, monthly service,
cash advance. A fee is Plaid's own BANK_FEES_* category (bankFee), or, for
imported history, its words on a line that reads as a bill or as nothing, up
to $100. Interest isn't a fee here (Cards counts it). Money of the same
amount back to the same account within 45 days, under the same name or with
refund words, marks a fee given back. Each fee still lost has a polite request
for it back, written in the page's language, that the person copies or shares
and sends themselves: Prism never sends it. The same amount at the same place
on the same day in the same account (at least $10, last 90 days) is asked
about as a possible double charge, never asserted. Nothing is stored.
/refunds (src/lib/finance/refunds.ts, get_refunds, an Overview
heads-up) is money on its way back. A signed-in person opens a purchase and
chooses Waiting on a refund (the whole amount or part of it), or says
they asked for one back from the page's suggestions: recent fees not given
back, possible double charges, and charges after a subscription was
cancelled. Each wait is a RefundMark sealed with the rest of their
transaction details (the shop, the day, the amount, why, and the day they
started waiting). Prism finds it come back on its own: money in for exactly
that amount, on or after the purchase and within 120 days, from the same
shop, or in the same account with refund words. Past its usual wait (a fee 10
days, a return 14, a dispute 30) it's late, and the Overview says so. When it
came back another way (store credit, cash) the person says so. The page also
lists what friends owe from purchases marked on Spending. Prism never asks
anyone for money.
The "Your paycheck, planned" card on Future (src/lib/finance/payday.ts):
the last paycheck, given a job for every dollar until the next payday. What
landed on payday; each bill and subscription before the next payday, paid or
still to come (recurring streams, never card payments, whose purchases are
already counted day to day); each goal's share of the days, from its monthly
amount; what that leaves to live on, what's been spent day to day since
payday, and about what's left a day. Prism moves no money: it's the plan,
laid out.
A signed-in person can mark any of their accounts as their business, under
Choose what counts on Net worth ("This is a business account"). A
business account is kept out of every personal total, like one left out: its
balance leaves net worth and none of its lines count in spending, income,
budgets, forecasts or the check-in. On its own, it shows as the Your
business card on Cash flow: what the business accounts hold now, and what
came in and went out this month, last month and the month before. Every line
counts there, the owner's pay included, except money moved between two
business accounts. The mark is a list of account ids (TxnDetails.business),
sealed with the rest of the person's details. list_accounts lists those
accounts apart, under business.
/business (src/lib/finance/schedule-c.ts) takes the year's lines in the
business accounts and sorts each one by where it goes on Schedule C, the form
a sole proprietor or single-member LLC reports its profit on:
Where a line goes is, most certain first: what the person said about that
line, then what they said about every line from its shop going the same way
(out: or in: plus the shop), then Prism's guess. The guess reads the name
first (a payroll service is wages, a card processor's fee is a fee), then the
line's category (a restaurant is a meal, a gas station is the car). Money back
from a purchase goes where the purchase went. The person's choices are kept in
TxnDetails.biz and TxnDetails.bizShops, sealed with the rest, and saved by
sortBusinessLine with an Undo.
The Taxes page shows the same year by Schedule C line, with what the accounts
saw left and a note on Schedule SE when that's $400 or more. Lines in a
business account no longer appear in the personal tax sections. The tax
download lists them by line, and get_tax_summary returns them under
business_schedule_c. It is information, not tax advice: a meal usually
counts at half, and a home office, mileage or depreciation aren't visible to
Prism. Part of Prism Plus with the tax year.
/taxes/return (src/components/return-upload.tsx,
src/lib/finance/tax-return.ts) adds the figures from a filed Form 1040.
TurboTax and the other tax programs have no API for reading a return, so the
person brings the PDF their software saved:
saveTaxReturn checks them again and keeps them in
TxnDetails.returns by tax year, sealed with the rest. removeTaxReturn
comes back with what Undo needs.Taxes shows last year's return beside this year: AGI, total tax, the
effective rate, the refund or the bill, last year's business income beside
this year's Schedule C, and, while the year is under way, the safe-harbor
amount (paying at least last year's total tax, or 110% of it when AGI was over
$150,000 or $75,000 married filing separately, generally avoids an
underpayment penalty). get_tax_summary returns the same under
returns_on_file. Part of Prism Plus. The example household has a return of
its own, so a visitor sees what it does.
src/lib/finance/estimated-tax.ts covers the IRS's four quarterly due dates
(April 15, June 15, September 15 and the next January 15, each moved past
weekends, Martin Luther King Jr. Day and D.C.'s Emancipation Day). It applies
to someone whose business accounts made money this year, or whose last return
had estimated payments or a balance due of $1,000 or more.
It shows as a card on Taxes, an item in the check-in when a payment is due
within 30 days, single-day events in the calendar feed (the dates and the
quarter's amount only), and estimated_taxes in get_tax_summary. Prism
states the rule and never says what to pay, and never pays anything.
Overview shows a signed-in person a Getting started checklist
(src/lib/finance/setup.ts, src/components/setup-checklist.tsx) until it's
done. Four steps are counted: connect a bank (own money, linked or imported),
set budgets, turn on alerts (only where the site can send them) and put bills
in the calendar. Two more are listed apart and never counted, because they
apply only to some people: marking a business account, and adding last year's
return. A step is done when the thing itself is, never because someone ticked
it. The card leaves once every counted step is done; Hide puts it away
sooner with the prism-setup cookie on that device.
/check-in (src/lib/finance/check-in.ts, get_check_in) is one page for
once a month. It has four parts:
Nothing is stored. An item leaves the list when what it's about is settled, never because it was ticked, and Prism never acts on any of it.
The "Your own inflation rate" card on Spending (src/lib/finance/inflation.ts,
get_my_inflation): what the same bills and subscriptions cost now against
a year ago, from the person's own charges. It compares the last 60 days with
60 days centred on a year ago. A place is a price when it repeated at one
price a year ago, and charges now at one price too, or at the old price until
the latest stepped. A one-off is never a price, and loan payments and
donations are left out. The rate is the change in prices, weighted by what's
spent on each now, and needs two prices to compare. What a typical visit
costs at the grocer or the gas station is listed apart and never counted in
the rate, since a visit moves with what's bought as well as with prices.
The "If your paychecks stopped" card on Future (src/lib/finance/runway.ts,
how_long_could_i_last): the cash in checking and savings divided by what a
month costs, averaged over the last three whole months (never a month the
history starts partway through). Three figures: at the usual spending, on
essentials only (housing, food, transport, health, bills), and essentials
counting brokerage and crypto (never retirement), each with about when it
would run out. A loan payment the lender reports is added when no spending
line in the last 45 days already paid it, so a loan filed as a bill counts
once. Three to six months of essentials is named as a common cushion, as an
observation, never advice.
The car insurance renewal, the water bill every three months, a yearly
membership: Prism finds these from the charges themselves
(detectRecurring in src/lib/finance/recurring.ts), over up to two years
of history (all Plaid sends), so the forecast, safe-to-spend, Can I afford
it?, the alert emails, the calendar and Claude all expect them. Monthly and
more frequent bills are still read from the last 200 days, exactly as before;
only what isn't one of those is tried as a bill that comes less often.
Two lookalike charges a year apart are far easier to come by than a monthly rhythm, so the rules are deliberately strict:
Future lists them with their next date, each one's share of a month, and
the total to put aside each month (setAside, the same figure
upcoming_bills gives Claude and ChatGPT). Any due from checking within the
60-day forecast is already in it, and the card says so. They are always
bills, never subscriptions, even a small fixed yearly plan, so the
Subscriptions card stays the monthly charges a person might cancel. Detected
once, they also stop counting as day-to-day spending in the forecast, and a
yearly renewal is no longer flagged as "a bigger one" among recent purchases.
On Net worth, every card and loan with something owed, each with its yearly
rate and monthly payment: the lender's own when Prism reads them (Plaid
Liabilities), typed by the person otherwise, never guessed. Add an extra each
month and see both orders side by side (src/lib/finance/payoff.ts):
Either way every debt keeps its own payment, the extra goes to the first in line, and each debt paid off adds its payment to the next (the roll-over). Paying only what each one asks, with no extra and nothing rolled over, is the yardstick, and the chart shows what's owed month by month against it. Interest is a twelfth of the yearly rate each month, to the cent, and payments stay as they are today. A debt whose payment doesn't cover its interest is named: it never gets paid off that way.
Which debts start ticked: every loan, and a card only when it charged
interest in the last two statements (65 days), from the bank's own "interest
charge" category or the words on the line. A card that's paid off every
month carries no debt, so it starts unticked and says why. The arithmetic
runs in the browser and nothing is saved; plan_debt_payoff gives Claude and
ChatGPT the same plans, takes the person's own rates and payments when they
give them, and asks for any that are missing. It says what each order would
do and never which to pick.
A signed-in person adds a wallet on Connections by its public address:
Prism can see what it holds and can never move it. There is nothing to sign,
and no recovery phrase or private key is ever asked for; one pasted by mistake
is stopped in the browser before it's sent, and refused again on the server
(src/lib/crypto/secrets.ts), never kept, logged or repeated back.
src/lib/crypto/xpub.ts):
an xpub, ypub or zpub, or the descriptor a wallet such as Sparrow exports
(pkh, sh(wpkh), wpkh, tr around one key). Prism works out every
address itself (BIP 44/49/84/86, receiving and change, until 20 in a row are
unused) and sends mempool.space one address at a time, never the key. A
bare xpub doesn't say which kind of address it pays to, so the first address
of each kind is checked and the used kinds are kept. Key arithmetic is
@scure/bip32 and @noble/curves (audited, pinned), never hand-rolled.
Reading one takes dozens of requests, so no page waits for it: it's read
after the response, at most every 30 minutes, claimed first so two pages
never read it at once, and all or nothing (no part-totals). A wallet past
500 addresses on a side is refused rather than half-read. Private,
multisig and test-network keys are named and refused.ALCHEMY_API_KEY (free tier,
Sensitive, in Vercel). ETH and SOL, plus a short list of well-known tokens
named by CONTRACT or MINT (USDC, USDT, DAI, WBTC), never by symbol: scam
tokens copy real names and airdrop themselves into every wallet.profiles.sealed_wallets), never sent to a household, and only an
address (never the key, never who it belongs to) goes to the balance service.Signed out, Prism works as before: a demo household, with budgets and goals
kept on the device. Signed in, banks, Coinbase, budgets, goals and a private
calendar link live in the person's account. Connecting a bank or Coinbase
needs an account (src/lib/linking.ts); pressing Connect while signed out
goes to sign-in first and comes back. Up to four adults can form a
household (Account page): each keeps their own login, chooses account by
account what to share (Connections), and flips between Me and
Household in the top bar.
supabase/migrations/*.sql.https://<your-domain>, and
redirect URLs https://<your-domain>/** (plus http://localhost:3100/**).Your Prism code is {{ .Token }} beside the {{ .ConfirmationURL }} link.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY and
PRISM_VAULT_KEY.How it's kept safe:
src/lib/supabase/schema.test.ts checks each
rule as a signed-in stranger and as nobody.PRISM_VAULT_KEY, which the
database never sees.imported_history). It's theirs alone: never shown to the household,
readable (never changeable) by a connected app.Prism is an MCP server at /mcp (production:
https://prism.bis-rgv.com/mcp). Add it to Claude (Customize → Connectors →
Add custom connector) or ChatGPT (Settings → Security and login → Developer
mode, then Plugins → +); the app sends the person to Prism to sign in and
approve it, and from then on can ask thirteen read-only questions: get_overview,
list_accounts, search_transactions, spending_breakdown, get_cash_flow,
get_budgets, get_goals, upcoming_bills, get_income, get_net_worth,
get_tax_summary, can_i_afford, plan_debt_payoff.
Answers cite the transactions they rest on, carry the person's own "today"
and time zone, and say demo: true when nothing is linked yet.
Two more, search and fetch, are the pair ChatGPT's deep research (and
its company knowledge) reads a connector through, in exactly the shapes
OpenAI specifies (src/lib/agent/research.ts). They present the person's
money as documents: the nine summaries, each month and each year, each
year's taxes, and each spending category and merchant of the last twelve
months. A document's text
cites transaction ids, and its link opens the Prism page showing the same
figures, so every citation in a report can be checked by clicking it. A
category or merchant link carries what to look for in its #fragment
(ledgerHash in src/lib/finance/view.ts), which a browser never sends to
the server: no merchant name ever lands in a request log.
Sign-in is Supabase Auth's OAuth 2.1 server, so nothing new holds a
secret. To switch it on (once per project): Authentication → OAuth Server →
enable, authorization path /oauth/consent, and allow dynamic client
registration. /.well-known/oauth-protected-resource/mcp points MCP clients
at it.
Why it's safe to connect:
client_id (the claim every connected-app token has and a person's own
session never does), and delete_my_account refuses them too. Proven in
schema.test.ts and against the live project.auth.users now refuses any change of password,
email or phone (Prism signs in by email code only, and has no change-email
feature), and currentAccount treats a connector token as signed out, so
it can't reach actions that act outside the database either./mcp, and Supabase Auth is
asked about every one — so an app disconnected on the Account page is cut
off immediately, not when its token expires.src/lib/agent/tools.ts), so a chat can't disagree with the app.Production: https://prism.bis-rgv.com (Vercel, auto-deploys every push to
main). vercel.json makes each deploy run lint and the unit tests before the
build, so a failing test blocks a release even if CI is skipped. With no Plaid
keys the deployment serves the demo household.
Any Next.js host works. On Vercel: import the repository, keep the detected
Next.js settings, and add the environment variables from .env.example.
Previews are demo-only. Every branch and pull request (Dependabot's
included) gets a Vercel preview, built from code that hasn't been merged. A
preview never reaches production's data: it has no database and no vault key
(src/lib/deployment.ts), and serves the demo household. Keep
PRISM_VAULT_KEY and every secret Production only in Vercel; the app
refuses a vault key on a preview anyway, should one be ticked by mistake.
PRISM_VAULT_KEY seals every bank and Coinbase token, every synced
transaction and the calendar's bill list. Replace it if it may have been
exposed (Incident Response Plan 5.2), when anyone who could see it leaves, or
as a drill. Nobody has to reconnect a bank.
Keys are numbered, because a Sensitive variable in Vercel can't be read back: you never move a key, you only add the next number and, later, delete the old one. The highest number seals; every number opens.
openssl rand -base64 32$b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b)PRISM_VAULT_KEY_2 (the next number after your highest, up to _99),
for Production only (never Preview), marked Sensitive. Leave the old one where it is.reseal in src/lib/server/account-store.ts). A connected AI app never
moves them: it can't write.unnamed. With a single key, seals
don't name their key at all, so a deploy changes nothing stored and an
older release can still read everything; during a rotation, unnamed and
any id but the new one are what's left to move. Your own visit shows you
which id is the new one.-- Which vault key sealed what. Reads key ids only, never a sealed value.
select what, key_id, count(*) from (
select 'bank token' as what, case when sealed_token like 'j2.%' then substr(sealed_token, 4, 8) else 'unnamed' end as key_id from plaid_items
union all
select 'bank transactions', case when sealed_sync like 'z2.%' then substr(sealed_sync, 4, 8) else 'unnamed' end from plaid_items where sealed_sync is not null
union all
select 'coinbase', case when sealed_tokens like 'j2.%' then substr(sealed_tokens, 4, 8) else 'unnamed' end from coinbase_links
union all
select 'coinbase value', case when sealed_snapshot like 'z2.%' then substr(sealed_snapshot, 4, 8) else 'unnamed' end from coinbase_links where sealed_snapshot is not null
union all
select 'calendar link', case when sealed_token like 'j2.%' then substr(sealed_token, 4, 8) else 'unnamed' end from calendar_feeds
union all
select 'calendar bills', case when snapshot->>'sealed' like 'j2.%' then substr(snapshot->>'sealed', 4, 8) else 'unnamed' end from calendar_feeds where snapshot ? 'sealed'
union all
select 'category fixes', case when sealed_category_rules like 'z2.%' then substr(sealed_category_rules, 4, 8) else 'unnamed' end from profiles where sealed_category_rules is not null
union all
select 'added by hand', case when sealed_manual_items like 'z2.%' then substr(sealed_manual_items, 4, 8) else 'unnamed' end from profiles where sealed_manual_items is not null
union all
select 'home addresses', case when sealed_home_values like 'z2.%' then substr(sealed_home_values, 4, 8) else 'unnamed' end from profiles where sealed_home_values is not null
union all
select 'vehicle VINs', case when sealed_vehicle_values like 'z2.%' then substr(sealed_vehicle_values, 4, 8) else 'unnamed' end from profiles where sealed_vehicle_values is not null
union all
select 'wallets', case when sealed_wallets like 'z2.%' then substr(sealed_wallets, 4, 8) else 'unnamed' end from profiles where sealed_wallets is not null
union all
select 'payment notes', case when sealed_p2p_notes like 'z2.%' then substr(sealed_p2p_notes, 4, 8) else 'unnamed' end from profiles where sealed_p2p_notes is not null
union all
select 'amazon orders', case when sealed_order_notes like 'z2.%' then substr(sealed_order_notes, 4, 8) else 'unnamed' end from profiles where sealed_order_notes is not null
union all
select 'splits and tags', case when sealed_txn_details like 'z2.%' then substr(sealed_txn_details, 4, 8) else 'unnamed' end from profiles where sealed_txn_details is not null
union all
select 'imported history', case when sealed like 'z2.%' then substr(sealed, 4, 8) else 'unnamed' end from imported_history
union all
select 'alert snapshot', case when sealed like 'z2.%' then substr(sealed, 4, 8) else 'unnamed' end from alert_snapshots
union all
select 'phone notifications', case when sealed like 'j2.%' then substr(sealed, 4, 8) else 'unnamed' end from push_subscriptions
) seals group by what, key_id order by what, key_id;
Connections kept on a device from before connecting needed an account open with any key in the ring too, so retiring a key ends those as well.
EN | ES in the top bar switches Prism's words between English and Spanish
at the same address, as bis-rgv.com does, and keeps the choice for a year in
the prism-lang cookie (set by the server, unreadable to the page's
scripts). Until someone picks, a browser that asks for Spanish first gets
Spanish. Dates read the Spanish way ("lun, 5 oct"); amounts stay $1,234.56,
as U.S. banks write them in either language.
src/lib/i18n/t.ts); the Spanish is in src/lib/i18n/es/, one
file per part of the app, written in "tú", in the voice of bis-rgv.com. A sentence with no Spanish
shows in English, and pnpm test names it.profiles.language). Connected apps (MCP) answer in
English.?lang=es from a Spanish page. Event IDs don't depend on the language, so
a calendar that changes language renames its events instead of adding more.src/lib/legal-languages.ts). Until then
/privacy and /terms read in English in either language. To publish one,
set its SPANISH_APPROVED entry to the English date the lawyer approved
(the page's "Last updated"). If the English changes afterwards, its Spanish
has to change in the same commit (legal-spanish.test.ts), and the page
goes back to English on its own until the new Spanish is approved. Each
Spanish page opens with a note that it's a translation and the English
governs.| Screen | What it shows |
|---|---|
| Overview | A Heads up for anything to act on now (a bank that has stopped updating or will within the week, a bill before payday the account won't cover), net worth hero, safe-to-spend, four headline numbers, spending pace vs last month, category donut, budget rings, evidence-backed insights, upcoming bills, recent activity |
| Cash flow | Income → categories → saved Sankey; your paychecks (who pays you, how often, what lands, the next payday) and where your income comes from, by kind; money in vs out by month, what you kept each month, savings-rate trend |
| Check-in | Once a month: last month in numbers against the month before, what's worth a look now from every part of Prism (each linked to the page that settles it), what changed, and the month ahead |
| Spending | Your own inflation rate (what the same bills and subscriptions cost now against a year ago, weighted by what you spend on each, with typical visits listed apart), stacked monthly bars by category, category change vs the prior period, top merchants, a year-long calendar heatmap, Owed to you and Your tags, and a searchable ledger where a signed-in person fixes any category (for one purchase or every purchase at that shop), splits a purchase across categories, tags it and notes who owes them for it, all kept sealed in their account and applied everywhere, Claude included |
| Refunds | Money coming back to you: refunds you're waiting on (found come back on their own, the late ones first), what friends owe you, money you may be owed and haven't asked for (recent fees, charges twice, charges after cancelling), and what came back this year |
| Budgets | Month plan left, bullet chart (spent · projected · limit), a ring per budget with a "today" tick, and an editor that sets each limit beside what that category usually costs; in a household, the household's own budgets, pacing spending from shared accounts |
| Future | 60-day checking balance forecast with an 80% band, paydays and bills marked, safe-to-spend, If your paychecks stopped (how many months checking and savings would last at the usual spending, on essentials only, and counting investments), (a purchase, a new monthly bill or a raise, answered as you type: fits, tight or doesn't fit, with the lowest point, safe-to-spend and what you usually keep before and after; nothing saved), (every three, six or twelve months, each with its next date and what to put aside for it each month), subscriptions with price-rise flags, and for bill reminders |
Next.js 16 (App Router), React 19, Tailwind 4, TypeScript, Vitest.
src/lib/brand.ts product and company names — one place to rename
src/lib/finance/ provider-neutral model + pure analytics (cash flow, budget
pacing, recurring detection, forecast, Sankey, insights)
src/lib/charts/ chart geometry (scales, curves, bars, arcs, treemap)
src/lib/plaid/ dependency-free Plaid client + mapping onto the model
src/lib/coinbase/ dependency-free Coinbase client (OAuth2 + PKCE) + mapping
src/proxy.ts keeps a Coinbase link alive (single-use refresh tokens)
src/lib/server/ data loading (account vs device, demo vs live), the sealed
token vault, plans, sign-in and account actions
src/lib/supabase/ per-request Supabase client (acts as the person, never admin)
src/lib/agent/ the MCP server: pure read-only tools over the analysis, and
their registration with @modelcontextprotocol/server
supabase/ the account schema, with row-level security
src/components/ chart kit (SVG, no chart library) and UI blocks
src/app/ the eight screens + /api/plaid/{link-token,exchange,disconnect}
+ /api/coinbase/{connect,callback,disconnect}
+ /calendar/{bills,demo}.ics, /calendar/feed/<secret>.ics
+ /sign-in, /auth/callback, /account
+ /mcp, /oauth/consent, /.well-known/oauth-protected-resource
/calendar/bills.ics is the person's own file;
/calendar/demo.ics is a public, subscribable feed of the demo household.pnpm typecheck
pnpm lint
pnpm test # vitest — analytics, geometry, Plaid + Coinbase, vault, plans, calendar
pnpm build
pnpm test:e2e # Playwright — the built app in a browser, desktop and 360px phone
# (signed in too, when the local Supabase stack is running)
CI (.github/workflows/ci.yml) runs all five on every push, the browser
tests inside the same verify job the rule on main already requires.
The browser tests (e2e/, playwright.config.ts) serve the production
build on the example household (they blank every key, so nothing real is
ever linked) and check what a visitor meets: every screen answers with its
title, at most one hero, no console error and no sideways scroll at either
width; the sidebar, the phone tab bar and its More sheet; the theme toggle;
Can I afford it?, the tax summary's years and the ledger's search and
links; and the doors a stranger tries (framing, downloads, the AI
connector). Locally, after pnpm build, pnpm test:e2e starts the server
itself (or reuses one on port 3100); on a machine without Chromium, run
pnpm exec playwright install chromium once.
The signed-in tests (e2e/accounts/) serve the same build a second time,
on port 3101, with accounts switched on against a local Supabase stack:
a throwaway copy of Prism's database on the machine running the tests,
built from supabase/migrations (supabase/config.toml). Nothing hosted is
involved, so they cost nothing, need no secret, and cannot reach
production: e2e/accounts/stack.ts refuses any address that isn't this
machine's and anything naming production. Each test signs up its own
account through the real sign-in form, reading the emailed code from the
stack's Mailpit, imports a few months of made-up transactions through the
CSV importer, and then works the screens as a person would: sign-in, a
mistyped code and sign-out; money that follows the account to a second
browser; splits, tags, who owes you, Remind and Undo; a split that follows a
shop; budgets and goals; a debt added by hand in the payoff planner; alert
email choices; and two-step sign-in with real authenticator codes, which
nothing opens without. To run them, with Docker running and the
Supabase CLI
installed:
supabase start -x imgproxy,postgres-meta,supavisor,vector,logflare
pnpm build && pnpm test:e2e
supabase stop
Without the stack, a local run leaves them out and says so; CI starts it before the browser tests and fails if it isn't there.
Proprietary. © 2026 Bespoke Intelligent Solutions. All rights reserved. The
source is public for reference only; see LICENSE.
/transactions/sync cursor is stored so each load fetches only what
changed (see Accounts, and Replacing the vault key).docs/ROADMAP.md — the product
roadmap in order, and the six owner-approved integrations (Plaid
investments, Coinbase, a credit-score partner, a home-value service,
calendar bill reminders, and read-only AI access to your own money).List-Unsubscribe (RFC 8058
one-click) and a link to /alerts/unsubscribe, both signed for that person
with an HMAC of CRON_SECRET; opening the link changes nothing until its
button is pressed. No images, no tracking.src/app/manifest.ts), and "Alerts on your phone" on the Account page
turns on Web Push for the device in hand (an iPhone only once Prism is on
its Home Screen: iOS 16.4+). The moment an email goes, the job sends each of
that person's devices (push_due, at most five) the email's first item in
the email's own words, so an amount the person turned off never reaches a
lock screen. Each message is encrypted for that one device (RFC 8291) and
signed with Prism's VAPID key (RFC 8292), both with node:crypto alone
(src/lib/alerts/webpush.ts), and goes only to Apple's, Google's,
Mozilla's or Microsoft's push service. The VAPID key is derived from
CRON_SECRET (HKDF), so there's nothing extra to set; replacing the secret
replaces the key, and each device is turned on again from the Account page.
Subscriptions are sealed (push_subscriptions), kept only while alert
emails are on, and forgotten when a push service says one is gone
(push_forget). The service worker (public/sw.js) only shows
notifications and opens Prism: it caches nothing.| Goals | A ring per goal, progress-as-share-of-target chart with projections, a what-if slider that can save its amount, and add / edit / delete; a goal can follow an account, so what's saved is that balance, month by month; in a household, goals saved toward together, which any member can update |
| Net worth | Own vs owe over 12 months, every account with its trend, holdings treemap, credit-score gauge and factors; a signed-in person adds what no bank reports (a home, a car, a loan from family) and updates its value, sealed in their account; paying off what you owe: highest rate first against smallest balance first, with an extra each month, against paying only what each asks (when each debt is gone, the interest, and what's owed month by month; nothing saved) |
| Your year | One calendar year on one page: what came in, what went out, what you kept (against the same stretch the year before when Prism holds it), month by month, where it went by category, the ten places you paid most, net worth from the year's start, pay, subscriptions, and the year in a few lines; honest about where the records start and a year still under way; print or save as PDF (always in the light theme) and download that year's transactions |
| Taxes | One year sorted into what a tax return asks about: pay, interest, dividends, benefits, other money and refunds in; gifts to charity, medical, taxes paid, mortgage, student loans, childcare and tuition out; each with its total, the form that holds the official figure and the transactions behind it; what was looked for and not found; print or save as PDF and download for your tax preparer |
| Connections | Per-institution health (with Sign in by a week before a bank's consent runs out), how data is protected, an honest catalogue of every integration and its real access path, Import history from a Mint, Monarch or bank CSV (read in the browser, never uploaded), and, in a household, Shared / Private for each of the person's accounts (Coinbase included: the household sees its total value as of the owner's last visit) |
| Household | Overview, Cash flow, Spending, Budgets, Future, Goals and Net worth over what every member shared, each account marked with whose it is; shared budgets and goals any member can change, showing who changed them last; invitations and members on the Account page, and a join page for the link |