尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
A single-user personal finance tracker for New Zealand (NZD, Pacific/Auckland).
It pulls ANZ + American Express data from Akahu, categorises it with your
own rules, tracks monthly budgets, and has a Claude-powered chat that
updates the app from plain English.
It runs with zero keys. Without keys you get realistic mock ANZ + Amex data,
a local JSON data store and an offline chat that uses the same tools. Add keys
one at a time to switch each piece to live.檢視完整 README ↗A single-user personal finance tracker for New Zealand (NZD, Pacific/Auckland). It pulls ANZ + American Express data from Akahu, categorises it with your own rules, tracks monthly budgets, and has a Claude-powered chat that updates the app from plain English.
It runs with zero keys. Without keys you get realistic mock ANZ + Amex data, a local JSON data store and an offline chat that uses the same tools. Add keys one at a time to switch each piece to live.
| Piece | No key (default) | With key |
|---|---|---|
| Bank data | Mock ANZ everyday/savings + Amex (13 months, deterministic) | Akahu API |
| Database/auth | .data/db.json, no login | Supabase Postgres + magic link, RLS on every table |
| Chat | Offline pattern-matcher (handles the example phrases) | Claude (claude-sonnet-5) with tool use |
npm install
npm run dev # http://localhost:3000 → "Run first sync"
npm test # mock-data suite + features + Claude loop (fake API) + migrations in PGlite
npm run test:race # optional: concurrency test against a real Postgres (PGHOST/PGPORT/PGUSER)
npm run build # production build + type check
npm run check-env # shows which integrations are live vs mock
npm run gen:seed # regenerate the seed migration after editing lib/seed.ts
app/ Next.js App Router (mobile-first UI + route handlers)
page.tsx Dashboard: budgets, totals, days left, top merchants, 6-month trend, pending, accounts
transactions/ Search + filters (account/category/date) + inline recategorise + add cash
inbox/ Uncategorised triage (grouped by merchant, one-tap, guesses)
budgets/ rules/ Category/subcategory CRUD, budgets, overall cap, rules
chat/ Chat UI (history persisted)
api/… JSON endpoints (all server-side; tokens never reach the browser)
api/cron/sync Daily Vercel cron (Bearer CRON_SECRET)
proxy.ts Refreshes Supabase session, redirects to /login (Next 16 "proxy" = middleware)
lib/
env.ts Every env check lives here (mock vs live decisions)
store/ Store interface → SupabaseStore (RLS) | LocalStore (JSON, demo)
akahu/ LiveAkahuClient (cursor pagination, retries) | MockAkahuClient
sync.ts Incremental sync, upsert on Akahu _id, pending replacement, transfer pairing
categorise.ts Rules engine, transfer detection, Akahu hint mapping, merchant memory
services.ts Budgets, categories, rules, spending analytics (shared by UI + chat)
reimburse.ts "Net off": link incoming money to expenses; net amounts for all totals
chat/tools.ts 10 Claude tools (zod-validated) + server-enforced confirmation tokens
chat/agent.ts Claude tool-use loop + per-turn app context
chat/mock.ts Offline planner used when ANTHROPIC_API_KEY is missing
supabase/migrations/ Schema + RLS + default-category seed trigger
scripts/test-*.ts End-to-end tests against mock data (incl. a fake Claude API server)
Data model: accounts, transactions, pending_transactions, categories
(with parent_id for subcategories), rules, budgets, settings (overall
cap, pay cycle, savings goal), chat_messages, sync_log, reimbursement_links (Net off),
ious, netoff_suggestions, trips, trip_transactions, subscription_prefs,
weekly_caps, weekly_recaps, and app_owner (single-owner guard). Every table has user_id + RLS policies
user_id = auth.uid() (enabled, not forced).
Everything is fetched and validated before anything is written, so a failed or partial sync never changes existing data. Akahu 429/5xx/network errors back off (Retry-After honoured, 5 attempts); token errors show a clear message on Settings → Bank sync.
GET /accounts); accounts Akahu stops returning are flagged
missing (history kept).BACKFILL_MONTHS months (default 3,
1–24 allowed); mock mode always generates 12 months. Later syncs start 7 days
before the newest stored transaction (overlap catches late-settling items).
A full resync (POST /api/sync?full=1) uses the same backfill window.GET /transactions?start&end, following cursor.next until exhausted._id (unique) → re-running never duplicates.GET /transactions/pending replaces the pending table wholesale (pending items have no stable id; they reappear with an _id once settled).removed_at (excluded,
never deleted; restored if they reappear). Amounts the bank edits are updated,
and any net-off link that no longer fits is removed.sync_log row records the result + warnings. Triggered by Sync now or the daily cron.supabase/migrations/20261002000000_merchant_logos.sql). Rows also show the category icon and ✨ Auto when categorised automatically. Tap a row to change its category, net off or add an IOU.Tap any category bar on the home page to open its breakdown for the same period (this month or pay cycle). It shows the total, "Where it went" by merchant with each one's share, and subcategories if you use them. Below that are the transactions themselves, which you can recategorise, net off or IOU right there. Tap a merchant to show only its transactions. The total always matches the bar: net offs count at their net amount, refunds reduce it, and trips kept separate are left out.
The inbox groups transactions by merchant (money in and money out separately), biggest groups first, e.g. "Sushi Sama ×33".
CLAUDE_MODEL) guesses a category for any new merchant group and pre-fills it with a confidence level. Tap Save all to apply them, or not this to drop one for good. Nothing is applied until you tap. ✨ Guess categories / Re-guess asks again for everything not dismissed.
category_guesses (run supabase/migrations/20261001000000_category_guesses.sql), so each merchant is only sent once.Your manual choice › your rules › own-account transfer detection › merchant memory › Akahu enrichment hint › obvious words in a card charge › Uncategorised.
Card purchases (EFTPOS, credit card, or a debit charge showing the masked card number) with words like coffee/cafe/bakery go to Eating Out, pizza/burger/kebab to Takeaways, and pub/tavern/brewery to Bars. Transfers and payments to or from people are never guessed this way, because their references are often inaccurate. Bank transfer lines like To: 06-0998-0835107-03 Debit Transfer 112621 / From: … Credit Transfer … go to Transfers, and own account numbers match whether the suffix is written -03 or -003. Each sync also re-checks older Uncategorised rows, so improvements apply to your history. "Apply to all" rules drop per-transaction ids, so FLIGHTNETWRK1133124432 ONLINE makes the rule flightnetwrk.
Expense: Groceries, Eating Out, Takeaways, Bars, Liquor Stores, Sports, Travel, Entertainment, Health & Wellness, Home Supplies, Rent, Transport/Fuel, Subscriptions, Clothes/Shopping, Bills, Insurance, Other. Income: Salary. Transfer: Transfers. Savings: Savings.
71 starter rules (contains-match) cover common NZ merchants — see DEFAULT_RULES
in lib/seed.ts, the single source of truth. More specific patterns get a lower
priority number so they win (uber eats → Takeaways beats uber → Transport/Fuel).
After editing the list run npx tsx --conditions=react-server scripts/gen-seed-sql.ts
to regenerate the seed migration (the tests fail if the two drift apart).
Akahu hints map pubs/bars → Bars and fast food/takeaway → Takeaways.
Money you put into Sharesies or Feijoa is tagged Savings (a starter rule for each, ahead of every other rule). Savings is never counted as spending or against budgets. The dashboard shows Saved this month (or this pay cycle): payments in minus withdrawals back out, with an optional monthly savings goal (set it on Budgets, or tell the chat "savings goal 500 a month"; weekly/fortnightly goals are converted to monthly, and the pay-cycle view pro-rates it). ANZ everyday ↔ savings sweeps and Amex repayments stay Transfers. To count another destination as savings, add a rule to the Savings category.
When a mate pays you back, tap Net off on the expense, search incoming money (name, amount, date — newest first) and link all or part of a payment. One payment can be split across several expenses and one expense can take several payments. The expense then counts at its net amount everywhere (category spend, budgets, trend, chat answers): $365 at Snus Direct − $100 from Sam = $265. The linked part of the incoming money is excluded from Salary/income so it isn't counted twice; any remainder still counts. You can unlink at any time. Links can't exceed the expense or the incoming amount (checked in the app and by a database trigger).
An incoming payment that's fully linked needs no category of its own: it shows " · netted off" (or just "Netted off" if the expense is uncategorised), leaves the Uncategorised inbox and isn't counted as uncategorised anywhere. The badge follows the expense if you recategorise it. A partially linked payment stays in the inbox with "$X unallocated" beside the dropdown — only that remainder needs a category. Unlinking returns the payment to Uncategorised, or to the category it had before. This is worked out from the links themselves, so it applies the same way whether you link in the app or ask the chat. Link labels use a cleaned-up bank description (e.g. "Everyday/Kak" instead of the full USD conversion text).
Tools: create_category, update_category, delete_category, set_budget,
create_rule, recategorise_transactions, add_manual_transaction,
query_spending, get_budget_status, link_reimbursement, create_iou,
list_ious, cancel_iou, create_trip, trip_status, set_weekly_cap,
weekly_status, list_subscriptions, set_savings_goal.
query_spending / get_budget_status (real DB numbers).confirmation_token. The change only
runs when a later request carries a token issued in a previous turn — i.e. after you
replied. The model can't self-confirm.supabase/migrations/ in order:
20260924000000_init.sql, 20260924000100_seed_defaults.sql,
20260925000000_reimbursement_links.sql, 20260926000000_features.sql,
20260930000000_savings.sql, 20261001000000_category_guesses.sql,
20261002000000_merchant_logos.sql.
(Already set up? Just run any you haven't: the last two are safe to re-run.
The app adds the Savings category and its rules to existing data on next
load. Until category_guesses exists, the inbox works but guesses aren't
saved, and sync shows a warning.)
(Or with the CLI: supabase link --project-ref <ref> && supabase db push.)insert into public.app_owner (email) values ('[email protected]');
From then on the database refuses to create any other user, even if someone
calls Supabase's sign-up API directly with your public anon key.http://localhost:3000/auth/callback and https://<your-app>.vercel.app/auth/callback
to Redirect URLs.OWNER_EMAIL on every route, the proxy and the cron, and the database
trigger above blocks other accounts.Default categories + starter rules are seeded automatically on first sign-in.
app_token_…) → AKAHU_APP_TOKEN
and the User Access Token (user_token_…) → AKAHU_USER_TOKEN.lib/env.ts, lib/akahu/live.ts) and are
never sent to the browser. Live mode also requires Supabase, so real bank data
is always behind login.Create a key at https://console.anthropic.com → ANTHROPIC_API_KEY.
Copy .env.example → .env.local and fill in. See the file for every variable.
Optional: BACKFILL_MONTHS (default 3) sets how much history the first live sync
pulls. Run npm run check-env to confirm what's live.
.env.example under
Settings → Environment Variables (Production + Preview).vercel.json schedules GET /api/cron/sync at 17:00 UTC daily (5am NZST / 6am NZDT).
Set CRON_SECRET in Vercel; Vercel automatically sends it as
Authorization: Bearer <CRON_SECRET>. The cron needs SUPABASE_SERVICE_ROLE_KEY
and OWNER_EMAIL to find your user. Test manually:
curl -H "Authorization: Bearer $CRON_SECRET" https://<your-app>.vercel.app/api/cron/sync
(Hobby plans allow one run per day, which is what this uses.)
Add the Akahu tokens (with Supabase configured) and redeploy / restart. On the
first live sync the app automatically deletes the mock accounts and
transactions (ids prefixed acc_mock_ / trans_mock_) and backfills
BACKFILL_MONTHS months of real data (default 3). To start with more history, set
e.g. BACKFILL_MONTHS=12 before that first live sync; later syncs are
incremental, so changing it afterwards only affects a full resync. Your categories, rules, budgets and chat history are kept.
Set AKAHU_MODE=mock to force mock data even with tokens present.
Some features need history to kick in: subscription detection needs 3 charges (2 for yearly), pay-cycle detection needs 3 paydays, and the 6-month trend chart fills in as data arrives. With the default 3 months, yearly subscriptions and the older trend months won't appear until more history accumulates.
Open the site in Safari (iOS) → Share → Add to Home Screen, or in Chrome (Android) → Install app.
Nearly all the time a screen takes is network round trips between Vercel and Supabase (each page makes 6–20 small queries; processing is ~25 ms). So:
vercel.json pins functions to syd1 (Sydney). Your
Supabase project should be in Sydney too (Supabase → Project Settings →
General → Region). If it isn't, change regions in vercel.json to the
Vercel region nearest to it. Across the Pacific each query costs ~0.2 s;
in the same region it's a few milliseconds.getClaims()
instead of calling Supabase Auth on every request. For this to skip the
network entirely, turn on asymmetric JWT signing keys (Supabase → Project
Settings → JWT Keys → migrate to signing keys). Without them it still works
but asks Supabase Auth each time.OWNER_EMAIL; any other email is refused. OWNER_EMAIL is required once Supabase is configured.tower ⟂ sky, farmers ⟂ market)./tmp and is not durable.middleware.ts is renamed proxy.ts), Tailwind v4, @anthropic-ai/sdk with adaptive thinking.