ray-finance 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/ray-logo.png" alt="Ray" width="120" />
2
+ <img src=".github/ray-logo.png" alt="Ray" width="108" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -14,40 +14,17 @@
14
14
 
15
15
  <br />
16
16
 
17
- ```
18
- Friday, Mar 28
19
-
20
- net worth $45,230 +$120
21
-
22
- spending $2,340 this month · $340 less vs last month
23
- Dining -$114 · Shopping -$142 · Groceries -$73
24
-
25
- ▓▓▓▓▓▓▓░ Dining 92%
26
-
27
- ▓▓▓▓░░░░ Emergency fund $18,200/$40,000
28
-
29
- upcoming Netflix $16 in 3d · Comcast $142 in 6d
30
-
31
- score 72/100 · 5d no dining · 3d on pace
32
-
33
- ──────────────────────────────────────────────────
34
-
35
- ❯ if I quit my job to freelance, how long can I survive?
36
-
37
- Based on your last 3 months: you burn $4,820/mo after
38
- fixed costs. With $18,200 in savings, that's
39
- 3.8 months of runway at current spend.
40
-
41
- Cut dining and shopping to last-month levels and
42
- you stretch to 5.1 months. Land one $8k contract
43
- in that window and you never dip below $10k.
44
- ```
17
+ <p align="center">
18
+ <img src=".github/ray-demo.png" alt="Ray demo" />
19
+ </p>
45
20
 
46
21
  Open Ray and it shows your net worth, spending vs last month, budget pacing, and upcoming bills — before you type a word. Ask a question and it answers from your real data, not guesses. Local-first. Encrypted. Open source.
47
22
 
48
23
  ## Features
49
24
 
50
25
  - **It already knows** — Every conversation starts with a real-time financial briefing. Net worth, spending velocity, budget alerts, goal pace, upcoming bills, and your daily score. No "let me look that up."
26
+ - **Persistent context** — Ray maintains a financial profile that evolves with you: income, goals, strategy, key decisions, and open items. It updates this context as your situation changes, so every conversation picks up where the last one left off.
27
+ - **Long-term memory** — Important details from conversations are saved as memories. Mention you're saving for a house or switching jobs and Ray remembers — without you repeating yourself.
51
28
  - **Bank sync via Plaid** — Connect checking, savings, credit cards, investments, and loans
52
29
  - **Encrypted local database** — All data stays on your machine in an AES-256 encrypted SQLite database
53
30
  - **Daily scoring** — A 0-100 behavior score with streaks and 14 unlockable achievements. No restaurants for a week? That's Kitchen Hero. Five zero-spend days? Monk Mode.
@@ -65,6 +42,26 @@ Open Ray and it shows your net worth, spending vs last month, budget pacing, and
65
42
  npm install -g ray-finance
66
43
  ```
67
44
 
45
+ ## Try It
46
+
47
+ Explore Ray with realistic fake data — no bank accounts needed.
48
+
49
+ ```bash
50
+ ray demo # seed a demo database
51
+ ray --demo status # financial overview
52
+ ray --demo accounts # linked accounts with balances
53
+ ray --demo spending # spending breakdown by category
54
+ ray --demo budgets # budget tracking
55
+ ray --demo goals # financial goal progress
56
+ ray --demo score # daily score, streaks, achievements
57
+ ray --demo alerts # financial alerts
58
+ ray --demo transactions # recent transactions
59
+ ```
60
+
61
+ The dashboard commands work with no setup at all. To also try the AI chat with demo data, run `ray setup` first and add an [Anthropic API key](https://console.anthropic.com) or Ray API key — then `ray --demo` will start an interactive session where you can ask questions about the fake portfolio.
62
+
63
+ When you're ready to connect real accounts, run `ray link`.
64
+
68
65
  ## Quick Start
69
66
 
70
67
  ```bash
@@ -98,6 +95,8 @@ Run `ray --help` to see all available commands.
98
95
  | Command | Description |
99
96
  |---------|-------------|
100
97
  | `ray` | Interactive AI chat with your financial advisor |
98
+ | `ray demo` | Seed a demo database with realistic fake data |
99
+ | `ray --demo <cmd>` | Run any command against demo data |
101
100
  | `ray setup` | Configure API keys and preferences |
102
101
  | `ray link` | Connect a new bank account |
103
102
  | `ray sync` | Pull latest transactions and balances |
@@ -155,6 +154,7 @@ Ray stores everything in `~/.ray/`:
155
154
  context.md # Persistent financial context for AI
156
155
  data/
157
156
  finance.db # Encrypted SQLite database
157
+ demo.db # Demo database (created by `ray demo`)
158
158
  sync.log # Daily sync output
