Inspiration
Open any banking app and you get a list of raw charges. The bank knows what every one of those lines means. The customer is left to work it out alone.
That gap is where money leaks. People pay for subscriptions they forgot they signed up for. Duplicate charges never get disputed. A gym membership keeps renting space in the budget long after the last visit. Nobody cancels what they can't see.
For the Capital One challenge at HackMTY 2026 we asked a simple question: what if the bank didn't just show you your transactions, but told you what was wrong with them, in plain Spanish?
That became 52Pay, a mobile bank that reads your movements, understands them, and turns what it finds into actions you can take with one tap.
What it does
52Pay pulls transactions from the Capital One Nessie API, normalizes the merchant behind every raw descriptor, and runs a set of engines on top of that enriched data:
- Subscription detection. Finds recurring charges from their cadence, a stable amount and a minimum of 3 occurrences, with no manual setup. It also cross-checks against Nessie bills, because the bank's own record of a recurring payment outranks a guess. Price increases get flagged.
- Anomaly alerts. Duplicate charges within a 30 minute window, charge bursts, amounts that fall outside a merchant's usual pattern, and purchases at hours the customer never shops.
- Cashflow health score. A score from 300 to 850 that measures cash flow rather than credit, built from five weighted components.
- Savings rules. Every finding becomes a concrete rule: round ups, a set-aside every payday, or canceling the subscription you don't use.
- My money in 30 days. A deterministic 30 day balance forecast with advice you can act on.
- Sentinel and Shield. A behavioral sentinel that scores each movement as it lands and looks for patterns, not single charges: transfer bursts, jumps across merchant categories, large transfers to never-seen CLABEs, and fast balance drains. When it sees risk, the Shield takes temporary, reversible protective measures.
- Real banking features. Account numbers are valid CLABEs, P2P transfers go through Nessie, and you can split a bill and have each friend pay their share with a real transfer.
The rule we held ourselves to: every signal carries an explanation. A score without reasons is just a number people ignore. Our backend never sends an alert, score or rule without an explanation field.
How we built it
| Piece | Stack | Why |
|---|---|---|
| App | Expo + TypeScript + Expo Router, development build | File based navigation, plus native camera, biometrics and push, which Expo Go doesn't expose |
| Data | Supabase (Postgres + Auth + Realtime) | Real Postgres with Row Level Security, and Realtime pushes new alerts to the phone without polling |
| Engines | FastAPI in Python, in Docker | Cadence and anomaly analysis is naturally Python, and FastAPI gives us types and OpenAPI for free |
| Source | Capital One Nessie API | Called only from the backend |
Contracts first, mocks first
We were four people working on the same repo at the same time, so we started with the contract rather than the code. A single contracts/ folder holds the TypeScript types and JSON fixtures: 1 customer, 2 accounts, 18 merchants, 51 transactions, detected subscriptions, alerts, a score and savings rules.
Screens depend only on a DataSource interface with two implementations. FixtureDataSource reads the JSON files and ApiDataSource calls the engine over HTTPS. That let the app and the engine move in parallel without either one blocking the other. The engine's job was to match the fixtures, never to change them.
The score
The cashflow score is a weighted combination of five components with weights $w_i$, where $\sum_i w_i = 1$:
| Component | Weight |
|---|---|
| Income stability | $0.25$ |
| Spending discipline | $0.25$ |
| Buffer days (target: 35 days) | $0.20$ |
| Recurring load | $0.20$ |
| Overdraft risk | $0.10$ |
The result is mapped onto $[300, 850]$ and bucketed into bands. Each component returns its points and a sentence explaining them, so the Health screen can show what raises the score and what drags it down.
Anomalies with statistics you can explain
For amount outliers we compare each charge to that merchant's history using a population z-score:
$$ z = \frac{x - \mu}{\sigma} $$
A charge is only flagged when $|z|$ passes $2.0$ and the merchant has at least 3 prior charges. That keeps the alert rate low on a thin history. The forecast works the same way: its lower band uses $z = 1.28$, roughly the 10th percentile of cumulative variable spending, so "you might come up short" means something specific.
Real CLABEs
Account numbers follow Banxico's CLABE rule: 3 digit bank code, 3 digit plaza, 11 digit account and a check digit. The check digit uses the repeating weights $(3, 7, 1)$:
$$ d_{18} = \left(10 - \left(\sum_{i=1}^{17} \left(w_i \cdot d_i \bmod 10\right) \bmod 10\right)\right) \bmod 10 $$
Challenges we ran into
Nessie has no DELETE. Broken seed data from earlier attempts, including corrupted text and absurd amounts, came back on every sync. We built a quarantine: listed customers are skipped before anything gets written, and customers that merely look broken are skipped and reported with reasons, so a person makes the final call.
HTTP in a mobile app. Nessie serves plain HTTP. iOS App Transport Security blocks it by default, and calling it from the phone would ship the API key inside the bundle. So the app never talks to Nessie. The engine talks HTTP to Nessie server side and HTTPS to the app.
Security that doesn't live on the phone. Our transaction PIN is checked on the device, so a stolen session token could call the engine directly and skip it. The Shield therefore checks verification on the server with short lived challenges: 5 minutes and 3 attempts. The rule we designed around: making things safer never needs verification, making them less safe always does.
Four people, one repo, 36 hours. Parallel branches meant constant merge pressure. Strict folder ownership, a read-only contract and small PRs kept us from quietly breaking each other's work, although we still had a few integration merges where two engine branches had to be reconciled by hand.
Time zones. Everything is stored as ISO 8601 UTC with Z. America/Monterrey (UTC-6) is applied only at display time. Detecting "unusual hours" and "payday" needed local time, so we kept that conversion in a single place too.
What we learned
- A contract is worth more than code on day one. Agreeing on types and fixtures first let four people build at the same time with almost no blocking.
- Deterministic beats clever when trust is on the line. Every number the app shows traces back to specific movements, so it can always answer "why are you telling me this?"
- Explainability is a feature, not paperwork. Writing the Spanish explanation for each signal forced us to understand exactly why it fired.
What's next
The same data model scales without a redesign: CDC into Kafka partitioned by account, stream processing for merchant normalization, an online and offline feature store, models served apart from the rules engine, and an alert outbox with deduplication and rate limiting. Next we want the Shield to enforce its protections directly on the transfer path instead of only recording and explaining them.
Log in or sign up for Devpost to join the conversation.