Building ARIP: A Production-Ready Agri-Risk Intelligence Platform
A deep dive into building a 15-day forward risk forecasting system for Indian agri-lending with instant credit decisioning, complete loan lifecycle management, and ecosystem integration.
Ever wondered what happens when you combine weather data, satellite imagery, and market signals to predict agricultural loan defaults 15 days before they happen? That's exactly what I built with ARIP (Agri-Risk Intelligence Platform), and in this post, I'll walk you through every decision, challenge, and solution that went into creating this production-ready digital lending solution.
The Problem That Sparked Everything
Indian agriculture contributes about 18% to the country's GDP and employs roughly 42% of the workforce. Yet, agri-loans have NPA (Non-Performing Asset) rates of 8-12% compared to just 2-3% for other sectors. Why? Because banks discover defaults only after they happen.
I kept asking myself: What if we could predict loan risk before it becomes a problem?
The data exists—weather patterns, crop health from satellites, market prices from mandis. It's just sitting in silos, not being used proactively. That's the gap ARIP fills.
What I Built: The 30,000-Foot View
ARIP is a full-stack platform that:
- Predicts risk 15 days in advance using a weighted formula combining weather, NDVI (crop health), and market volatility
- Makes instant credit decisions in under 30 minutes (typically under 5 seconds)
- Manages the entire loan lifecycle from application to collection
- Integrates with the ecosystem—mandis, insurance (PMFBY), government schemes (PM-KISAN)
- Supports group lending for FPOs, JLGs, and SHGs
The Tech Stack
Backend: Python + FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(
title="Agri-Risk Intelligence Platform",
description="Production-ready digital lending solution for Indian agri-finance",
version="0.4.0",
)
I went with FastAPI for several reasons:
- Async by default - External API calls to weather services, satellite data, and market APIs need to be non-blocking
- Automatic OpenAPI docs - Critical for a platform with 50+ endpoints
- Pydantic validation - Type safety without the boilerplate
- Performance - One of the fastest Python frameworks available
Frontend: React + TypeScript + Tailwind
The dashboard needed to be responsive and data-heavy. React with TypeScript gave me the type safety I needed when dealing with complex financial data structures.
Database: PostgreSQL
Financial data needs ACID compliance. No compromises here. PostgreSQL also gave me excellent JSON support for storing flexible metadata.
The Risk Engine: The Heart of ARIP
The risk score formula is deceptively simple:
Risk Score = (0.40 × Weather) + (0.35 × NDVI) + (0.25 × Market Volatility)
But implementing it correctly took serious thought.
The Risk Calculation Code
from decimal import Decimal
RAINFALL_WEIGHT = Decimal("0.4")
NDVI_WEIGHT = Decimal("0.35")
MARKET_WEIGHT = Decimal("0.25")
LOW_THRESHOLD = 40
MEDIUM_THRESHOLD = 70
def compute_risk_score(
rainfall_normalized: Decimal,
ndvi_normalized: Decimal,
market_normalized: Decimal
) -> Decimal:
weighted_score = (
RAINFALL_WEIGHT * rainfall_normalized +
NDVI_WEIGHT * ndvi_normalized +
MARKET_WEIGHT * market_normalized
)
return clamp(weighted_score)
Why Decimal Instead of Float?
Financial calculations with floating-point numbers can introduce subtle rounding errors:
# This is dangerous in financial code
>>> 0.1 + 0.2
0.30000000000000004
# Decimal handles it correctly
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3')
Building Resilient External API Adapters
ARIP depends on external data sources—weather APIs, satellite imagery, market prices. These services can be slow, rate-limited, or completely down.
The Base Adapter with Retry Logic
class BaseAdapter(ABC):
async def _execute_with_retry(
self,
operation: Callable[[], Any],
operation_name: str = "request",
) -> AdapterResult[Any]:
last_error = None
for attempt in range(self.retry_config.max_attempts):
try:
result = await asyncio.wait_for(
operation(),
timeout=self.retry_config.timeout_seconds,
)
return AdapterResult(success=True, data=result)
except asyncio.TimeoutError:
last_error = self._create_error(AdapterErrorType.TIMEOUT)
# Apply backoff before next retry
if attempt < self.retry_config.max_attempts - 1:
backoff = self.retry_config.get_backoff(attempt)
await asyncio.sleep(backoff)
return AdapterResult(success=False, error=last_error)
The key insight: stale data is better than no data, but we need to track when we're using fallback data.
The Credit Decision Engine
The hackathon requirement was clear: credit decisions in under 30 minutes. I aimed for under 5 seconds for automated decisions.
RISK_SCORE_REJECT_THRESHOLD = 80
RISK_SCORE_REFER_THRESHOLD = 65
def _make_decision(self, application, factors) -> DecisionResult:
risk_score = factors.get("risk_score")
if risk_score is not None:
if risk_score > RISK_SCORE_REJECT_THRESHOLD:
return DecisionResult(
status=CreditDecisionStatus.REJECTED,
factors={"rejection_reasons": ["Risk score exceeds threshold"]},
)
elif risk_score > RISK_SCORE_REFER_THRESHOLD:
return DecisionResult(
status=CreditDecisionStatus.REFERRED,
requires_manual_review=True,
)
return DecisionResult(
status=CreditDecisionStatus.APPROVED,
approved_amount=approved_amount,
)
Security: Tightening Everything Down
Security in a financial platform isn't optional.
JWT Authentication with Role-Based Access Control
class Role(str, Enum):
ADMIN = "Admin"
OPERATOR = "Operator"
VIEWER = "Viewer"
ROLE_PERMISSIONS = {
Role.ADMIN: {Permission.READ, Permission.WRITE, Permission.ADMIN},
Role.OPERATOR: {Permission.READ, Permission.WRITE},
Role.VIEWER: {Permission.READ},
}
Input Validation with Pydantic
class LoanCreate(BaseModel):
farmer_id: UUID
amount: Decimal = Field(gt=0, description="Loan amount must be positive")
disbursement_date: datetime
due_date: datetime
@field_validator("due_date")
@classmethod
def due_date_after_disbursement(cls, v, info):
disbursement = info.data.get("disbursement_date")
if disbursement and v <= disbursement:
raise ValueError("due_date must be after disbursement_date")
return v
Testing: Property-Based Tests
For financial software, "it works on my machine" isn't good enough. I used Hypothesis for property-based testing:
from hypothesis import given, strategies as st
class TestRiskBasedAutoReject:
@given(risk_score=st.floats(min_value=80.01, max_value=100.0))
def test_high_risk_triggers_rejection(self, risk_score: float):
# For any risk score > 80, the decision must be REJECTED
assert risk_score > RISK_SCORE_REJECT_THRESHOLD
decision = make_decision(risk_score)
assert decision.status == CreditDecisionStatus.REJECTED
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND (React + TypeScript) │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ API LAYER (FastAPI) │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ RiskEngine │ CreditDecision │ Underwriting │ Disbursement │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ ADAPTER LAYER │
│ Weather (IMD) │ NDVI (Sentinel) │ Mandi │ PMFBY │ SMS Gateway │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL + Redis │
└─────────────────────────────────────────────────────────────────┘
Performance Benchmarks
| Operation | Target | Achieved | |-----------|--------|----------| | Credit Decision | < 30 min | < 5 sec | | Risk Computation | < 60 sec | ~2-3 sec | | API Response (p95) | < 500ms | ~150ms | | Dashboard Load | < 3 sec | ~1.5 sec |
What I Learned
-
Start with the domain model. The 30+ database tables emerged from understanding the lending lifecycle.
-
Async is worth the complexity. When you're hitting 3-4 external APIs per risk computation, blocking calls kill performance.
-
Property-based testing catches bugs unit tests miss. Hypothesis found edge cases I never would have thought to test.
-
Fallback strategies are essential. External services fail. Plan for it.
-
Security isn't a feature—it's a foundation. JWT, RBAC, input validation should be there from day one.
What's Next?
ARIP is production-ready, but there's always more to build:
- Mobile app for field agents
- WhatsApp bot integration for farmer notifications
- ML models for more sophisticated risk prediction
- Blockchain audit trail for regulatory compliance
Building ARIP taught me that the best software solves real problems. Indian farmers deserve better financial tools, and banks deserve better risk visibility. This platform is my contribution to bridging that gap.
This post was written based on my experience building ARIP for the NABARD Agri Credit Hackathon 2025.