Domain Models & Core Lifecycles
Roja's business logic is partitioned into explicit domain services located in backend/internal/services/. Each service owns its database tables, outbox event emissions, and state transition invariants.
1. The Ticket Marketplace
The Ticket (backend/internal/services/tickets, table tickets) is the fundamental unit of negotiation in Roja's peer-to-peer lending marketplace.
┌────────────────────────────────────────────────────────┐
│ BORROWER CREATES TICKET │
│ (Requested Principal, Max Interest, Desired Duration) │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ MARKETPLACE (OPEN) │
│ Publicly browseable by vetted lenders │
└─────────────┬────────────────────────────┬─────────────┘
│ │
Direct Match │ │ Counter-Offer
(Lender Accepts)│ │ (Modified Rate/Tenor)
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ MATCHED / ACCEPTED │ │ TICKET OFFERS TABLE │
│ Escrow funding reserved │ │ Borrower accepts offer │
└─────────────┬────────────┘ └────────────┬─────────────┘
│ │
└─────────────┬──────────────┘
▼
┌────────────────────────────────────────────────────────┐
│ FUNDED & LOCKED │
│ Lender wallet debited; funds placed in escrow sweep │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ TRANSITION TO LOAN │
│ New row in `loans`; Amortization schedule generated; │
│ Disbursement payout scheduled. │
└────────────────────────────────────────────────────────┘
Ticket Types
- Borrow Ticket: Created by a vetted borrower requesting capital. Defines maximum APR, loan purpose, and repayment frequency.
- Lend Ticket: Created by a lender advertising available capital with preferred risk tiers and return expectations.
Ticket Lifecycle States
draft: Incomplete ticket being configured by the user.open: Active on the marketplace, discoverable by counter-parties.matched: Both parties have agreed to the financial terms (principal, interest rate, duration).funding: Lender capital is in-flight or being debited from wallet / bank rail.funded: Capital secured; ready for loan origination and disbursement.cancelled/expired: Cancelled by the creator or expired past the marketplace TTL.
Liquidity Bot Agents (agent-worker)
To prevent cold-start liquidity traps, Roja runs an autonomous background agent service (backend/cmd/agent-worker). Platform liquidity agents:
- Monitor open borrow tickets from high-scoring borrowers (Roja Score ≥ 700).
- Post competitive counter-offers matching institutional yield criteria.
- Adhere to strict portfolio caps and require staff sign-off for large disbursements.
2. Loan Lifecycle & Settlement
When a ticket is fully funded, the DirectPay engine (backend/internal/services/directpay & loansettlement) originates a loans record and creates the child loan_installments.
Disbursement Rails
Disbursements are routed across three banking rails based on availability and provider health:
- Providus Bank (Direct NIBSS Instant Payments)
- Monnify (Account transfers)
- Mono (DirectPay payout API)
Ticket Funded
│
├─► Preflight: Validate borrower linked account & name match (BVN paired)
├─► Write marker: Insert `disbursement_payout_attempt` (pending)
├─► Threshold Check:
│ ├─► > N500,000: Insert `merchant_transfer_approval_requests` (Staff Queue)
│ └─► <= N500,000: Auto-approved
├─► Invoke Banking Rail
├─► Update attempt marker to `posted`
└─► Emit domain event: `loan.disbursed`
Installment Repayment Sagas
Repayments are calculated on a deterministic fixed-amortization schedule. Borrowers can repay through three distinct mechanisms:
- Direct-Debit Mandate (Automated):
The daily Asynq scheduler triggers
ClaimInstallmentMandateTransactionagainstmandate_transactionsstrictly before calling the clearing house. Funds settle into the platform float, andinstallment_payout_attemptdistributes principal and interest to the lender's wallet. - Mono DirectPay (Instant Checkout):
Borrower initiates an instant bank transfer from web or mobile. An in-flight row is registered in
loan_repayment_directpay_pendingand tracked viawallet_repayment_saga. - In-App Wallet: Borrower uses funds already held in their Roja customer wallet.
3. Multi-Tier Identity Verification (KYC)
Roja implements a four-tier progressive KYC architecture (backend/internal/services/kyc) in accordance with CBN regulations.
| Tier | Required Credentials | Capabilities | Verification Rail |
|---|---|---|---|
| Tier 0 | Phone (SMS OTP) + Verified Email | Browse marketplace; view ticket rates | In-house OTP / Resend |
| Tier 1 | BVN or NIN + Full Legal Name + DOB | Borrow up to N50,000; Lend up to N100,000 | QoreID / Dojah / Monnify |
| Tier 2 | Government Photo ID + Selfie Liveness Match | Borrow up to N250,000; Lend up to N1,000,000 | Dojah AI Facial Verification |
| Tier 3 | Proof of Address + Bank Statement + Salary Account | Uncapped borrowing & lending (subject to Score) | OCR Document Scanner + Mono Connect |
Paired Verification
To eliminate identity theft and synthetic identity fraud, Roja enforces Paired Verification:
- The legal name retrieved from the BVN database must match the name registered on the National Identity Number (NIN).
- The bank account linked for disbursements must bear the identical legal name confirmed during BVN resolution.
Webhook Ingestion & Replay Resilience
External KYC providers occasionally suffer network degradation. Roja solves this via webhook_events:
- Every incoming webhook is recorded verbatim pre-authentication in
webhook_events. - Signatures are verified using cryptographic headers (HMAC SHA-256).
- If processing fails due to a temporary database lock or downstream timeout, the KYC Reconciliation worker (
internal/jobs/kyc_reconcile_handler.go) replays unprocessed webhooks automatically.
4. Bank Linking & Repayment Mandates
Repayment automation is backed by Nigeria's Open Banking standards and Direct Debit Mandates (backend/internal/services/banking & directdebit).
The borrower connects their bank account via Mono Open Banking or provides their NUBAN (10-digit account number) and Bank Sort Code. Roja executes an instant name enquiry.
The salary analysis service (backend/internal/services/salaryanalysis) parses 6–12 months of statement data. It identifies regular payroll deposits, detects employer patterns, and calculates the borrower's Net Disposable Income.
A direct debit mandate (direct_debit_mandates) is registered with the clearing house (NIBSS or Monnify). The borrower authorizes the mandate via bank OTP, debit card tokenization, or biometric mobile banking prompt.
If the primary salary account fails an installment debit due to insufficient funds, Roja's collection coordinator executes a fallback sweep across authorized secondary accounts as specified in the loan agreement.
5. Credit Risk & The Roja Score
The Roja Score (backend/internal/services/score) is Roja's proprietary credit scoring engine. It generates an integer credit score ranging from 300 to 850.
The Append-Only Score Ledger
Scores are never updated in-place. Every score change writes an immutable transaction to roja_scores (21_roja_scores.sql):
INSERT INTO roja_scores (
user_id,
previous_score,
new_score,
score_delta,
event_type,
metadata
) VALUES (...);
Scoring Factors
- Repayment Punctuality (35%): On-time installment payments earn positive points; missed due dates trigger progressive penalties.
- Debt Service Ratio (25%): Verified monthly income vs. outstanding debt across Roja and external credit bureaus (CRC / FirstCentral).
- Platform Longevity & KYC Completeness (20%): Higher verified tiers and historical completed loans unlock higher score brackets.
- Marketplace Conduct (10%): Ticket acceptance rate, counter-offer behavior, and responsiveness.
- Social & Endorsement Graph (10%): Verified employer confirmation and guarantor standing.
Markov-Chain Default Modeling
The scoring service incorporates a continuous-time Markov chain model (backend/internal/services/score/markov_*):
- Models loan state transitions:
Current -> 30 DPD -> 60 DPD -> 90 DPD -> Default. - Computes transition probability matrices to predict the likelihood of default before funding approval.
- Allows staff operators to backtest scoring calibrations against historical loan cohorts using DuckDB.
6. Collections & Dunning Framework
When a borrower misses an installment due date, the loan enters the 6-Stage Collections Framework (backend/internal/services/collections):
Stage 1: Grace Period (1–3 DPD)
└── Gentle email & push reminders; automatic mandate retry on day 3.
Stage 2: Early Arrears (4–14 DPD)
└── Daily mandate retry; Roja Score penalization; restricted marketplace access.
Stage 3: Mid Arrears (15–30 DPD)
└── Formal collections case opened; dedicated recovery officer outreach; SMS dunning.
Stage 4: Serious Arrears (31–60 DPD)
└── Restructuring options; Promise-to-Pay (PTP) agreement (`collections_promises_to_pay`).
Stage 5: Pre-Default (61–90 DPD)
└── Secondary bank mandate sweep; guarantor notification; credit insurance claim filed.
Stage 6: Charged-Off Default (90+ DPD)
└── Reporting to national credit bureaus (CRC, FirstCentral); legal recovery escalation.
Staff agents manage cases, record borrower promises-to-pay, and approve restructuring plans directly through the Staff Operations Dashboard (apps/staff).
Next Steps
Learn how Next.js, Expo, @roja/common, and Storybook interact.
Deep-dive into Asynq workers, outbox events, and production operations.
Double-Entry LedgerReview the financial safety rules, chart of accounts, and money precision.
Local DevelopmentSet up your machine and run the full stack locally.