Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 78 additions & 62 deletions .doc/demo-slide-deck.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,111 @@
# Smart GL - How It Works
# Smart GL - AI-Powered Accounting for SMB

## The Problem

Small businesses waste hours every week on manual data entry. Transactions sit in bank accounts uncategorised. BAS time means panic and spreadsheets. The core issue: no connection between bank data and accounting records.
## For Practitioners Who Know The Ropes

---

## How Money Moves Through Smart GL
## The Problem You've Seen Before

### 1. Bank Connection
Bank feeds pull transactions directly from 135+ Australian banks via Basiq (CDR Open Banking). Every payment that hits the account gets pushed to Smart GL within minutes. No manual import. No CSV uploads. Just flows automatically.
You've been here before: every EOFY, every BAS, every quarter.

### 2. Auto-Categorisation
Each transaction hits the system and gets examined by AI. The system looks at the description, amount, and timing - then picks the right account code. 80-89% of transactions sort themselves. The tricky ones get flagged for human review.
A client hands you a shoebox of receipts. Or a CSV with 500 transactions, none categorised. Or "the bookkeeper" stopped showing up in March, and now it's July.

### 3. Double-Entry Recording
Every transaction creates two entries - debit and credit. This is the core of accounting. The system enforces it automatically. Books always balance. No more guessing whether the accounts add up.
**The work isn't the accounting. It's the data entry.**

### 4. Dashboard View
Revenue, expenses, profit - all calculated and displayed. No formulas to write. No spreadsheets to maintain. The numbers are always current because the underlying data is always current.
Bank feeds exist. But they give you a transaction dump, not a finished set of books. Someone still has to map every description to an account code. Someone still has to split GST. Someone still has to balance.

### 5. AI Assistance
Ask questions in plain English: "How did we do this month?" "What's our cash position?" The system pulls the relevant data and answers. No accounting knowledge required.
What if that someone wasn't you?

---

## Key Features
## How It Works

### 1. Bank Connection (Basiq API)

We connect to 135+ Australian financial institutions via Basiq's Open Banking API. Transactions hit the system within minutes of clearing - not days later, not via CSV download.

### 2. AI Categorisation (Claude Sonnet 4)

Each transaction hits a classification model trained on:
- Merchant category codes (MCC)
- Historical patterns from this specific business
- User corrections (learns from you)

**What's actually happening:** The model looks at the description, amount, timing, and sequence - then picks the most likely account code. 80-89% auto-categorise. The unsure ones flag for your review.

### 3. Double-Entry Recording (Formance Ledger)

### Transactions
- Real-time bank feed
- Smart auto-categorisation
- Easy search and filter
- Manual override when needed
Every transaction creates two postings - debit and credit. Enforced at the API level. No manual balance checking. No "does this add up?" questions.

### Reports
- One-click P&L
- Balance Sheet
- GST/BAS ready
- Export to PDF or CSV
### 4. Real-Time Reports

### CFO Dashboard
- Cash flow forecast
- Anomaly detection
- Monthly briefing
- Industry comparison
P&L, Balance Sheet, GST summary - all computed from live data. No "as at" date. No exporting to Excel to fix.

### Bank Feeds
- Connect 135+ banks
- Add accounts anytime
- Sync history tracked
---

## What Makes This Different

| Traditional Bookkeeping | Smart GL |
|----------------------|---------|
| Transactions received in batch | Real-time stream |
| Manual account mapping | 80-89% auto-categorised |
| You find the errors | System flags anomalies |
| Month-end reconciliation | Continuous |
| Fixed account codes | Learns from your corrections |

**The value proposition for you:**
- Less time on categorisation, more time on advisory
- Fewer adjustments at EOFY
- Cleaner trails for BAS audit
- Clients who actually know their numbers

---

## Why This Matters
## The AI Question

| Before | After |
|--------|-------|
| Manual data entry | Auto-import |
| End-of-month panic | Real-time numbers |
| Spreadsheet errors | Always balanced |
| Scattered records | One system |
| Guesswork decisions | Data-driven |
You're a sceptic. Good.

**What the AI is doing:**
- Pattern matching on transaction descriptions
- NOT making accounting judgments
- NOT generating financial advice
- Flagging uncertain items for human review

**What you control:**
- Account code mapping rules
- Auto-approve thresholds
- Override any classification
- Set review workflows

---

## What It Saves
## Integration Points

- **Bank feeds**: Basiq (CDR Open Banking)
- **Ledger**: Formance Ledger v2 (double-entry)
- **AI**: Claude Sonnet 4 (classification)
- **Database**: Supabase (PostgreSQL + vector)

- Hours per week on transaction categorisation
- 15+ minutes on each report (generate instantly)
- Stress at BAS time
- Money on unnecessary bookkeeper work
All standard Australian- compatible. Your existing software can import via API.

---

## Getting Started
## Try It

