Skip to content

feat(backend+frontend): KYC/AML identity verification for offramp eligibility #212

@0xDeon

Description

@0xDeon

Problem

Nester's core value proposition is getting yield-bearing savings out to fiat in Nigeria and other African markets. In every one of those markets, processing a fiat payout to a bank account is a regulated activity. Without KYC (Know Your Customer) and basic AML (Anti-Money Laundering) checks:

  • Payment providers (Flutterwave, Paystack, etc.) will reject settlements above micro-transaction thresholds
  • The product is not legally operable at scale in NG/GH/KE
  • Fraudulent offramp attempts have no friction

This is a prerequisite for issue #100 (real fiat settlement integration) working at any meaningful volume.

Scope

This issue covers the minimum viable KYC layer needed to unblock offramp. Full regulatory compliance is beyond scope here — the goal is to gate settlement behind a basic identity check and pass verified user data to the payment provider.

What's Needed

Backend

New domain: apps/api/internal/domain/kyc/

model.go      // KYCSubmission, KYCStatus (pending/approved/rejected/expired)
service.go    // Submit, GetStatus, Verify
repository.go // Interface

New DB migration:

CREATE TABLE kyc_submissions (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id       UUID NOT NULL REFERENCES users(id),
  status        TEXT NOT NULL DEFAULT 'pending',  -- pending | approved | rejected | expired
  full_name     TEXT NOT NULL,
  date_of_birth DATE NOT NULL,
  country       TEXT NOT NULL,        -- ISO 3166-1 alpha-2
  id_type       TEXT NOT NULL,        -- nin | bvn | passport | drivers_license
  id_number     TEXT NOT NULL,        -- encrypted at rest
  submitted_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  reviewed_at   TIMESTAMPTZ,
  rejection_reason TEXT
);

Endpoints:

POST   /api/v1/users/{id}/kyc          — submit KYC details
GET    /api/v1/users/{id}/kyc          — get current KYC status

Settlement guard:

  • POST /api/v1/settlements must check kyc_submissions.status = 'approved' before creating
  • Return 403 + { code: "KYC_REQUIRED", message: "..." } if not verified

For MVP: verification can be manual (admin reviews and approves via PATCH /api/v1/admin/kyc/{id}) with a future path to automated NIN/BVN lookup APIs.

Frontend

New settings page section: "Identity Verification"

  • Show current KYC status badge (Not Started / Pending Review / Verified / Rejected)
  • Form to submit: full name, date of birth, country, ID type, ID number
  • On rejection: show reason and allow resubmission
  • Lock settlement UI with an inline prompt to complete KYC if status is not approved

Admin Panel

  • GET /api/v1/admin/kyc — list pending submissions
  • PATCH /api/v1/admin/kyc/{id} — approve or reject with reason

Acceptance Criteria

  • User can submit KYC details once; resubmission only allowed after rejection
  • ID number is encrypted at rest (not stored plaintext)
  • Settlement endpoint returns 403 KYC_REQUIRED if user has no approved KYC
  • Admin can approve/reject KYC submissions via API
  • Frontend shows KYC status in settings and blocks settlement UI if not verified
  • Full name and country from KYC passed through to payment provider on settlement
  • Unit tests for KYC service (status transitions, guard logic)

Notes

  • NIN (National Identification Number) and BVN (Bank Verification Number) are the two most important ID types for Nigeria
  • Do NOT store the raw ID number in plaintext — encrypt with the app secret or a separate KMS key
  • Future iteration: automate BVN lookup via NIBSS or a licensed data broker

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendGo backend infrastructurefeatureNew feature or functionalityfrontendDApp frontend (Next.js)priority:highHigh priority - foundational work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions