Answers based on the actual underwrite codebase at underwrite/ and pyproject.toml.
Underwrite is a nano-service platform for unsecured lending underwriting, implementing a Delegated Underwriting Protocol. It provides 28 purpose-built nano-services (risk scoring, fraud detection, KYC/AML, collateral management, loan origination, collections, recovery, governance, and more) that communicate over an in-process event bus with Ed25519 cryptographic attestation. The project is defined in pyproject.toml as "Delegated Underwriting Protocol — nano-service platform for unsecured lending".
A nano-service is a lightweight, independently deployable service that extends the NanoService abstract base class (underwrite/services/base.py:93). Each service:
- Has a unique
service_idand an Ed25519Identityfor signing emitted events. - Subscribes to typed domain events on a shared
EventBus. - Implements
handle(event) -> Noneto process incoming events. - Emits events via
emit(event_type, payload)which auto-signs. - Participates in saga orchestration and idempotency.
The 28 services are listed in SERVICE_NAMES in underwrite/__config__.py:461.
Exclusively through typed domain events over the event bus. A service calls self.emit(event_type, payload) which creates an Event dataclass (underwrite/__events__.py:22), signs it with the service's Ed25519 private key, and publishes it to the bus. Subscribers registered in the WIRING dict (underwrite/__service_registry__.py:80) receive matching events. The bus supports wildcard "*" subscriptions. Backends are pluggable: LocalBus (in-process, default), SQS, or Modal queues.
- Create a new sub-package under
underwrite/services/<name>/with__init__.pyandservice.py. - In
service.py, create a class extendingNanoService(orStatefulService) and implementhandle(self, event). - Register the service in three places:
SERVICE_MAPinunderwrite/__service_registry__.py:18— maps name tomodule.class.SERVICE_CLASSESinunderwrite/__service_registry__.py:49— maps name to class name.SERVICE_NAMESinunderwrite/__config__.py:461— adds to the known service list.
- Add wiring entries in
WIRINGdict to subscribe the service to relevant event types. - Configuration: add a
ServiceConfig(enabled=True)entry underservicesinunderwrite.json. - Run:
underwrite run <name>.
Plugin discovery is also supported via importlib.metadata.entry_points under the "underwrite.services" group (underwrite/__plugins__.py:34).
| Backend | Class | Use Case |
|---|---|---|
memory |
MemoryStore |
Development, testing, single-process. Data is lost on restart. |
filesystem |
FileStore |
Local development with persistence. Atomic writes with fsync. Circuit breaker optional. |
postgres |
PostgresStore |
Production. Connection pooling, circuit breaker, retry policy, migration engine. |
Configured via store.backend in underwrite.json or UNDERWRITE_STORE_BACKEND env var. CQRS is supported via CQRSStore — separate read and write stores (underwrite/__store__.py:566). Read replica is configured via store.read_backend and store.read_dsn.
A saga is a distributed transaction with compensating rollbacks. Defined in underwrite/__saga__.py:
- Define
SagaStepobjects — each has aforward_event_type/forward_payloadand acompensate_event_type/compensate_payload. - Call
orchestrator.start_saga(name, steps)to create a saga — returns asaga_id. - Call
orchestrator.execute_all(saga_id)to execute steps sequentially. - If any step fails, all completed steps are rolled back in reverse order via
__rollback(). - Each step is idempotent via store key
saga_step:{saga_id}:{step_index}— safe replay after crashes. - Incomplete sagas can be resumed with
orchestrator.replay_saga(saga_id).
The orchestrator registers itself with NanoService instances as emitters. Persisted sagas survive restarts.
Every emitted event carries an Ed25519 signature:
- The emitting service holds an
Identity(Ed25519 keypair), created viaIdentity.create()(underwrite/__identity__.py:48). - On
emit(), the payload is serialised and signed:sign(f"{event_id}:{timestamp}:{event_type}:{payload}")(underwrite/services/base.py:266). - The signature and
source_key(public key) are embedded in theEventenvelope. - On delivery,
AccessControl.assert_verified()verifies the signature against the trusted key forevent.source(underwrite/__authz__.py:207). - ACL policies control which services may publish/subscribe to which event types.
- Keys can be rotated automatically via
KeyRotationManagerwith a configurable TTL and grace period.
The ServiceSupervisor (underwrite/__supervisor__.py) tracks handler failures:
- On exception in
NanoService.__handle_event(),supervisor.record_failure(service_id)is called. - If failures exceed
max_restarts(default 3), the service is permanently marked unhealthy. Runtime.restart_failing_services()stops, re-registers, rewires, and restarts the service with exponential backoff.- Crashed handler events go to the
DeadLetterQueuefor later inspection and replay. - The circuit breaker on the bus opens for that subscriber after 5 consecutive failures (
CircuitBreakerin__bus__.py:223), preventing further dispatch until the recovery timeout.
Underwrite is designed for local-first, scale-up (single process). Scaling strategies:
- Vertical: Increase worker threads via
bus.max_workersandNanoServicemax_concurrent. - Backend swap: Replace
LocalBusandMemoryStore/FileStorewith SQS + Postgres for cross-process deployments. - CQRS: Use
CQRSStorewith a read replica to offload query traffic. - Service segregation: Run separate underwrite processes for different service groups (e.g. one for risk/fraud, another for servicing/collections).
- Plugin services run in the same process but are independently deployable via
discover_plugins().
The FraudService (underwrite/services/fraud/service.py) monitors loan origination and repayment events:
- Wash lending detection (
__check_wash): Detects rapid origination→repayment cycles. 3+ consecutive cycles trigger aWASH_FLAGevent. - Velocity/burst detection (
__check_burst): Flags borrowers with more than 3 recent originations asVELOCITY_FLAG. - Large origination: Originals >$1M trigger a
FRAUD_ALERT. - Records are kept per borrower (up to
MAX_BORROWERS=100000, 1000 events per borrower) and persisted to the store.
The delegation graph (underwrite/services/mechanism/graph.py) is the core state machine of the Delegated Underwriting Protocol:
- Seeds: Trusted entities with a
base_budget(e.g. banks, institutional lenders). - Users: Participants sponsored by seeds or other users, with a delegation edge (
sponsor→user,amount). - Each user has
earned(repayment credits) andprincipal(outstanding loans). - Credit limit = budget + earned − outgoing delegations.
- Default propagation: When a borrower defaults, losses propagate up the delegation chain: borrower's earned → sponsor's earned → sponsor's delegation edge → seed's base budget.
- Queries include path-to-seed, credit-limit, and user listing via
GraphService(underwrite/services/graph/service.py).
When a borrower defaults (DelegationGraph.default() at underwrite/services/mechanism/graph.py:142):
- The borrower's earned amount absorbs losses first.
- Remaining loss propagates to the sponsor: sponsor's earned is reduced, then the delegation edge amount is reduced.
- This repeats up the chain until the loss reaches a seed, where the seed's
base_budgetabsorbs it. - If any step cannot absorb the loss, a
ProtocolErroris raised. - The borrower's principal is set to 0 and outstanding loans are cleared.
The NPAService (underwrite/services/npa/service.py) tracks non-performing assets per RBI Master Circular guidelines:
| Bucket | Days Past Due |
|---|---|
| Standard | 0–90 days |
| Substandard | 91–180 days |
| Doubtful | 181–360 days |
| Loss | >360 days |
When DEFAULT_OCCURRED is received, the service checks if the borrower's overdue days exceed the DLG threshold (__trigger_days, default 120). If so, it emits DLG_TRIGGERED and marks the account with dlg_invoked = True. The classify_overdue_days() static method maps days to buckets. The mark_overdue() method allows external updates to days-past-due counters.
Multiple observability mechanisms built-in:
- Health checks:
HealthRegistryaggregates per-subsystem checks (bus, store, services, saga, tracer, DLQ, supervisor). Accessible viaunderwrite healthCLI or/v1/healthHTTP endpoint. - Metrics:
MetricsCollectortracks counters, timers, and gauges. Snapshots viaunderwrite metricsCLI, Prometheus export at/v1/metrics, or OTLP export viaconfig.tracing.exporter = "otlp". - Tracing:
Tracerwith console or OTLP span export. Each event carriestrace_idandparent_span_idfor distributed tracing correlation. - Structured logging: JSON log format configurable via
UNDERWRITE_LOG_FORMAT=json. PII fields auto-redacted. Correlation IDs attached to log records. - Dead-letter queue: Inspect and replay failed events with
underwrite dlq.
- Check if the event is in the dead-letter queue:
underwrite dlq - Inspect the error message for each failed entry.
- Check the runtime logs for exception tracebacks (look for
handler {service} failed processing {event_type}). - Check circuit breaker state — if open, the subscriber is not receiving events.
- Check idempotency — if the event is a duplicate, it is silently dropped (logged at DEBUG level).
- For signature failures, check
AuthzErrorlogs indicating invalid signatures. - Replay after fixing the issue:
underwrite dlq --replay
The Event envelope carries correlation_id, trace_id, and parent_span_id for cross-service trace correlation.
The full configuration schema is in underwrite/__config__.py. Key sections:
| Section | Key Settings | Env Var Prefix |
|---|---|---|
bus |
backend, rate_limit, max_workers, max_futures |
UNDERWRITE_BUS_* |
store |
backend, dsn, pool_size, read_backend, read_dsn |
UNDERWRITE_STORE_* |
logging |
level, output, format |
UNDERWRITE_LOG_* |
identity |
private_key, public_key, key_ttl, key_grace |
UNDERWRITE_IDENTITY_* |
tracing |
enabled, exporter |
UNDERWRITE_TRACING_* |
saga |
enabled |
UNDERWRITE_SAGA_ENABLED |
secrets |
backend, url, token, region |
UNDERWRITE_SECRETS_* |
recovery |
auto_restart, max_restarts, backoff_seconds |
UNDERWRITE_RECOVERY_* |
audit |
max_ledger, export_url |
UNDERWRITE_AUDIT_* |
fee |
schedules (late_payment, origination, prepayment, service) |
— |
governance |
param_ranges, param_defaults |
— |
Config is loaded from a JSON file, then overlaid with UNDERWRITE_* environment variables.
Migrations are defined in underwrite/__migrate__.py using the MigrationPlan and Migration classes:
- Add a new
Migrationtodefault_plan()with an incrementing version number and SQL statements. - If
migration.auto_migrateistrue(default), migrations run automatically atRuntime.start(). - Or run manually:
underwrite migrate. - Applied versions are tracked in the
migrationstable (version INT, description TEXT, applied_at TIMESTAMPTZ). - To roll back:
DELETE FROM migrations WHERE version = N;and manually revert the schema.
The project uses standard Python tooling:
pip install -e ".[dev,risk,postgres]"
make test # Runs pytest with coverage
make lint # Runs ruff
make typecheck # Runs mypy- Ruff linter config:
[tool.ruff.lint] select = ["E", "F", "I", "UP", "B"], line length 120. - mypy config:
ignore_missing_imports = true. - Pre-commit hooks are configured in
.pre-commit-config.yaml. - Mutation testing via
mutmut(optional:pip install underwrite[mutation]). - Tox for multi-env testing (
tox.ini). - See
CONTRIBUTING.mdfor full details.
- Python: 3.10, 3.11, 3.12, or 3.13 (declared in
pyproject.tomlrequires-python = ">=3.10"). - OS: Linux, macOS, or Windows (pure Python, no platform-specific dependencies).
- Optional: PostgreSQL (for
PostgresStore), Docker (fortestcontainersin integration tests). - No external message broker required —
LocalBusis fully in-process.
The project is in Beta (Development Status :: 4 - Beta). The TODO (TODO.md) and CHANGELOG (CHANGELOG.md) document planned and completed work. Key areas of ongoing development:
- SQS and Modal bus backends for distributed deployment.
- Enhanced fraud detection models and ML risk scoring integration.
- Governance parameter evolution via on-chain proposals.
- Collateral liquidation and settlement workflows.
- Recovery and collections automation.
- Additional state store backends.
- Enhanced OTLP/metrics observability.