Inspiration
Debt in the Philippines is rarely a spreadsheet. It is a 5-6 with a neighbour, a salary deduction you find out about on payday, a cousin who said "next week" in March. The apps that exist treat this as accounting, which is the one framing that guarantees you stop opening them — nobody opens a ledger to feel better.
So the question was not "how do I track a loan," it was "what would make someone open a debt app voluntarily." The answer turned out to be that someone should be watching, and should care.
What it does
Two ledgers, one on each side of the same problem.
- Loans — what you owe. Amortization schedule, due dates, running balance, local reminders before each due date.
- Collections — what you're owed. Debtors, receivables, nudges you can send, and agreements, because the informal side of utang is where the real money goes missing.
- Scan a receipt — point the camera at an InstaPay or GCash receipt, or a payslip deduction line, and the amount, date, and reference are read on-device and filled in. No typing at the exact moment you least want to type.
- Sharky — the feed reacts to what actually happened. Paid four days early on a three-payment streak gets a different pose and a different line than scraping in two days late. Nothing is a generic "Great job!" — the reaction is derived from the payment's own facts.
- Levels and Bites — settling a payment earns Bites, Bites move you up the food chain from Fry to Megalodon, and there is a wall of achievements to fill. The progression is attached to paying, so the only way to advance is the thing the app exists to get you to do.
- Reef Run — a small arcade game. Tap to swim, eat the money, miss the reef. Three runs a day free, unlimited on Sharky+. It is the reward at the end of the loop rather than a separate app bolted on.
- Encrypted backups — export a passphrase-encrypted file to wherever you keep things. There is no cloud, no account, and no server.
How I built it
React Native + Expo (SDK 57), expo-router, SQLite via Drizzle ORM, NativeWind for styling, Reanimated for Sharky.
The architecture decision that shaped everything else was append-only. A
payment is an event, not a mutation — balances and schedules are projected from
the event log (src/feed/projection.ts, src/db/appendOnly.ts). It costs more
up front and buys the thing a money app needs most: history that cannot be
silently rewritten by a bug.
OCR is Google ML Kit running locally. The parsing on top of it is the
unglamorous half — a receipt's trace ID is a large number sitting next to a
smaller one, and a naive "biggest number is the amount" heuristic reads ₱948,010
off a ₱500 transfer. src/ocr/extract.ts is mostly the rules that stop that.
Sharky is a pose-and-template engine, not a chatbot. Buckets are classified from
the payment (final outranks late, so a last payment that happens to be late
still gets a celebration), then a line is drawn from that bucket while avoiding
recently used ones.
Monetization is RevenueCat end to end: react-native-purchases for the
subscription, and RevenueCat's Ads → Rewards for the rewarded-video unlock, so
the entitlement grant is server-verified rather than decided on the device.
Challenges I ran into
The paywall could not be allowed to lie. Trial length and price were the
obvious place to hardcode a string, and the obvious way to end up telling
someone "7 days free" while Play charges them on day 3 after a base plan was
edited in the console. src/purchases/offerTerms.ts reads every factual claim
off the store's own PurchasesPackage and only owns the grammar around it.
A rewarded video that pays out nothing is worse than no ad at all. If the grant were decided on the device, any rooted phone unlocks for free; if the reward is configured under the wrong entitlement id, it verifies successfully and unlocks nothing after the user has sat through the whole video. The flow goes token → AdMob server-side verification → RevenueCat → entitlement, and the app only ever re-reads what the server decided.
Degrading without lying. No RevenueCat key, no ad unit, offline — each has a defined behaviour. A corrupt entitlement cache reads as "no entitlements," never as a grant, and a RevenueCat outage serves the last known state rather than silently downgrading someone who paid.
Accomplishments that I'm proud of
Sharky reacts to the actual event and not to a counter. And the whole thing works with the network off — the only data that ever leaves the phone is a purchase.
What I learned
Monetization design is mostly deciding what the free app is. The 3-loan limit was chosen because it is the point where someone is clearly using the app for their real life rather than testing it, so the ask arrives after the value does.
What's next for LoanShark
iOS. Shared collections, so two people can look at the same utang. More of Sharky's wardrobe.
Built With
- android
- drizzle-orm
- expo-router
- expo.io
- google-admob
- google-ml-kit
- nativewind
- next.js
- react-native
- reanimated
- revenuecat
- sqlite
- tailwind-css
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.