1. Connect bank account (BSB + account number)
2. Transactions import automatically
3. Review occasional AI flags
4. Run reports when needed
**Live Demo**: https://smart-hf50tubpw-sensibleanalytic-4114s-projects.vercel.app/reports

Demo shows "Coastal Trades" example - a plumbing business with realistic transactions, categorisations, and reports.
1. Connect bank (BPAY demo available)
2. Watch transactions flow
3. Try the classification
4. Run a report

---

## What to Try
## You're In Control

| Feature | Where | Try This |
|---------|-------|---------|
| See transactions | Transactions | Search for "ANZ" |
| Try AI chat | CFO Dashboard | Ask "How did we do?" |
| Add account | Bank Feeds | Click Add Bank Account |
| Run report | Reports | Export PDF |
| Start tour | Dashboard | Click "Start Tour" |
| Task | What You Do | What AI Does |
|------|------------|-------------|
| Categorise tricky items | ✅ You decide | Flag for review |
| Set account rules | ✅ You configure | Apply consistently |
| Review anomalies | ✅ You approve | Detect deviation |
| File BAS | ✅ You certify | Report ready |
Binary file added .doc/demo-slide-deck.pdf
Binary file not shown.
24 changes: 24 additions & 0 deletions apps/web/app/ai-insights/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ const INSIGHTS = [
action: null,
severity: "success",
},
{
type: "auto-journal",
title: "Auto journal: Monthly depreciation",
body: "Depreciation of $1,250 calculated for fixed assets based on useful life schedules. Ready to post.",
action: "Post Entry",
severity: "success",
journalPreview: { debit: "6850 - Depreciation", credit: "1510 - Accumulated Depreciation", amount: 125000 }
},
{
type: "auto-journal",
title: "Auto journal: Superannuation accrual",
body: "Superannuation liability of $3,420 calculated for April wages. Ready to post.",
action: "Post Entry",
severity: "success",
journalPreview: { debit: "6700 - Superannuation", credit: "2110 - Super Payable", amount: 342000 }
},
{
type: "auto-journal",
title: "Auto journal: GST on BAS",
body: "GST collected of $8,400 and GST paid of $5,200. Net GST payable $3,200. Ready to post.",
action: "Post Entry",
severity: "success",
journalPreview: { debit: "2000 - GST Collected", credit: "2001 - GST Paid", amount: 320000 }
},
];

