Expense tracker, validator and analyser for Indian credit card statements. Unlocks password-protected statement PDFs, validates them against the printed totals, categorises every transaction, and anal
翻译准备中
金融TypeScript · CSSGitHub @AnkitKumarMishra5在线运营者未确认
试用过了吗? 点一下就好,无需登录。
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Expense tracker, validator and analyser for Indian credit card statements. Derives each PDF password from the cardholder profile, reconciles parsed totals against the figures the bank printed, and turns every card's statements into spend analytics. Next.js 16, Postgres, AES-256-GCM per-account encryption.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Your statements, reconciled. Every month.
Expense tracker, validator and analyser for Indian credit card statements.
Banks email a password-protected PDF every month. Outlay derives that password from the cardholder profile, opens the file in memory, extracts the transactions, reconciles the computed totals against the totals printed on the statement, flags fee and anomaly lines, and turns the result into spend analytics across every card.
Scope is narrow on purpose: credit card statements only. Not budgeting, not net worth, not account aggregation.
The dashboard. Pick a statement month and every card lines up by what it still owes, settled bills carry a stamp, and the headline figures show how the total due is made up.
AI that shows its work. Saving a batch sends only merchant names to the model, never amounts or card details, and every card in the batch is scanned in turn while the categories are refined.
What changed. Each category with its last six months on its own scale, this month's spend and the move from last month.
Over time. Billed spend by month, the subscriptions that repeat, and where the money went month by month. Pick a category to see it on its own scale.
The calendar. Daily spend and every bill due on one grid. Amber is due, green is settled, and a ring marks a date that is only partly settled.
A statement, verified. Twelve checks run before anything is saved, and the result is stored with the statement. Fees get flagged rather than buried. Every category is a dropdown, so a wrong guess takes one click to fix.
Every transaction, one page. Filter by statement month, card, statement, category or text, and correct a category inline. Or have AI re-sort a statement month, or the whole history, and review every change before it lands.
Statements. Grouped by statement month, each one stamped settled or showing what is still due.
Upload. A month of PDFs at once. Each one is unlocked, read and matched to a card before anything is written.
Settings. The name and date of birth that unlock statements, any password patterns you teach it, and your cards, all encrypted.
The landing page runs this whole dashboard on invented cards and invented spend, so you can try it before signing up.
For anyone using the hosted instance. No install, nothing to run.
Sessions lock after fifteen idle minutes. Settings holds a full JSON export and a wipe gated on a typed confirmation phrase.
| Area | Capability |
|---|---|
| Unlock | Fourteen password patterns derived from the cardholder profile, plus templates learned from any password you type once |
| Extract | Deterministic heuristic parser, no network, same result every time |
| Verify | Twelve checks per statement, including computed totals against the totals the bank printed |
| Correct | Edit an amount, delete a mis-read row, change a category. Totals and checks recalculate on every edit |
| Categorise | Rule-based on the merchant string, editable everywhere, with an optional AI review capped at two runs per card per month |
| Recurring | Same merchant, same amount, steady cycle, detected across statements and costed per year |
| Settle | Per-statement due, overdue and settled status, with the dashboard counting the current cycle |
| Analyse | Spend by card, category and month across five ranges, plus a spend-and-dues calendar |
| Browse | Every transaction across every card on one page, filtered by card, statement, category, type or text |
| Export | Full JSON export, and a wipe gated on a typed confirmation phrase |
PDF (browser)
-> proxy: session check, same-origin check
-> /api/statements/parse
unlock pdf.js, candidate passwords, in memory only
extract heuristic parser, no network
validate 12 checks, including totals vs the printed figures
-> review in the UI, nothing persisted yet
-> POST /api/statements
transaction rows + statement metadata + the check record
-> Postgres
The uploaded file exists only for the duration of the parse request. Nothing is written to disk, and only rows you approve are persisted.
There is no password vault to seed. Statement passwords are derived at upload time from the cardholder name, date of birth and optional last four digits, tried in an order the issuer registry specifies.
| Pattern | Candidate for Priya Nair, 15/03/1992, card 4321 | Issuers that prefer it |
|---|---|---|
NAME4+DDMM | PRIY1503 | HDFC, Axis, IDFC, IndusInd, YES, RBL |
name4+ddmm | priy1503 | ICICI |
DDMMYYYY | 15031992 | SBI Card, HSBC |
NAME4+DDMMYYYY | PRIY15031992 | fallback |
last4+DDMM | 43211503 | card-number schemes |
Nine further permutations are tried after the preferred ones, deduplicated. The candidate that opens the file is encrypted and stored against that card, so subsequent months open on the first attempt. Issuers using a CRN or another scheme fall back to a single manual entry, remembered the same way.
If none of the built-in combinations fits, the password you type once is decomposed against your own name, date of birth and card digits, and the resulting template is saved to your profile and tried first from then on. A statement locked with USER1503 becomes {SURN4U}{DD}{MM}, so the next card at that issuer opens without being asked. Settings also has a builder that assembles the same templates from plain-English pieces with a live preview.
Candidates never leave the process. They are not logged, not returned in any response, and not written anywhere in plaintext.
The upload screen takes any number of PDFs at once. Each file is read independently, so one failure never blocks the rest, and every file lands in one of five states before anything is written:
| State | Meaning |
|---|---|
| Ready | Parsed, and the card was identified from the statement |
| Card needed | Parsed, but no card in the wallet matches. Offers to create it from what was read |
| Password needed | Protected, and no derived candidate opened it. Takes a password inline |
| Not a statement | Too few statement markers and no transactions, so it is rejected |
| Failed | Unreadable file |
Card matching requires an exact last-four match when the statement prints readable digits, so a second card at the same issuer is never silently assigned. Only when no digits can be read, and the issuer has exactly one card, is that card assumed. Nothing is persisted until you press save, and mappings stay editable until then.
The endpoint takes one file and returns one result, so an automated fetcher can post to it exactly as the browser does.
Extraction is deterministic. heuristicParse reads the common DD/MM/YYYY DESCRIPTION 1,234.56 [Cr] layout with no network access, and the twelve checks below are what prove it read the file correctly. No model is involved in getting numbers out of a statement, so an upload costs nothing and behaves the same every time.
The model is used for one thing: rereading merchant names when the category rules get them wrong. Rules match on substrings, so a merchant the registry has never seen lands in "Other", and an occasional one lands in the wrong bucket. That is a language problem, which is what a model is actually good at.
It is metered because it is the only thing that costs money. Each card gets two reviews per calendar month, counted in an ai_usage row. The review is taken before the model is called and handed straight back if nothing usable comes of it, so you only spend one on an answer you actually got. Only the merchant descriptions are sent, redacted first, never amounts or dates. With no key configured the feature is visibly disabled and the app makes no outbound request at all.
On upload, the whole batch goes to the model in one call, taking one review off each card involved. A panel then shows what changed, each pick editable, and none of it is a required step. The same engine sits behind the recheck button on a statement page.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/auth/register | public | create an account, invite code required |
| POST | /api/auth/login | public | email and password exchange for a session |
| POST | /api/auth/session | session | slide the idle window forward |
| POST | /api/auth/logout | public | clear the session |
| GET, POST | /api/profile | session | encrypted cardholder profile |
| GET, POST | /api/cards | session | card registry |
| PATCH, DELETE | /api/cards/[id] | session | rename, or remove a card and its statements |
| POST | /api/statements/parse | session | unlock, extract, validate, return for review |
| GET, POST | /api/statements | session | list, persist a reviewed statement |
| GET, PATCH, DELETE | /api/statements/[id] | session | statement detail, settled status, delete |
| POST | /api/statements/recategorize | session | model re-reads every category across a batch in one call, one review off each card involved |
| POST | /api/statements/[id]/recategorize | session | the same for a single statement |
| GET | /api/transactions | session | every row, filtered by card, statement, category, type or text |
| PATCH, DELETE | /api/transactions/[id] | session | correct or remove a row, then re-run the statement checks |
| GET | /api/analytics | session | aggregates by range and card |
| GET | /api/export | session | full JSON export, secrets excluded |
Five tables. profile holds the encrypted name and date of birth, one row. cards holds issuer, label, optional last four digits and the encrypted statement password. statements holds the period, the printed totals, and the serialised check record. transactions holds date, description, amount, type, category and the fee and international flags, indexed on date and card. ai_usage holds one row per account, card and month, and is the whole of the model budget.
Storage is Postgres over pg, one pool per process. The schema is created on first connection inside a transaction held under an advisory lock, so concurrent cold starts cannot race each other into a duplicate table. CREATE TABLE IF NOT EXISTS is a no-op once a table exists, so columns added later are declared separately as ADD COLUMN IF NOT EXISTS and applied in the same transaction. An existing database upgrades itself on the next connection without losing rows.
Needs a Postgres database. A free Neon database takes a minute to create and works locally and in production alike:
postgresql://user:[email protected]/neondb?sslmode=require.Any other hosted or locally installed Postgres works too. Connections to anything other than localhost use TLS automatically.
npm install
npm run keygen # writes APP_ENCRYPTION_KEY and SIGNUP_INVITE_CODE into .env.local, prints the invite code
Add the connection string to .env.local:
DATABASE_URL=postgresql://user:[email protected]/neondb?sslmode=require
npm run dev -- --port 3020
The schema is created on first connection, so there is nothing to migrate. Create an account with the invite code the keygen printed, then enter the cardholder name and date of birth exactly as they appear on the statement. Those two values derive the passwords.
Use a separate Neon database, or a separate branch of the same project, for local work. The local APP_ENCRYPTION_KEY is different from the deployed one, so rows written by one deployment cannot be decrypted by the other.
Free tier end to end: Neon for the database, Vercel for the app.
Generate the two secrets, printed and not written to any file:
node -e "const c=require('node:crypto');console.log('APP_ENCRYPTION_KEY='+c.randomBytes(32).toString('hex'));console.log('SIGNUP_INVITE_CODE='+c.randomBytes(9).toString('base64url'))"
Set three variables in Vercel, for Production and Preview:
| Variable | Value |
|---|---|
DATABASE_URL | the pooled Neon connection string |
APP_ENCRYPTION_KEY | the 64 hex characters printed above |
SIGNUP_INVITE_CODE | the code printed above, which is what you give people |
The app returns 503 on every route until all three are present, so a half-configured deployment never serves a login form.
APP_ENCRYPTION_KEY is not rotatable. Every name, date of birth, card digit and statement password is sealed under a key derived from it, so changing it after data exists makes that data permanently unreadable. Use a different key from your local one, and keep a copy somewhere safe.
OPENAI_API_KEY is optional and the only variable that ever costs money.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router), TypeScript, Tailwind CSS v4 |
| Database | Postgres over pg, no ORM |
| Auth | scrypt password hashing, HMAC-signed session cookie, no auth provider |
| Encryption | AES-256-GCM, per-account keys derived from one master key |
pdf.js (pdfjs-dist/legacy), unlocked and read in memory | |
| Charts | Recharts |
| Motion | Motion (motion.dev) for spring physics and layout animation, plus CSS keyframes. A card under a reading beam while the model works; the card lands and the balance runs down to nothing when a bill clears |
| Sound | Sixteen cues synthesised with the Web Audio API, no audio files: a bell chord on settle, a riffle when the deck reorders, friction when a card slides, a scan sweep around AI review. Levels were set by rendering each cue offline and measuring, so they land between -5 and -12 dBFS through a limiter. One toggle in the nav |
| AI | OpenAI gpt-4o-mini for category review only, optional, capped at two runs per card per month |
| Hosting | Vercel and Neon, both free tier |
No analytics, no telemetry, no third-party fonts or scripts. With OPENAI_API_KEY unset the app makes zero outbound requests.
Card faces carry no issuer logo and no payment-network mark. Those are trademarks and cannot ship under MIT. Each issuer is identified by a monogram and its own colours, and the physical realism comes from the published standards instead: ISO/IEC 7810 ID-1 geometry, ISO/IEC 7816-2 chip module proportions, and ISO/IEC 7811 magnetic stripe placement and embossing.
Ankit Kumar Mishra. Designed and built Outlay, end to end.
MIT. See LICENSE.
Issuers are rendered as colour monograms rather than logos. Naming a bank to identify a statement is nominative use; redistributing trademarked artwork under MIT is not.
| POST | /api/reset | session | wipe, gated on a confirmation phrase |