Dev updates

Changelog

What changed in Zedi and when, written from the commit log by the one person who writes the commits. Each entry lists work that is merged on the dev branch as of that date. Production trails dev by a bit, so promotions are called out on their own line.

Found a bug or want something moved up? Email zedi@sneeks.ai. To follow along in a feed reader, subscribe to the RSS feed.

  1. A new home page, and Geist as the default font

    • A brand new home page that gets straight to it: all your money in one honest picture. It leads with what Zedi does for you (every account in one clean list, copies skipped, card payments and Venmo cash-outs paired), walks through each step in a tabbed tour, and says in plain words what Zedi can and can’t see in a new You stay in control section.
    • Pricing reads like a receipt: a short What you get table shows what Free and Pro include and exactly who you pay. Pro is still five bucks a month.
    • A note from Nico, Zedi’s founder, on why Zedi exists, with a new photo. The Ledger link in the site menu is now How it works.
    • Geist is the new default font across the app, for text, headings and numbers, with Geist Mono for code-like strings. A typeface set you picked yourself stays (if you picked Bookkeeper while it was the default, pick it again); Bookkeeper, the old default, is one click away in Settings > Appearance. The public pages are set in Geist too.
  2. A sharper Spending by category chart

    • Analytics: a Measure control (Spend, Transactions or Average) on the Spending by category card, and its merchant view, now drives the donut, the center total, the tooltip and the percentages. Picking a transaction count used to re-sort the list and leave everything else in dollars. Average swaps the donut for ranked bars with a tick at the overall average and reads each row as a multiple of it.
    • The same card shows the top 5, 8 or 12 rows or all of them, folding the rest into an Other row that opens in place. A Hide Uncategorized toggle says what it hides and links to triage, and clicking a slice or row focuses it until Escape or a second click. The alphabetical sort is gone, and the merchant view says it covers the top 20 merchants by spend.
  3. Bulk recategorize no longer stops on old rules

    • Fixed: bulk recategorize, including the Categorize button on the bank sync card, could stop with a database error. Some accounts carry older default keyword rules with no category attached (payroll, direct deposit, gym, Planet Fitness), and one matching transaction failed the whole run. Rules without a category are now skipped.
  4. Triage for uncategorized rows, a new category picker, and card payment pairing

    • File uncategorized, a new page under Transactions, takes uncategorized rows one merchant group at a time, biggest first, with a suggested category already picked and a hint saying where the suggestion came from. Enter files the whole group and remembers the merchant (on by default), the toast offers Undo, and keyboard shortcuts skip, hide, pick a recent category or file as Transfer. The Transactions header, the import success message, the bank sync card after a sync and the dashboard attention list link to it with a live count.
    • Rule patterns of five characters or fewer now match whole words only, so a short pattern no longer catches a longer word that happens to contain it. Transactions that were auto-filed only through such a match, and that the rules no longer file the same way, went back to Uncategorized for triage, and Memory labels that change. New default rules recognize ExxonMobil, NYTimes and Steam game purchases.
    • One category picker on every category field: the transaction detail panel, the add form, bulk edit, account detail, receipt items and import staging. Suggested and Recent come first, search narrows the list, and Create adds a category without leaving the form. A category pill in the transaction list opens the picker in place.
    • Changing one transaction's category now offers the rest of that merchant's rows: the toast has File N similar, or Review to pick them from a checklist. Rows you set by hand, receipt rows and hidden rows are left alone. Clearing a category also clears its Auto badge.
    • Learned rules match on a cleaned merchant key, with store numbers, reference ids and dates removed, so a rule learned from one month's payment still catches the next month's when the reference changes. Existing learned rules moved to the new key where the original bank text was on record.
    • A date range picker on Transactions and Memory: six one-click presets, a month grid with quarters and whole years, Last N days, weeks, months, quarters or years, and On, Since, Until and Between. Existing date links keep working. The Transactions overview totals now follow the Date filter; they used to show all time whatever it said.
    • Memory shows category and account names instead of internal ids, and rows without a merchant show the cleaned bank text.
    • Credit card payoffs pair up. A checking or savings payment and the matching credit on the card (same amount, card side up to 2 days before or 5 days after) are linked as a card payment and both move to Transfer when the card's last four digits or payment wording on either side back the match, so the payoff stops counting as spending and income; unlinking restores them. A match on the amount alone, or a card payment the statement stored as a charge, waits under Transfers to review until you confirm it. A row with more than one possible partner and no card number to tell them apart is never linked on its own, rows you had hidden can still pair, and a payoff credit with no partner is filed as Transfer.
    • A Pair transfers button on Transactions, Accounts and Import (it replaces Scan for transfers) runs the matching on demand and asks you to choose when a row has more than one possible partner. Imports and bank syncs say how many card payments they paired, and a linked row reads Card payment, linked to the other account and its date.
  5. Export or delete your account yourself, one pricing table, and bank sync on Pro

    • Settings > Help & data: send a support request, export your data, or delete your account without emailing anyone. Export my data downloads a zip with one JSON file per table, a transactions.csv and a manifest of row counts. Stored files are listed by key rather than included, secrets such as a saved Anthropic key are left out, and you can export once every 10 minutes.
    • Delete account asks you to type your email. A Pro subscription is canceled at the end of the paid period first, and if that call to Stripe fails, nothing is deleted. Then every row your account owns and its sign-ins are deleted in one step, and every file stored under your account (PDF statements, receipt images, your profile picture, feedback screenshots) is purged. Zedi keeps a short deletion record with your plan and row counts but no email or name, and Stripe keeps its own customer record.
    • Pricing is one Free, BYOK and Pro table: each feature sits in exactly one tier, BYOK and Pro start from Everything in Free, and nothing is listed as coming. Settings > Billing lists the same three plans with yours marked.
    • Bank sync is part of Pro wherever billing is set up. On Free, connecting a bank or starting a sync shows an Upgrade to Pro notice, while viewing, renaming, scheduling and removing an existing connection stay open, and scheduled syncs wait until the account upgrades. Bank sync itself is still development only.
    • Billing: after checkout Stripe sends you back to Billing, which waits for the upgrade to land and then says You're on Pro; a canceled checkout says nothing was charged. A subscription update from Stripe that arrives before checkout has finished no longer leaves a paying account on Free.
    • Security hardening: the restricted database role that serves your requests now gets its table permissions restated from one exact list every time the server starts, so they cannot drift wider than the app needs.
  6. A new look, profile pictures, scheduled bank sync, and totals that leave transfers out

    • Appearance: 15 new themes named for Zedi's own world, from Paper and Ledger to Banker's Lamp and OLED, replace the old set, and a retired theme moves to its successor before the page paints. Light, Dark and Auto, where Auto follows your system between your two picks, plus six typeface sets, an interface size and a corner radius, all under Settings > Appearance. Fonts are served by Zedi itself, so no request goes to Google Fonts.
    • One header bar on wide screens, with the navigation in the middle and your account on the right, and a settings rail listing the pages of the section you are in.
    • Profile pictures: upload a photo and frame it in the circle before it is sent, or use your Google or Apple photo, an emoji or your initials on a colour. Double-click the account circle to see it large.
    • Bank sync (SimpleFIN, still development only): connecting is a four-step guide that opens SimpleFIN Bridge in a popup, waits for you to come back with the setup token, then maps accounts and a schedule. Syncs run daily, twice a week, every other week or monthly at an hour you pick in your own timezone, and the card says when the next one is.
    • Your timezone is read from the browser once if none is saved, and can be changed on the Settings profile page. Bank feeds can be renamed, and account names that arrived garbled from the bank are cleaned, including Zedi accounts already created from them.
    • Onboarding leads with Connect your bank, next to uploading a statement, demo data or starting blank. Leaving the bank step part way brings it back at the next sign-in, at the point you left it.
    • Money moving between your own accounts, once it is linked as a transfer or filed under Transfer, no longer counts as income or spending. One rule now decides what counts toward totals (not hidden, not a transaction whose receipt items carry the money, not in a transfer category, not the transfer side of a linked pair), and the dashboard, Analytics and the Transactions sums all use it, so a linked transfer you had categorized by hand stops showing up on both sides. The Transactions sums say how many filtered rows they leave out.
    • Spending composition on Analytics is worked out on the server, so a long history is no longer cut off. Its merchant view names merchants from the bank text instead of piling them into Unknown merchant, and a failed load says so instead of showing an empty chart.
    • Long bank descriptions stay inside their column on Transactions and in import staging: rows show the merchant name with the raw text under it, and staging folds the raw description away until you open it. The Income vs expenses tooltip no longer covers the bars.
  7. People, round two: balances both ways, Zelle, merges and cash payments

    • Balances run both ways. A tagged transaction has a Counts as choice: They owe me this, I owe them this, Settles a balance or Not counted. Each direction settles oldest first on its own, and the two net to one balance per person.
    • Record a payment or a debt that never touched a statement, such as cash or calling it square, from the person page. It settles in the same oldest-first order and shows on the Activity timeline, where it can be removed.
    • Merge a duplicate person into another from the person page. The preview shows both balances and the balance after merging, to the cent; transactions, receipt shares, handles and the name move to the person you keep, and the duplicate is archived. A person can be renamed on their page too, and a name that is already taken offers Merge instead.
    • Transactions show the tagged person and link to them. The detail panel sets the person and Counts as, the list filters and sorts by person, and the bulk bar tags or clears a person on many rows at once. Tagging a row by hand remembers the Venmo or Zelle name on it for the next statement.
    • Zelle: a Zelle line in a bank statement, whether CSV, QFX, PDF or synced, is matched to an existing person by name or waits in the review tray, and resolving it remembers the name. A Zelle line never creates a person on its own.
    • Venmo: a row is named after the other party instead of the payment note, and a new name on a Venmo CSV becomes a person during the import rather than waiting in the review tray. Rows imported before the change were updated to match.
    • The person page and the Add person dialog offer all six handle kinds, from Venmo username and Venmo display name to Cash App and PayPal, each saying whether imports match on it. Adding someone while splitting a receipt can take their Venmo username, and a person without one gets a note on how to add it so Request via Venmo works.
    • The dashboard's Owed to you card and the People tab always show the same number, and onboarding and the empty People tab explain what People is for.
  8. Bank sync grows up, use-case pages, and a partners page

    • Bank sync (SimpleFIN, still development only): a connection can sync itself once a day after 10:00 UTC, switched on per connection from Settings > Connections & keys. The card shows the last manual and automatic sync, pending charges the bank has not posted yet, a history of recent syncs, and a one-click Categorize button for synced rows the rules missed. Reconnecting or remapping a bank account no longer re-reads up to 89 days, and a statement uploaded for a period already synced is caught as a duplicate even though the descriptions differ.
    • New users who can use bank sync get a Connect your bank step during onboarding, the import page points them to Connections & keys, and the Accounts page marks synced accounts with their last sync time.
    • Use-case pages under /for, one each for splitting receipts with friends, Venmo and Zelle reconciliation, statement import without a bank login, itemized grocery tracking and PDF statement import. A /partners page explains how ?ref= credit works and what Zedi will never do with your data.
    • Landing page: Venmo CSV import is listed as shipped instead of coming soon, and the Pro card no longer promises priority support. Nothing about pricing changed.
    • Admins can see signups, first imports and Pro conversions per referral tag and per source for the last 30 or 90 days.
  9. Duplicate detection across formats and merchant renames everywhere

    • A transaction imported once from a PDF statement and again from a CSV or QFX export is now caught as a duplicate. Whole-dollar amounts from PDFs used to be compared as 400 against 400.00, so the second import slipped through; stored rows were rebuilt so earlier imports match too.
    • Merchant renames you told Zedi to remember now apply to every import, including PDF statements that commit automatically, not only files reviewed in the staging station. A rename you make while staging still wins, and resetting a merchant keeps the name from the statement.
    • Removing the sample data now removes the demo accounts as well, and only ever touches your own rows.
    • People: adding a person saves their nickname, emoji, notes and handles in one step, so a failure partway never leaves a half-made person. The Owed to you card on the dashboard shows each person's nickname and emoji avatar.
    • Signup links shared with a ?ref= tag are credited even when they point at the landing page or the changelog instead of the signup page.
  10. Production update: the import overhaul and a week of fixes

    • Production now runs everything in the 2026-09-06 and 2026-09-07 entries except investments, which stays in development: background PDF extraction, People and the Venmo review tray, sign-in connections, and email verification for new signups.
    • A failed PDF extraction can be retried from the archived PDF, and failed statements keep their PDF for 14 days.
    • Password-protected PDFs are rejected before they are sent for extraction, and a PDF batch that needs more quota than you have left fails right away instead of partway through.
    • Categorization rules normalize their patterns when saved so they match, and characters like % and _ in a pattern no longer act as wildcards.
    • The receipt upload dialog shows your remaining scans, and there is a feedback button inside the app.
    • Signup asks how you heard about Zedi.
    • The landing page says which Claude model reads your uploads and what it is sent. The terms cap liability at the fees paid in the prior 12 months.
    • On phones the dashboard summary cards stack as rows so values never truncate, and the analytics donut shows readable merchant names.
    • Forward-by-email ingest checks sender authentication (SPF, DKIM, DMARC) and is rate limited. It stays hidden until the inbound worker is deployed.
  11. Mobile menu, this page, and small fixes

    • The marketing site has a mobile menu. Below the desktop breakpoint the section links moved into a slide-out sheet behind a hamburger, so phone visitors can reach Product, Compare, Pricing and FAQ.
    • This changelog. Entries live in the repo next to the code, and this page is where dev updates get posted from now on.
    • Import: when a PDF extraction succeeds but the automatic commit fails, the extracted preview is kept so you can review and commit it by hand instead of losing the work.
    • People: the person detail page links back to the People list.
    • Landing page copy corrected: forward-by-email gives you one inbound address for your whole account, not one per bank.
    • CI now runs on every push and pull request to the dev branch, and the Docker build contexts got a .dockerignore.
  12. Import overhaul: PDF extraction runs in the background

    • PDF statement extraction is a background job. Upload returns right away and the import page polls the statement until the preview is ready, so a long statement no longer times out behind the web proxy. The copy no longer promises a 30 second extraction.
    • Stuck and abandoned statements are swept automatically: an extraction that never finished is marked failed, and a preview nobody committed expires.
    • QFX rows now carry the same fingerprint as CSV rows, so a transaction imported once as CSV and again as QFX is deduplicated across formats.
    • Reconciliation counts interest lines, and the extraction request keeps its schema key order, which cut extraction latency by a wide margin.
    • The import page was split into smaller components: the PDF panel with polling, the statements table, and the import history table.
  13. Every feature branch merged onto dev

    • Investments, in development and not yet in production: Schwab brokerage CSV exports (activity history and positions snapshots) import into a dedicated investments domain instead of the transactions table, with a new Investments page showing holdings, activity, allocation and value over time. The net position card buckets investment balances separately from cash.
    • People: Venmo and Zelle counterparties are tagged during CSV import, each person shows a net balance, unmatched names land in a review tray on the import page, and receipt splits get a Request via Venmo button. Venmo exports are detected by header and there is a Venmo account type.
    • Forward-by-email ingest, prototype: a private inbound address per account, an approved-sender list, and an activity log under Settings, Connections. The inbound worker is not deployed yet, and the card hides itself until an ingest domain is configured.
    • Settings, Connections: connect and disconnect Google and Apple sign-in on an existing account.
    • New signups must verify their email address before reaching the dashboard, with a dedicated page explaining the wait.
    • Theme: an info status color joins success, warning and danger across all 14 themes, and the import page stopped using raw palette classes.
    • A Yahoo Finance client and an internal-secret middleware are in place for the upcoming daily price refresh; the refresh route itself is still being rebuilt on the investments tables. Nothing prices holdings yet.
  14. Hotfix: intermittent 500s on ordinary requests

    • Fixed bursts of 500 errors that could hit any signed-in request, including PDF imports that failed with "internal server" and no log line. The backend fetched the signing keys for session tokens once per concurrent request when its cache expired and tripped the auth rate limit. Refreshes are now single-flight, a failed refresh serves the last good key set for 15 seconds, and the keys endpoint gets the same rate-limit rule as session checks.
    • Deleting an account that had PDF statements no longer fails: the migration that cascades import runs on statement delete existed on disk but was never registered with the migration runner. It is now.
    • The receipt library is paginated, 20 receipts per page, with the total counted under the same filters as the page.
    • Receipts: the store name input no longer stretches the card while editing.
    • Shipped to production the same evening.
  15. Filter-aware overview and analytics filters

    • Transactions: the Overview tiles follow whatever filter is active (category, search, type, amount, hidden, review flags) and are labeled Filtered when they do.
    • Analytics: a category filter including Uncategorized, plus include and exclude modes on both the category and account dimensions, applied to the KPIs, spending mix, trends, merchants and the pivot card. Choices persist locally and mirror into the mobile filter sheet.
    • Analytics: the spending donut labels its slices and wraps its legend instead of clipping it.
    • Import: the PDF statements table shows which account each statement targets.
  16. Rendering sweep

    • Dashboard KPI sparkline tooltips are no longer clipped by their card.
    • The Uncategorized filter on transactions works again.
    • Assorted rendering fixes across the signed-in app.
  17. Email verification, durable Stripe webhooks, rate limits

    • Email verification is enforced for new accounts.
    • Stripe webhooks are recorded before they are processed, so a failure during processing can be retried instead of losing a subscription change.
    • Auth rate limiting works as configured, and the API has its own rate limits.
    • Error boundaries around the signed-in app, and database pool sizing tuned for the hosted environment.