Travel Kitty is an alpha-stage group expense splitter for trips and friends. It records split bills on-chain (Base Sepolia), stores receipt data on IPFS (Helia), and supports optional mock on-chain settlement with mUSD.
โ ๏ธ Alpha notice โ This is a hackathon demo. Contracts & code are unaudited. Do not use on mainnet.
-
Wallet connect (RainbowKit + wagmi) on Base Sepolia
-
Join a trip (on-chain
join()) -
Add expense with:
- OCR JSON / receipt image uploaded to IPFS via Helia โ CID
- On-chain
addExpense(amountUsd6, cid, splitWith[])
-
Mock settlement (approve mUSD โ
settleToken()) -
Off-chain history (Supabase + Prisma) for lists/filters
-
Clean, minimal Next.js 15 App Router UX with TailwindCSS
- Frontend: Next.js 15 (App Router), TypeScript, TailwindCSS
- Web3: wagmi + viem, RainbowKit, Base Sepolia
- IPFS: Helia (
helia+@helia/unixfs) โ browser node - Data: Supabase Postgres + Prisma (server actions/route handlers)
- Server-state: React Query
- AI/OCR: optional via OpenRouter (vision model) โ stubbed in demo API
travel-kitty-web/
โโ app/
โ โโ api/ocr/route.ts # (optional) OCR proxy (stub)
โ โโ globals.css
โ โโ layout.tsx # App providers (wagmi, RainbowKit, React Query)
โ โโ page.tsx # Home (Join, Faucet, Add Expense, Settle)
โโ lib/
โ โโ wagmi.ts # chain config
โ โโ contracts.ts # addresses + ABIs
โ โโ helia.ts # Helia + unixfs helpers
โ โโ react-query.tsx # QueryClient provider
โ โโ prisma.ts # Prisma client (server)
โ โโ supabase.ts # Supabase client (browser)
โโ prisma/
โ โโ schema.prisma # Trip, TripMember, Expense
โโ public/
โโ next.config.ts
โโ package.json
โโ tsconfig.json
Create .env.local in the repo root:
# Chain
NEXT_PUBLIC_CHAIN_ID=84532
NEXT_PUBLIC_RPC_URL=https://base-sepolia.g.alchemy.com/v2/<YOUR_ALCHEMY_KEY>
# Deployed contract addresses (from contracts repo or broadcast/_addresses_84532.json)
NEXT_PUBLIC_TRAVEL_KITTY=0x...
NEXT_PUBLIC_MOCK_USD=0x...
NEXT_PUBLIC_FAUCET=0x...
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON=<public-anon-key>
SUPABASE_SERVICE_ROLE=<service-role-key> # only for server actions if needed
# Optional OCR via OpenRouter
OPENROUTER_API_KEY=sk-...
OPENROUTER_MODEL=openai/gpt-4o-miniAnd set DATABASE_URL for Prisma (Supabase โ Settings โ Database):
DATABASE_URL="postgresql://postgres:<PASSWORD>@db.<host>.supabase.co:5432/postgres"
Keep
SERVICE_ROLE&DATABASE_URLserver-only (not exposed to client).
# 1) Install deps
npm i
# 2) Init Prisma schema to your Supabase DB
npx prisma db push
# 3) Dev server
npm run dev
# Build & start
npm run build
npm startFill envs from your contracts repo (or manually):
- TravelKitty โ
NEXT_PUBLIC_TRAVEL_KITTY=0x... - MockUSD (mUSD) โ
NEXT_PUBLIC_MOCK_USD=0x... - MockUSDFaucet โ
NEXT_PUBLIC_FAUCET=0x...
If you used Foundry script that writes
broadcast/_addresses_84532.json, you can copy those addresses here.
-
Connect wallet (RainbowKit) on Base Sepolia
-
Join trip โ calls
TravelKitty.join() -
Claim mUSD (rate-limited faucet)
-
Add expense
- Uploads receipt JSON (or image) to IPFS (Helia) โ CID
- Calls
TravelKitty.addExpense(amountUsd6, cid, splitWith[])
-
Settle (optional)
approve(mUSD, kitty, amount)thensettleToken(creditor, amount, mUSD)
-
History stored in Supabase (off-chain list + filters)
export const ADDR = {
TRAVEL_KITTY: process.env.NEXT_PUBLIC_TRAVEL_KITTY as `0x${string}`,
MOCK_USD: process.env.NEXT_PUBLIC_MOCK_USD as `0x${string}`,
FAUCET: process.env.NEXT_PUBLIC_FAUCET as `0x${string}`,
};
// travelKittyAbi, erc20Abi, faucetAbi โฆ (see file)Creates a browser Helia node and exposes putToIpfs(data) that returns a CID string.
Landing with Join, Faucet, Add Expense (โ IPFS + on-chain), Settle actions.
model User { id String @id @default(cuid()); wallet String @unique; createdAt DateTime @default(now()); trips TripMember[] }
model Trip { id String @id @default(cuid()); name String; owner String; createdAt DateTime @default(now()); members TripMember[]; expenses Expense[] }
model TripMember{ id String @id @default(cuid()); tripId String; wallet String; joinedAt DateTime @default(now()); trip Trip @relation(fields:[tripId], references:[id]) }
model Expense { id String @id @default(cuid()); tripId String; payer String; amountUsd6 Int; cid String; txHash String?; createdAt DateTime @default(now()); trip Trip @relation(fields:[tripId], references:[id]) }Populate via server actions after a successful on-chain call (see app/actions.ts).
-
Open app โ Connect wallet (Base Sepolia)
-
Click Join Trip โ show tx in Basescan
-
Click Claim mUSD โ see balance in MetaMask
-
Select a small receipt (or use demo JSON), click Add Expense
- Show IPFS CID in tx input
- Open Basescan link for the tx
-
Paste creditor address, click Approve + Settle to send mUSD and reduce balances
-
Show Supabase table updating (expenses list)
- Do not expose service keys to the client.
- All contracts & app code is for demo. No guarantees.
- Helia runs a browser IPFS node; for production youโd pin through a gateway/pinning service.
- โwrong networkโ โ wallet must be Base Sepolia (84532)
- โexecution reverted NOT_MEMBERโ โ call Join first
- Faucet fails โ faucet has cooldown; wait or use a 2nd account
- IPFS errors โ check browser permissions; retry (Helia boot takes a moment)
{
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start",
"prisma:push": "prisma db push",
"prisma:studio": "prisma studio"
}