Split expenses. Stay friends.
Live app: https://tally-frontend-xi.vercel.app/
Tally is a mobile-first group expense app built for trips with friends. Create a trip, invite people, log shared costs, see who owes whom, and mark settlements when money changes hands offline. Tally does not move money — it keeps a clear shared record so nobody has to dig through chat threads for “who paid for dinner.”
The product differentiator is AI receipt scanning with line-item assignment: scan a restaurant bill, let Claude extract each item, then tag who ordered what in under a minute — instead of typing every amount by hand.
Most expense splitters stop at “enter a total and split equally.” That works for an Uber. It fails at a shared meal where Ada had pepper soup, Bola had jollof, and Chidi took both shawarmas.
Tally’s answer:
- AI extracts line items from the receipt (name, quantity, price).
- You assign each line to one or more trip members (or everyone).
- Balances update from a derived split map — same debt math as manual expenses.
Equal-split paths (transport, lodging) still work in one tap. Line-item assignment is additive, not a replacement.
- Create trips with destination, dates, and a base currency
- Invite members via shareable join links
- Organizer and member roles on the trip detail screen
- Manual entry with equal or custom splits
- Categories (food, transport, lodging, activities, other)
- Multi-currency amounts with FX conversion into the trip base currency
- Optional receipt photo attachment
- Camera capture or gallery upload
- Claude OCR returns merchant, total, currency, confidence, and line items
- Split Mode Decision sheet after a successful itemized scan:
- Assign by item → tag members per line (progress bar + unassigned counter; save blocked until every unit is assigned)
- Split equally → jump to the existing Add Expense form, prefilled
- Total-only / unreadable receipts fall back to the manual form
- Per-trip and aggregate balance views
- Debt simplification (fewer payments, same outcome)
- Confirm offline settlements; history stays on the trip
- Google OAuth and email magic-link sign-in
- Onboarding (display name + home currency)
- Profile preferences and sign-out
- In-app notifications (joins, expenses, settlements)
- Public Terms of Service and Privacy Policy
| Layer | Choice |
|---|---|
| Framework | Next.js 14 (App Router), TypeScript |
| UI | Tailwind CSS, Lucide icons, Inter, mobile shell (≈375–430px) |
| Forms | React Hook Form + Zod |
| State | Zustand (store/) with typed hooks |
| Auth | Mock session (localStorage) until backend auth ships |
| Data | In-memory / Zustand (backend API TBD) |
| Optional | AWS DynamoDB for some user fields |
| AI | Anthropic Claude (POST /api/expenses/scan) |
| FX | exchangerate.host (optional key) with open.er-api.com fallback |
- Node.js 18+
- Anthropic API key (for receipt scan)
- Optional: AWS credentials if you use DynamoDB user sync
npm install
cp .env.local.example .env.local
# Fill in the variables below, then:
npm run devOpen https://tally-frontend-xi.vercel.app/ for the live app. For local development, the server runs on your machine after npm run dev.
| Command | Description |
|---|---|
npm run dev |
Development server |
npm run build |
Production build |
npm start |
Serve production build |
npm run lint |
ESLint |
npm test |
Debt-simplification unit tests |
Copy from .env.local.example:
NEXT_PUBLIC_APP_URL=https://tally-frontend-xi.vercel.app
# Optional DynamoDB user storage
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
DYNAMODB_TABLE_NAME=
# Receipt OCR (required for AI scan)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL= # optional; defaults to claude-sonnet-4-5
# FX (optional)
FX_API_KEY=For local development, set NEXT_PUBLIC_APP_URL to your local origin.
app/(auth)/ Landing + onboarding
app/(app)/ Authenticated shell (bottom nav)
app/(legal)/ Terms + Privacy
app/join/ Invite join flow
app/api/ scan, fx, notifications, user
components/ui/ Primitives (BottomSheet, Avatar, …)
components/layout/ MobileShell, BottomNav, StickyHeader
features/ auth, trips, expenses, balances, settlements,
notifications, profile
store/ Zustand slices + selectors
lib/ currency, FX, debt math, DB helpers, storage
types/ Shared TypeScript models
| Route | What it does |
|---|---|
/ |
Landing + sign-in |
/onboarding |
Display name + home currency |
/dashboard |
Active / past trips |
/trips/new |
Create trip |
/trips/[tripId] |
Trip detail, members, expenses |
/trips/[tripId]/invite |
Invite link |
/join/[token] |
Accept invite |
/trips/[tripId]/expenses/new |
Manual / prefilled add expense |
/trips/[tripId]/expenses/scan |
Camera + OCR |
/trips/[tripId]/expenses/assign |
Line-item member assignment |
/balances |
Aggregate balances |
/trips/[tripId]/balances |
Trip balances |
/trips/[tripId]/settlements |
Settlement history / confirm |
/profile |
Account preferences |
/notifications |
Activity feed |
/terms, /privacy |
Legal |
Scan receipt
→ Claude extracts line items (+ total, merchant, currency)
→ If line items exist: Split Mode sheet
├─ Assign by item → /expenses/assign → derive splits → save
└─ Split equally → /expenses/new (prefilled)
→ If total only / OCR failed: /expenses/new (existing fallback)
Per-line splitMap values are summed into the expense-level splitMap before save. Balance calculation, debt simplification, and settlements read that top-level map — they do not care whether the expense came from OCR or manual entry.
Import typed hooks from @/store — avoid reaching for the raw store unless necessary:
import {
useTrips,
useExpenses,
useBalances,
useUnassignedTotal,
useAddToast,
} from "@/store";Unstyled page / chunk 404s (layout.css or main-app.js 404):
# Stop the dev server (Ctrl+C), then:
rm -rf .next
npm run devHard-refresh the browser (Ctrl+Shift+R).
Receipt scan always fails: confirm ANTHROPIC_API_KEY is set and the server was restarted after editing .env.local.
Create group / invite flows: data is kept in memory (Zustand + lib/db) until the real backend ships. Refreshing the page clears local groups.
- Dark canvas (
#0A0A0F), surfaces#13131A/#1C1C27 - Brand primary
#5921B5(--primary-500); accent gradient#5921B5 → #2563EB - Status: green
#10B981(assigned / settled), rose#F43F5E(owed / left) - Built mobile-first; desktop centers a phone-width frame
Private / hackathon project — all rights reserved unless otherwise noted.