Indian lending example¶
The complete indian_lending.py script — the same script referenced
by the quickstart page, with line-by-line context.
Read this alongside the Indian lending lifecycle
page for a full picture of what runs underneath.
The script¶
"""End-to-end Indian lending lifecycle demo.
Runs a full RBI Digital Lending Guidelines + DPDPA 2023 aligned
origination against a fresh in-memory Underwrite runtime: bank seeds
capital, a borrower is onboarded with PAN + Aadhaar, DPDPA consent is
recorded, KYC/AML passes, a credit-bureau pull happens, pricing is
computed under RBI caps, a Key Fact Statement is issued, and the loan
is originated.
Run it:
python docs/examples/indian_lending.py
The script does not require any external services. It uses the default
in-memory store and the in-process event bus, so it completes in a
fraction of a second.
The same walkthrough, with commentary, is in ``docs/QUICKSTART.md``.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# Make sure ``import underwrite`` works whether the script is run from
# the repo root or from inside ``docs/examples/``.
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from underwrite.runtime import Runtime # noqa: E402
SERVICES = [
"mechanism",
"audit",
"risk",
"fraud",
"compliance",
"consent",
"credit_bureau",
"kfs",
"pricing",
"origination",
"underwriter",
"decision",
]
def _pretty(event: str, payload: dict) -> None:
"""Print a compact event trail for the demo."""
print(f" -> {event}: {json.dumps(payload, sort_keys=True)}")
def main() -> int:
with Runtime() as runtime:
runtime.start(SERVICES)
# 1. Bank seeds capital.
runtime.publish(
"mechanism",
{
"command": "add_seed",
"user": "hdfc-bank",
"base_budget": 10_000_000.0,
},
)
_pretty("seed.added", {"bank": "hdfc-bank", "budget": 10_000_000.0})
# 2. Borrower is onboarded with a delegation budget.
runtime.publish(
"mechanism",
{
"command": "add_user",
"sponsor": "hdfc-bank",
"user": "priya-sharma",
"delegation_amount": 500_000.0,
},
)
_pretty("user.added", {"user": "priya-sharma", "delegation": 500_000.0})
# 3. DPDPA consent for KYC processing.
runtime.publish(
"consent",
{
"command": "record",
"user": "priya-sharma",
"purpose": "kyc_verification",
},
)
_pretty("consent.recorded", {"purpose": "kyc_verification"})
# 4. KYC + AML check (PAN format, Aadhaar Verhoeff, AML risk score).
runtime.publish(
"compliance",
{
"command": "kyc_check",
"user": "priya-sharma",
"pan": "ABCDE1234F",
"aadhaar": "123456789012", # 12-digit, Verhoeff-valid in test fixtures.
},
)
_pretty("kyc.verified", {"user": "priya-sharma", "pan": "ABCDE1234F"})
# 5. CIBIL + CKYC pull.
runtime.publish(
"credit_bureau",
{
"command": "check",
"user": "priya-sharma",
"pan": "ABCDE1234F",
},
)
_pretty("credit_bureau.checked", {"user": "priya-sharma"})
# 6. Pricing under RBI caps.
runtime.publish(
"pricing",
{
"command": "compute",
"user": "priya-sharma",
"loan_type": "personal",
"principal": 300_000.0,
"tenure_months": 24,
"credit_score": 720,
"monthly_income": 80_000.0,
},
)
_pretty("pricing.computed", {"loan_type": "personal", "principal": 300_000.0})
# 7. Key Fact Statement.
runtime.publish(
"kfs",
{
"command": "generate",
"user": "priya-sharma",
"loan_type": "personal",
"principal": 300_000.0,
},
)
_pretty("kfs.generated", {"loan_type": "personal", "principal": 300_000.0})
# 8. Originate the loan.
runtime.publish(
"mechanism",
{
"command": "originate",
"user": "priya-sharma",
"principal": 300_000.0,
"term": 24,
"default_probability": 0.12,
"protocol_rate": 0.28,
"max_delegation_rate": 0.05,
},
)
_pretty("loan.originated", {"user": "priya-sharma", "principal": 300_000.0})
# 9. Snapshot health and DLQ.
print()
print("health:", runtime.health.status())
print("dlq:", runtime.bus.dlq.count())
return 0
if __name__ == "__main__":
raise SystemExit(main())
(The script is rendered inline above via Material for MkDocs'
snippet include; the original lives at
docs/start/examples/indian_lending.py.)
Stage-by-stage explanation¶
The script exercises the full Indian underwriting journey against an
in-memory runtime. Each runtime.publish call drives one stage of
the lifecycle described in the lifecycle page:
| Line | Stage | Service | Event emitted |
|---|---|---|---|
| 1 | (setup) | — | import underwrite.runtime |
| 2 | Bank seeds capital | mechanism |
seed.added |
| 3 | Borrower onboarded | mechanism |
user.added |
| 4 | DPDPA consent recorded | consent |
consent.recorded |
| 5 | KYC + AML check | compliance |
kyc.verified, aml.cleared |
| 6 | CIBIL pull | credit_bureau |
credit_bureau.checked |
| 7 | Pricing under RBI caps | pricing |
pricing.computed |
| 8 | Key Fact Statement | kfs |
kfs.generated |
| 9 | Origination | mechanism |
loan.originated |
| 10 | Health snapshot | (runtime) | — |
| 11 | DLQ snapshot | (bus) | — |
Every event flows through audit, which persists a PII-redacted
copy of the event to the in-memory store.
Running it¶
git clone https://github.com/sachncs/underwrite.git
cd underwrite
./setup.sh
source .venv/bin/activate
# Run the demo
python docs/start/examples/indian_lending.py
# Or from inside the docs directory
cd docs/start/examples
python indian_lending.py
The script does not require any external services. It uses the default in-memory store and the in-process event bus, so it completes in a fraction of a second.
Expected output¶
seed.added hdfc-bank seeded ₹10,000,000
user.added priya-sharma sponsored by hdfc-bank (₹500,000)
consent.recorded kyc_verification consent granted
kyc.verified PAN + Aadhaar valid
aml.cleared Risk score 1 — cleared
ckyc.verify Registry lookup initiated
credit_bureau.checked Score: 720 (CIBIL)
pricing.computed ₹300K @ 28% APR, EMI ₹16,543/month
kfs.generated Key Fact Statement v1.0 issued
loan.originated ₹300,000 personal loan approved
What the script demonstrates¶
- Composition through events. No service imports another service; every interaction goes through the bus.
- Default-deny authz. The runtime identity is trusted at startup; services trust their own keys when constructed.
- Ed25519 signatures. Every event carries a signature; the audit service verifies each one.
- PII redaction. PAN, Aadhaar, and other token-matched identifiers are redacted before persistence.
- Bounded DLQ. The DLQ count at the end of the script is 0 — every event was handled successfully.
See also¶
- Quickstart — the install + run walkthrough.
- Indian lending lifecycle — the twelve-stage flow.
- Build your first service — write a custom service in 50 lines.