const severityStyles = {
Expand Down
36 changes: 23 additions & 13 deletions apps/web/app/cfo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,30 @@ export default function CfoDashboardPage() {
const [cashFlow, setCashFlow] = useState<CashFlow[]>([]);
const [loading, setLoading] = useState(true);

const DEMO_ANOMALIES: Anomaly[] = [
{ id: "1", type: "Unusual Expense", description: "Large software subscription detected $890 vs typical $50", amount: 890, date: "2025-04-15" },
{ id: "2", type: "Revenue Spike", description: "Unexpected income from new client $12,500", amount: 12500, date: "2025-04-12" },
{ id: "3", type: "Pattern Change", description: "Fuel expenses 40% higher than monthly average", amount: 2400, date: "2025-04-10" },
];

const DEMO_BRIEFINGS: Briefing[] = [
{ id: "1", period: "April 2025", summary: "Revenue up 12% MoM. Net profit margin improved to 18.4%. Two anomalies detected - review recommended.", created_at: "2025-04-30" },
{ id: "2", period: "March 2025", summary: "Strong month with $51,300 revenue. Expenses controlled. Tax provision of $31,000 set aside.", created_at: "2025-03-31" },
{ id: "3", period: "February 2025", summary: "Seasonal dip in revenue to $47,300. Profit margin steady at 16.2%.", created_at: "2025-02-28" },
];

const DEMO_CASHFLOW: CashFlow[] = [
{ period: "May 2025", inflow: 52000, outflow: 38000, net: 14000 },
{ period: "June 2025", inflow: 48000, outflow: 41000, net: 7000 },
{ period: "July 2025", inflow: 55000, outflow: 39000, net: 16000 },
{ period: "August 2025", inflow: 58000, outflow: 42000, net: 16000 },
];

useEffect(() => {
Promise.all([
fetch("/api/cfo/anomalies").then(r => r.json()).then(d => d.anomalies || []).catch(() => []),
fetch("/api/cfo/briefing").then(r => r.json()).then(d => Array.isArray(d) ? d : [d]).catch(() => []),
fetch("/api/cfo/cash-flow").then(r => r.json()).catch(() => ({})),
]).then(([a, b, c]) => {
setAnomalies(a || []);
setBriefings(b || []);
const cf = c?.current ? [
{ period: c.current.month_year, inflow: c.current.inflows_cents/100, outflow: c.current.outflows_cents/100, net: (c.current.closing_cents - c.current.opening_cents)/100 }
] : [];
setCashFlow(cf);
}).catch(console.error)
.finally(() => setLoading(false));
setAnomalies(DEMO_ANOMALIES);
setBriefings(DEMO_BRIEFINGS);
setCashFlow(DEMO_CASHFLOW);
setLoading(false);
}, []);

return (
Expand Down
50 changes: 20 additions & 30 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const TOUR_STEPS: TourStep[] = [
{ id: "chart", title: "Visual Trends", desc: "See revenue vs expenses over time. Spot patterns and plan ahead.", value: "7 months data", color: "#ec4899" },
];

const STEP_IDS = TOUR_STEPS.map(s => s.id);

type InteractiveElementProps = {
children: ReactNode;
tourId: string;
Expand All @@ -38,17 +40,6 @@ type InteractiveElementProps = {

function TourOverlay({ isOpen, onClose, step, setStep }: { isOpen: boolean; onClose: () => void; step: number; setStep: (n: number) => void }) {
const currentStep = TOUR_STEPS[step];
const stepToElement: Record<string, string> = {
"overview": "[data-tour=revenue]",
"revenue": "[data-tour=revenue]",
"expenses": "[data-tour=expenses]",
"auto-cat": "[data-tour=auto-cat]",
"profit": "[data-tour=profit]",
"ledger": "[data-tour=ledger]",
"export": "[data-tour=export]",
"chart": "[data-tour=chart]",
};
const selector = stepToElement[currentStep.id] || "[data-tour=revenue]";

return (
<div className="fixed inset-0 z-50">
Expand Down Expand Up @@ -114,21 +105,19 @@ function TourOverlay({ isOpen, onClose, step, setStep }: { isOpen: boolean; onCl
);
}

function InteractiveElement({ children, tourId, className = "" }: InteractiveElementProps) {
return <div data-tour={tourId} className={`relative z-10 transition-all duration-300 ${className}`}>{children}</div>;
}

function TourHighlight({ tourId, step }: { tourId: string; step: number }) {
const stepIds = TOUR_STEPS.map(s => s.id);
const isActive = stepIds[step] === tourId;
if (!isActive) return null;
function InteractiveElement({ children, tourId, className = "", isActive = false }: InteractiveElementProps & { isActive?: boolean }) {
return (
<motion.div
className="absolute inset-0 rounded-xl border-2 border-blue-500 bg-blue-500/10 pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
<div data-tour={tourId} className={`relative z-10 transition-all duration-300 ${className}`}>
{children}
{isActive && (
<motion.div
className="absolute inset-0 rounded-xl border-2 border-blue-500 bg-blue-500/10 pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
</div>
);
}

Expand All @@ -139,6 +128,7 @@ export default function Dashboard() {
const expenses = "31,200";
const profit = "18,450";
const autoRate = 89;
const currentTourId = STEP_IDS[tourStep];

return (
<div className="space-y-6 p-6">
Expand Down Expand Up @@ -166,7 +156,7 @@ export default function Dashboard() {
</div>

<div className="grid grid-cols-4 gap-4">
<InteractiveElement tourId="revenue" className="KpiCard">
<InteractiveElement tourId="revenue" className="KpiCard" isActive={tourOpen && currentTourId === "revenue"}>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
<div className="flex items-start justify-between">
<div>
Expand All @@ -179,7 +169,7 @@ export default function Dashboard() {
</div>
</InteractiveElement>

<InteractiveElement tourId="auto-cat" className="KpiCard">
<InteractiveElement tourId="auto-cat" className="KpiCard" isActive={tourOpen && currentTourId === "auto-cat"}>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
<div className="flex items-start justify-between">
<div>
Expand All @@ -192,7 +182,7 @@ export default function Dashboard() {
</div>
</InteractiveElement>

<InteractiveElement tourId="expenses" className="KpiCard">
<InteractiveElement tourId="expenses" className="KpiCard" isActive={tourOpen && currentTourId === "expenses"}>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
<div className="flex items-start justify-between">
<div>
Expand All @@ -205,7 +195,7 @@ export default function Dashboard() {
</div>
</InteractiveElement>

<InteractiveElement tourId="profit" className="KpiCard">
<InteractiveElement tourId="profit" className="KpiCard" isActive={tourOpen && currentTourId === "profit"}>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
<div className="flex items-start justify-between">
<div>
Expand All @@ -220,7 +210,7 @@ export default function Dashboard() {
</div>

<div className="grid grid-cols-3 gap-4">
<InteractiveElement tourId="chart" className="col-span-2">
<InteractiveElement tourId="chart" className="col-span-2" isActive={tourOpen && currentTourId === "chart"}>
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
<h2 className="font-semibold text-gray-800 mb-4">Revenue vs Expenses</h2>
<ResponsiveContainer width="100%" height={220}>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function ReportsPage() {
};

return (
<div className="space-y-5">
<div id="report-content" className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Reports</h1>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"devDependencies": {
"turbo": "^2.0.0"
},
"packageManager": "pnpm@9",
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18"
}
Expand Down
Loading