Project Overview
EasySwap is a Nigerian fintech platform that enables users to seamlessly swap between fiat currency (NGN) and cryptocurrencies (BTC, ETH, USDT, etc.). The platform provides managed fiat wallets backed by Flutterwave virtual accounts, crypto wallets powered by Quidax sub-accounts, and an instant swap engine — all wrapped in a tiered KYC compliance system with multi-layered security.
I served as the sole backend engineer, responsible for the full system architecture, API design, third-party integrations, infrastructure, and CI/CD pipeline.
The Problem
Nigeria's crypto-to-fiat corridor is fragmented. Users typically juggle multiple platforms — one for crypto custody, another for bank transfers, and ad-hoc P2P channels for conversion. Each hop introduces friction, fees, and trust issues. There was no single, API-first product that unified:
- Fiat on/off-ramp (NGN deposits via bank transfer, withdrawals to any Nigerian bank)
- Multi-asset crypto custody (BTC, ETH, USDT across multiple networks)
- Instant crypto-to-crypto and crypto-to-fiat swaps with live quotations
- Regulatory-grade KYC with tiered transaction limits
EasySwap needed to solve all four in a single backend, while remaining compliant with Nigerian financial regulations and integrating with providers whose APIs have their own quirks and limitations.
Technology Stack
| Layer | Technology |
|---|---|
| Language / Framework | Python 3.12, Django 5.1, Django REST Framework 3.15 |
| Database | PostgreSQL 16 |
| Cache / Broker | Redis (django-redis) |
| Task Queue | Celery 5.5 with Redis broker |
| Authentication | JWT via SimpleJWT + dj-rest-auth (cookie-based + Bearer header) |
| KYC Provider | Dojah (BVN, NIN, Driver's License, Passport, Selfie verification) |
| Fiat Infrastructure | Flutterwave (Permanent Virtual Accounts, bank transfers, account verification) |
| Crypto Infrastructure | Quidax (sub-accounts, multi-chain wallets, instant swaps) |
| Phone Verification | Twilio Verify (SMS OTP) |
| Email Service | Resend (transactional emails) |
| Object Storage | Cloudflare R2 (via django-storages + boto3) |
| API Documentation | drf-spectacular (Swagger UI + ReDoc) |
| Containerisation | Docker + Docker Compose |
| CI/CD | GitHub Actions |
| Monitoring | New Relic APM |
| Audit Trail | django-easy-audit |
| Code Quality | flake8, pre-commit hooks, Commitizen (Conventional Commits) |
Architecture
High-Level System Design
┌─────────────────────────────────────────────────────────────────┐
│ Mobile / Web Client │
└──────────────────────────────┬──────────────────────────────────┘
│ HTTPS (JWT Bearer / Cookie)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Django REST Framework │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Users App │ │ Wallet App │ │ API Docs (Swagger) │ │
│ │ - Auth │ │ - Fiat │ │ drf-spectacular │ │
│ │ - Profile │ │ - Crypto │ └───────────────────────┘ │
│ │ - KYC │ │ - Swap │ │
│ │ - 2FA │ │ - PIN │ │
│ │ - Devices │ │ - Txns │ │
│ └──────┬───────┘ └──────┬────────┘ │
│ │ │ │
│ ┌──────▼─────────────────▼────────┐ │
│ │ Service Layer │ │
│ │ twilio_service pin_service │ │
│ │ tier_upgrade fiat_service │ │
│ │ crypto_service swap_service │ │
│ │ flutterwave_svc quidax_svc │ │
│ └──────┬──────────────────┬───────┘ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Webhooks │ │ Signals │ │
│ │ - Dojah │ │ (post_save │ │
│ │ - Quidax │ │ hooks) │ │
│ │ - Flutterwave│ └─────────────┘ │
│ └─────────────┘ │
└──────────┬──────────────────┬──────────────┬─────────────────────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌────▼─────┐
│ PostgreSQL │ │ Redis │ │ Celery │
│ (Data) │ │ (Cache + │ │ (Async │
│ │ │ Broker) │ │ Tasks) │
└─────────────┘ └─────────────┘ └──────────┘
Django App Structure
The project is split into two core Django apps with clear domain boundaries:
easyswap.users— Identity, authentication, security, and compliance (14,500+ lines across models, views, serializers, services, signals, webhooks, and tasks)easyswap.wallet— Financial operations across fiat and crypto (34,500+ lines across models, views, serializers, services, and webhooks)
The service layer isolates all third-party API interactions (Twilio, Dojah, Flutterwave, Quidax) behind clean interfaces, making provider swaps possible without touching view logic.
Core Engineering Decisions
1. Polymorphic Wallet Architecture
One of the most critical design decisions was how to model wallets. Fiat and crypto wallets share fundamental properties (balances, status, feature flags) but diverge significantly in their specifics (virtual account numbers vs. blockchain addresses, single currency vs. multi-network).
Solution: An abstract BaseWallet model with shared fields (available/ledger/frozen balances, status, tier level, feature flags) that FiatWallet and CryptoWallet inherit from. Transactions use Django's ContentType framework (Generic Foreign Keys) to reference any wallet type through a single WalletTransaction ledger.
# BaseWallet (abstract) provides:
# - Triple balance tracking: available, ledger, frozen
# - Status lifecycle: active → frozen → suspended → closed
# - Feature flags: deposit_enabled, withdrawal_enabled, transfer_enabled
# - can_transact() validation, freeze/unfreeze operations
class FiatWallet(BaseWallet):
# Flutterwave PVA fields: account_number, bank_name, flw_ref, etc.
currency = models.CharField(default='NGN')
class CryptoWallet(BaseWallet):
# Blockchain fields: symbol, network, address, tag_or_memo, sync_status
# Unique constraint: (user, symbol, network)
This approach gives us a single transaction ledger across all wallet types while keeping wallet-specific logic cleanly separated.
2. Triple-Balance Accounting
Every wallet tracks three balances rather than one:
available_balance— Funds the user can spend right nowledger_balance— The true balance including pending settlementsfrozen_balance— Funds locked for in-progress operations (withdrawals, swaps)
When a user initiates a withdrawal, funds move from available to frozen before the provider confirms. This prevents double-spending without pessimistic database locks, and the WalletTransaction model captures balance_before and balance_after snapshots for every operation, enabling full auditability.
3. Immutable Audit Trail
Every wallet operation — credits, debits, freezes, PIN changes, virtual account creation — produces a WalletAuditLog entry via Generic Foreign Keys. These logs are append-only and include metadata (IP address, device info, admin notes), creating a forensic-grade trail for compliance and dispute resolution.
4. Idempotent Transaction Processing
The WalletTransaction model includes an idempotency_key field. Webhook handlers from Flutterwave and Quidax use this to guarantee at-most-once processing — critical when payment providers retry webhook delivery on timeout.
5. Multi-Layer Authentication
The authentication system implements defence in depth:
- JWT with rotation — 60-minute access tokens, 2-day refresh tokens with automatic rotation and blacklisting after use
- Device fingerprinting — Unknown devices trigger an SMS OTP challenge via
PendingLoginVerification - Trusted device management — Verified devices are trusted for 7 days (configurable via
DEVICE_TRUST_DAYS) with rolling expiration - TOTP-based 2FA — Optional authenticator app support via
pyotpwith 8 one-time backup codes - Login audit log — Every login attempt (success or failure) is recorded with device, IP, and trust status
# Login flow decision tree:
# 1. Validate credentials → JWT pair
# 2. Check device fingerprint against TrustedDevice table
# → Known + not expired? Issue tokens immediately
# → Unknown? Create PendingLoginVerification, send OTP via Twilio
# 3. If 2FA enabled → require TOTP verification via TwoFactorSession
# 4. Log everything to DeviceLoginLog
Tiered KYC System
The platform implements a tiered compliance model mirroring Nigerian regulatory requirements:
| Tier | Requirements | Transaction Limits |
|---|---|---|
| Tier 0 | Email + phone only | View-only / minimal |
| Tier 1 | BVN or NIN verified via Dojah | Standard daily/monthly limits |
| Tier 2 | BVN + NIN + Government ID (auto-approved) | Elevated limits |
| Tier 3 | All Tier 2 + manual admin review | Maximum limits |
Verification Flow
- User submits a document (BVN number, NIN, driver's license image, passport, selfie)
- The
KYCDocumentis created withstatus=uploaded - A Celery task dispatches the verification to Dojah's API
- Dojah processes and calls our webhook endpoint
- The
dojah_webhooks.pyhandler updates the document status and triggers thetier_upgrade_service - For Tier 1 and 2, upgrades are auto-approved if all required documents are verified
- Tier 3 requires explicit admin approval via the Django admin panel
- Wallet tier levels sync automatically on save —
FiatWallet.save()andCryptoWallet.save()both pullkyc_tierfrom the user's profile
Third-Party Integrations
Flutterwave — Fiat Infrastructure
Purpose: NGN deposits and withdrawals for Nigerian users.
- Permanent Virtual Accounts (PVA): Each user gets a dedicated virtual bank account (typically on Wema Bank). When anyone transfers NGN to this account, Flutterwave sends a webhook, and we credit the user's fiat wallet.
- Bank Transfers: Withdrawals go through Flutterwave's transfer API with account verification (name resolution) before execution.
- Webhook Processing: The
flutterwave_webhooks.pyhandler (11,700+ lines) verifies webhook signatures usingFLUTTERWAVE_SECRET_HASH, processes deposit confirmations, and handles transfer status updates.
Quidax — Crypto Infrastructure
Purpose: Multi-chain crypto custody, wallet address generation, and instant swaps.
- Sub-Account Model: Each user maps 1:1 to a Quidax sub-account (
QuidaxSubAccountmodel), provisioned on registration. This isolates user funds at the provider level. - Multi-Network Wallets: Users can create wallets for BTC, ETH, USDT, and others across Bitcoin, Ethereum, BSC, Tron, and Polygon networks. The
(user, symbol, network)unique constraint prevents duplicate wallets. - Instant Swap Engine: The
SwapServiceimplements a three-step swap flow: - Temp Quotation — Rate preview with no commitment (for UI "live rate" display)
- Create Quotation — Binding quotation valid for 15 seconds
- Confirm Swap — Execute with PIN verification; supports refresh if the quotation expires
- Webhook Processing: The
quidax_webhooks.pyhandler (51,600+ lines) is the most complex webhook handler, processing deposit confirmations, withdrawal status updates, and swap completions with HMAC signature verification.
Dojah — Identity Verification
Purpose: KYC document verification (BVN lookup, NIN validation, document OCR, selfie matching).
- Async verification via webhook callbacks
- Results trigger automatic tier upgrades through the
tier_upgrade_service - Widget initialisation endpoint for client-side verification flows
Twilio — Phone Verification
Purpose: SMS OTP delivery for phone verification, login security challenges, and password reset.
- The
twilio_service.py(12,100+ lines) wraps Twilio's Verify API - Rate limiting at 60-second intervals (
PHONE_VERIFICATION_RATE_LIMIT) - 10-minute OTP expiration (
PHONE_VERIFICATION_TIMEOUT) - Supports both verification and phone number update flows
Resend — Transactional Email
Purpose: Email verification codes and PIN reset OTPs.
- Used for email verification during registration
- PIN reset flow sends OTP to registered email as a security measure
Wallet PIN Security
All financial operations (withdrawals, swaps) require a 4–6 digit wallet PIN, separate from the account password. The PIN system includes:
- Hashed storage using Django's
make_password/check_password(PBKDF2 by default) - Brute-force protection with failed attempt counting and time-based lockout via
locked_until - Secure reset flow — OTP sent to registered email → verify OTP → set new PIN
- Dedicated permission class (
HasWalletPIN) that gates all wallet endpoints — users must set a PIN before accessing any financial feature
Webhook Architecture
The system handles webhooks from three external providers, each with distinct verification mechanisms:
| Provider | Verification Method | Handler Complexity |
|---|---|---|
| Flutterwave | Secret hash comparison | Deposit crediting, transfer status |
| Quidax | HMAC signature verification | Deposits, withdrawals, swaps, sync |
| Dojah | Webhook payload validation | KYC document status updates |
All webhook handlers follow a consistent pattern: 1. Verify the webhook signature/hash 2. Parse and validate the payload 3. Perform idempotency check (has this event already been processed?) 4. Execute the business logic within a database transaction 5. Log the operation to the audit trail 6. Return an appropriate HTTP response to the provider
Asynchronous Processing
Celery handles background operations that shouldn't block API responses:
- KYC verification dispatch — Sending documents to Dojah for processing
- Expired device cleanup — Nightly cron job (
cleanup_expired_devices) runs at 2:00 AM via Celery Beat to remove stale trusted devices - Webhook processing — Heavy webhook handlers can offload processing to Celery tasks
The Celery configuration uses Redis as both broker and result backend, with a 30-minute task time limit and Africa/Lagos timezone for scheduled tasks.
Data Model Summary
Users App — 9 Models
| Model | Purpose |
|---|---|
User |
Custom user with UUID PK, email-based auth |
Profile |
KYC tier, verification flags, DiceBear avatar fallback |
KYCDocument |
Document submissions with provider tracking |
KYCTierUpgrade |
Upgrade request audit trail |
VerificationAttempt |
Provider verification attempt log |
TrustedDevice |
Device fingerprint trust with rolling expiration |
DeviceLoginLog |
Immutable login attempt audit log |
PendingLoginVerification |
Temp storage for OTP-pending logins |
TwoFactorAuth |
TOTP secret, backup codes, 2FA state |
Wallet App — 7 Models
| Model | Purpose |
|---|---|
BaseWallet |
Abstract: triple-balance, status, feature flags |
FiatWallet |
NGN wallet with Flutterwave virtual account |
CryptoWallet |
Multi-network crypto wallet with sync status |
WalletTransaction |
Universal ledger with Generic FK to any wallet |
WalletPIN |
Hashed PIN with lockout protection |
WalletAuditLog |
Immutable operation audit trail |
WithdrawalFee |
Configurable per-currency, per-network fees |
QuidaxSubAccount |
1:1 user-to-Quidax mapping |
API Design
The API follows RESTful conventions with versioned endpoints under /api/v1/. Key design choices:
- ViewSets with custom actions for domain-specific operations (e.g.,
@action(detail=False, url_path='create')for wallet creation) - Read-only base ViewSets for wallets and transactions (creation goes through service layer actions, not standard CRUD)
- drf-spectacular decorators on every endpoint for comprehensive auto-generated documentation
- Custom exception handler (
authenticator_exception_handler) for consistent error response formatting - Pagination with configurable page size (default 10, environment-variable-driven)
Endpoint Groups
| Group | Endpoints | Description |
|---|---|---|
| Auth | 12 endpoints | Register, login, OTP verify, 2FA, logout |
| Profile & KYC | 8 endpoints | Profile CRUD, KYC status, tier upgrades, document management |
| Phone | 5 endpoints | Send code, verify, status, update, complete update |
| Password | 3 endpoints | Reset request, verify OTP, confirm new password |
| Crypto Wallet | 5 endpoints | Create, list, balance, withdraw, transactions |
| Fiat Wallet | 7 endpoints | Create, list, balance, withdraw, banks, verify account, transactions |
| Swap | 6 endpoints | Temp quote, create quote, refresh, confirm, history, detail |
| PIN | 5 endpoints | Set, verify, status, reset request, reset confirm |
| Security | 2 endpoints | Trusted devices, login history |
CI/CD Pipeline
GitHub Actions runs on every push and pull request to main, master, and develop:
- Environment setup — PostgreSQL 16 service container, Python 3.12, pip cache
- Dependency installation — Full
requirements.txtinstall - Database migration —
python manage.py migrateagainst the CI Postgres instance - Conventional commit check — Commitizen validates commit message format
Secrets are managed via GitHub repository secrets for all API keys, database credentials, and service configurations.
Infrastructure
Containerisation
The application is containerised with a multi-stage Dockerfile:
- Base image:
python:3.12-slim - Security: Runs as non-root user (
nonroot) - Production server: Gunicorn with configurable port (
PORTenv var, default 8000) - Docker Compose orchestrates the full local development stack: PostgreSQL, Django dev server, and MkDocs documentation server
Storage
- Database: PostgreSQL 16 with connection pooling (
CONN_MAX_AGE=600) - Cache: Redis with connection pool (50 max connections), 5-second timeouts, 1-hour default TTL
- Media/Static: Cloudflare R2 via django-storages (S3-compatible)
Key Technical Challenges
1. Webhook Reliability at Scale
Payment webhooks are the heartbeat of the system — a missed webhook means a user doesn't see their deposit. The solution involved:
- Idempotency keys on every transaction to handle provider retries safely
- Balance snapshot fields (balance_before, balance_after) for reconciliation
- Comprehensive logging at every webhook processing step
- Generic Foreign Key transactions that work across both wallet types
2. Swap Quotation Timing
Quidax quotations expire in 15 seconds. The UX challenge was keeping the client in sync: - Three-tier quotation system: temp (preview) → binding (15s) → confirmed - Refresh endpoint to extend expiring quotations without re-entering details - PIN verification happens at confirmation time, not quotation time, to avoid wasting the window on auth
3. KYC-Wallet Tier Synchronisation
Wallet tier levels must stay in sync with the user's KYC status without explicit sync calls:
- Both FiatWallet.save() and CryptoWallet.save() pull kyc_tier from the user's profile
- Django signals on profile changes cascade updates to dependent models
- The tier_upgrade_service orchestrates the full upgrade flow, including auto-approval logic for Tier 1-2
4. Device Trust vs. Security
Balancing security (challenge unknown devices) with UX (don't OTP-gate every login): - Device fingerprinting identifies returning devices - Rolling 7-day trust window refreshes on each successful login - Expired device cleanup runs nightly via Celery Beat to prevent table bloat - Login audit log enables security forensics without impacting the user flow
Codebase Metrics
| Metric | Value |
|---|---|
| Total Python files | 84 |
| Total lines of Python | ~15,700 |
| Git commits | 94 |
| Users app views | 2,286 lines |
| Wallet app views | 962 lines |
| Serializers (combined) | 1,111 lines |
| Django models | 16 (across both apps) |
| Service layer modules | 9 files |
| Webhook handlers | 3 (Dojah, Flutterwave, Quidax) |
| API endpoints | ~53 |
What I Would Do Differently
-
Event-driven architecture — Replace direct webhook-to-database writes with an event bus (e.g., Django Channels or a lightweight message queue). This would decouple webhook ingestion from business logic processing and improve resilience.
-
Database-level balance constraints — Add PostgreSQL CHECK constraints on
available_balance >= 0as a safety net beyond application-level validation. -
Structured integration tests — The 71 tests that were written and commented out early in development should have been maintained as the codebase evolved. I would invest more in integration tests that cover the full webhook-to-balance-update pipeline.
-
Rate limiting middleware — While individual endpoints have some rate limiting (e.g., phone verification), a global rate limiting middleware (e.g., django-ratelimit backed by Redis) would provide better DDoS protection.
-
Separate read/write database — For the transaction ledger, a read replica would prevent heavy reporting queries from impacting transactional throughput.
Outcome
The backend successfully delivers a unified fintech platform with:
- Full fiat on/off-ramp — NGN deposits via virtual accounts, withdrawals to any Nigerian bank
- Multi-chain crypto custody — BTC, ETH, USDT across 5 blockchain networks
- Instant swap engine — Crypto-to-crypto swaps with 15-second binding quotations
- Regulatory compliance — 4-tier KYC system with automated and manual verification paths
- Defence-in-depth security — JWT + device trust + 2FA + wallet PIN + audit logging
- Production-grade infrastructure — Containerised, CI/CD automated, monitored, and audited