⊞ Dashboard
Import a statement to get started
Total Spend
$0.00
Import a statement to begin
$
Transactions
0
—
≡
Auto-Code Rate
—
After first import
🎯
Needs Attention
0
Unassigned · 0 missing receipts
⚠
Spend by category
No data
Spend by job
MTD
Recent transactions
| Date | Merchant | Card | Job | Cost Code | Category | Amount | Receipt |
|---|
↓ Import
Drop any bank statement CSV to parse, auto-code, and load transactions
Processing: file.csv
Parsing
Detecting bank format and parsing columns
Matching vendor rule library (40+ rules)
Applying keyword coding for unrecognized vendors
Checking for duplicates and writing to ledger
Drop a bank statement CSV or click to browse
Transactions will be parsed, auto-coded, and loaded into the ledger
Chase
Amex
Bank of America
Capital One
US Bank
Wells Fargo
Generic CSV
Supported statement formats
🏦 Chase Business
Transaction Date, Post Date, Description, Category, Type, Amount
💳 Amex Business
Date, Description, Amount, Extended Details
🏛️ Bank of America
Posted Date, Reference Number, Payee, Address, Amount
🔴 Capital One
Transaction Date, Posted Date, Card No., Description, Debit, Credit
🏦 US Bank
Date, Transaction, Name, Memo, Amount
📄 Generic CSV
Any CSV with date, description, amount — auto-detects columns
≡ Ledger
Click any Job or Cost Code cell to reassign. Select rows to bulk-assign.
| Date | Merchant | Card · Holder | Job ▾ | Cost Code | Category | Amount | Conf. | Receipt | Note | Flags |
|---|
▣ Cards
Manage cardholders, set default jobs, and view per-card spend
⇌ Assignments
Assign default jobs to cards and manage cost codes per job site
Card → Default Job
Charges auto-assign to this job by default
Job → Active Cost Codes
Click a job to manage its cost codes
Vendor overrides for: —
These rules override global vendor rules for this job only. Higher confidence than global rules.
Select a job above to see its vendor overrides
◈ Code Library
CSI MasterFormat library — 37 codes preloaded. Add custom codes as needed.
Jobs
—
| Code | Name | Spend | Budget | Status |
|---|
Cost Codes
—
| Code | Description | Category | Scope | Spend |
|---|
⚡ Vendor Rules
Pattern-matching rules that auto-assign cost codes on import. 40 rules preloaded.
△ Reports
Spend analysis by job, cost code, and trend
Spend by job
Category mix
Job summary
| Job | Name | Materials | Fuel | Equip | Parts | Other | Total | Budget |
|---|
◎ Activity log
Complete audit trail of all changes with before/after values
📂 History & snapshots
Monthly spend summaries and all imported statements
📅 Monthly snapshots
Click "View in ledger" to filter transactions by month
◉ Settings
Company profile, integrations, and notification settings
🏢 Company
🤖 AI configuration
🔔 Budget alerts
🗄️ Supabase
📤 Export
💬 SMS alerts
👥 Team access
Company profile
Applied to all exported reports and PDFs
AI configuration
Controls how unrecognized vendors are coded on import
Transactions below this threshold go to manual review
Budget alerts
Get notified when job spending approaches or exceeds budget. Alerts appear as banners in the ledger.
Enable budget alerts
Off by default — turn on to monitor job budgets
Supabase
Connect your Supabase project to persist data across sessions
Go to Project Settings → API, then copy the Project URL and anon public key below.
Database schema — paste into Supabase SQL Editor and run
-- Run all at once in Supabase SQL Editor
CREATE TABLE IF NOT EXISTS jobs (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
code text NOT NULL, name text, type text, location text,
budget numeric, status text DEFAULT 'Active', cost_codes text[],
created_at timestamptz DEFAULT now(),
UNIQUE(user_id, code));
CREATE TABLE IF NOT EXISTS cards (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
last4 text, bank text, holder text, default_job text, purpose text,
UNIQUE(user_id, last4));
CREATE TABLE IF NOT EXISTS transactions (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
date date, merchant text, amount numeric,
card_last4 text, job_code text, cost_code text,
category text, confidence integer, status text DEFAULT 'pending',
has_receipt boolean DEFAULT false, receipt_url text,
note text, anomalies text[], source_file text,
imported_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS statements (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
filename text, bank text, tx_count integer, total_amount numeric,
imported_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS audit_log (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
user_email text, action text, entity text, entity_id text,
old_val text, new_val text, created_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS profiles (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid() UNIQUE,
company text, license_id text, phone text, email text,
created_at timestamptz DEFAULT now());
-- Enable Row Level Security
ALTER TABLE jobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE cards ENABLE ROW LEVEL SECURITY;
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE statements ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policies (drop first to allow re-running)
DROP POLICY IF EXISTS "own data" ON jobs; DROP POLICY IF EXISTS "own data" ON cards;
DROP POLICY IF EXISTS "own data" ON transactions; DROP POLICY IF EXISTS "own data" ON statements;
DROP POLICY IF EXISTS "own data" ON audit_log; DROP POLICY IF EXISTS "own data" ON profiles;
CREATE POLICY "own data" ON jobs FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON cards FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON transactions FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON statements FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON audit_log FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON profiles FOR ALL USING (auth.uid() = user_id);
-- Receipts storage bucket (run once separately if needed)
INSERT INTO storage.buckets (id, name, public) VALUES ('receipts','receipts',false) ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS jobs (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
code text NOT NULL, name text, type text, location text,
budget numeric, status text DEFAULT 'Active', cost_codes text[],
created_at timestamptz DEFAULT now(),
UNIQUE(user_id, code));
CREATE TABLE IF NOT EXISTS cards (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
last4 text, bank text, holder text, default_job text, purpose text,
UNIQUE(user_id, last4));
CREATE TABLE IF NOT EXISTS transactions (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
date date, merchant text, amount numeric,
card_last4 text, job_code text, cost_code text,
category text, confidence integer, status text DEFAULT 'pending',
has_receipt boolean DEFAULT false, receipt_url text,
note text, anomalies text[], source_file text,
imported_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS statements (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
filename text, bank text, tx_count integer, total_amount numeric,
imported_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS audit_log (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid(),
user_email text, action text, entity text, entity_id text,
old_val text, new_val text, created_at timestamptz DEFAULT now());
CREATE TABLE IF NOT EXISTS profiles (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users DEFAULT auth.uid() UNIQUE,
company text, license_id text, phone text, email text,
created_at timestamptz DEFAULT now());
-- Enable Row Level Security
ALTER TABLE jobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE cards ENABLE ROW LEVEL SECURITY;
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE statements ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policies (drop first to allow re-running)
DROP POLICY IF EXISTS "own data" ON jobs; DROP POLICY IF EXISTS "own data" ON cards;
DROP POLICY IF EXISTS "own data" ON transactions; DROP POLICY IF EXISTS "own data" ON statements;
DROP POLICY IF EXISTS "own data" ON audit_log; DROP POLICY IF EXISTS "own data" ON profiles;
CREATE POLICY "own data" ON jobs FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON cards FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON transactions FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON statements FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON audit_log FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "own data" ON profiles FOR ALL USING (auth.uid() = user_id);
-- Receipts storage bucket (run once separately if needed)
INSERT INTO storage.buckets (id, name, public) VALUES ('receipts','receipts',false) ON CONFLICT DO NOTHING;
Export
Configure report format and delivery preferences
Include company logo on exported PDFs
Include unassigned transactions in export
SMS alerts
Twilio integration — approximately $1.50/month at current volume
Enable SMS alerts
$1.15/month + $0.01/message · Twilio
Team access
Invite team members and manage their access level. Viewers can see all transactions and add notes. Owners can import, edit, and export.
Team invitations require Supabase to be configured. Set up Supabase first in the Supabase tab.
Viewer: read + notes. Owner: full access including import and export.
Current team
Connect Supabase to manage team members