159
159
  ```
160
160
 
@@ -174,6 +174,7 @@ RAY_API_KEY= # Ray API key (managed mode, replaces the above)
174
174
 
175
175
  ## Roadmap
176
176
 
177
+ - [ ] Bring your own model — use any LLM provider (OpenAI, Ollama, open-source models, etc.)
177
178
  - [ ] Daily digest email — morning summary of your finances
178
179
 
179
180
  Have an idea? [Open a PR](https://github.com/cdinnison/ray-finance/pulls).
package/dist/cli/index.js CHANGED
@@ -1,8 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { resolve } from "path";
3
+ import { homedir } from "os";
4
+ // --demo flag: use dedicated demo database (must run before config import)
5
+ const isDemoMode = process.argv.includes("--demo");
6
+ if (isDemoMode) {
7
+ process.argv = process.argv.filter(a => a !== "--demo");
8
+ }
2
9
  import { Command } from "commander";
3
10
  import { createRequire } from "module";
4
11
  import { config, isConfigured, useManaged, RAY_PROXY_BASE } from "../config.js";
5
12
  import { helpScreen } from "./format.js";
13
+ // Override config for demo mode (demo DB is unencrypted)
14
+ if (isDemoMode) {
15
+ config.dbPath = resolve(homedir(), ".ray", "data", "demo.db");
16
+ config.dbEncryptionKey = "";
17
+ }
6
18
  const require = createRequire(import.meta.url);
7
19
  const { version } = require("../../package.json");
8
20
  const program = new Command();
@@ -181,6 +193,14 @@ program
181
193
  const { runDoctor } = await import("./doctor.js");
182
194
  await runDoctor();
183
195
  });
196
+ program
197
+ .command("demo")
198
+ .description("Seed a demo database with realistic fake data")
199
+ .action(async () => {
200
+ const demoPath = resolve(homedir(), ".ray", "data", "demo.db");
201
+ const { seedDemoDb } = await import("../demo/seed.js");
202
+ seedDemoDb(demoPath);
203
+ });
184
204
  program
185
205
  .command("completions")
186
206
  .description("Install shell completions")
@@ -189,6 +209,8 @@ program
189
209
  installCompletions();
190
210
  });
191
211
  function ensureConfigured() {
212
+ if (isDemoMode)
213
+ return;
192
214
  if (!isConfigured()) {
193
215
  console.error("Ray is not configured. Run 'ray setup' first.");
194
216
  process.exit(1);
@@ -213,6 +235,7 @@ program.configureHelp({
213
235
  { name: "billing", desc: "Manage your Ray subscription" },
214
236
  { name: "update", desc: "Update Ray to the latest version" },
215
237
  { name: "doctor", desc: "Check system health" },
238
+ { name: "demo", desc: "Seed a demo database with fake data" },
216
239
  { name: "completions", desc: "Install shell completions" },
217
240
  ]),
218
241
  });
@@ -0,0 +1,252 @@
1
+ export declare const institutions: {
2
+ item_id: string;
3
+ access_token: string;
4
+ name: string;
5
+ products: string;
6
+ logo: string;
7
+ primary_color: string;
8
+ }[];
9
+ export declare const accounts: ({
10
+ account_id: string;
11
+ item_id: string;
12
+ name: string;
13
+ official_name: string;
14
+ type: string;
15
+ subtype: string;
16
+ mask: string;
17
+ current_balance: number;
18
+ available_balance: number;
19
+ currency: string;
20
+ balance_limit: null;
21
+ } | {
22
+ account_id: string;
23
+ item_id: string;
24
+ name: string;
25
+ official_name: string;
26
+ type: string;
27
+ subtype: string;
28
+ mask: string;
29
+ current_balance: number;
30
+ available_balance: null;
31
+ currency: string;
32
+ balance_limit: null;
33
+ } | {
34
+ account_id: string;
35
+ item_id: string;
36
+ name: string;
37
+ official_name: string;
38
+ type: string;
39
+ subtype: string;
40
+ mask: string;
41
+ current_balance: number;
42
+ available_balance: number;
43
+ currency: string;
44
+ balance_limit: number;
45
+ } | {
46
+ account_id: string;
47
+ item_id: string;
48
+ name: string;
49
+ official_name: null;
50
+ type: string;
51
+ subtype: string;
52
+ mask: null;
53
+ current_balance: number;
54
+ available_balance: null;
55
+ currency: string;
56
+ balance_limit: null;
57
+ })[];
58
+ export declare const transactions: ({
59
+ transaction_id: string;
60
+ account_id: string;
61
+ amount: number;
62
+ date: string;
63
+ name: string;
64
+ merchant_name: string;
65
+ category: string;
66
+ subcategory: string;
67
+ pending: number;
68
+ payment_channel: string;
69
+ } | {
70
+ transaction_id: string;
71
+ account_id: string;
72
+ amount: number;
73
+ date: string;
74
+ name: string;
75
+ merchant_name: null;
76
+ category: string;
77
+ subcategory: string;
78
+ pending: number;
79
+ payment_channel: string;
80
+ })[];
81
+ export declare const securities: {
82
+ security_id: string;
83
+ name: string;
84
+ ticker: string;
85
+ type: string;
86
+ close_price: number;
87
+ close_price_as_of: string;
88
+ }[];
89
+ export declare const holdings: ({
90
+ account_id: string;
91
+ security_id: string;
92
+ quantity: number;
93
+ value: number;
94
+ cost_basis: number;
95
+ price: number;
96
+ price_as_of: string;
97
+ vested_value: null;
98
+ vested_quantity: null;
99
+ } | {
100
+ account_id: string;
101
+ security_id: string;
102
+ quantity: number;
103
+ value: number;
104
+ cost_basis: number;
105
+ price: number;
106
+ price_as_of: string;
107
+ vested_value: number;
108
+ vested_quantity: number;
109
+ })[];
110
+ export declare const liabilities: ({
111
+ account_id: string;
112
+ type: string;
113
+ interest_rate: number;
114
+ origination_date: null;
115
+ original_balance: null;
116
+ current_balance: number;
117
+ minimum_payment: number;
118
+ next_payment_due: string;
119
+ last_payment_amount: number;
120
+ last_payment_date: string;
121
+ credit_limit: number;
122
+ last_statement_issue_date: string;
123
+ is_overdue: number;
124
+ apr_type: string;
125
+ maturity_date: null;
126
+ loan_type: null;
127
+ property_address: null;
128
+ escrow_balance: null;
129
+ loan_status: null;
130
+ loan_name: null;
131
+ repayment_plan: null;
132
+ expected_payoff_date: null;
133
+ ytd_interest_paid: null;
134
+ ytd_principal_paid: null;
135
+ } | {
136
+ account_id: string;
137
+ type: string;
138
+ interest_rate: number;
139
+ origination_date: string;
140
+ original_balance: number;
141
+ current_balance: number;
142
+ minimum_payment: number;
143
+ next_payment_due: string;
144
+ last_payment_amount: number;
145
+ last_payment_date: string;
146
+ credit_limit: null;
147
+ last_statement_issue_date: null;
148
+ is_overdue: number;
149
+ apr_type: string;
150
+ maturity_date: string;
151
+ loan_type: string;
152
+ property_address: string;
153
+ escrow_balance: number;
154
+ loan_status: string;
155
+ loan_name: string;
156
+ repayment_plan: null;
157
+ expected_payoff_date: string;
158
+ ytd_interest_paid: number;
159
+ ytd_principal_paid: number;
160
+ })[];
161
+ export declare const recurring: ({
162
+ stream_id: string;
163
+ account_id: string;
164
+ merchant_name: string;
165
+ description: string;
166
+ frequency: string;
167
+ category: string;
168
+ subcategory: string;
169
+ avg_amount: number;
170
+ last_amount: number;
171
+ first_date: string;
172
+ last_date: string;
173
+ is_active: number;
174
+ status: string;
175
+ stream_type: string;
176
+ } | {
177
+ stream_id: string;
178
+ account_id: string;
179
+ merchant_name: null;
180
+ description: string;
181
+ frequency: string;
182
+ category: string;
183
+ subcategory: string;
184
+ avg_amount: number;
185
+ last_amount: number;
186
+ first_date: string;
187
+ last_date: string;
188
+ is_active: number;
189
+ status: string;
190
+ stream_type: string;
191
+ })[];
192
+ export declare const budgets: {
193
+ category: string;
194
+ monthly_limit: number;
195
+ period: string;
196
+ }[];
197
+ export declare const goals: {
198
+ name: string;
199
+ target_amount: number;
200
+ current_amount: number;
201
+ target_date: string;
202
+ status: string;
203
+ }[];
204
+ export declare const dailyScores: {
205
+ date: string;
206
+ score: number;
207
+ restaurant_count: number;
208
+ shopping_count: number;
209
+ food_spend: number;
210
+ total_spend: number;
211
+ zero_spend: number;
212
+ no_restaurant_streak: number;
213
+ no_shopping_streak: number;
214
+ on_pace_streak: number;
215
+ }[];
216
+ export declare const achievements: {
217
+ key: string;
218
+ name: string;
219
+ description: string;
220
+ unlocked_at: string;
221
+ }[];
222
+ export declare const netWorthHistory: {
223
+ date: string;
224
+ total_assets: number;
225
+ total_liabilities: number;
226
+ net_worth: number;
227
+ }[];
228
+ export declare const investmentTransactions: {
229
+ investment_transaction_id: string;
230
+ account_id: string;
231
+ security_id: string;
232
+ date: string;
233
+ name: string;
234
+ quantity: number;
235
+ amount: number;
236
+ price: number;
237
+ fees: number;
238
+ type: string;
239
+ subtype: string;
240
+ iso_currency_code: string;
241
+ }[];
242
+ export declare const recurringBills: {
243
+ name: string;
244
+ amount: number;
245
+ day_of_month: number;
246
+ type: string;
247
+ account_id: string;
248
+ }[];
249
+ export declare const memories: {
250
+ content: string;
251
+ category: string;
252
+ }[];
@@ -0,0 +1,272 @@
1
+ import { LOGOS } from "./logos.js";
2
+ // ─── Date Helpers ─── //
3
+ const now = new Date();
4
+ function today() {
5
+ return now.toISOString().slice(0, 10);
6
+ }
7
+ function yesterday() {
8
+ return daysAgo(1);
9
+ }
10
+ function daysAgo(n) {
11
+ const d = new Date(now);
12
+ d.setDate(d.getDate() - n);
13
+ return d.toISOString().slice(0, 10);
14
+ }
15
+ /** First of the month, N months ago (0 = this month) */
16
+ function monthStart(monthsAgo) {
17
+ const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, 1);
18
+ return d.toISOString().slice(0, 10);
19
+ }
20
+ /** Specific day of the month, N months ago. For current month (0), clamps to today. */
21
+ function monthDay(monthsAgo, day) {
22
+ const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, day);
23
+ if (monthsAgo === 0 && d > now) {
24
+ // Clamp future current-month dates to recent past
25
+ return daysAgo(Math.max(1, day % (now.getDate()) + 1));
26
+ }
27
+ return d.toISOString().slice(0, 10);
28
+ }
29
+ /** Date N months from now */
30
+ function monthsFromNow(n) {
31
+ const d = new Date(now.getFullYear(), now.getMonth() + n, now.getDate());
32
+ return d.toISOString().slice(0, 10);
33
+ }
34
+ /** Date N years ago */
35
+ function yearsAgo(n) {
36
+ const d = new Date(now.getFullYear() - n, now.getMonth(), now.getDate());
37
+ return d.toISOString().slice(0, 10);
38
+ }
39
+ // ─── Institutions ─── //
40
+ export const institutions = [
41
+ { item_id: "demo-chase", access_token: "demo-access-chase", name: "Chase", products: '["transactions","liabilities"]', logo: LOGOS.chase, primary_color: "#003087" },
42
+ { item_id: "demo-robinhood", access_token: "demo-access-robinhood", name: "Robinhood", products: '["transactions","investments"]', logo: LOGOS.robinhood, primary_color: "#00C805" },
43
+ { item_id: "demo-schwab", access_token: "demo-access-schwab", name: "Charles Schwab", products: '["transactions","investments"]', logo: LOGOS.schwab, primary_color: "#00A0DF" },
44
+ { item_id: "demo-amex", access_token: "demo-access-amex", name: "American Express", products: '["transactions","liabilities"]', logo: LOGOS.amex, primary_color: "#006FCF" },
45
+ ];
46
+ // ─── Accounts ─── //
47
+ export const accounts = [
48
+ { account_id: "demo-chase-checking", item_id: "demo-chase", name: "Total Checking", official_name: "Chase Total Checking", type: "depository", subtype: "checking", mask: "4521", current_balance: 4200, available_balance: 4200, currency: "USD", balance_limit: null },
49
+ { account_id: "demo-chase-savings", item_id: "demo-chase", name: "Chase Savings", official_name: "Chase Savings", type: "depository", subtype: "savings", mask: "7890", current_balance: 12500, available_balance: 12500, currency: "USD", balance_limit: null },
50
+ { account_id: "demo-robinhood-brokerage", item_id: "demo-robinhood", name: "Individual", official_name: "Robinhood Individual", type: "investment", subtype: "brokerage", mask: "3344", current_balance: 34000, available_balance: null, currency: "USD", balance_limit: null },
51
+ { account_id: "demo-schwab-401k", item_id: "demo-schwab", name: "401(k)", official_name: "Schwab 401(k)", type: "investment", subtype: "401k", mask: "9012", current_balance: 67000, available_balance: null, currency: "USD", balance_limit: null },
52
+ { account_id: "demo-amex-gold", item_id: "demo-amex", name: "Gold Card", official_name: "American Express Gold Card", type: "credit", subtype: "credit card", mask: "1008", current_balance: 1830, available_balance: 13170, currency: "USD", balance_limit: 15000 },
53
+ { account_id: "demo-chase-mortgage", item_id: "demo-chase", name: "Home Mortgage", official_name: "Chase Home Mortgage", type: "loan", subtype: "mortgage", mask: "6601", current_balance: 285000, available_balance: null, currency: "USD", balance_limit: null },
54
+ { account_id: "demo-chase-home", item_id: "demo-chase", name: "Primary Residence", official_name: null, type: "other", subtype: "property", mask: null, current_balance: 425000, available_balance: null, currency: "USD", balance_limit: null },
55
+ ];
56
+ // ─── Transactions ─── //
57
+ let txCounter = 0;
58
+ function txId() {
59
+ return `demo-tx-${++txCounter}`;
60
+ }
61
+ const CHK = "demo-chase-checking";
62
+ const AMEX = "demo-amex-gold";
63
+ export const transactions = [
64
+ // ── Income (negative = inflow in Plaid) ──
65
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(0, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
66
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(0, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
67
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(1, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
68
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(1, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
69
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(2, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
70
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(2, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
71
+ { transaction_id: txId(), account_id: CHK, amount: -800, date: monthDay(1, 20), name: "Wire Transfer - Client LLC", merchant_name: "Client LLC", category: "INCOME", subcategory: "INCOME_OTHER", pending: 0, payment_channel: "online" },
72
+ // ── Rent (3 months) ──
73
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(0, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
74
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(1, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
75
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(2, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
76
+ // ── Utilities ──
77
+ { transaction_id: txId(), account_id: CHK, amount: 138.42, date: monthDay(0, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
78
+ { transaction_id: txId(), account_id: CHK, amount: 125.80, date: monthDay(1, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
79
+ { transaction_id: txId(), account_id: CHK, amount: 142.15, date: monthDay(2, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
80
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(0, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
81
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(1, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
82
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(2, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
83
+ // ── Food & Drink (this month — targeting ~$450 spend) ──
84
+ { transaction_id: txId(), account_id: AMEX, amount: 89.42, date: monthDay(0, 3), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
85
+ { transaction_id: txId(), account_id: AMEX, amount: 52.18, date: monthDay(0, 7), name: "Trader Joe's", merchant_name: "Trader Joe's", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
86
+ { transaction_id: txId(), account_id: AMEX, amount: 34.67, date: monthDay(0, 14), name: "Safeway", merchant_name: "Safeway", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
87
+ { transaction_id: txId(), account_id: AMEX, amount: 67.30, date: monthDay(0, 5), name: "Olive Garden", merchant_name: "Olive Garden", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
88
+ { transaction_id: txId(), account_id: AMEX, amount: 42.80, date: monthDay(0, 10), name: "Thai Kitchen", merchant_name: "Thai Kitchen", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
89
+ { transaction_id: txId(), account_id: AMEX, amount: 12.45, date: monthDay(0, 8), name: "Chipotle", merchant_name: "Chipotle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_FAST_FOOD", pending: 0, payment_channel: "in store" },
90
+ { transaction_id: txId(), account_id: AMEX, amount: 6.50, date: yesterday(), name: "Starbucks", merchant_name: "Starbucks", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
91
+ { transaction_id: txId(), account_id: AMEX, amount: 5.75, date: today(), name: "Blue Bottle Coffee", merchant_name: "Blue Bottle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
92
+ { transaction_id: txId(), account_id: AMEX, amount: 78.50, date: monthDay(0, 18), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
93
+ { transaction_id: txId(), account_id: AMEX, amount: 58.35, date: daysAgo(3), name: "Sushi Roku", merchant_name: "Sushi Roku", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
94
+ // ── Food & Drink (last month) ──
95
+ { transaction_id: txId(), account_id: AMEX, amount: 92.10, date: monthDay(1, 5), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
96
+ { transaction_id: txId(), account_id: AMEX, amount: 48.75, date: monthDay(1, 12), name: "Trader Joe's", merchant_name: "Trader Joe's", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
97
+ { transaction_id: txId(), account_id: AMEX, amount: 55.20, date: monthDay(1, 18), name: "Nobu", merchant_name: "Nobu", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
98
+ { transaction_id: txId(), account_id: AMEX, amount: 14.30, date: monthDay(1, 22), name: "Chipotle", merchant_name: "Chipotle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_FAST_FOOD", pending: 0, payment_channel: "in store" },
99
+ { transaction_id: txId(), account_id: AMEX, amount: 6.50, date: monthDay(1, 8), name: "Starbucks", merchant_name: "Starbucks", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
100
+ // ── Shopping (this month — large Apple purchase triggers alert) ──
101
+ { transaction_id: txId(), account_id: AMEX, amount: 34.99, date: daysAgo(12), name: "Amazon", merchant_name: "Amazon", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ONLINE_MARKETPLACES", pending: 0, payment_channel: "online" },
102
+ { transaction_id: txId(), account_id: AMEX, amount: 67.23, date: daysAgo(8), name: "Target", merchant_name: "Target", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_SUPERSTORES", pending: 0, payment_channel: "in store" },
103
+ { transaction_id: txId(), account_id: AMEX, amount: 29.99, date: daysAgo(4), name: "Uniqlo", merchant_name: "Uniqlo", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_CLOTHING_AND_ACCESSORIES", pending: 0, payment_channel: "in store" },
104
+ { transaction_id: txId(), account_id: AMEX, amount: 849.99, date: today(), name: "Apple Store", merchant_name: "Apple", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ELECTRONICS", pending: 0, payment_channel: "in store" },
105
+ // ── Shopping (last month) ──
106
+ { transaction_id: txId(), account_id: AMEX, amount: 299.99, date: monthDay(1, 10), name: "Best Buy", merchant_name: "Best Buy", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ELECTRONICS", pending: 0, payment_channel: "in store" },
107
+ { transaction_id: txId(), account_id: AMEX, amount: 42.50, date: monthDay(1, 16), name: "Amazon", merchant_name: "Amazon", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ONLINE_MARKETPLACES", pending: 0, payment_channel: "online" },
108
+ // ── Transportation (this month — targeting ~$100) ──
109
+ { transaction_id: txId(), account_id: CHK, amount: 52.40, date: monthDay(0, 6), name: "Shell", merchant_name: "Shell", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_GAS", pending: 0, payment_channel: "in store" },
110
+ { transaction_id: txId(), account_id: CHK, amount: 18.75, date: monthDay(0, 11), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
111
+ { transaction_id: txId(), account_id: CHK, amount: 24.30, date: yesterday(), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
112
+ // ── Transportation (last month) ──
113
+ { transaction_id: txId(), account_id: CHK, amount: 48.90, date: monthDay(1, 8), name: "Shell", merchant_name: "Shell", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_GAS", pending: 0, payment_channel: "in store" },
114
+ { transaction_id: txId(), account_id: CHK, amount: 35.00, date: monthDay(1, 14), name: "Clipper Card", merchant_name: "BART", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_PUBLIC_TRANSIT", pending: 0, payment_channel: "online" },
115
+ { transaction_id: txId(), account_id: CHK, amount: 22.15, date: monthDay(1, 25), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
116
+ // ── Entertainment (this month — targeting ~$85) ──
117
+ { transaction_id: txId(), account_id: CHK, amount: 15.99, date: monthDay(0, 2), name: "Netflix", merchant_name: "Netflix", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "online" },
118
+ { transaction_id: txId(), account_id: CHK, amount: 10.99, date: monthDay(0, 2), name: "Spotify", merchant_name: "Spotify", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", pending: 0, payment_channel: "online" },
119
+ { transaction_id: txId(), account_id: CHK, amount: 28.50, date: monthDay(0, 16), name: "AMC Theaters", merchant_name: "AMC", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "in store" },
120
+ { transaction_id: txId(), account_id: CHK, amount: 49.99, date: daysAgo(5), name: "Steam", merchant_name: "Steam", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_GAMES", pending: 0, payment_channel: "online" },
121
+ // ── Entertainment (last month) ──
122
+ { transaction_id: txId(), account_id: CHK, amount: 15.99, date: monthDay(1, 2), name: "Netflix", merchant_name: "Netflix", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "online" },
123
+ { transaction_id: txId(), account_id: CHK, amount: 10.99, date: monthDay(1, 2), name: "Spotify", merchant_name: "Spotify", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", pending: 0, payment_channel: "online" },
124
+ // ── Personal Care ──
125
+ { transaction_id: txId(), account_id: CHK, amount: 35.00, date: monthDay(0, 17), name: "Supercuts", merchant_name: "Supercuts", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_HAIR_AND_BEAUTY", pending: 0, payment_channel: "in store" },
126
+ { transaction_id: txId(), account_id: CHK, amount: 22.47, date: daysAgo(2), name: "CVS Pharmacy", merchant_name: "CVS", category: "MEDICAL", subcategory: "MEDICAL_PHARMACIES_AND_SUPPLEMENTS", pending: 0, payment_channel: "in store" },
127
+ // ── Loan Payments ──
128
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(0, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
129
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(1, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
130
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(2, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
131
+ // ── Gym ──
132
+ { transaction_id: txId(), account_id: CHK, amount: 95, date: monthDay(0, 5), name: "Equinox", merchant_name: "Equinox", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", pending: 0, payment_channel: "online" },
133
+ { transaction_id: txId(), account_id: CHK, amount: 95, date: monthDay(1, 5), name: "Equinox", merchant_name: "Equinox", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", pending: 0, payment_channel: "online" },
134
+ // ── Car Insurance ──
135
+ { transaction_id: txId(), account_id: CHK, amount: 145, date: monthDay(0, 20), name: "GEICO", merchant_name: "GEICO", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_INSURANCE", pending: 0, payment_channel: "online" },
136
+ { transaction_id: txId(), account_id: CHK, amount: 145, date: monthDay(1, 20), name: "GEICO", merchant_name: "GEICO", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_INSURANCE", pending: 0, payment_channel: "online" },
137
+ // ── iCloud ──
138
+ { transaction_id: txId(), account_id: CHK, amount: 2.99, date: monthDay(0, 3), name: "Apple iCloud", merchant_name: "Apple", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", pending: 0, payment_channel: "online" },
139
+ { transaction_id: txId(), account_id: CHK, amount: 2.99, date: monthDay(1, 3), name: "Apple iCloud", merchant_name: "Apple", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", pending: 0, payment_channel: "online" },
140
+ ];
141
+ // ─── Securities ─── //
142
+ export const securities = [
143
+ { security_id: "demo-sec-aapl", name: "Apple Inc", ticker: "AAPL", type: "equity", close_price: 227.50, close_price_as_of: daysAgo(1) },
144
+ { security_id: "demo-sec-tsla", name: "Tesla Inc", ticker: "TSLA", type: "equity", close_price: 172.30, close_price_as_of: daysAgo(1) },
145
+ { security_id: "demo-sec-voo", name: "Vanguard S&P 500 ETF", ticker: "VOO", type: "etf", close_price: 532.80, close_price_as_of: daysAgo(1) },
146
+ { security_id: "demo-sec-vtsax", name: "Vanguard Total Stock Market Index", ticker: "VTSAX", type: "mutual fund", close_price: 118.45, close_price_as_of: daysAgo(1) },
147
+ { security_id: "demo-sec-vbtlx", name: "Vanguard Total Bond Market Index", ticker: "VBTLX", type: "mutual fund", close_price: 10.82, close_price_as_of: daysAgo(1) },
148
+ { security_id: "demo-sec-vttvx", name: "Vanguard Target Retirement 2035", ticker: "VTTVX", type: "mutual fund", close_price: 28.15, close_price_as_of: daysAgo(1) },
149
+ ];
150
+ // ─── Holdings ─── //
151
+ export const holdings = [
152
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-aapl", quantity: 50, value: 11375, cost_basis: 8500, price: 227.50, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
153
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-tsla", quantity: 35, value: 6030.50, cost_basis: 7000, price: 172.30, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
154
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", quantity: 31, value: 16516.80, cost_basis: 14200, price: 532.80, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
155
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", quantity: 280, value: 33166, cost_basis: 28000, price: 118.45, price_as_of: daysAgo(1), vested_value: 33166, vested_quantity: 280 },
156
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vbtlx", quantity: 1200, value: 12984, cost_basis: 12500, price: 10.82, price_as_of: daysAgo(1), vested_value: 12984, vested_quantity: 1200 },
157
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vttvx", quantity: 740, value: 20831, cost_basis: 18500, price: 28.15, price_as_of: daysAgo(1), vested_value: 20831, vested_quantity: 740 },
158
+ ];
159
+ // ─── Liabilities ─── //
160
+ export const liabilities = [
161
+ {
162
+ account_id: "demo-amex-gold", type: "credit", interest_rate: 24.99, origination_date: null,
163
+ original_balance: null, current_balance: 1830, minimum_payment: 35,
164
+ next_payment_due: monthDay(0, 28), last_payment_amount: 500, last_payment_date: monthDay(1, 25),
165
+ credit_limit: 15000, last_statement_issue_date: monthDay(1, 28), is_overdue: 0,
166
+ apr_type: "variable", maturity_date: null, loan_type: null, property_address: null,
167
+ escrow_balance: null, loan_status: null, loan_name: null, repayment_plan: null,
168
+ expected_payoff_date: null, ytd_interest_paid: null, ytd_principal_paid: null,
169
+ },
170
+ {
171
+ account_id: "demo-chase-mortgage", type: "mortgage", interest_rate: 6.875,
172
+ origination_date: yearsAgo(3), original_balance: 320000, current_balance: 285000,
173
+ minimum_payment: 2100, next_payment_due: monthDay(-1, 1),
174
+ last_payment_amount: 2100, last_payment_date: monthDay(0, 1),
175
+ credit_limit: null, last_statement_issue_date: null, is_overdue: 0,
176
+ apr_type: "fixed", maturity_date: monthsFromNow(27 * 12), loan_type: "conventional",
177
+ property_address: "123 Main St, San Francisco, CA 94102",
178
+ escrow_balance: 4200, loan_status: "active", loan_name: "30yr Fixed",
179
+ repayment_plan: null, expected_payoff_date: monthsFromNow(27 * 12),
180
+ ytd_interest_paid: 6125, ytd_principal_paid: 2175,
181
+ },
182
+ ];
183
+ // ─── Recurring ─── //
184
+ export const recurring = [
185
+ { stream_id: "demo-rec-salary", account_id: CHK, merchant_name: "Acme Corp", description: "Payroll - Acme Corp", frequency: "BIWEEKLY", category: "INCOME", subcategory: "INCOME_WAGES", avg_amount: -4250, last_amount: -4250, first_date: yearsAgo(2), last_date: monthDay(0, 15), is_active: 1, status: "MATURE", stream_type: "inflow" },
186
+ { stream_id: "demo-rec-freelance", account_id: CHK, merchant_name: "Client LLC", description: "Wire Transfer - Client LLC", frequency: "MONTHLY", category: "INCOME", subcategory: "INCOME_OTHER", avg_amount: -800, last_amount: -800, first_date: yearsAgo(1), last_date: monthDay(1, 20), is_active: 1, status: "MATURE", stream_type: "inflow" },
187
+ { stream_id: "demo-rec-rent", account_id: CHK, merchant_name: null, description: "Rent Payment", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", avg_amount: 1850, last_amount: 1850, first_date: yearsAgo(2), last_date: monthDay(0, 1), is_active: 1, status: "MATURE", stream_type: "outflow" },
188
+ { stream_id: "demo-rec-netflix", account_id: CHK, merchant_name: "Netflix", description: "Netflix", frequency: "MONTHLY", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", avg_amount: 15.99, last_amount: 15.99, first_date: yearsAgo(3), last_date: monthDay(0, 2), is_active: 1, status: "MATURE", stream_type: "outflow" },
189
+ { stream_id: "demo-rec-spotify", account_id: CHK, merchant_name: "Spotify", description: "Spotify Premium", frequency: "MONTHLY", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", avg_amount: 10.99, last_amount: 10.99, first_date: yearsAgo(2), last_date: monthDay(0, 2), is_active: 1, status: "MATURE", stream_type: "outflow" },
190
+ { stream_id: "demo-rec-electric", account_id: CHK, merchant_name: "PG&E", description: "PG&E Electric", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", avg_amount: 135, last_amount: 138.42, first_date: yearsAgo(2), last_date: monthDay(0, 15), is_active: 1, status: "MATURE", stream_type: "outflow" },
191
+ { stream_id: "demo-rec-internet", account_id: CHK, merchant_name: "Comcast", description: "Comcast Internet", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", avg_amount: 79.99, last_amount: 79.99, first_date: yearsAgo(2), last_date: monthDay(0, 12), is_active: 1, status: "MATURE", stream_type: "outflow" },
192
+ { stream_id: "demo-rec-mortgage", account_id: CHK, merchant_name: "Chase", description: "Mortgage Payment", frequency: "MONTHLY", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", avg_amount: 2100, last_amount: 2100, first_date: yearsAgo(3), last_date: monthDay(0, 1), is_active: 1, status: "MATURE", stream_type: "outflow" },
193
+ { stream_id: "demo-rec-gym", account_id: CHK, merchant_name: "Equinox", description: "Equinox Membership", frequency: "MONTHLY", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", avg_amount: 95, last_amount: 95, first_date: yearsAgo(1), last_date: monthDay(0, 5), is_active: 1, status: "MATURE", stream_type: "outflow" },
194
+ { stream_id: "demo-rec-icloud", account_id: CHK, merchant_name: "Apple", description: "iCloud Storage", frequency: "MONTHLY", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", avg_amount: 2.99, last_amount: 2.99, first_date: yearsAgo(4), last_date: monthDay(0, 3), is_active: 1, status: "MATURE", stream_type: "outflow" },
195
+ ];
196
+ // ─── Budgets ─── //
197
+ export const budgets = [
198
+ { category: "FOOD_AND_DRINK", monthly_limit: 600, period: "monthly" },
199
+ { category: "GENERAL_MERCHANDISE", monthly_limit: 400, period: "monthly" },
200
+ { category: "ENTERTAINMENT", monthly_limit: 150, period: "monthly" },
201
+ { category: "TRANSPORTATION", monthly_limit: 200, period: "monthly" },
202
+ ];
203
+ // ─── Goals ─── //
204
+ export const goals = [
205
+ { name: "Emergency Fund", target_amount: 15000, current_amount: 6200, target_date: monthsFromNow(8), status: "active" },
206
+ { name: "Japan Vacation", target_amount: 5000, current_amount: 2800, target_date: monthsFromNow(5), status: "active" },
207
+ { name: "New Car Down Payment", target_amount: 8000, current_amount: 1200, target_date: monthsFromNow(14), status: "active" },
208
+ ];
209
+ // ─── Daily Scores (14 days, building streaks) ─── //
210
+ export const dailyScores = [
211
+ { date: daysAgo(13), score: 72, restaurant_count: 1, shopping_count: 0, food_spend: 55.20, total_spend: 180.40, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 1, on_pace_streak: 1 },
212
+ { date: daysAgo(12), score: 68, restaurant_count: 0, shopping_count: 1, food_spend: 12.30, total_spend: 210.50, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 0, on_pace_streak: 0 },
213
+ { date: daysAgo(11), score: 75, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 95.00, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 1, on_pace_streak: 1 },
214
+ { date: daysAgo(10), score: 92, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 0, zero_spend: 1, no_restaurant_streak: 3, no_shopping_streak: 2, on_pace_streak: 2 },
215
+ { date: daysAgo(9), score: 78, restaurant_count: 1, shopping_count: 0, food_spend: 42.80, total_spend: 120.30, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 3, on_pace_streak: 3 },
216
+ { date: daysAgo(8), score: 65, restaurant_count: 0, shopping_count: 1, food_spend: 6.50, total_spend: 250.00, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 0, on_pace_streak: 0 },
217
+ { date: daysAgo(7), score: 80, restaurant_count: 0, shopping_count: 0, food_spend: 34.67, total_spend: 34.67, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 1, on_pace_streak: 1 },
218
+ { date: daysAgo(6), score: 85, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 15.99, zero_spend: 0, no_restaurant_streak: 3, no_shopping_streak: 2, on_pace_streak: 2 },
219
+ { date: daysAgo(5), score: 70, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 49.99, zero_spend: 0, no_restaurant_streak: 4, no_shopping_streak: 3, on_pace_streak: 3 },
220
+ { date: daysAgo(4), score: 82, restaurant_count: 0, shopping_count: 0, food_spend: 29.99, total_spend: 29.99, zero_spend: 0, no_restaurant_streak: 5, no_shopping_streak: 4, on_pace_streak: 4 },
221
+ { date: daysAgo(3), score: 74, restaurant_count: 1, shopping_count: 0, food_spend: 58.35, total_spend: 58.35, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 5, on_pace_streak: 5 },
222
+ { date: daysAgo(2), score: 83, restaurant_count: 0, shopping_count: 0, food_spend: 22.47, total_spend: 22.47, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 6, on_pace_streak: 6 },
223
+ { date: yesterday(), score: 88, restaurant_count: 0, shopping_count: 0, food_spend: 6.50, total_spend: 30.80, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 7, on_pace_streak: 7 },
224
+ { date: today(), score: 76, restaurant_count: 0, shopping_count: 0, food_spend: 5.75, total_spend: 855.74, zero_spend: 0, no_restaurant_streak: 3, no_shopping_streak: 8, on_pace_streak: 0 },
225
+ ];
226
+ // ─── Achievements ─── //
227
+ export const achievements = [
228
+ { key: "on_pace_7", name: "Clean Week", description: "7 consecutive days with all budgets on pace", unlocked_at: yesterday() },
229
+ { key: "no_shopping_7", name: "Window Shopper", description: "7 days with zero shopping purchases", unlocked_at: daysAgo(2) },
230
+ { key: "no_restaurant_7", name: "Kitchen Hero", description: "7-day no-restaurant streak", unlocked_at: daysAgo(5) },
231
+ { key: "zero_hero", name: "Zero Hero", description: "A zero-spend day", unlocked_at: daysAgo(10) },
232
+ ];
233
+ // ─── Net Worth History (30 days) ─── //
234
+ export const netWorthHistory = [];
235
+ {
236
+ const baseAssets = 539000;
237
+ const baseLiabilities = 287200;
238
+ for (let i = 29; i >= 0; i--) {
239
+ const jitter = Math.sin(i * 0.7) * 150 + (29 - i) * 130;
240
+ const assets = Math.round((baseAssets + jitter) * 100) / 100;
241
+ const liabJitter = Math.sin(i * 0.5) * 50 - (29 - i) * 8;
242
+ const liab = Math.round((baseLiabilities + liabJitter) * 100) / 100;
243
+ netWorthHistory.push({
244
+ date: daysAgo(i),
245
+ total_assets: assets,
246
+ total_liabilities: liab,
247
+ net_worth: Math.round((assets - liab) * 100) / 100,
248
+ });
249
+ }
250
+ }
251
+ // ─── Investment Transactions ─── //
252
+ export const investmentTransactions = [
253
+ { investment_transaction_id: "demo-inv-tx-1", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-aapl", date: daysAgo(30), name: "Buy AAPL", quantity: 5, amount: -1100.75, price: 220.15, fees: 0, type: "buy", subtype: "buy", iso_currency_code: "USD" },
254
+ { investment_transaction_id: "demo-inv-tx-2", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", date: daysAgo(45), name: "Buy VOO", quantity: 10, amount: -5284.00, price: 528.40, fees: 0, type: "buy", subtype: "buy", iso_currency_code: "USD" },
255
+ { investment_transaction_id: "demo-inv-tx-3", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", date: daysAgo(15), name: "Dividend VOO", quantity: 0, amount: -32.50, price: 0, fees: 0, type: "cash", subtype: "dividend", iso_currency_code: "USD" },
256
+ { investment_transaction_id: "demo-inv-tx-4", account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", date: daysAgo(14), name: "401k Contribution", quantity: 8.5, amount: -1006.83, price: 118.45, fees: 0, type: "buy", subtype: "contribution", iso_currency_code: "USD" },
257
+ { investment_transaction_id: "demo-inv-tx-5", account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", date: daysAgo(28), name: "401k Contribution", quantity: 8.5, amount: -1006.83, price: 118.45, fees: 0, type: "buy", subtype: "contribution", iso_currency_code: "USD" },
258
+ { investment_transaction_id: "demo-inv-tx-6", account_id: "demo-schwab-401k", security_id: "demo-sec-vbtlx", date: daysAgo(20), name: "Dividend VBTLX", quantity: 0, amount: -18.40, price: 0, fees: 0, type: "cash", subtype: "dividend", iso_currency_code: "USD" },
259
+ ];
260
+ // ─── Recurring Bills ─── //
261
+ export const recurringBills = [
262
+ { name: "Rent", amount: 1850, day_of_month: 1, type: "housing", account_id: CHK },
263
+ { name: "Mortgage", amount: 2100, day_of_month: 1, type: "housing", account_id: CHK },
264
+ { name: "Electric", amount: 135, day_of_month: 15, type: "utility", account_id: CHK },
265
+ { name: "Internet", amount: 79.99, day_of_month: 12, type: "utility", account_id: CHK },
266
+ { name: "Car Insurance", amount: 145, day_of_month: 20, type: "insurance", account_id: CHK },
267
+ ];
268
+ // ─── Memories ─── //
269
+ export const memories = [
270
+ { content: "User prefers index fund investing and dollar-cost averaging", category: "preference" },
271
+ { content: "User is saving for a trip to Japan next year", category: "goal" },
272
+ ];
@@ -0,0 +1,8 @@
1
+ /** Generate a minimal 4x4 solid-color PNG as base64 from a hex color string. */
2
+ export declare function colorPng(hex: string): string;
3
+ export declare const LOGOS: {
4
+ chase: string;
5
+ robinhood: string;
6
+ schwab: string;
7
+ amex: string;
8
+ };
@@ -0,0 +1,58 @@
1
+ import { deflateSync } from "zlib";
2
+ /** Generate a minimal 4x4 solid-color PNG as base64 from a hex color string. */
3
+ export function colorPng(hex) {
4
+ const r = parseInt(hex.slice(1, 3), 16);
5
+ const g = parseInt(hex.slice(3, 5), 16);
6
+ const b = parseInt(hex.slice(5, 7), 16);
7
+ // PNG signature
8
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
9
+ // IHDR chunk: 4x4, 8-bit RGB
10
+ const ihdrData = Buffer.alloc(13);
11
+ ihdrData.writeUInt32BE(4, 0); // width
12
+ ihdrData.writeUInt32BE(4, 4); // height
13
+ ihdrData[8] = 8; // bit depth
14
+ ihdrData[9] = 2; // color type: RGB
15
+ const ihdr = makeChunk("IHDR", ihdrData);
16
+ // IDAT chunk: raw pixel data (filter byte 0 + 4 RGB pixels per row)
17
+ const rowLen = 1 + 4 * 3; // filter byte + 4 pixels * 3 channels
18
+ const raw = Buffer.alloc(rowLen * 4);
19
+ for (let y = 0; y < 4; y++) {
20
+ const offset = y * rowLen;
21
+ raw[offset] = 0; // no filter
22
+ for (let x = 0; x < 4; x++) {
23
+ raw[offset + 1 + x * 3] = r;
24
+ raw[offset + 2 + x * 3] = g;
25
+ raw[offset + 3 + x * 3] = b;
26
+ }
27
+ }
28
+ const compressed = deflateSync(raw);
29
+ const idat = makeChunk("IDAT", compressed);
30
+ // IEND chunk
31
+ const iend = makeChunk("IEND", Buffer.alloc(0));
32
+ return Buffer.concat([signature, ihdr, idat, iend]).toString("base64");
33
+ }
34
+ function makeChunk(type, data) {
35
+ const len = Buffer.alloc(4);
36
+ len.writeUInt32BE(data.length);
37
+ const typeBuffer = Buffer.from(type, "ascii");
38
+ const crcInput = Buffer.concat([typeBuffer, data]);
39
+ const crc = Buffer.alloc(4);
40
+ crc.writeUInt32BE(crc32(crcInput) >>> 0);
41
+ return Buffer.concat([len, typeBuffer, data, crc]);
42
+ }
43
+ function crc32(buf) {
44
+ let c = 0xffffffff;
45
+ for (let i = 0; i < buf.length; i++) {
46
+ c ^= buf[i];
47
+ for (let j = 0; j < 8; j++) {
48
+ c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
49
+ }
50
+ }
51
+ return c ^ 0xffffffff;
52
+ }
53
+ export const LOGOS = {
54
+ chase: colorPng("#003087"),
55
+ robinhood: colorPng("#00C805"),
56
+ schwab: colorPng("#00A0DF"),
57
+ amex: colorPng("#006FCF"),
58
+ };
@@ -0,0 +1 @@
1
+ export declare function seedDemoDb(dbPath: string, encryptionKey?: string): void;
@@ -0,0 +1,125 @@
1
+ import Database from "libsql";
2
+ import { resolve, dirname } from "path";
3
+ import { mkdirSync, existsSync, unlinkSync } from "fs";
4
+ import { migrate } from "../db/schema.js";
5
+ import { institutions, accounts, transactions, securities, holdings, liabilities, recurring, budgets, goals, dailyScores, achievements, netWorthHistory, investmentTransactions, recurringBills, memories, } from "./data.js";
6
+ export function seedDemoDb(dbPath, encryptionKey) {
7
+ const resolved = resolve(dbPath);
8
+ const dir = dirname(resolved);
9
+ mkdirSync(dir, { recursive: true });
10
+ // Remove existing demo DB for a clean slate
11
+ for (const suffix of ["", "-wal", "-shm"]) {
12
+ const f = resolved + suffix;
13
+ if (existsSync(f))
14
+ unlinkSync(f);
15
+ }
16
+ const opts = {};
17
+ if (encryptionKey) {
18
+ opts.encryptionCipher = "aes256cbc";
19
+ opts.encryptionKey = encryptionKey;
20
+ }
21
+ const db = new Database(dbPath, opts);
22
+ db.pragma("journal_mode = WAL");
23
+ db.pragma("foreign_keys = ON");
24
+ migrate(db);
25
+ const seed = db.transaction(() => {
26
+ // Institutions
27
+ const instStmt = db.prepare(`INSERT INTO institutions (item_id, access_token, name, products, logo, primary_color) VALUES (?, ?, ?, ?, ?, ?)`);
28
+ for (const i of institutions) {
29
+ instStmt.run(i.item_id, i.access_token, i.name, i.products, i.logo, i.primary_color);
30
+ }
31
+ // Accounts
32
+ const acctStmt = db.prepare(`INSERT INTO accounts (account_id, item_id, name, official_name, type, subtype, mask, current_balance, available_balance, currency, balance_limit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
33
+ for (const a of accounts) {
34
+ acctStmt.run(a.account_id, a.item_id, a.name, a.official_name, a.type, a.subtype, a.mask, a.current_balance, a.available_balance, a.currency, a.balance_limit);
35
+ }
36
+ // Transactions
37
+ const txStmt = db.prepare(`INSERT INTO transactions (transaction_id, account_id, amount, date, name, merchant_name, category, subcategory, pending, payment_channel) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
38
+ for (const t of transactions) {
39
+ txStmt.run(t.transaction_id, t.account_id, t.amount, t.date, t.name, t.merchant_name, t.category, t.subcategory, t.pending, t.payment_channel);
40
+ }
41
+ // Securities
42
+ const secStmt = db.prepare(`INSERT INTO securities (security_id, name, ticker, type, close_price, close_price_as_of) VALUES (?, ?, ?, ?, ?, ?)`);
43
+ for (const s of securities) {
44
+ secStmt.run(s.security_id, s.name, s.ticker, s.type, s.close_price, s.close_price_as_of);
45
+ }
46
+ // Holdings
47
+ const holdStmt = db.prepare(`INSERT INTO holdings (account_id, security_id, quantity, value, cost_basis, price, price_as_of, vested_value, vested_quantity) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
48
+ for (const h of holdings) {
49
+ holdStmt.run(h.account_id, h.security_id, h.quantity, h.value, h.cost_basis, h.price, h.price_as_of, h.vested_value, h.vested_quantity);
50
+ }
51
+ // Liabilities
52
+ const liabStmt = db.prepare(`INSERT INTO liabilities (account_id, type, interest_rate, origination_date, original_balance, current_balance, minimum_payment, next_payment_due, last_payment_amount, last_payment_date, credit_limit, last_statement_issue_date, is_overdue, apr_type, maturity_date, loan_type, property_address, escrow_balance, loan_status, loan_name, repayment_plan, expected_payoff_date, ytd_interest_paid, ytd_principal_paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
53
+ for (const l of liabilities) {
54
+ liabStmt.run(l.account_id, l.type, l.interest_rate, l.origination_date, l.original_balance, l.current_balance, l.minimum_payment, l.next_payment_due, l.last_payment_amount, l.last_payment_date, l.credit_limit, l.last_statement_issue_date, l.is_overdue, l.apr_type, l.maturity_date, l.loan_type, l.property_address, l.escrow_balance, l.loan_status, l.loan_name, l.repayment_plan, l.expected_payoff_date, l.ytd_interest_paid, l.ytd_principal_paid);
55
+ }
56
+ // Recurring
57
+ const recStmt = db.prepare(`INSERT INTO recurring (stream_id, account_id, merchant_name, description, frequency, category, subcategory, avg_amount, last_amount, first_date, last_date, is_active, status, stream_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
58
+ for (const r of recurring) {
59
+ recStmt.run(r.stream_id, r.account_id, r.merchant_name, r.description, r.frequency, r.category, r.subcategory, r.avg_amount, r.last_amount, r.first_date, r.last_date, r.is_active, r.status, r.stream_type);
60
+ }
61
+ // Budgets
62
+ const budgetStmt = db.prepare(`INSERT INTO budgets (category, monthly_limit, period) VALUES (?, ?, ?)`);
63
+ for (const b of budgets) {
64
+ budgetStmt.run(b.category, b.monthly_limit, b.period);
65
+ }
66
+ // Goals
67
+ const goalStmt = db.prepare(`INSERT INTO goals (name, target_amount, current_amount, target_date, status) VALUES (?, ?, ?, ?, ?)`);
68
+ for (const g of goals) {
69
+ goalStmt.run(g.name, g.target_amount, g.current_amount, g.target_date, g.status);
70
+ }
71
+ // Daily Scores
72
+ const scoreStmt = db.prepare(`INSERT INTO daily_scores (date, score, restaurant_count, shopping_count, food_spend, total_spend, zero_spend, no_restaurant_streak, no_shopping_streak, on_pace_streak) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
73
+ for (const s of dailyScores) {
74
+ scoreStmt.run(s.date, s.score, s.restaurant_count, s.shopping_count, s.food_spend, s.total_spend, s.zero_spend, s.no_restaurant_streak, s.no_shopping_streak, s.on_pace_streak);
75
+ }
76
+ // Achievements
77
+ const achStmt = db.prepare(`INSERT INTO achievements (key, name, description, unlocked_at) VALUES (?, ?, ?, ?)`);
78
+ for (const a of achievements) {
79
+ achStmt.run(a.key, a.name, a.description, a.unlocked_at);
80
+ }
81
+ // Net Worth History
82
+ const nwStmt = db.prepare(`INSERT INTO net_worth_history (date, total_assets, total_liabilities, net_worth) VALUES (?, ?, ?, ?)`);
83
+ for (const nw of netWorthHistory) {
84
+ nwStmt.run(nw.date, nw.total_assets, nw.total_liabilities, nw.net_worth);
85
+ }
86
+ // Investment Transactions
87
+ const invTxStmt = db.prepare(`INSERT INTO investment_transactions (investment_transaction_id, account_id, security_id, date, name, quantity, amount, price, fees, type, subtype, iso_currency_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
88
+ for (const it of investmentTransactions) {
89
+ invTxStmt.run(it.investment_transaction_id, it.account_id, it.security_id, it.date, it.name, it.quantity, it.amount, it.price, it.fees, it.type, it.subtype, it.iso_currency_code);
90
+ }
91
+ // Recurring Bills
92
+ const billStmt = db.prepare(`INSERT INTO recurring_bills (name, amount, day_of_month, type, account_id) VALUES (?, ?, ?, ?, ?)`);
93
+ for (const b of recurringBills) {
94
+ billStmt.run(b.name, b.amount, b.day_of_month, b.type, b.account_id);
95
+ }
96
+ // Memories
97
+ const memStmt = db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`);
98
+ for (const m of memories) {
99
+ memStmt.run(m.content, m.category);
100
+ }
101
+ });
102
+ seed();
103
+ console.log("Demo database seeded successfully!\n");
104
+ console.log(` Institutions: ${institutions.length}`);
105
+ console.log(` Accounts: ${accounts.length}`);
106
+ console.log(` Transactions: ${transactions.length}`);
107
+ console.log(` Securities: ${securities.length}`);
108
+ console.log(` Holdings: ${holdings.length}`);
109
+ console.log(` Liabilities: ${liabilities.length}`);
110
+ console.log(` Recurring: ${recurring.length}`);
111
+ console.log(` Budgets: ${budgets.length}`);
112
+ console.log(` Goals: ${goals.length}`);
113
+ console.log(` Daily Scores: ${dailyScores.length}`);
114
+ console.log(` Achievements: ${achievements.length}`);
115
+ console.log(` Net Worth Days: ${netWorthHistory.length}`);
116
+ console.log(` Invest. Txns: ${investmentTransactions.length}`);
117
+ console.log(` Recurring Bills: ${recurringBills.length}`);
118
+ console.log(` Memories: ${memories.length}`);
119
+ console.log(`\n Database: ${resolve(dbPath)}`);
120
+ console.log(`\n Try it out:`);
121
+ console.log(` ray --demo status`);
122
+ console.log(` ray --demo accounts`);
123
+ console.log(` ray --demo spending`);
124
+ db.close();
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ray-finance",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Local-first CLI that turns your bank data into a personal AI financial advisor",
5
5
  "type": "module",
6
6
  "license": "MIT",