About Churnager: Autonomous Churn Intelligence for M-Pesa B2B SaaS 💡 Inspiration In East Africa's booming B2B SaaS ecosystem, M-Pesa (via Paybill and Till numbers) is the dominant payment rail for monthly software subscriptions. However, unlike traditional credit cards with standard recurring billing webhooks, M-Pesa payments frequently fail due to temporary liquidity bottlenecks, PIN timeouts, or customer-side payment delays.
When an M-Pesa payment fails, traditional analytics portals bury the failure deep inside transaction logs. By the time a Customer Success Manager (CSM) manually spots the drop in usage or missed invoice 30 days later, the account has already churned.
We built Churnager to solve this exact problem: an autonomous, real-time churn intelligence engine that continuously monitors customer telemetry, evaluates multi-factor risk rules, and delivers instant, AI-narrated retention alerts directly to decision-makers on WhatsApp.
🛠️ How We Built It Churnager was engineered as a decoupled, high-performance web system using Python, TypeScript, and modern AI orchestration.
+------------------------------------+ | Customer Telemetry / Webhooks | +------------------------------------+ | v +------------------------------------+ | FastAPI Rules Engine | +------------------------------------+ | v +------------------------------------+ | Deterministic Risk Scoring | +------------------------------------+ | v +------------------------------------+ | AI Narrator (Ollama / Template) | +------------------------------------+ | v +------------------------------------+ | WhatsApp Deep Link Dispatch | +------------------------------------+
- Deterministic Risk Engine & Mathematical Model Rather than relying solely on black-box machine learning models, Churnager uses a deterministic YAML configuration (risk_rules_v1.yaml) to calculate a normalized Churn Risk Score S i ∈ [ 0 , 100 ] S i ∈[0,100] for every customer cohort:
S
i
min ( 100 , ∑
k
1 n w k ⋅ I ( v k , i ≥ θ k ) ) S i =min(100, k=1 ∑ n w k ⋅I(v k,i ≥θ k )) Where:
w k w k is the predefined weight for risk signal vector k k. v k , i v k,i is the measured raw telemetry value for customer i i. θ k θ k represents the activation threshold parameter. I ( ⋅ ) I(⋅) is the indicator function returning 1 1 when the condition is met and 0 0 otherwise. The risk score maps directly into three operational intervention tiers:
Risk Tier ( S i
)
{ Critical , if S i ≥ 70 High , if 50 ≤ S i < 70 Medium , if 30 ≤ S i < 50 Low , if S i < 30 Risk Tier(S i )= ⎩ ⎨ ⎧
Critical, High, Medium, Low,
if S i ≥70 if 50≤S i <70 if 30≤S i <50 if S i <30
- Multi-Vector Telemetry Signals We track 5 primary churn vectors:
Login Recency Decay ( w
1
30 w 1 =30): Days since last active user session ( θ 1 ≥ 14 days θ 1 ≥14 days). M-Pesa Payment Failures ( w
2
25 w 2 =25): Count of consecutive M-Pesa transaction drops ( θ 2 ≥ 2 θ 2 ≥2). Usage Volume Drop ( w
3
20 w 3 =20): Percentage decline in weekly active API calls ( θ 3 ≥ 50 % θ 3 ≥50%). Plan Downgrade Intent ( w
4
15 w 4 =15): Explicit downgrade requests or seat reductions ( θ
4
1 θ 4 =1). Support Ticket Spikes ( w
5
10 w 5 =10): Unresolved high-priority support tickets ( θ 5 ≥ 3 θ 5 ≥3).
- AI Risk Narration & WhatsApp Delivery When a customer reaches a Critical ( S i ≥ 70 S i ≥70) or High ( S i ≥ 50 S i ≥50) tier, the AI Narrator compiles the active signal vectors into a structured, human-readable WhatsApp alert copy. Using WhatsApp deep-linking (https://wa.me/{phone}?text={encoded_narrative}), CSMs can dispatch intervention messages with a single tap.
⚡ Challenges We Faced
- Timezone-Aware vs. Timezone-Naive Timestamp Traps Challenge: When flushing newly generated events parsed from Pydantic ISO string payloads (2026-07-21T08:00:00Z), Python produced timezone-aware datetime objects in SQLAlchemy memory, while past SQLite entries were stored as timezone-naive UTC strings. Comparing them caused runtime TypeError: can't compare offset-naive and offset-aware datetimes.
Solution: We built explicit timestamp sanitization in main.py, stripping tzinfo from incoming event timestamps before inserting into the SQLAlchemy session:
python
def sanitize_timestamp(dt: datetime) -> datetime: return dt.replace(tzinfo=None) if dt else datetime.utcnow()
- Next.js Hydration Safety & Schema Evolution Challenge: Client-side rendering in Next.js threw Application error: a client-side exception has occurred when rendering signal badges due to subtle property mismatch (sig.type vs sig.name).
Solution: We completely hardened all UI components ( TierBadge.tsx , SignalBar.tsx , WhatsAppButton.tsx ) with safe property fallback chaining ((sig.name || sig.type || "Signal").replace(/_/g, " ")), ensuring 100% hydration stability across all build targets.
📚 What We Learned High-Trust AI Requires Deterministic Guardrails: AI LLMs excel at generating persuasive outreach narratives, but calculating financial churn risk must be deterministic and verifiable. Combining a transparent rules engine with AI narrative copy creates the ideal balance of accuracy and speed. Zero-Friction Delivery Beats Complex Portals: CSMs in fast-growing markets do not want to monitor yet another web portal. Bringing actionable risk alerts into WhatsApp—where teams already communicate—dramatically increases intervention response times.
Built With
- apscheduler
- css3
- docker
- docker-compose
- fastapi
- git
- html5
- m-pesa
- make
- next.js
- ollama
- openai-codex
- postgresql
- pydantic
- python
- railway
- react
- rest-api
- sqlalchemy
- sqlite
- tailwind-css
- typescript
- vercel
- yaml
Log in or sign up for Devpost to join the conversation.