This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
In Nigeria, "fake alert" is a household phrase. A customer says "I don send am", a message lands on the trader's phone, and the goods leave the shop. The message came from a fake-alert app that can print any bank's SMS with any amount. The money never arrives.
Banks give the right advice: check your account, not the message. But at 6pm, with five customers waiting and an alert every minute, nobody opens a banking app for every sale.
Alertproof checks the alert for you, in the few seconds you have. Paste or share the alert (or photograph the customer's "Transfer Successful" screen) and it answers in big letters, out loud, in English or Pidgin:
- PAID: money don land.
- NO RELEASE: don't hand over the goods, with the exact reason.
- CAN'T TELL: there isn't enough to prove it, so check your bank app first.
It rests on one observation. A fraudster can copy everything about an alert except your balance. Every real alert shows your new balance, and it has to equal your last balance plus the amount, to the kobo. The fake-alert app has to guess that number. Alertproof keeps the chain of your real balances on your device, and arithmetic catches the guess.
08:14 CR +₦4,500 Bal ₦56,810.50 ✓ 56,810.50
08:52 CR +₦1,200 Bal ₦58,010.50 ✓ = 56,810.50 + 1,200
09:30 DR −₦12,500 Bal ₦45,510.50 ✓ = 58,010.50 − 12,500
11:22 CR +₦25,000 Bal ₦264,870.20 ✗ should be ₦70,510.50 → NO RELEASE
Who pays for it: provisions stalls, phone shops, pharmacies, POS agents counting out cash against a transfer, and Instagram sellers who dispatch after "payment". The plan is free forever on your own laptop, ₦1,500 a month for phone-only shops, and ₦10,000 a month for a market association. One stopped ₦25,000 fake pays for sixteen months.
Demo
The video is a real walkthrough of the live site: the landing page, the demo shop, then Setup and a real check with Gemma reading the alert. It's narrated in Nigerian English, and the app's own voice speaks Pidgin.
Try it in a minute: open alertproof.tech/app.html?demo. There's no sign-up and nothing to install. It opens a demo shop, Ngozi Eze Provisions, with six real-format GTBank alerts from this morning already in its book. Then play the customers one by one:
- Real transfer: PAID.
- Fake alert: NO RELEASE, with the numbers that don't add up.
- Old alert sent again: NO RELEASE.
- Someone else's account: NO RELEASE.
- Customer shows a screenshot: WAIT FOR ALERT. Its session ID checks out, but a screenshot still isn't money.
- Fake screenshot: NO RELEASE. The receipt was edited, and its session ID gives it away.
Here's what the answers look like on a phone:
The landing page also has a small game, Spot the fake: five situations from the counter where you pick the real alert. It's harder than it sounds. One of them is off by seventy-five kobo.
Code
angelraph
/
alertproof
Check a Nigerian bank alert before you release the goods. Open-weight Gemma reads it; your balance chain and the transfer's session ID decide. Hacktoberfest 2026 Side Hustle.
Alertproof
A fake alert can copy your bank's words. It can't know your balance.
Alertproof checks a bank alert before a trader hands over the goods. Gemma, an open-weight model, reads the alert (any bank's SMS, app notification or email, or a customer's "Transfer Successful" screenshot) on the trader's own device. Then plain code decides:
-
Grounding (
lib/ground.js): every number Gemma read must be written in the alert. Anything else is thrown away, and Gemma is asked once more, told which values were wrong. -
The balance chain (
lib/chain.js): each genuine alert's balance equals the last balance plus the amount. A fraudster can fake the sender, the wording and the amount, but not the trader's balance. -
Format fingerprint (
lib/fingerprint.js): how the trader's real alerts are written (labels, currency, date shape). This is supporting evidence only.
The verdict is one of three:
Verdict
When
PAID
The balance
No npm dependencies: plain HTML, CSS and JavaScript, IndexedDB, a service worker, and Gemma through Ollama (or hosted Gemma 4 through one small serverless function).
How I Built It
The model reads. Arithmetic decides.
I wanted Gemma to do what it is good at, and nothing it shouldn't be trusted with.
Nigerian banks all word their alerts differently. GTBank writes Avail Bal: NGN87,450.20. Zenith writes Bal:87,450.20 under a masked account like 0134521. UBA appends CR to the balance even on debits. OPay sends a sentence. Emails arrive as long HTML notifications. A regular expression per bank breaks the day a bank changes its template, and they do. So Gemma 3 4B reads the alert into one JSON schema: bank, credit or debit, amount, balance, account digits, reference, time.
Then three pieces of plain code take over:
- Grounding (
lib/ground.js). Every number Gemma returns must be written in the alert. So must the bank name, and the credit/debit has to agree with the alert's own words. Anything else is thrown away, and Gemma gets one retry that names the values it invented. - The balance chain (
lib/chain.js). Direction, account, replay and balance. Only when all four pass does it say PAID. - A format fingerprint (
lib/fingerprint.js). This learns how your bank writes its alerts (labels, currency token, date shape) and warns when a "GTBank" alert uses labels your real GTBank alerts never use. It's supporting evidence only, because banks do change formats.
The model never says whether money is real. It can misread; it can't say PAID.
Grounding earned its keep on day one
The first alert I gave Gemma was a Zenith SMS with the account masked as 0134521. Gemma confidently returned the account as 45421, gluing the visible digit groups together. Grounding threw it away. On other alerts it returned the bank as "Acct" (the first label it saw) and called a GTBank alert "OPay". The 1B model labelled a credit as a debit, which is why credit/debit now has to be backed by the alert's own words.
Screenshots are not money
For a customer's "Transfer Successful" screenshot, Gemma's vision reads the recipient, account, amount and status. Alertproof checks the money was sent to your account and business name, and then refuses to call it paid. Instead it tells you exactly what your real alert must say: "CR ₦15,000 · Avail Bal ₦92,160.50". The payment goes on a waiting list, and when that alert arrives it turns green by itself. A number on a picture can't be grounded against text, so Alertproof only uses it to know which alert to wait for.
Real receipts carry their own lie detector
The first time I tested Alertproof on a real payment I'd received, it was a Moniepoint "Transaction Details" screen. The verdict was a feeble "wait for alert", and pasting the same text crashed the online reader. Looking at the real receipt fixed both, because it was carrying something I'd ignored: a Session ID.
Session ID 090645 261016191057 403015967666
│ │ └ unique number
│ └ 26-10-16 19:10:57, when the transfer happened
â”” 090645: the sending bank's NIBSS code (Nombank MFB)
Every NIP interbank transfer in Nigeria has one, and the receipt prints the time and the bank right next to it. Fake-receipt generators and edited screenshots rarely keep all three consistent. So lib/session.js decodes the ID and checks it against the receipt:
- The date inside the ID has to be possible. Month 13 or hour 26 means it was typed by hand.
- The time inside the ID has to match the time printed on the receipt. Apps show Nigerian time while some banks stamp the ID in UTC, so exactly one hour apart is allowed and nothing else.
- The bank code has to belong to a bank the receipt names.
My real receipt passes all three: 19:10:57 in the ID, 20:10 on the screen, Nombank on both. Change the time to 14:35, or move the receipt to another day, or bump ₦21,907 to ₦121,907 without regenerating the ID, and the verdict is NO RELEASE: "the session ID says the transfer happened 16 Oct 2026, 19:10:57, but the receipt shows 14:35." None of this needs a model. Gemma reads the screenshot, and arithmetic on its own digits decides. A receipt can still never be PAID, because it isn't the trader's balance. But now a forged one is caught on the spot, instead of only after the alert never arrives.
Gemma once, then instant
My laptop has no graphics card, so Gemma 3 4B takes about 38 seconds to read an alert. That's fine for a test, and useless with a customer waiting.
The fix came from looking at what a trader's alerts actually are: the same one or two banks, in the same layout, all day. So the first time Gemma reads an alert in a layout, and grounding confirms every value, Alertproof remembers the words that sit just before each value (Avail Bal: NGN, Amt:, Acct:). The next alert in that layout is read by finding those words: 0.06 milliseconds instead of 38 seconds. That reading goes through exactly the same grounding and balance chain.
Two rules keep it honest. A learned layout is kept only if re-reading the alerts it learned from gives exactly what Gemma read. And anything that doesn't fit the layout exactly, like a new bank, a changed template, or a debit worded differently, goes back to Gemma, which then teaches that layout too. "Teach it your alerts" in Setup does all of this before the first customer arrives.
Gemma is still the reason it works. It turns a layout nobody has written a parser for into one Alertproof can read, for any bank, without me writing a regular expression per bank.
Built for a counter, not a desk
- Share to check: the app is an installable PWA with an Android share target, so you long-press the SMS, tap Share, and the check starts.
- Voice: the verdict is spoken. In Pidgin: "No release am. This alert no follow your balance."
- Today's book: every checked payment, which fakes were stopped and why, and whether the chain is unbroken.
- Offline: the shell is cached, and with Gemma on a laptop no network is needed. SMS still arrives when data doesn't.
- Never an error at the counter: if Gemma can't answer (no network, or the hosted model is busy or down), a backup reader reads the common labels (
Amount,Avail Bal,Acct) with plain rules, and its numbers go through the same grounding and balance chain. It's a safety net, not a replacement: it read 80 of 80 synthetic alerts and 31 of 31 real ones, but only Gemma can read a layout nobody wrote a rule for. A customer is never left waiting on an error message.
Does it actually work? 70 alerts, measured
I built eval/run.mjs to answer that with numbers instead of a demo. There are 10 alert formats: SMS, push and email, modelled on GTBank, Access, Zenith, UBA, First Bank, OPay, Moniepoint and PalmPay. Three of them never appear in any prompt example. For each format, the trader already has a morning of genuine alerts in their book, and then 7 new alerts arrive: two genuine credits (one with kobo), a debit, a fake with an invented balance, a fake with a round balance, an alert for someone else's account, and a real alert from earlier sent again.
Gemma 3 4B ran on my laptop, which has no graphics card.
| Run 1 | After the fix | |
|---|---|---|
| Invented-balance fakes released | 0 of 20 | 0 of 20 |
| Replayed alerts stopped | 10 of 10 | 10 of 10 |
| Genuine payments proven PAID | 20 of 20 | 20 of 20 |
| Genuine payments wrongly blocked | 0 | 0 |
| "Someone else's account" fakes released | 4 of 10 | 2 of 10 |
| Amount and balance read correctly | 70 of 70 | 70 of 70 |
| Median seconds per alert, Gemma on CPU | 39.6 | 37.9 |
Then the learned layouts: Gemma read one genuine alert per format, and Alertproof learned each layout from that single reading. It then read the other 60 alerts with no model: 56 of 60 read instantly (median 0.06 ms), 0 wrong amounts, balances or directions, and every verdict matched the Gemma path. The 4 that didn't fit (mostly debits, worded differently from credits) went back to Gemma. The only fakes that got through were the same two no-account cases explained below.
What it got wrong
Run 1 released 4 fakes, and all four were the same kind: an alert for someone else's account that also carried the trader's exact balance (the worst case, where the attacker somehow knows your balance). In Zenith SMS and Access emails, the alert shows only two digits of the account (0187632). My grounding check demanded three, so it threw away correct readings and the account check was skipped. I changed it to accept two digits when they come straight after a mask, keeping only the digits the alert actually shows. Re-running those formats: both Zenith and Access-email fakes are now stopped, and all 14 re-run cases came out right, with no new false alarms.
The other two can't be fixed from the alert alone. OPay and PalmPay notifications don't show your account number at all. If such an alert also has your exact balance to the kobo, nothing in it can tell it apart from a real one. In practice that requires someone who has seen your latest alert, so the app and the site say plainly: keep your alerts private.
Then I tested it on real alerts
Synthetic alerts only prove so much, so I ran Alertproof on my own bank: 31 genuine Zenith Bank alert emails from two accounts, 2024 to 2025, pulled from my inbox. They stay on my laptop and only the counts are published. eval/real.mjs reads them exactly the way the app does. The real balance chain grades every reading, and each real credit is turned into three fakes: the same email with a forged balance, an older real alert sent again, and the same alert with another account's digits.
| Real alerts read | 31 |
| Amount, balance and credit/debit all read right | 31 of 31 |
| Read by Gemma (first alert of a layout, or one that didn't fit) | 1, median 24.2 s |
| Read instantly from a learned layout | 30, median 0.22 ms |
| Balance links that add up exactly | 14 of 29 |
| Links with a gap (the bank changed the balance without an alert) | 15, of which 5 within ₦100 (interest, small charges) |
| Real credits, against the chain as emailed | 6 PAID, 2 CAN'T TELL, 7 NO RELEASE |
| Real credits, after setting the balance from the app once | 15 of 15 PAID |
| Fakes with an invented balance stopped | 15 of 15 |
| Old alerts sent again stopped | 15 of 15 |
| Alerts with another account's digits stopped | 15 of 15 |
Gemma read exactly one email. Alertproof learned Zenith's table layout from it and read the other 30 in a fifth of a millisecond, all correct.
The real data also taught me two things the synthetic set couldn't:
- Banks move your balance without telling you. ₦13.81 of interest arrived with no email, and ₦32 of charges left the same way. My first rule called any surplus NO RELEASE, so a genuine payment would have been blocked over thirteen naira. Now a difference within ₦100 either way is CAN'T TELL (check your app). A fraudster can't guess your balance to within ₦100 any more than to the kobo.
- Alert emails have gaps. Some months, credits arrived with no email at all. Against the chain as emailed, 7 genuine credits got NO RELEASE, because from the outside a payment after a missing alert looks exactly like a fake. That's the safe answer, and the message now says so: either it's fake or an alert since then is missing: check your bank app, then set your balance. After that one reset, 15 of 15 genuine credits were PAID.
Why Does Open Innovation Matter?
The secret that protects you can't go to someone else's cloud. The entire check works because nobody else knows your balance. A tool that ships every alert and balance to a third-party API hands over the one number that protects the trader. With open-weight Gemma on the shop's own laptop, the alerts never leave it.
₦0 per check is what makes it a business. A provisions stall can afford ₦1,500 a month. It can't afford a fee per alert on fifty alerts a day. Local Gemma costs nothing per check, and the hosted fallback is a small open model, not a frontier API.
It works when the network doesn't. Data drops; SMS still arrives. On a laptop, Alertproof reads and checks with the network off.
I could measure and change everything. Open weights let me run the exact same model, prompt and decoding over 70 cases in Node, read every answer, and change the grounding rules where the numbers told me to. When banks change their formats, the same harness tells me what broke, and the model can be swapped or fine-tuned on Nigerian alerts without asking anyone.
Where a closed model would be better: the first reading of a brand-new layout. A hosted frontier model reads one in about two seconds; Gemma on a CPU-only laptop takes about forty. But that happens once per layout, before the shop opens, and every alert after that is under a millisecond, which no API call can match. For phone-only shops, the online Gemma 4 option reads a new layout in a few seconds and stores nothing.
My First User
The first real user was me, with my own money.
I ran Alertproof on 31 genuine alert emails from my own two Zenith Bank accounts, going back to 2024. It read all 31 correctly, and it stopped all 45 fakes I made from them: forged balances, old alerts sent again, and another account's digits.
Then I tested it at my own counter. A payment of ₦21,907 came in through Moniepoint from a real customer. I checked the transaction screenshot in Alertproof, and its session ID matched the time and the sending bank. When I edited the same receipt's time and amount, Alertproof said NO RELEASE straight away.
That test also exposed real bugs. Pasting the receipt text crashed the online reader. A long PalmPay email failed. On my phone, the header pushed the logo off the screen. Each one is fixed, and Alertproof now falls back to a built-in reader instead of ever showing an error.
I've sent the link to traders and POS agents around me. Their feedback will be added here as it comes in.
Prize Categories
Best Use of Gemma. Gemma 3 4B reads every alert locally through Ollama, and Gemma 4 reads alerts and customer screenshots in the hosted fallback. All of it is grounded and measured over 70 cases, and most of this post is about making a small model's readings safe enough to protect someone's money.
Best Use of a .tech Domain. Alertproof lives at alertproof.tech: the landing page, the demo shop, the app and the online Gemma reader, with www redirecting to it.





Top comments (0)