Skip to content

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