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
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.next
.env
.env*.local
.git
coverage
*.log
41 changes: 41 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Core
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/afriramp?schema=public
REDIS_URL=redis://localhost:6379

# Admin auth
ADMIN_EMAIL=admin@afriramp.local
# Generate with: node -e "require('bcryptjs').hash('change-me', 12).then(console.log)"
ADMIN_PASSWORD_HASH=
ADMIN_JWT_SECRET=replace-with-32-byte-random-secret

# Mobile money provider: mock | flutterwave | paychangu | lipila | pesapal
MOBILE_MONEY_PROVIDER=mock
MOBILE_MONEY_WEBHOOK_SECRET=replace-with-mobile-money-webhook-secret
MOBILE_MONEY_API_BASE_URL=
MOBILE_MONEY_API_KEY=
MOBILE_MONEY_API_SECRET=
MOBILE_MONEY_CALLBACK_URL=http://localhost:3000/api/webhooks/mobile-money/mock

# Blockchain
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
NEXT_PUBLIC_DEFAULT_CHAIN=polygon
ENABLE_BLOCKCHAIN_BROADCAST=false
HOT_WALLET_PRIVATE_KEY=
HOT_WALLET_ADDRESS=
DEPOSIT_WALLET_ADDRESSES=
POLYGON_RPC_URL=https://polygon-rpc.com
BASE_RPC_URL=https://mainnet.base.org
BNB_RPC_URL=https://bsc-dataseed.binance.org

# Notifications
SMS_PROVIDER=mock
SMS_API_KEY=
EMAIL_FROM=receipts@afriramp.example
RESEND_API_KEY=

# Risk controls
MAX_SINGLE_TRANSACTION_ZMW=25000
QUOTE_LOCK_SECONDS=300
REQUIRED_EVM_CONFIRMATIONS=12
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.env
.env*.local
.next
node_modules
dist
coverage
*.log
tsconfig.tsbuildinfo
prisma/dev.db
Dockerfile.local
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma

USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
# AfriRamp

Production-ready MVP for a non-custodial crypto on-ramp/off-ramp for Zambia.

## What it supports

- Buy crypto with MTN Mobile Money or Airtel Money
- Sell crypto for MTN Mobile Money or Airtel Money
- MetaMask wallet connection through Wagmi + Viem
- USDC on Base, USDT on Polygon, and WBTC on Polygon for MVP launch
- Fixed 3% platform fee shown before confirmation
- Prisma/PostgreSQL state machine, webhook replay protection, idempotency, audit logs, admin review, and freeze controls

Native Bitcoin is modeled in the schema but intentionally disabled in the MVP. Use WBTC on Polygon first to keep wallet UX, fees, and verification simple.

## Stack

- Next.js 15+ app router
- TypeScript
- TailwindCSS + shadcn-style local UI primitives
- Prisma ORM + PostgreSQL
- Wagmi + Viem
- Redis-ready queue abstraction
- Docker and Vercel-ready configuration

## Local setup

```bash
cp .env.example .env
npm install
npm run prisma:generate
npm run prisma:migrate
npm run dev
```

For Postgres and Redis locally:

```bash
docker compose up postgres redis
```

## Deployment

1. Create a managed PostgreSQL database.
2. Configure the variables in `.env.example` in Vercel.
3. Run `npx prisma migrate deploy` during deployment or from a release command.
4. Configure your mobile money provider webhook to:
`/api/webhooks/mobile-money/{provider}`
5. Keep `ENABLE_BLOCKCHAIN_BROADCAST=false` until hot-wallet custody, treasury limits, and test transactions are reviewed.

## Critical security notes

- Never expose `HOT_WALLET_PRIVATE_KEY` to the browser.
- Use a dedicated low-balance hot wallet, multisig treasury, and strict refill procedures.
- Use provider-specific webhook signing secrets in production.
- Keep admin routes behind strong password hashes and ideally Vercel auth/VPN.
- Review failed, frozen, and manual-review transactions before releasing funds.

