The protocol state machine lives in DelegationGraph (underwrite/services/mechanism/graph.py) — a pure, testable domain model with no infrastructure dependencies.
| Entity | Type | Description |
|---|---|---|
| Seed | set[str] |
Root participants with a base_budget (e.g. banks providing capital). Added via add_seed(user, budget). |
| User | dict[str, float] (earned) |
Non-seed participant, added via sponsor relationship (add_user(sponsor, user, amount)). Every user has earned, principal, and a parent sponsor. |
| Delegation | dict[tuple[str,str], float] |
Directed edge sponsor → delegate with an allocated amount. Outgoing delegations consume credit limit. |
| Credit Limit | computed | budget + earned - outgoing_delegations. For non-seeds, budget = incoming delegation amount. |
| Loan | dict[str, list[...]] |
Originated against available credit. Carries principal, term, default_probability, protocol_rate, protocol_premium. |
| Required Delegation | computed recursively | Minimum delegation a user needs to maintain solvency: max(0, principal + sum(child_required) - earned). Max depth = 50. |
| Default | cascading loss | Loss propagates up the chain: borrower's earned → sponsor's delegation edge → seed base_budget. |
| Path to Seed | list[str] |
Delegation chain with cycle detection; reversed so index 0 is the seed. |
credit_limit(user) =
if user is seed: base_budget[user] + earned[user] - sum(outgoing_delegations)
if user is non-seed: delegation[(sponsor, user)] + earned[user] - sum(outgoing_delegations)
loss = min(principal[borrower], MAX)
1. absorb from borrower.earned
2. propagate remaining loss up to sponsor:
a. absorb from sponsor.earned
b. if loss remains, reduce delegation( sponsor → current )
3. if loss reaches a seed, deduct from seed.base_budget
stateDiagram-v2
state "DelegationGraph" as G {
[*] --> Seeds : add_seed
Seeds --> Users : add_user (sponsor → delegate)
Users --> Loans : originate (needs credit_limit)
Loans --> Defaults : default
Defaults --> Seeds : loss propagation
Users --> Users : repay (increases earned)
Users --> Users : revoke (changes delegation)
}
erDiagram
SEED ||--o{ USER : sponsors
SEED {
string user_id PK
float base_budget
}
USER ||--o{ USER : delegates-to
USER {
string user_id PK
string sponsor FK
float earned
float principal
}
USER ||--o{ LOAN : borrows
LOAN {
string borrower FK
float principal
float term
float default_probability
float protocol_rate
float protocol_premium
}
DELEGATION {
string sponsor FK
string delegate FK
float amount
}
All 80+ event types are defined as an EventType enum in underwrite/__events__.py:62. Convention: <domain>.<action>[.<outcome>].
| Event | Trigger |
|---|---|
seed.added |
add_seed command processed |
user.added |
add_user command processed |
loan.originated |
Loan originated against credit limit |
repaid |
Repayment applied |
default.occurred |
Default processed, loss propagated |
revoked |
Delegation edge amount changed |
| Event | Trigger |
|---|---|
quote |
Raw quote request |
quote.calculated |
Break-even rate computed |
pricing.computed |
Price set |
pricing.request |
Quote → pricing hand-off |
| Event | Trigger |
|---|---|
kyc.verified |
PAN + Aadhaar valid |
kyc.rejected |
Invalid PAN or Aadhaar |
kyc.video_initiated |
Video KYC session started |
kyc.video_verified |
Video KYC completed successfully |
aml.cleared |
Low-risk: AML score below threshold |
aml.flagged |
Medium-risk: AML score warrants review |
aml.frozen |
High-risk: AML score above freeze threshold |
| Event | Trigger |
|---|---|
ckyc.verify |
Initiate CKYC registry lookup |
ckyc.verified |
CKYC identity matched |
ckyc.rejected |
CKYC identity mismatch |
credit_bureau.check |
Credit report requested |
credit_bureau.checked |
Credit report received with score |
credit_bureau.check_failed |
Bureau API error |
| Event | Trigger |
|---|---|
consent.recorded |
Consent granted for a purpose |
consent.withdrawn |
Consent withdrawn by data subject |
consent.expired |
Consent period ended |
| Event | Trigger |
|---|---|
dsr.request |
DSR submitted by data subject |
dsr.requested |
DSR forwarded for fulfillment |
dsr.fulfilled |
DSR completed within 30-day window |
dsr.rejected |
DSR denied with rationale |
| Event | Trigger |
|---|---|
kfs.generate |
KFS generation requested |
kfs.generated |
KFS document produced with full disclosure |
| Event | Trigger |
|---|---|
pricing.request |
Rate/fee computation requested |
pricing.computed |
Price set with APR, EMI, fees |
penal_interest.assessed |
Penal interest applied on overdue |
foreclosure.computed |
Foreclosure charges calculated |
| Event | Trigger |
|---|---|
prepayment.request |
Prepayment initiated by borrower |
prepayment.processed |
Prepayment completed with charges |
provisioning.computed |
NPA provisioning amount calculated |
sma.classified |
SMA-0/1/2 classification assigned |
income_recognition.suspended |
Income recognition suspended for NPA |
| Event | Trigger |
|---|---|
breach.detected |
Potential data breach identified |
breach.notified |
Breach notification sent to DPB/authority |
breach.closed |
Breach investigation closed |
grievance.logged |
Complaint/grievance received |
grievance.resolved |
Grievance resolved |
data.purged |
Expired data purged per retention policy |
data.archived |
Historical data archived |
| Event | Trigger |
|---|---|
razorpay.order.create |
Payment order creation to Razorpay |
razorpay.order.created |
Razorpay order confirmed |
razorpay.payment.captured |
Payment successfully captured |
razorpay.payment.failed |
Payment failed |
razorpay.payment.refunded |
Payment refunded |
razorpay.subscribe |
Mandate/e-NACH subscription created |
razorpay.subscription.created |
Subscription active |
razorpay.subscription.charged |
Recurring charge collected |
razorpay.subscription.failed |
Recurring charge failed |
razorpay.mandate.active |
e-NACH mandate activated |
razorpay.mandate.inactive |
e-NACH mandate deactivated |
razorpay.webhook.received |
Razorpay webhook event received |
| Event | Trigger |
|---|---|
fraud.alert |
Large origination (>1M) |
fraud.wash.flag |
3+ origination/repayment cycles |
fraud.velocity.flag |
3+ originations in window |
| Event | Trigger |
|---|---|
risk.scored |
ML model score computed |
risk.early_warning |
Default probability > 0.30 |
| Event | Trigger |
|---|---|
npa.bucket.changed |
Days-past-due crosses threshold |
npa.dlg.triggered |
120+ days overdue triggers DLG |
| Event | Trigger |
|---|---|
collateral.marked |
LTV computed on origination |
collateral.liquidated |
Collateral sold on default |
| Event | Trigger |
|---|---|
governance.proposal |
Parameter change proposed |
governance.executed |
Proposal accepted and applied |
Recovery / Identity / Notification / Reporting / Underwriting / Document / Disbursement / Collection / Settlement / Origination / Servicing / Payment / Fee / Statement / Communication / Workflow / Decision / Graph / Mechanism / Saga / Idempotency
Full registry in underwrite/__events__.py. Includes:
identity.register,identity.rotateunderwrite.request,underwriter.approved,underwriter.rejectedpayment.receive,payment.schedule,payment.check_overdueworkflow.start,workflow.advancedecision.evaluate,decision.madesaga.started,saga.completed,saga.rolled_back,saga.compensateidempotency.duplicate_dropped- Graph queries:
graph_path,graph_credit_limit,graph_users(+_resultvariants) mechanism.rejected
@dataclass(frozen=True, slots=True)
class Event:
event_id: str # uuid4
event_type: str # e.g. "loan.originated"
source: str # service_id of emitter
source_key: str # Ed25519 public key
timestamp: str # ISO-8601 UTC
payload: dict # max 1000 keys, 1 MB serialized
correlation_id: str # uuid4 chain
signature: str # Ed25519 sig over event_id:timestamp:event_type:payload
trace_id: str
parent_span_id: strsequenceDiagram
participant Client
participant Mechanism
participant Fraud
participant Risk
participant Compliance
participant Decision
participant Underwriter
participant Audit
Client->>Mechanism: add_seed(bank, 1M)
Mechanism->>Audit: seed.added
Mechanism-->>Client: event_id
Client->>Mechanism: add_user(bank, alice, 100K)
Mechanism->>Fraud: user.added
Mechanism->>Compliance: user.added
Mechanism->>Risk: user.added
Mechanism->>Audit: user.added
Compliance->>Audit: kyc.verified
Compliance->>Audit: aml.cleared
Client->>Mechanism: originate(alice, 50K, 12, dp=0.15)
Mechanism->>Fraud: loan.originated
Mechanism->>Risk: loan.originated
Mechanism->>NPA: loan.originated
Mechanism->>Collateral: loan.originated
Mechanism->>Payment: loan.originated
Mechanism->>Fee: loan.originated
Mechanism->>Audit: loan.originated
Risk->>Decision: risk.scored
Fraud->>Decision: fraud.alert (if applicable)
Client->>Decision: decision.evaluate
Decision->>Underwriter: decision.made(approve)
Underwriter->>Audit: underwriter.approved
Client->>Mechanism: default(alice)
Mechanism->>NPA: default.occurred
Mechanism->>Collateral: default.occurred
Mechanism->>Recovery: default.occurred
Mechanism->>Audit: default.occurred
NPA->>Audit: npa.bucket.changed(loss)
underwrite/services/mechanism/service.py — The protocol state machine. Owns the DelegationGraph, processes commands (add_seed, add_user, originate, repay, default, revoke, quote), and emits domain events. Uses snapshot/rollback pattern: state is serialized to store on every mutation; on write failure, in-memory state is restored.
Commands arrive as service-name events — i.e. events with event_type == "mechanism" and a command field in the payload. Unknown commands are silently dropped. Protocol violations emit mechanism.rejected.
underwrite/services/audit/service.py — Append-only event ledger. Subscribes to almost every domain event and maintains an ordered ledger. Configurable max_ledger cap with optional export_url for offloading. Every event that any other service emits is tracked here.
underwrite/services/risk/service.py — Computes default-probability scores. Optionally integrates with sklearn RiskModel (controllable via RISK_MODEL_PATH env var). Emits risk.scored with the model's score, and risk.early_warning if default_probability > 0.30.
underwrite/services/fraud/service.py — In-memory activity tracking with batched store persistence. Maintains OrderedDict[str, deque] of borrower activity records (max 100K borrowers, 1000 entries per borrower). Rules:
- Wash lending: 3+ alternating origination/repayment cycles →
fraud.wash.flagwith score - Velocity: 3+ originations total →
fraud.velocity.flag - Large origination: Principal > 1,000,000 →
fraud.alertwith rule"large_origination"
underwrite/services/compliance/service.py — Indian KYC/AML compliance. Validates PAN format with category detection (Individual/Company/Firm/Trust/HUF etc.) and Aadhaar Verhoeff check-digit verification (not just regex). AML screening uses weighted keyword matching for PEPs, sanctions, fraud flags, and terror financing. Returns one of three states:
- cleared — low risk (score < threshold)
- flagged — medium risk, needs manual review
- frozen — high risk, blocked
Emits:
kyc.verifiedon PAN + Aadhaar format passkyc.rejectedon validation failureaml.cleared/aml.flagged/aml.frozenbased on risk scoreckyc.verifyto initiate CKYC registry lookupkyc.video_initiatedwhen video KYC is triggeredkyc.video_verifiedon video KYC completion
Also performs consent pre-check before initiating KYC, emitting consent.expired if consent is needed.
underwrite/services/decision/service.py — Signal aggregation. Collects signals from fraud, risk, and compliance for a given entity. On decision.evaluate:
- Any
highseverity signal →reject - 3+
mediumsignals →escalate - 1-2
mediumsignals →review - No signals →
approve
underwrite/services/underwriter/service.py — Loan application evaluation. Rejects if default_probability > 0.25 or principal <= 0.
underwrite/services/fee/service.py — Fee assessment with configurable schedules. Default schedules:
| Fee Type | Amount | Notes |
|---|---|---|
late_payment |
25.0 (flat) | Assessed on payment.overdue |
origination |
0.01 (1% of principal) | Assessed on fee.assess |
prepayment |
0.005 (0.5%) | Assessed on fee.assess |
service |
5.0 (flat) | Assessed on fee.assess |
underwrite/services/npa/service.py — RBI NPA classification. Buckets:
| Bucket | Days Past Due |
|---|---|
standard |
0–90 |
substandard |
91–180 |
doubtful |
181–360 |
loss |
>360 |
DLG (Default Loss Guarantee) triggers at 120+ days overdue, emitting npa.dlg.triggered.
underwrite/services/collateral/service.py — LTV tracking. On origination, marks collateral at ltv_ratio = 0.75 (75%). On default, liquidates and emits collateral.liquidated.
underwrite/services/payment/service.py — Payment scheduling, receipt, and overdue detection. Uses payment.schedule, payment.receive, payment.check_overdue commands. Overdue detection uses a 30-day cutoff. Integrates with Razorpay for UPI Autopay and e-NACH mandate collection.
underwrite/services/pricing/service.py — RBI-compliant interest rate and fee computation. Enforces per-product rate caps (home: 12%, gold: 18%, personal: 28%, micro-loans under ₹50K: 30% p.a. all-in-cost). Computes:
- EMI (equated monthly installment with amortization schedule)
- APR (annual percentage rate reflecting all-in-cost per RBI Master Direction)
- Penal interest (capped at 24% p.a.)
- Foreclosure charges (0% for personal/home loans per RBI)
- GST on fees (18% IGST on all processing and service fees)
- Debt-to-income ratio and credit score thresholds
underwrite/services/kfs/service.py — Key Fact Statement generation per RBI Master Direction on Digital Lending. The KFS is a standardized disclosure document that includes: loan amount, APR, repayment schedule, fees, penal interest, cooling-off period, and grievance redressal contact. The cooling-off period of 3 days allows borrowers to exit without penalty.
underwrite/services/consent/service.py — DPDPA 2023 consent lifecycle management. Tracks consent for each data processing purpose (KYC verification, credit bureau reporting, loan servicing, collection, communication). Supports consent recording, withdrawal, expiry, and re-consent workflows. Each consent record includes: purpose, grant timestamp, expiry timestamp, and withdrawal timestamp.
underwrite/services/credit_bureau/service.py — Multi-bureau credit report integration (CIBIL, Experian, Equifax) and CKYC identity verification. On a credit check request:
- Queries CIBIL (primary bureau) for credit score and report
- Optionally queries Experian and Equifax for supplementary data
- Performs CKYC check to verify identity against central KYC registry
- Emits
credit_bureau.checkedwith score, report summary, and CKYC status
underwrite/services/dsr/service.py — DPDPA 2023 data subject rights fulfillment. Handles DSR requests (access, correction, erasure, portability, grievance). On receipt of dsr.request:
- Validates the requestor's identity
- Fulfills within the DPDPA-mandated 30-day window
- Emits
dsr.fulfilledordsr.rejectedwith rationale
| Service | File | Responsibility |
|---|---|---|
| QuoteService | quote/service.py |
Quote generation from pricing |
| OriginationService | origination/service.py |
Application lifecycle |
| ServicingService | servicing/service.py |
Loan servicing |
| CollectionService | collection/service.py |
Collections tracking |
| DisbursementService | disbursement/service.py |
Fund disbursement |
| SettlementService | settlement/service.py |
Settlement processing |
| RecoveryService | recovery/service.py |
Default recovery actions (store-backed) |
| GovernanceService | governance/service.py |
Protocol parameter governance |
| GraphService | graph/service.py |
Delegation graph queries |
| IdentityService | identity/service.py |
Key registration/rotation |
| NotificationService | notification/service.py |
Alerts and notifications |
| CommunicationService | communication/service.py |
Email/SMS dispatch |
| ReportingService | reporting/service.py |
Report generation |
| DocumentService | document/service.py |
Document generation |
| WorkflowService | workflow/service.py |
Stage-based workflows |
| StatementService | statement/service.py |
Loan statements |
| Rule | Value | Source |
|---|---|---|
| Max default probability for approval | 0.25 (25%) | underwriter/service.py:13 |
| Risk early warning threshold | 0.30 (30%) | risk/service.py:56 |
| Wash lending cycle count | ≥ 3 | fraud/service.py:99 |
| Velocity origination count | > 3 | fraud/service.py:110 |
| Large origination alert | > 1,000,000 | fraud/service.py:60 |
| Fee: late_payment | 25.0 (flat) | fee/service.py:20 |
| Fee: origination | 0.01 (1%) | fee/service.py:21 |
| Fee: prepayment | 0.005 (0.5%) | fee/service.py:22 |
| Fee: service | 5.0 (flat) | fee/service.py:23 |
| Collateral LTV ratio | 0.75 (75%) | collateral/service.py:19 |
| NPA standard bucket | ≤ 90 days | npa/service.py:97 |
| NPA substandard bucket | 91–180 days | npa/service.py:99 |
| NPA doubtful bucket | 181–360 days | npa/service.py:101 |
| NPA loss bucket | > 360 days | npa/service.py:103 |
| DLG trigger | ≥ 120 days overdue | npa/service.py:27 |
| Required delegation max depth | 50 | graph.py:57 |
| Max payload keys | 1,000 | __events__.py:46 |
| Max payload size | 1 MB | __events__.py:17 |
| Decision: any high signal | reject | decision/service.py:75 |
| Decision: ≥ 3 medium signals | escalate | decision/service.py:78 |
| Decision: 1–2 medium signals | review | decision/service.py:80 |
| Decision: no signals | approve | decision/service.py:82 |