Smart Wallet for Nigerian Youth — Your money, automatically sorted.
PocketWise is a fintech application built for Nigerian youth aged 16–25 that automatically splits every deposit into four purpose-driven wallets the moment money arrives — no manual budgeting, no willpower required. It runs on real money via Anchor BaaS from day one.
- The Core Idea
- Features
- Tech Stack
- Project Structure
- Database Schema
- API Reference
- Authentication
- Anchor BaaS Integration
- Auto-Split Logic
- Environment Variables
- Build Phases
- Coding Standards
- Design System
- Success Metrics
The 50/30/10/10 Split — every deposit is automatically divided into four wallets the instant it lands:
| Wallet | Allocation | Example (₦10,000) | Purpose |
|---|---|---|---|
| 💸 Spend | 50% | ₦5,000 | Day-to-day expenses |
| 💰 Savings | 30% | ₦3,000 | Long-term wealth building |
| 🚨 Emergency | 10% | ₦1,000 | Safety net, locked by default |
| ✨ Flex | 10% | ₦1,000 | Guilt-free treats and extras |
Rounding remainders are always assigned to the Flex wallet to ensure the total always equals the deposit amount.
- ⚡ Auto-split on deposit — every incoming deposit is split 50/30/10/10 atomically
- 📝 Reason-required transfers — every spend requires a stated reason, enforced at API level
- 🔒 Emergency wallet lock — locked by default; override requires explicit friction
- 🎯 Savings goals — create goals with automated weekly contribution tracking from the Savings wallet
- 🤖 AI Money Coach — weekly spending analysis, behavioral insights, and celebrations
- 📊 Weekly financial summary — delivered via push notification
- 🏦 Real money — powered by Anchor BaaS, no simulated balances
- 16+ — real money features (Anchor BaaS, KYC, transfers)
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15+ (App Router) | Framework — all pages use App Router |
| TypeScript | 5+ | Type safety throughout |
| Tailwind CSS | 4+ | Utility-first styling |
| Lucide React | Latest | All icons — no other icon library |
| React Hook Form | Latest | All form state management |
| Zod | Latest | Schema validation (shared frontend/backend) |
| Axios | Latest | HTTP client for API calls |
| Zustand | Latest | Global state (auth, wallet data) |
| Technology | Version | Purpose |
|---|---|---|
| Node.js | 20+ LTS | Runtime |
| Express.js | 4+ | API framework |
| TypeScript | 5+ | Type safety |
| PostgreSQL | 15+ | Primary database |
| Neon | Serverless | PostgreSQL hosting |
| Prisma | Latest | ORM and migrations |
| jsonwebtoken | Latest | JWT auth tokens |
| bcryptjs | Latest | Password hashing |
| Resend | Latest | Transactional email |
| Termii | API v3 | SMS and OTP |
| Anchor | BaaS API | Real banking — deposits, transfers, KYC |
| Sentry | Latest | Error tracking and monitoring |
| Cloudinary | Latest | Profile image uploads |
| Service | Usage |
|---|---|
| Vercel | Frontend hosting (Next.js native) |
| Vercel | Backend API hosting (Node/Express) |
| Neon | PostgreSQL database (serverless, auto-scaling) |
| GitHub | Version control — main + dev branches |
src/
├── app/
│ ├── page.tsx # Landing page
│ ├── layout.tsx # Root layout
│ ├── (auth)/
│ │ ├── signup/page.tsx # Signup screen
│ │ ├── login/page.tsx # Login screen
│ │ ├── onboarding/page.tsx # Pre-signup slides
│ │ └── setup/page.tsx # Post-signup wallet init
│ ├── (dashboard)/
│ │ ├── wallet/page.tsx # Home screen
│ │ ├── goals/page.tsx # Savings goals
│ │ ├── summary/page.tsx # Weekly summary
│ │ └── profile/page.tsx # Profile + settings
│ └── waitlist/page.tsx # Coming soon page
├── components/
│ ├── ui/ # Shared UI components
│ ├── layout/ # Navbar, BottomNav, etc.
│ ├── wallet/ # WalletCard, BalanceCard, etc.
│ └── forms/ # SignupForm, LoginForm, etc.
├── lib/
│ ├── api.ts # Axios instance + interceptors
│ ├── utils.ts # formatNaira, calculateSplit, etc.
│ └── validations.ts # Zod schemas
└── store/
├── auth.store.ts # User auth state (Zustand)
└── wallet.store.ts # Wallet balances state (Zustand)
src/
├── routes/
│ ├── auth.routes.ts
│ ├── wallet.routes.ts
│ ├── transactions.routes.ts
│ ├── goals.routes.ts
│ └── waitlist.routes.ts
├── controllers/
│ ├── auth.controller.ts
│ ├── wallet.controller.ts
│ └── transactions.controller.ts
├── middleware/
│ ├── auth.middleware.ts # JWT verification
│ ├── validate.middleware.ts # Zod request validation
│ ├── rateLimit.middleware.ts # Rate limiting
│ └── logger.middleware.ts # Request logging
├── services/
│ ├── anchor.service.ts # All Anchor BaaS API calls
│ ├── email.service.ts # Resend email sending
│ ├── sms.service.ts # Termii OTP
│ ├── split.service.ts # Auto-split calculation logic
│ └── ai.service.ts # AI coach insight generation
├── prisma/
│ ├── schema.prisma # Database schema
│ └── migrations/ # Migration files
├── app.ts # Express app setup
└── server.ts # Entry point
All money fields are
DECIMAL(12,2). Never useFloatfor financial values.
| Table | Key Fields |
|---|---|
users |
id (UUID), name, email, phone, password_hash, kyc_tier (1/2/3), date_of_birth, is_verified, created_at |
wallets |
id (UUID), user_id (FK), type (spend/savings/emergency/flex), balance DECIMAL(12,2), is_locked, created_at |
transactions |
id (UUID), user_id (FK), wallet_id (FK), type (deposit/transfer/split), amount DECIMAL(12,2), reason, anchor_ref, status, created_at — IMMUTABLE ROWS |
savings_goals |
id (UUID), user_id (FK), title, target_amount, current_amount, deadline, auto_contribute (bool), created_at |
ai_insights |
id (UUID), user_id (FK), week_start (date), summary (text), tips (JSON), warnings (JSON), created_at |
waitlist |
id (UUID), email (unique), name (nullable), created_at |
referrals |
id (UUID), referrer_id (FK), referee_id (FK), credit_amount DECIMAL(12,2), status, created_at |
kyc_records |
id (UUID), user_id (FK), tier, bvn_hash, nin_hash, verified_at, created_at |
- All money stored as
DECIMAL(12,2)— neverFLOATorInt - Transaction rows are IMMUTABLE — never
UPDATEa transaction, onlyINSERT - Every deposit triggers exactly 4 split transaction rows atomically (wrapped in a DB transaction)
- If any wallet credit fails, the entire split is rolled back
anchor_refstored on every transaction row for reconciliation- Wallet balance is always derived from the transaction log — never stored independently in business logic
| Method | Endpoint | Description |
|---|---|---|
| POST | /signup |
Create account, initialize 4 wallets, send verification email |
| POST | /login |
Return access token (15min) + set refresh token cookie (7d) |
| POST | /logout |
Clear refresh token cookie |
| POST | /refresh |
Issue new access token from refresh cookie |
| GET | /me |
Return authenticated user profile |
| POST | /verify-email |
Verify email with token from email link |
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
All 4 wallets with current balances |
| POST | /deposit |
Deposit via Anchor, trigger auto-split into 4 wallets |
| POST | /transfer |
Transfer between wallets — reason field required |
| POST | /emergency/unlock |
Request emergency unlock — requires confirmation step |
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
All transactions (paginated, 20 per page) |
| GET | /:walletId |
Transactions for a specific wallet |
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
All savings goals for user |
| POST | / |
Create new savings goal |
| PATCH | /:id |
Update goal title, target, deadline |
| DELETE | /:id |
Delete goal |
| Method | Endpoint | Description |
|---|---|---|
| POST | / |
Add email to waitlist, send confirmation email |
| GET | /count |
Public — returns total waitlist count |
All API responses follow a consistent structure:
{
"success": true,
"data": {},
"message": "Operation successful",
"error": null
}- Access token — 15-minute expiry, sent in
Authorizationheader asBearer <token> - Refresh token — 7-day expiry, stored in
httpOnlycookie (not accessible to JavaScript) - On every protected request: verify access token → if expired, call
/api/auth/refresh→ retry request - Axios interceptor on the frontend handles token refresh automatically
⚠️ Store the access token in memory (Zustand) orsessionStorage— neverlocalStorage. The refresh token stays inhttpOnlycookie only.
- Minimum 8 characters
- Hashed with
bcrypt,saltRounds: 12 - Never stored as plain text, never logged
- Forgot password: generate secure random token, store hashed version, email link with raw token
| Tier | Requirements | Limit | Notes |
|---|---|---|---|
| Tier 1 | Phone + email | Basic transactions | Signup only; no transfers above ₦50k |
| Tier 2 | NIN verification | Up to ₦500k/month | Required before withdrawals |
| Tier 3 | BVN + NIN | Above ₦500k/month | High-volume users only |
- Virtual account creation — every user gets a dedicated Nigerian bank account number
- Deposit detection — webhook fired when money lands
- P2P transfers between accounts
- KYC verification (Tier 1–3)
- CBN regulatory compliance and reporting
- Auto-split logic — when Anchor deposit webhook fires, PocketWise splits 70/30/10/10
- Reason enforcement — transfer endpoint requires a
reasonfield before calling Anchor - Emergency lock — check wallet lock status in PocketWise DB before any Anchor call
- Transaction history UI — read from PocketWise DB (which mirrors Anchor data)
POST /api/webhooks/anchor/deposit
│
├── 1. Verify webhook signature with Anchor secret
├── 2. Find user by account number
├── 3. Calculate split amounts (70/30/10/10, remainder to Flex)
├── 4. Open DB transaction
├── 5. Credit all 4 wallet rows atomically
├── 6. If any step fails → rollback entire transaction
└── 7. Send push notification to user
⚠️ Start all Anchor work in sandbox mode. Do not use production credentials until full testing is complete.
function calculateSplit(amount: number): { spend: number; savings: number; emergency: number; flex: number } {
const spend = Math.floor(amount * 0.70 * 100) / 100;
const savings = Math.floor(amount * 0.30 * 100) / 100;
const emergency = Math.floor(amount * 0.10 * 100) / 100;
const flex = amount - spend - savings - emergency; // remainder always goes to flex
return { spend, savings, emergency, flex };
}- Same logic as frontend but using
Decimalprecision (via Prisma's Decimal type) - Assert in tests: the sum of all 4 amounts must always equal the original deposit amount exactly
DATABASE_URL=
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
ANCHOR_API_KEY=
ANCHOR_WEBHOOK_SECRET=
RESEND_API_KEY=
TERMII_API_KEY=
CLOUDINARY_URL=
SENTRY_DSN=
NODE_ENV=developmentNEXT_PUBLIC_BACKEND_URL=http://localhost:1000
NEXT_PUBLIC_SENTRY_DSN=- Coming Soon page with waitlist email capture
- Landing page (all 3 breakpoints)
- Onboarding slides (3 screens)
- Signup and Login forms with Zod validation
- Post-signup wallet setup flow
- Wallet home screen with mock data
- Backend:
POST /api/waitlist,POST /api/auth/signup,POST /api/auth/login,GET /api/auth/me - Connect auth screens to real API; JWT middleware on all
/dashboardroutes
- Anchor sandbox account setup
- Virtual account creation on signup
- Deposit webhook handler with auto-split logic
GET /api/walletsendpoint- Transaction history screen
- Transfer screen with reason requirement
- Emergency wallet lock UI and override flow
- Savings goals CRUD
- Auto-contribute from Savings wallet to goals
- AI Money Coach — weekly cron job for insight generation
- Weekly summary screen
- Push notifications for deposits and weekly summaries
- Anchor KYC integration — Tier 1 on signup, Tier 2 before withdrawals
- Move Anchor from sandbox to production
- Transaction fee logic (0.5–1% above ₦10,000)
- Sentry error monitoring setup
- Performance audit and optimization
- Beta test with 10–20 real users (UNILAG, UI, FUTO)
- A/B test 50/30/10/10 split ratio with real behavior data
- Bug fixes from beta feedback
- Referral system —
POST /api/referrals, ₦1000 wallet credit per successful referral - React Native evaluation for mobile app post-MVP
- TypeScript strict mode on both frontend and backend
- No
anytypes — define proper interfaces for everything - All errors handled — no silent failures, no unhandled promises
- Environment variables in
.env— never hardcode secrets - Git: feature branches off
dev, PR intodev,devmerges intomainfor releases
'use client'only when necessary — prefer Server Components where possible- All forms: React Hook Form + Zod validation
- Loading and error states for every async operation — no blank screens
- Mobile-first CSS — base styles for 390px, use
lg:for desktop overrides - No inline styles except for dynamic values (progress bar widths, wallet colors)
- All money displayed with
formatNaira()utility — never format manually
- All routes have Zod validation middleware before hitting the controller
- Controllers are thin — business logic lives in services
- All DB operations go through Prisma — no raw SQL except for reporting queries
- All financial operations use DB transactions for atomicity
- Rate limiting on all auth endpoints — max 5 failed attempts per 15 minutes
- Log all requests with
morgan, all errors withSentry - Never log sensitive data — no passwords, tokens, BVN, or NIN in logs
| Token | Hex | Usage |
|---|---|---|
| Primary | #5B4FCF |
Buttons, links, active states |
| Primary Light | #EDE9FF |
Icon backgrounds, badge fills |
| Primary Dark | #4338A8 |
Button hover states |
| Background | #F9F9FB |
Page background |
| Surface | #FFFFFF |
Cards, modals, inputs |
| Border | #E5E7EB |
Dividers, card borders, input borders |
| Text Primary | #0F0F1A |
Headings and body text |
| Text Secondary | #6B7280 |
Labels, captions, placeholders |
| Spend (Orange) | #F97316 |
Spend wallet only |
| Savings (Green) | #22C55E |
Savings wallet only |
| Emergency (Red) | #EF4444 |
Emergency wallet only |
| Flex (Blue) | #3B82F6 |
Flex wallet only |
| Warning | #F59E0B |
Warnings, low balance alerts |
| Role | Font | Weight | Usage |
|---|---|---|---|
| Display / Hero | Plus Jakarta Sans | 700 Bold | Hero headlines, landing page |
| Heading 1 | Plus Jakarta Sans | 700 Bold | Screen titles |
| Heading 2 | Plus Jakarta Sans | 600 SemiBold | Section headings |
| Body | Inter | 400 Regular | All body text, descriptions |
| Body Strong | Inter | 600 SemiBold | Labels, emphasized text |
| Caption | Inter | 400 Regular | Timestamps, helper text |
| Balance Display | Plus Jakarta Sans | 700 Bold | Large money amounts — 48px, -1px tracking |
- 8px base grid — all spacing is multiples of 8:
8, 16, 24, 32, 48, 64, 80 - Screen padding (mobile):
16pxhorizontal - Border radius: buttons
12px, cards16px, inputs10px, modals20px(top), pills9999px
| Metric | Target | Why It Matters |
|---|---|---|
| Day-7 return rate | 60% | Signals habit formation in first week |
| Week-2 retention | 40% | Industry benchmark for fintech apps |
| Month-1 retention | 30% | Threshold for sustainable growth |
| Deposits per user per week | 2+ | Active engagement signal |
| Savings goal completion (90d) | 25% | Proves behavioral change |
| Emergency override rate | < 15% | Measures discipline effectiveness |
PocketWise — Building the financial backbone of Nigerian youth.
Document last updated: March 2026 | Version 2.0
This is a Next.js project bootstrapped with create-next-app.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun devOpen http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.
This project uses next/font to automatically optimize and load Inter, a custom Google Font.
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.