See `docs/ARCHITECTURE.md` for the full architecture, API design, compliance notes, and build workflow.
19 changes: 19 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
services:
app:
build: .
depends_on:
- postgres
- redis
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/afriramp?schema=public
REDIS_URL: redis://redis:6379
NEXT_PUBLIC_APP_URL: http://localhost:3000
MOBILE_MONEY_PROVIDER: mock
MOBILE_MONEY_WEBHOOK_SECRET: local-webhook-secret
ADMIN_EMAIL: admin@afriramp.local
ADMIN_PASSWORD_HASH: ""
ADMIN_JWT_SECRET: local-admin-secret-change-me
ports:
- "3000:3000"

postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: afriramp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data

redis:
image: redis:7-alpine
ports:
- "6379:6379"

volumes:
postgres-data:
160 changes: 160 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# AfriRamp MVP Architecture

## Product posture

AfriRamp is non-custodial for users: users connect MetaMask, receive crypto directly to their wallet, and send crypto from their wallet when selling. The platform temporarily handles operational liquidity through hot wallets and mobile-money provider balances while a transaction is in flight.

## Recommended MVP blockchain approach

Launch with:

1. **USDC on Base** - reliable stablecoin liquidity and low fees.
2. **USDT on Polygon** - broad user recognition and low fees.
3. **WBTC on Polygon** - Bitcoin exposure without native Bitcoin address management.

Keep native Bitcoin disabled initially. Add it once you have unique deposit address generation, UTXO monitoring, confirmation policies, and treasury reconciliation.

## Folder structure

```txt
src/app Next.js app router pages and API routes
src/app/api/buy Buy transaction creation
src/app/api/sell Sell transaction creation
src/app/api/quote Real-time quote and 3% fee calculation
src/app/api/webhooks Mobile-money webhook handling
src/app/api/admin Admin authentication, analytics, review actions
src/components Mobile-first UI and shadcn-style primitives
src/lib/blockchain Viem chain, hot-wallet, and deposit verification helpers
src/lib/payments Swappable mobile-money provider abstraction
src/lib Prisma, auth, risk, rates, audit, security, state machine
prisma/schema.prisma PostgreSQL schema
```

## Database model summary

- `User`: wallet-first profile, KYC-ready status, risk score, block flag.
- `Quote`: locked short-lived rate with fiat, crypto, fee, and expiry.
- `Transaction`: state machine for buy/sell, provider refs, chain refs, fees, freeze/failure fields.
- `WebhookEvent`: unique provider event IDs to prevent replay.
- `IdempotencyKey`: prevents duplicate transaction creation and duplicate payouts.
- `AuditLog`: immutable operational trail for user, webhook, system, and admin actions.
- `AdminSession`: future-ready admin session persistence.

## API design

| Route | Purpose |
| --- | --- |
| `POST /api/quote` | Creates a locked quote with exchange rate and 3% fee. |
| `POST /api/buy` | Creates a buy transaction and initiates mobile-money collection. |
| `POST /api/sell` | Creates a sell transaction and allocates a deposit address. |
| `GET /api/transactions` | Wallet-based transaction history. |
| `GET /api/transactions/:publicId` | Status tracking receipt. |
| `POST /api/transactions/:publicId/deposit` | Verifies sell-side EVM token deposit and triggers payout. |
| `POST /api/webhooks/mobile-money/:provider` | Verifies mobile-money webhook signatures and updates state. |
| `POST /api/admin/login` | HTTP-only cookie admin login. |
| `GET /api/admin/transactions` | Admin transaction review. |
| `PATCH /api/admin/transactions/:publicId` | Freeze, fail, or move to manual review. |
| `GET /api/admin/analytics` | Volume, completion, review, and 3% fee analytics. |
| `GET /api/admin/liquidity` | Hot-wallet token balance snapshot. |

## Transaction state machine

### Buy

`QUOTE_CREATED -> PAYMENT_PENDING -> CRYPTO_DISBURSEMENT_PENDING -> COMPLETED`

Failure/review states:

`FAILED`, `FROZEN`, `EXPIRED`, `MANUAL_REVIEW`

### Sell

`QUOTE_CREATED -> CRYPTO_DEPOSIT_PENDING -> PAYOUT_PENDING -> COMPLETED`

