This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
Every Indian phone has this SMS somewhere: "Rs.1250.00 debited from A/c XX4521 ... Txn status: PENDING. If not credited to beneficiary, amount will be reversed." The money left. It didn't arrive. And then you wait, check the balance twice a day, maybe call the bank, and eventually forget about it. If it comes back in a week, you're relieved.
Here's what almost nobody knows: since 15 October 2019, the bank has to pay you ₹100 for every day that reversal is late. Not if you complain. Automatically. It's in RBI's circular on failed transactions, paragraph 5: compensation "shall be effected to the customer's account suo moto, without waiting for a complaint or claim from the customer."
So the money is owed. Getting it just needs someone patient: someone who remembers the date, counts the days, writes the letter, waits 30 days, writes the next letter. That's the most sloth-shaped job I could think of, so I built the sloth.
What I Built
Paisa Wapas ("money back"). You forward the failed-payment SMS once. Then you do nothing.
- Gemma reads the SMS: is this a UPI payment to a person or to a shop, an ATM that didn't give cash, a card, IMPS, Aadhaar Pay, a wallet, a reversal, or just a normal payment? It pulls out the amount, date, reference, bank and payee.
- Code checks every value Gemma gives against the SMS text. An amount, date or reference that isn't in the message is dropped and listed. The model never decides a deadline or a rupee.
- Code applies the RBI rule: UPI to a person must be reversed by T+1, UPI to a shop by T+5, an ATM by T+5, and so on (ten rows from the circular's annex). It works out the deadline, how late it is, and what's owed.
- A Temporal workflow does the waiting. A durable timer runs to the deadline. If the money isn't back, Gemma drafts a letter to the bank, from facts code worked out. When you send it, a 30-day timer starts, and if nothing happens, Gemma drafts the complaint to the RBI Ombudsman (free, at cms.rbi.org.in). When the money finally comes back, it tells you the compensation the bank owes.
- Nothing is sent for you. Every letter is a draft you copy.
Who gets to be lazier: anyone who's ever had a payment fail. Especially the people at home who would never write to a bank's nodal officer, or know that there's a nodal officer at all.
Demo
Live (no sign-up): https://paisa-wapas-1037019152152.asia-south1.run.app
Tap a sample SMS, then pick "It takes 45 days" and Fast-forward. You're watching the real workflow run on Temporal's time-skipping test server: 45 days of patient waiting in about 15 seconds, with two real Gemma-written letters along the way.
Code
Ankith-m1006
/
paisa-wapas
Forward a failed UPI/bank SMS once: Gemma reads it, code applies the RBI deadline and Rs 100/day, a Temporal workflow does the waiting.
Paisa Wapas
Money cut, payment failed? Forward the SMS once. Paisa Wapas knows the RBI deadline, counts the ₹100 a day the bank owes you, and does the waiting for you.
Live: https://paisa-wapas-1037019152152.asia-south1.run.app (no sign-up)
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 3: Slow and Steady.
The rule almost nobody uses
Since 15 Oct 2019, RBI's circular on failed transactions (DPSS.CO.PD No.629/02.01.014/2019-20) sets a deadline for every kind of failed payment: a UPI transfer that left your account but never reached the other person must be reversed by T + 1, an ATM that didn't give cash by T + 5, and so on. After that the bank owes you ₹100 for every day of delay, and paragraph 5 says it must credit that without waiting for a complaint. If the…
How I Built It
1. The rule first, from the source
Before any model, I read the circular itself (RBI/2019-20/67, DPSS.CO.PD No.629/02.01.014/2019-20, 20 September 2019) and turned its annex into a table in rules.py:
| Failed payment | Back by | Then |
|---|---|---|
| UPI to a person, IMPS, card to card, NACH, wallet | T + 1 | ₹100/day |
| UPI to a shop, card at a shop or online, ATM, Aadhaar Pay | T + 5 | ₹100/day |
T is the day of the transaction. Ten rows, one verdict() function, no model involved. Then the RBI Integrated Ombudsman Scheme, 2021 for the escalation: complain to the bank in writing first, and if there's no reply in 30 days, you can go to RBI, within a year and 30 days.
2. The model reads, code decides
Gemma 4 (gemma-4-26b-a4b-it) gets one SMS and returns a small JSON object: kind, amount, date, ref, bank, payee, each copied exactly as written. Then check() treats every field as a claim:
- the amount must match a number in the SMS,
- the reference must appear in the SMS,
- the date must appear in the SMS and parse (
18-10-26,09-Oct-26,16Oct26,21 Oct 2026...), - anything that fails is dropped and shown as a problem, never guessed.
Why not just let Gemma do everything? Because the one place a model must not be creative is a number you'll put in a letter to your bank.
3. The waiting is a workflow, not a reminder
This is the part I'm proudest of, and the reason I picked Temporal. A failed payment is a slow process by law: one day, five days, then thirty more. A cron job and a database row can do it, but every edge case (the laptop sleeps, the server restarts, the money comes back while you're writing the letter) becomes my bug.
In Temporal it's just code that waits:
deadline_end = date.fromisoformat(self.status.deadline) + timedelta(days=1)
if await self._wait_until(deadline_end, lambda: self.reversed_on is not None):
return self._close(case, "Money came back on time")
self.status.letters["bank"] = await workflow.execute_activity(draft_letter, Draft(...), **LETTER_OPTS)
...
rbi_day = self.complaint_sent_on + timedelta(days=30)
if await self._wait_until(rbi_day, lambda: self.reversed_on is not None):
break
_wait_until is a durable timer that also wakes up early on a signal ("the money's back", "I sent the letter"). The workflow sleeps for weeks, survives restarts, and the code reads like the story.
4. Watching 45 days in 15 seconds
A demo of something that waits a month is a problem. Temporal's time-skipping test server is the answer: it fast-forwards the clock whenever the workflow is only waiting. The live preview runs the real PaisaWapas workflow on it, so what you see is the real logic, not an animation.
It took three tries to get right, and that's where most of my evening went:
- The clock can't go back. The test server starts at "now", so an SMS from 18 October can't be replayed from its own date. The preview now runs as if the SMS arrived today, and says so on the page.
-
Blocking the event loop. The letter activity called Gemma with a blocking HTTP request inside
asynccode, which froze the whole server. Fix:await asyncio.to_thread(gemma, ...). - Time kept skipping during the model call. While Gemma was writing (15-20 s), the test server skipped hours ahead, so the activity's 3-minute timeout fired in skipped time and it retried forever ("Activity not found on completion" in the logs). Fix: in the preview, the workflow records each letter request instantly, and Gemma writes those exact letters once the run is done. In real mode the real activity runs inside Temporal, with a timeout in days, because a letter is a draft a person sends later anyway.
5. The worst failure, and its guard
The worst thing this app could do is put a wrong number in a letter to a bank. So after Gemma writes a letter, code checks that the amount, the compensation and the deadline it was given all appear in the text. If any is missing or changed, the Gemma letter is thrown away and a plain letter built straight from the facts is used instead. Boring, correct, sendable.
A second, real one from tonight: a Gemma reply came back with extra text after the JSON and crashed my eval. Now the reader parses the first JSON object only, and if a reply can't be read at all, it records "the model's answer couldn't be read; nothing was assumed" instead of guessing.
Gemma vs the regex app I'd otherwise write
The test set is 40 SMS written by hand in the real formats of SBI, HDFC, ICICI, Axis, Kotak, PNB, Canara, Bank of Baroda, Paytm and PhonePe (no real customer data): failed UPI to people and shops, ATMs, cards, IMPS, AePS, wallets, reversals, and normal payments, salary credits, OTPs and offers that must not be chased. 25 of them are failed payments.
The baseline is the honest alternative: keywords for the status ("failed", "pending", "not credited", "timed out"...), the first Rs/INR amount, the first 12-digit reference, the first date. It was written while looking at SMS 1-30; SMS 31-40 were written afterwards (Hinglish, a balance printed before the amount, a reversal mentioned inside a success message, a forwarded PhonePe notification), so those ten are held out for it. Gemma was never tuned on any of them.
"Letter-ready" means everything a letter needs is right: the deadline (the right T+1 / T+5 row), the amount and the date.
| Letter-ready (deadline, amount and date right) | Wrong chase / don't-chase call | Amount right | Held-out 31-40: letter-ready | Time per SMS | |
|---|---|---|---|---|---|
| Regex baseline (written while looking at SMS 1-30) | 20/25 | 3/40 | 23/25 | 4/8 | instant |
| Gemma 4 26B A4B, thinking on | 24/25 | 1/40 | 25/25 | 7/8 | 23.9 s |
| Gemma 4 26B A4B, thinking minimal (default) | 23/25 | 1/40 | 25/25 | 7/8 | 3.3 s |
What the numbers say:
- On the SMS the regex was written for, it looks great. On the ten it hadn't seen, it fell apart: it took the balance (₹3,412.50) as the amount, missed a Hinglish ATM failure ("ATM se paise nahi nikle par Rs 10,000 debit ho gaye") as "nothing to chase", and filed a failed wallet payment as a reversal. That's what every regex app does in real life: it works on the formats its author saw.
-
Gemma's misses are the interesting kind. An SMS that says a new payment succeeded but an earlier one failed. A masked reference (
6284XXXX) it copied faithfully. An ATM ID it offered as a reference. None of them would have produced a wrong deadline in a letter; the guard catches values that aren't in the text. -
Thinking costs time, not much accuracy. With Gemma's thinking set to
minimala read takes about 3 s instead of about 20 s, so that's the default.
Two places, timed
| Read one SMS | Fast-forward 45 days (2 letters) | |
|---|---|---|
| My laptop (Windows, 8 GB) | 5.2 s | 16.2 s |
| Cloud Run, asia-south1 | 3.6 s | 14.9 s |
Gemma runs on Google AI Studio in both; the workflow, the rules and the checks run where the app runs.
Why Does Open Innovation Matter?
Both halves of Paisa Wapas are open, and each one is open for a reason.
Gemma is an open-weight model. Bank SMS are about as personal as data gets: account numbers, amounts, who you pay. The job here is small and fixed (read one SMS, fill six fields), which is exactly the kind of job an open model can do on hardware you control, with nobody else's terms deciding whether your messages are kept. For this week's build I called Gemma 4 through Google AI Studio, and I haven't run it on my own laptop yet; the reader only needs one HTTP call, so pointing it at a local Gemma is the next step, not a rewrite. I'd rather say that plainly than pretend.
Where would a closed model do better? Probably on the hardest SMS, the "your new payment worked, your old one failed" kind. But it would also be the one part of the app I couldn't move, audit or run offline.
Temporal is open source (MIT). The whole point of Paisa Wapas is to wait for weeks without losing track. With Temporal, the waiting is plain code I can read, test (the 36-day case runs in seconds in pytest), and self-host with temporal server start-dev on the same laptop. And the time-skipping server, the thing that makes a 45-day story demo-able in 15 seconds, exists because the project is open and built to be tested.
The rules are open too. RBI publishes the circular; I just turned it into ten rows of code with the annex row number next to each one, so anyone can check the app against the source.
The Lazy Payoff
What it saves, for one late UPI transfer:
- Remembering: the T+1 date, the 30-day date. 0 minutes of your attention.
- Counting: days late × ₹100, on the day it's checked.
- Writing: two letters, each with the circular's number, the annex row, the amounts and the dates, already right. Writing one from scratch starts with finding the circular and the right annex row.
- Not giving up: the one that matters. The ₹100 a day only adds up for the people who keep going after day 5. The sloth always keeps going.
Prize Categories
- Best Use of Temporal: the whole product is a Temporal workflow that waits days by law (durable timers, signals for "money back" and "letter sent", queries for the status), plus Temporal's time-skipping test server to run the real workflow in the live demo and in the tests.
- Best Use of Gemma: Gemma 4 reads every SMS (with every value checked against the text by code) and writes every letter (with every number checked again), evaluated against a regex baseline on 40 SMS, with held-out messages and both thinking levels measured.
How I worked: I built this with an AI coding assistant, dictating my side through Wispr Flow. The idea and the decisions are mine, and every claim here was checked against the code, the eval output and the RBI texts.





Top comments (0)