Analytics Dashboard
This guide covers pulling Ozzie data into a BI or analytics dashboard. You can aggregate transaction data, track plan adherence, measure goal completion rates, and build reporting views β either for your own internal use or as a customer-facing product feature.
What data is availableβ
| Data type | Endpoint | Key fields |
|---|---|---|
| Transactions | GET /users/{id}/transactions | amount, category, merchant, date, source |
| Plan allocations | POST /users/{id}/plan | needs/wants/savings/debt % breakdown |
| Goal progress | GET /users/{id}/goals | target_amount, starting_amount, next_move_date |
| Money move completion | GET /users/{id}/money-moves | status per cycle, task completion rates |
| Financial intake | GET /users/{id}/financial-intake | income, expenses, debt snapshot |
Key metrics you can computeβ
| Metric | How to compute |
|---|---|
| Monthly spending by category | Sum amount_cents grouped by category for transactions in the current month |
| Savings rate | (monthly_net_income - total_expenses) / monthly_net_income * 100 from intake |
| Goal progress % | (current_amount - starting_amount) / (target_amount - starting_amount) * 100 |
| Money move completion rate | completed cycles / total cycles * 100 per user or across all users |
| Average savings per cycle | Sum of amount_cents for do tasks marked done, divided by cycle count |
| Plan adherence | Compare actual spending categories from transactions vs. plan allocations targets |
Pagination strategy for bulk exportβ
Ozzie uses cursor-based pagination on all list endpoints. For a full data export, iterate until has_more is false:
// lib/ozzie.js
import { ozzieRequest } from './ozzie-client.js';
/**
* Fetches all pages of a list endpoint and returns the full dataset.
* @param {string} path - e.g. `/users/usr_abc/transactions`
* @param {Object} params - Query parameters (excluding cursor)
*/
export async function fetchAllPages(path, params = {}) {
const allItems = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const queryParams = new URLSearchParams({ limit: '50', ...params });
if (cursor) queryParams.set('cursor', cursor);
const { data, pagination } = await ozzieRequest('GET', `${path}?${queryParams}`);
allItems.push(...data);
hasMore = pagination.has_more;
cursor = pagination.next_cursor;
}
return allItems;
}
Data export functionβ
This function exports all financial data for a user into a structured object ready for a BI tool or data warehouse:
// lib/export.js
import { fetchAllPages, ozzieRequest } from './ozzie.js';
export async function exportUserFinancialData(ozzieUserId) {
const [transactions, moneyMoves, goalsRes, planRes, intakeRes] = await Promise.all([
fetchAllPages(`/users/${ozzieUserId}/transactions`),
fetchAllPages(`/users/${ozzieUserId}/money-moves`),
ozzieRequest('GET', `/users/${ozzieUserId}/goals`).catch(() => ({ data: null })),
ozzieRequest('POST', `/users/${ozzieUserId}/plan`).catch(() => ({ data: null })),
ozzieRequest('GET', `/users/${ozzieUserId}/financial-intake`).catch(() => ({ data: null })),
]);
return {
user_id: ozzieUserId,
exported_at: new Date().toISOString(),
transactions,
money_moves: moneyMoves,
goal: goalsRes.data,
plan: planRes.data,
intake: intakeRes.data,
};
}
Sample queries and API callsβ
Monthly spending by categoryβ
Fetch all transactions for a month and aggregate by category:
async function getMonthlySpendingByCategory(ozzieUserId, year, month) {
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of month
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
start_date: startDate,
end_date: endDate,
});
// Group and sum by category
const byCategory = transactions.reduce((acc, tx) => {
const category = tx.category ?? 'Uncategorized';
acc[category] = (acc[category] ?? 0) + tx.amount_cents;
return acc;
}, {});
// Convert cents to dollars and sort descending
return Object.entries(byCategory)
.map(([category, cents]) => ({ category, amount: cents / 100 }))
.sort((a, b) => b.amount - a.amount);
}
// Example output:
// [
// { category: "Housing", amount: 1200.00 },
// { category: "Food & Dining", amount: 487.50 },
// { category: "Transportation", amount: 210.00 },
// { category: "Entertainment", amount: 134.00 },
// { category: "Utilities", amount: 120.00 },
// ]
Savings rate over timeβ
Compute the savings rate for each of the past N months across your user base:
async function getSavingsRateTrend(ozzieUserIds, monthsBack = 6) {
const results = [];
for (let i = 0; i < monthsBack; i++) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const label = `${year}-${String(month).padStart(2, '0')}`;
let totalSaved = 0;
let totalIncome = 0;
for (const userId of ozzieUserIds) {
const { data: intake } = await ozzieRequest('GET', `/users/${userId}/financial-intake`).catch(() => ({ data: null }));
if (!intake) continue;
const monthTransactions = await fetchAllPages(`/users/${userId}/transactions`, {
start_date: `${year}-${String(month).padStart(2, '0')}-01`,
});
const spent = monthTransactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const income = intake.monthly_net_income;
const saved = Math.max(0, income - spent);
totalIncome += income;
totalSaved += saved;
}
results.push({
month: label,
savings_rate: totalIncome > 0 ? ((totalSaved / totalIncome) * 100).toFixed(1) : '0.0',
total_saved: totalSaved.toFixed(2),
});
}
return results.reverse(); // Oldest first
}
// Example output:
// [
// { month: "2024-12", savings_rate: "9.2", total_saved: "3450.00" },
// { month: "2025-01", savings_rate: "11.4", total_saved: "4275.00" },
// { month: "2025-02", savings_rate: "10.8", total_saved: "4050.00" },
// { month: "2025-03", savings_rate: "13.1", total_saved: "4912.50" },
// { month: "2025-04", savings_rate: "14.7", total_saved: "5512.50" },
// { month: "2025-05", savings_rate: "15.0", total_saved: "5625.00" },
// ]
Goal completion percentageβ
Compute progress toward a savings or debt goal:
async function getGoalProgress(ozzieUserId) {
const { data: goal } = await ozzieRequest('GET', `/users/${ozzieUserId}/goals`);
if (!goal) return null;
const { goal_type, goal_name, target_amount, starting_amount } = goal;
if (goal_type === 'savings') {
// For savings: progress is current balance vs target
// (In a real app, current balance = starting_amount + total deposits since goal creation)
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
category: 'Savings Transfer',
});
const deposited = transactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const currentAmount = starting_amount + deposited;
const progressPct = Math.min(100, (currentAmount / target_amount) * 100);
return {
goal_name,
goal_type,
starting_amount,
current_amount: currentAmount,
target_amount,
progress_pct: progressPct.toFixed(1),
remaining: Math.max(0, target_amount - currentAmount),
};
}
if (goal_type === 'debt') {
// For debt: progress is how much has been paid down from starting_amount
const transactions = await fetchAllPages(`/users/${ozzieUserId}/transactions`, {
category: 'Debt Payment',
});
const paid = transactions.reduce((sum, tx) => sum + tx.amount_cents, 0) / 100;
const remaining = Math.max(0, starting_amount - paid);
const progressPct = Math.min(100, (paid / starting_amount) * 100);
return {
goal_name,
goal_type,
starting_amount,
paid_down: paid,
remaining,
target_amount: 0,
progress_pct: progressPct.toFixed(1),
};
}
}
// Example output (savings goal):
// {
// goal_name: "Emergency Fund",
// goal_type: "savings",
// starting_amount: 1200.00,
// current_amount: 2850.00,
// target_amount: 10000.00,
// progress_pct: "28.5",
// remaining: 7150.00
// }
Money move completion rateβ
async function getMoneyMoveCompletionRate(ozzieUserId) {
const allMoves = await fetchAllPages(`/users/${ozzieUserId}/money-moves`);
const terminal = allMoves.filter(m => ['completed', 'skipped'].includes(m.status));
const completed = allMoves.filter(m => m.status === 'completed');
if (terminal.length === 0) return { completion_rate: null, cycles: 0 };
return {
total_cycles: terminal.length,
completed_cycles: completed.length,
skipped_cycles: terminal.length - completed.length,
completion_rate: ((completed.length / terminal.length) * 100).toFixed(1),
};
}
Bulk export across all usersβ
// scripts/export-all.js β run as a nightly batch job
import { db } from '../lib/db.js';
import { exportUserFinancialData } from '../lib/export.js';
import { writeFileSync } from 'fs';
async function exportAll() {
const users = await db.users.findAll({ where: { ozzie_user_id: { not: null } } });
console.log(`Exporting data for ${users.length} users...`);
const results = [];
const errors = [];
for (const user of users) {
try {
const data = await exportUserFinancialData(user.ozzie_user_id);
results.push({ internal_user_id: user.id, ...data });
} catch (err) {
errors.push({ user_id: user.id, error: err.message });
}
// Rate-limit-friendly delay between users
await new Promise(r => setTimeout(r, 100));
}
const output = {
exported_at: new Date().toISOString(),
user_count: results.length,
error_count: errors.length,
users: results,
errors,
};
writeFileSync(`exports/ozzie-${Date.now()}.json`, JSON.stringify(output, null, 2));
console.log(`Export complete. ${results.length} users, ${errors.length} errors.`);
}
exportAll();
For very large user bases (10k+ users), run exports in parallel batches of 10β20 users, staying within your rate limit. Check X-RateLimit-Remaining response headers to avoid hitting the limit.
Suggested chartsβ
Spending breakdown β pie or donut chartβ
Use getMonthlySpendingByCategory() data:
Category | Amount | % of Total
--------------------|---------|------------
Housing | $1,200 | 45.6%
Food & Dining | $487 | 18.5%
Transportation | $210 | 8.0%
Entertainment | $134 | 5.1%
Utilities | $120 | 4.6%
Other | $480 | 18.2%
Chart type: Pie or donut. Color-code by category. Add a center label showing total monthly spend.
Overlay: Draw a dotted ring showing the plan's allocations.wants and allocations.needs targets to show budget vs. actual.
Savings rate trend β line chartβ
Use getSavingsRateTrend() data over 6β12 months:
Month | Savings Rate
---------|-------------
Dec 2024 | 9.2%
Jan 2025 | 11.4%
Feb 2025 | 10.8%
Mar 2025 | 13.1%
Apr 2025 | 14.7%
May 2025 | 15.0%
Chart type: Line chart with monthly x-axis. Add a horizontal dashed line at the plan's target_savings_rate so users can see when they crossed the threshold.
Goal progress β horizontal progress barβ
Use getGoalProgress() data:
Emergency Fund
[$1,200 ββββββββββββββββββββββββββββββββ $10,000]
28.5% β $7,150 to go β Est. Nov 2029
Chart type: Horizontal progress bar with label showing percentage, amount remaining, and projected date from plan.key_metrics.projected_goal_date.
Money move completion β bar or gauge chartβ
Use getMoneyMoveCompletionRate() data:
Completion Rate: 78% (14 completed / 18 total cycles)
Chart type: Gauge or simple stat card. For a user-facing dashboard, show a streak counter (consecutive completed cycles) to encourage consistency.