Problem
According to the reports collected in the repo's SOURCES.md, more than 20 million UPI Autopay mandates are revoked every month in India because the customer's balance was short. Payment gateways already retry them.
What merchants can't easily show is which retries were allowed, which were refused, what a wrong retry cost, and which debits the system left alone. A recovery figure on its own doesn't answer those questions.
Users
- Payments and collections operations at a merchant running UPI Autopay: they need more of the failed debits recovered.
- The compliance reviewer who signs off: they need to see that no retry broke a rule, and why each decision was taken.
Constraints
- NPCI peak-hour windows. Effective 1 August 2025, non-customer-initiated debits (which is what a mandate debit is) are restricted to non-peak hours. Blocked: 10:00–13:00 and 17:00–21:30 IST, about 40% of the day.
- Five hard rules: revoked mandate, attempt cap, cooling period, recovery horizon and the NPCI peak window. No score and no agent decision may override one.
- Not every constant is equally trustworthy. In
policy.py, each one is graded as regulation, industry convention or assumption. The primary NPCI circular could not be fetched, and the repo says so. - Synthetic data only. No real merchant data is used, so the scorer's AUC describes this pipeline on generated data.
- Must reproduce without an API key, so a reviewer on a fresh clone gets the same numbers.
What I built
500 failed mandate debits
│
[ SCORER ] LightGBM · P(recover | features)
│
[ GUARDRAILS ] five hard rules, evaluated first and always
│
[ DECIDER ] LLM agent, only for the ambiguous band (0.15–0.65);
│ guardrails re-validate every proposal
[ EXECUTOR ] simulated rail over a 14-day clock
│
[ LEDGER ] append-only: input, score, rules fired, action, outcome, ₹
│
[ DASHBOARD ] where the 500 went, rule fire-counts, NPCI 24h clock,
and every record it failed to recover
- Dashboard. It shows the pipeline as a rail, a Sankey of where all 500 debits went (a test pins that edges sum to 500), the rulebook with fire and override counts, and a 24-hour clock with every executed debit as a tick.
- Explainability. Select any unrecovered record to see its SHAP explanation, or call
/explain/<id>. No API key is needed. - Independent total check.
verify_totalsre-derives the headline figure from the ledger without importing the ledger code, so the two totals are computed independently.
Metrics
Replay: where the 500 debits went
- Recovered170
- Stopped by a rule187
- Not retried, score too low128
- Ran out of time15
| Measure | Value |
|---|---|
| Records in the batch | 500 |
| Amount at risk | ₹1,26,32,606 |
| Recovered | ₹44,25,090 (35.0%) |
| Records recovered | 170 |
| Not recovered, listed in full | ₹82,07,516 |
| Stopped by a hard rule | 187 |
| Routed to the agent | 248 |
| Agent proposals vetoed by a rule | 11 |
| Debits executed | 266 |
| …of which inside an NPCI peak window | 0 |
| Automated tests | 214 |
All figures come from the committed seed on synthetic data, and a fresh clone with no API key reproduces every one.
Trade-offs I made
- Rules run before the score, and again after the agent.Some debits the model rates as likely to succeed are never attempted. I accepted lower recovery in exchange for zero compliance breaches.
- The agent only sees the ambiguous middle (score 0.15–0.65).Clear cases are handled deterministically, which keeps the agent's decisions few, cheap and auditable.
- Blocked retries are deferred.A retry that falls inside a peak window moves to the end of that window. It never runs inside the window, and every deferral is recorded.
- No API key required.A committed response cache, a deterministic fallback policy and an ablation script mean anyone can reproduce the numbers. The cost is that with an empty cache the ablation shows a ₹0 agent contribution, and the script prints a note saying so next to that zero.
- The dashboard lists every unrecovered record.They're shown largest first, next to the recovered total, so anyone can check the headline number against them.
What I'd do next
- Run the ablation with a key (
--populate) to measure the agent's actual rupee contribution. - Replace the secondary sources for the NPCI rules with the primary circular.
- Test the synthetic generator's assumptions against real failure distributions from a merchant or gateway.