The deposit verification route validates sender, receiver, token contract, amount, transaction success, and confirmations before initiating mobile-money payout.

## Mobile money provider abstraction

`src/lib/payments/types.ts` defines:

- `initiateCollection`
- `initiatePayout`
- `parseWebhook`

The current implementation includes:

- `mock` provider for local testing
- `HttpMobileMoneyProvider` for Lipila, Flutterwave, PayChangu, or Pesapal-style REST integrations

Provider-specific payload mapping should be tightened once the selected provider contract is finalized. Keep provider-specific logic inside `src/lib/payments`, not in route handlers.

## Security best practices implemented

- Server-only private key access.
- Webhook HMAC verification hook.
- Unique provider webhook event IDs for replay prevention.
- Idempotency keys for transaction creation.
- Atomic status locking before crypto disbursement or mobile-money payout.
- Strict Zod validation for public API inputs.
- Wallet and Zambia mobile-number validation.
- Admin HTTP-only cookie with signed JWT.
- Full audit logging for user, system, webhook, and admin actions.
- Manual review and freeze statuses.

## Security practices required before mainnet launch

- Use provider-native webhook signature verification for the chosen mobile-money provider.
- Store secrets in Vercel encrypted environment variables or a dedicated secret manager.
- Use a dedicated hot wallet with low operational limits; keep treasury in multisig/cold storage.
- Add withdrawal limits, velocity rules, device fingerprinting, and sanctions/PEP screening hooks.
- Add background workers for retry queues rather than relying only on request lifecycle.
- Add automated reconciliation between provider ledger, database, and chain transactions.
- Use separate production, staging, and test wallets.

## African fintech and Zambia compliance considerations

- Engage Zambian counsel on Bank of Zambia, SEC, AML/CFT, data protection, and payment-service rules.
- Treat mobile money numbers as sensitive personal data.
- Build KYC thresholds from day one even if KYC collection is deferred.
- Maintain audit logs, transaction receipts, and suspicious-activity review notes.
- Keep language beginner-friendly: always show fees, rates, irreversible crypto warnings, and support channels.
- Design for intermittent network connectivity: small pages, clear retry states, and SMS/WhatsApp receipts.

## Liquidity management

- Start with narrow asset support and conservative limits.
- Keep a daily hot-wallet cap and refill manually from multisig.
- Maintain separate float for MTN and Airtel payout balances.
- Reconcile at least daily: database status, mobile-money settlement reports, hot-wallet balances, and fee revenue.
- Freeze transactions automatically when provider references, sender wallets, or phone numbers do not match expected values.

## Fraud risk reduction

- Lock quotes for short periods.
- Enforce idempotency and webhook replay protection.
- Block phone-prefix/payment-rail mismatches.
- Add velocity checks by phone number, wallet, IP, and device.
- Hold high-value, repeated, or mismatched transactions for manual review.
- Require enhanced KYC before raising limits.

## Cursor AI coding workflow

1. Build the schema and state machine first.
2. Implement quote, buy, sell, webhook, and deposit verification APIs.
3. Add the payment-provider abstraction with mock provider tests.
4. Add wallet UI and mobile-first flows.
5. Add admin dashboard and operational controls.
6. Add provider-specific integration once a mobile-money provider is selected.
7. Run `npm run typecheck`, `npm run build`, Prisma migration checks, and webhook simulations.
8. Deploy to staging with broadcast disabled, test end-to-end, then enable blockchain broadcasting with small limits.

## Step-by-step build order for launch

1. Select mobile money provider and finalize API contract.
2. Configure Postgres, Redis, Vercel, and secrets.
3. Run Prisma migrations.
4. Test mock buy/sell flows.
5. Test provider sandbox collections and payouts.
6. Test EVM token transfers on low-value mainnet amounts.
7. Configure monitoring, alerting, and reconciliation reports.
8. Launch with low limits and manual review for first transactions.
6 changes: 6 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
14 changes: 14 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
experimental: {
serverActions: {
bodySizeLimit: "1mb"
}
},
output: "standalone",
poweredByHeader: false,
reactStrictMode: true
};

export default nextConfig;
Loading