Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기a self hostable personal finance dashboard
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A private finance dashboard — see WORK.md for the full spec and milestone plan, DESIGN.md for the design system. All eight milestones in WORK.md §12 (M0–M7) are built. This README is the runbook: a fresh clone should reach a working Sandbox demo in under 10 minutes following Run it below.
DESIGN.md token set, Inter/Instrument Serif/JetBrains Mono, light + dark mode, /styleguide route showing every token and component live./login and the Plaid webhook.db/schema.ts) for the whole product per WORK.md §5, with an initial migration generated./plaid/oauth), encrypted access-token storage (AES-256-GCM envelope encryption, lib/crypto.ts), account + institution sync on exchange, item removal via /item/remove, webhook endpoint with full JWT signature verification./transactions/sync with a persisted cursor, idempotent upserts, pending→posted reconciliation that preserves user edits, and removed[] handling (lib/plaidSync.ts).app/api/plaid/webhook/route.ts) process the matching sync inline within the request — no queue/worker process (Vercel Functions don't support a persistent listener, and one item's sync takes a few seconds); a nightly job refreshes balances as a native Vercel Cron Job (vercel.json → app/api/cron/nightly, authenticated via CRON_SECRET). The twice-daily transactions safety net (app/api/cron/sync-all) runs on Upstash QStash instead (scripts/setup-qstash-schedule.ts) — Vercel Hobby caps native Cron Jobs at once/day — authenticated via QStash's signed-request verification (lib/cronAuth.ts); the route also still accepts a native Vercel-cron GET in case a future Pro plan re-adds it to vercel.json. Both mechanisms run in UTC (no per-project timezone). webhook_events and sync_runs track every attempt.npm run seed:categories); a rules engine (lib/rulesEngine.ts) that matches on description/merchant/amount/account/PFC/direction and sets category/tags/exclude/transfer/split, with priority ordering and a preview count before "apply to existing" commits; transfer pairing (lib/transferDetection.ts) that auto-excludes matched pairs (e.g. a card payment and its funding-account debit) from spend on both sides; manual category edits on a transaction (with an optional "always categorize this merchant" one-click rule) that sync can never overwrite; monthly budgets with optional rollover and a burn-down view at /budgets. A /rules screen manages rules (linked from Transactions)./investments/holdings/get + /investments/transactions/get (lib/plaidInvestments.ts), credit card APR/statement/due-date via /liabilities/get (lib/plaidLiabilities.ts) — both wired into initial link, manual sync, webhooks (HOLDINGS/INVESTMENTS_TRANSACTIONS/LIABILITIES), and the nightly cron. /investments shows portfolio value, allocation (cash as its own slice), per-account holdings, and recent activity; /cards shows APR/statement/min payment/due date and utilization, with a null-limit card excluded from the ratio and footnoted (§6.5). Both institutions and account types that don't support these products degrade quietly rather than erroring (§6.4)./overview — hero net-worth card with a 12-month trend chart, four stat tiles (spend/income/cash-flow/utilization, each with a vs.-last-month delta), a 13-month cash-flow chart (diverging bars from a zero baseline), a budget burn-down list, a ranked "where it went" category breakdown, upcoming bills (subscriptions + card due dates within 30 days), and recent activity. Recurring/subscription detection (lib/recurringDetection.ts) clusters by merchant+account+amount band and needs ≥3 stable-interval occurrences before it promotes a stream; /subscriptions lists them with monthly/annualized totals. Nightly net-worth snapshots (lib/networth.ts) back the trend chart. Every chart uses the DESIGN.md §7 series palette via CSS custom properties (so dark mode needs no separate handling) and there is no dual-axis chart anywhere. /api/analytics/*, /api/recurring, and /api/export (CSV/JSON) round out the API surface from WORK.md §10.error.tsx boundaries (app shell + root) instead of Next's default crash screen; loading.tsx skeletons (shape-matched --sunken blocks, no full-page spinners) on every data-heavy screen; an Export card in Settings; a MASTER_KEY rotation script; this runbook, kept current milestone by milestone./demo — one shareable link, no sign-up, every write refused. See below.Known simplifications, by milestone:
lib/transfers.ts, app/api/categories/[id]/route.ts).liabilities_credit, matching v1 scope); /api/investments/holdings?asOf doesn't do a historical lookup yet — there's no snapshot history to query on a fresh install; "Portfolio return"'s net-contributions math (lib/portfolio.ts) is implemented from Plaid's documented sign convention but not verified against live Sandbox investment transactions — check it against a real transfer before trusting the number.j/k/c/x// keyboard shortcuts (§10.2); no 1M/3M/YTD/1Y/All performance switcher on Investments (§10.5); no mobile bottom-tab layout (§10 "Mobile"); no toasts (sync completion/failure still surface via the freshness badge and banners, just not a toast). Net worth and portfolio-return trends are only as long as the snapshot history that's accumulated since this milestone shipped — a fresh install has one data point.WORK.md §13's "record real Sandbox responses to JSON and replay them") — the existing mock fixtures (lib/mock/*Fixtures.ts) are hand-authored dev/demo data, not a substitute for that test infrastructure; there's no CI pipeline running typecheck/vitest/build on push, only the local scripts below.docker compose up -d
(No Docker? Install Postgres locally and point DATABASE_URL at it instead.)
cp .env.example .env
Fill in AUTH_SECRET (npx auth secret) and MASTER_KEY (openssl rand -base64 32). Leave PLAID_* blank for now — mock mode doesn't need them.
npm install
npm run db:migrate
[email protected] ADMIN_PASSWORD=your-password-here npm run seed:user
npm run seed:categories
(Or set ADMIN_EMAIL/ADMIN_PASSWORD in .env first, then just npm run seed:user.) seed:categories is idempotent — safe to re-run after pulling changes to lib/categoryTaxonomy.ts; it only adds what's missing and never touches a category you've already renamed. Without it, transactions still sync fine but land with no default category until you run it.
npm run dev
Visit http://localhost:3000, sign in, and go to Accounts. In dev, Mock data is on by default (see below) — "Add account" instantly connects a fixture institution, no Plaid account needed.
Automatic sync (twice-daily safety net + nightly balance refresh) runs as Vercel Cron Jobs in production (vercel.json) — there's no separate worker process to run locally. Webhook-triggered syncs process inline in /api/plaid/webhook itself. The app's manual "Sync now" buttons always work regardless.
lib/config.ts resolves MOCK_MODE: dev defaults to mock, production defaults to live. Override explicitly with MOCK_DATA=true / MOCK_DATA=false in .env in either direction (e.g. to exercise real Plaid Sandbox while developing).
In mock mode, "Add account" calls /api/mock/connect instead of opening Plaid Link, inserting one of three canned institutions (lib/mock/fixtures.ts) with realistic checking/savings/credit/brokerage accounts and balances directly into Postgres. Everything downstream — balances, health badges, delete, the Overview net worth figure — reads real DB rows, so it exercises the same code paths mock or live. A "Mock data" chip appears in the top bar whenever it's active so it's never mistaken for a real connection.
A public, read-only tour of the app at one shareable URL — /demo. Distinct from mock mode above: mock mode is a developer convenience (no Plaid credentials, but you're still a real user who can change things), while demo mode is for showing Tally to someone else.
npm run seed:demo # that's the whole setup (safe to re-run; resets it)
There is no env var to set. The demo is live exactly when the demo account exists, so seeding it is what switches it on — nothing to configure, nothing to keep in sync, and no way to half-enable one. An instance that never seeds it simply never has a demo: /demo redirects to the homepage and the buttons don't render.
Send anyone https://your-host/demo. One GET signs them into the demo account server-side and lands them on the Overview — no sign-up, no credentials to hand out, nothing to connect. "Explore the demo" buttons on the homepage and login page point at the same route.
What the demo account holds (npm run seed:demo, ~620 transactions): the three mock institutions with their checking/savings/credit/brokerage accounts, 13 months of generated transactions for one fictional household (semi-monthly payroll, rent, seasonal utilities, six subscriptions, a monthly card payment paired against its funding debit, and seeded-random discretionary spend), 7 months of budgets, detected transfer pairs and recurring streams, holdings and card liabilities, 13 months of net-worth history, and FIRE planner defaults. The generator (lib/demo/demoTransactions.ts) uses a fixed PRNG seed, so every re-seed reproduces the same household. Dates are relative to the run — re-run it whenever you want the demo's "today" to catch up.
How read-only is enforced. Every write in this app is a non-GET request to /api/* (there are no Server Actions), so one gate covers all of them: the authorized callback in lib/auth.config.ts returns 403 {code: "DEMO_READ_ONLY"} for any write carried by a session whose token has demo: true. That claim is stamped at sign-in by the provider used (lib/auth.ts) and lives in the signed, httpOnly session cookie, so it can't be cleared client-side. requireUserId() re-checks it as a backstop. A visitor can click anything; the UI surfaces the refusal as a toast (components/demo/DemoReadOnlyNotice.tsx) and a persistent banner marks the session throughout.
Two reasons the demo can't become a back door into a real account:
lib/demo.ts, and only if that account has been seeded. Its stored password hash is random and held by nobody, so neither the login form nor the mobile login route can reach it./demo never replaces an existing session — a signed-in visitor is redirected to their own Overview untouched. (It's a GET, so a hostile <img src=".../demo"> elsewhere could otherwise have switched a real user into the demo.)seed:demo resets by deleting the demo account, so it refuses to run if ADMIN_EMAIL is pointed at the demo address, or if that account has any real Plaid connection. The address lives on the reserved .invalid TLD so it can't belong to a real person.
PLAID_CLIENT_ID, PLAID_SECRET, PLAID_ENV=sandbox in .env.MOCK_DATA=false.PLAID_REDIRECT_URI / PLAID_WEBHOOK_URL to the tunnel URL, and register the redirect URI in the Plaid dashboard.user_good / pass_good (MFA: mfa_device).With npm run dev and a tunnel running:
POST /sandbox/item/fire_webhook (Plaid API, webhook_code: "SYNC_UPDATES_AVAILABLE", the item's access_token) — the app's /api/plaid/webhook should verify it, insert a webhook_events row, and sync it inline within that same request. Check sync_runs and the Accounts freshness badge.curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/sync-all (and /api/cron/nightly) — set CRON_SECRET in .env first; these 401 without it, same as production./api/plaid/webhook without a valid Plaid-Verification header, or with the body altered after signing — expect 400.POST /sandbox/item/reset_login for a Sandbox item, then either wait for the ITEM_LOGIN_REQUIRED webhook or trigger a sync manually — the item flips to login_required, the Accounts screen shows the "Login expired" banner, and its Reconnect button opens Link in update mode. Completing Link with user_good/pass_good should clear the banner.lib/categorize.ts runs after every sync.category_source flips to manual and both mapPlaidTransaction.ts and categorize.ts skip manual rows unconditionally./rules, build a match (e.g. merchant contains "Amazon"), click Preview to see the affected count, then create it with "Apply to existing transactions" checked — the exact previewed count should get recategorized (§7.2's core acceptance bullet).is_transfer = true (check via db:studio) and neither should appear in that month's /budgets spend for any category./investments, and compare the portfolio value / per-holding values against what the institution actually reports./investments, the allocation bar's slices (plus their percentages) should sum to 100%, with cash equivalents grouped into their own "Cash" slice regardless of the security's nominal type./cards should show "Unknown (no limit reported)" for that card and the overall utilization card should foot-note it as excluded — check computeUtilization in lib/portfolioMath.test.ts for the unit-level version of this same case.sync_runs should show holdings/inv_tx/liabilities rows with no error (Plaid's "not supported" response is treated as a no-op, not a failure).transfer=0&excluded=0&kind=... precisely so a transfer or excluded transaction in range can't inflate what you see versus what was summed.var(--...) reference to the same tokens the rest of the app uses, so there's nothing chart-specific to break.components/charts/NetWorthChart.tsx, CashFlowChart.tsx), each with a single Y-axis./subscriptions has something to show out of the box; on Sandbox, recurring detection needs ≥3 real occurrences of the same merchant/account/amount before it appears.This app is your entire financial history — back up Postgres like you mean it.
# Backup (run anywhere with network access to the DB)
pg_dump "$DATABASE_URL" -Fc -f tally-$(date +%Y%m%d).dump
# Restore into a fresh database
pg_restore -d "$DATABASE_URL" --clean --if-exists tally-20260101.dump
For Docker Compose, run pg_dump/pg_restore from a container with network access to the postgres service (docker compose exec postgres pg_dump -U tally tally -Fc -f /tmp/backup.dump, then copy it out with docker cp). Encrypt backups at rest (they contain everything raw JSONB has ever seen) and schedule them — a cron job calling the pg_dump line above, piped to your storage of choice, is enough at this scale. /api/export (Settings → Export) is a lighter-weight, human-readable alternative for just the transaction data, not a substitute for a real pg_dump backup.
PLAID_SECRET: generate a new secret in the Plaid dashboard, set it in .env, restart the app, confirm a sync still works, then revoke the old secret in the dashboard. No data migration needed — it's only ever sent to Plaid, never stored.MASTER_KEY: this one is stored — it's what every plaid_items.access_token_ciphertext is encrypted under, so rotating it means re-encrypting every row, not just swapping an env var.
openssl rand -base64 32.OLD_MASTER_KEY=<current MASTER_KEY> NEW_MASTER_KEY=<new key> npm run rotate:master-key
MASTER_KEY to the new key in .env (and in Vercel for production), restart/redeploy, and confirm a manual sync works.| Command | What it does |
|---|---|
npm run dev | Start the app |
npm run build / npm run start | Production build / run |
npm run typecheck | tsc --noEmit |
npm run db:generate | Generate a Drizzle migration from db/schema.ts |
npm run db:migrate | Apply migrations |
npm run db:studio | Drizzle Studio (browse the DB) |
npm run seed:user | Create/update the single admin user from ADMIN_EMAIL/ADMIN_PASSWORD |
npm run seed:categories | Seed system categories from Plaid's PFC taxonomy (idempotent) |
npm run seed:demo | Build (or reset) the public read-only demo household — see Demo mode |
npm run rotate:master-key | Re-encrypts every stored access token under a new MASTER_KEY — see Secret rotation |
lib/crypto.ts) and decrypted only inside lib/plaid.ts, server-side. They are never selected into an API response and never reach a client component.lib/plaidWebhook.ts) — body hash, kid→key lookup, iat freshness — before persisting or acting on anything.middleware.ts uses an edge-safe NextAuth config (lib/auth.config.ts) with no DB/bcrypt in it, so the Node-only credentials logic (lib/auth.ts) never gets bundled into the Edge runtime.Functional Source License 1.1, Apache 2.0 Future Grant (FSL-1.1-ALv2). Free to self-host, modify, and use for anything other than offering it (or a substantially similar service) to others commercially. Each release converts to Apache 2.0 automatically two years after it's published.