Skip to main content

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

  1. Borrow Ticket: Created by a vetted borrower requesting capital. Defines maximum APR, loan purpose, and repayment frequency.
  2. 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:

  1. Direct-Debit Mandate (Automated): The daily Asynq scheduler triggers ClaimInstallmentMandateTransaction against mandate_transactions strictly before calling the clearing house. Funds settle into the platform float, and installment_payout_attempt distributes principal and interest to the lender's wallet.
  2. Mono DirectPay (Instant Checkout): Borrower initiates an instant bank transfer from web or mobile. An in-flight row is registered in loan_repayment_directpay_pending and tracked via wallet_repayment_saga.
  3. 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.

TierRequired CredentialsCapabilitiesVerification Rail
Tier 0Phone (SMS OTP) + Verified EmailBrowse marketplace; view ticket ratesIn-house OTP / Resend
Tier 1BVN or NIN + Full Legal Name + DOBBorrow up to N50,000; Lend up to N100,000QoreID / Dojah / Monnify
Tier 2Government Photo ID + Selfie Liveness MatchBorrow up to N250,000; Lend up to N1,000,000Dojah AI Facial Verification
Tier 3Proof of Address + Bank Statement + Salary AccountUncapped 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).

1. Account Resolution & Authentication

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.

2. Salary Account Detection

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.

3. Mandate Authorization

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.

4. Multi-Account Sweep Strategy

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

  1. Repayment Punctuality (35%): On-time installment payments earn positive points; missed due dates trigger progressive penalties.
  2. Debt Service Ratio (25%): Verified monthly income vs. outstanding debt across Roja and external credit bureaus (CRC / FirstCentral).
  3. Platform Longevity & KYC Completeness (20%): Higher verified tiers and historical completed loans unlock higher score brackets.
  4. Marketplace Conduct (10%): Ticket acceptance rate, counter-offer behavior, and responsiveness.
  5. 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