Masareef (مصاريف, "expenses") is a fast, private, offline-first personal spending tracker for any country and currency. It has wallets, budgets, insights, subscriptions and trips, and an auto-capture engine that reads your bank's notifications and logs the spending by itself, even while the app is closed. Built with Expo SDK 57, React Native 0.86, a local SQLite source of truth, and last-write-wins sync against a Cloudflare Workers API. English and full Arabic with a right-to-left layout.
Masareef (مصاريف) is Egyptian Arabic for expenses. The goal is to see where your money goes without the chores most finance apps ask of you: log spending in seconds, let bank notifications fill in the rest, and keep the app out of your way.
Download#
Masareef is live on Google Play and on the App Store. It works in any country and currency, in light and dark, and fully in Arabic with a right-to-left layout.
One link that sends each phone to its own store: the install page.
Automatic capture differs by platform, because the operating systems differ. On Android it reads your bank's payment notifications and SMS. On iPhone no app can read another app's notifications, so capture there is bank SMS, handed over by a Shortcut you set up once. Everything else is the same on both: wallets, budgets, insights, subscriptions, nearby ATMs and sync.
Privacy Policy · Delete your account
What it costs#
Masareef is free, and the free tier is not a trial: every transaction you enter, cloud sync on one phone, all your wallets, budgets, goals and debts, and 25 automatic captures a month. Masareef Pro is the optional upgrade, and what it buys is not typing your spending in yourself.
| Plan | Price in Egypt | Billing |
|---|---|---|
| Monthly | EGP 99 | charged monthly, cancel any time |
| Annual | EGP 599 | charged yearly, cancel any time |
| Lifetime | EGP 1,999 | paid once, never expires |
Every other country is priced in its own currency, and the store shows the exact amount before you pay. There is one Pro, with no tiers, no add-ons, and nothing sold separately later.
Pro lifts the monthly cap on automatic capture entirely. It adds receipt photos, with the total, shop and date read off the picture on your own phone. It adds the insight screens: where the money actually went, the charge that is not like the others, everyone you pay ranked, your year in one scroll. And it adds statements for any date range, automatic backup to your Drive, and sync across every device instead of one.
Pro is sold inside the app, through Google Play on Android and the App Store on iPhone: open Masareef, tap Get Pro, pick a plan. The purchase follows your account, so signing in on another phone brings it with you. Refunds, support and terms are at the foot of this page.
Screens#
A tour of Masareef. Sixteen screens, covering every area of the app.
















Why Masareef#
Most finance apps make you fight for every entry, then ask you to trust the cloud with your raw transactions. Masareef does the opposite on both counts: logging is a couple of taps, your bank's own notifications do the data entry, and everything lives on your device first.
- Offline-first, instant. A local SQLite database is the single source of truth, so every read and write is immediate and works with no signal. Cloud sync is a best-effort background reconciler, never a blocker.
- Auto-capture, not auto-trust. Masareef parses a bank notification or SMS into amount, merchant, bank, card tail and reference, and lands it in a review queue. High-confidence captures can post on their own with one-tap undo; nothing else touches a balance until you confirm.
- Privacy by design. An optional app lock, hidden contents in the app switcher, a one-tap Hide balances mask, and analytics that never carry amounts, merchants, or balances, only structural events.
- Light and dark, English and Arabic. A "Slate Teal" identity in both themes, and a full right-to-left layout where every screen mirrors.
Features#
Automatic spending capture
- A notification-listener bridge (Play-safe) plus an optional SMS parser reads payment alerts from banks and wallets across many countries
- Card payments, transfers, top-ups, ATM withdrawals and incoming money, each routed to the right wallet by name
- Keeps capturing while the app is closed: a headless task can parse, accept and notify with nothing on screen
- Recovers alerts Android redacts or hides, in every language, and says why a message went to review instead of posting
- A bank-only allowlist gate plus word-bounded Arabic-aware matching, so an OTP, a promo, or a customer named مبروك never becomes (or deletes) a transaction
- Reference-based dedupe: one bank SMS relayed by three apps is one transaction, not three
- Learns merchant → category rules as you confirm, and auto-categorizes on the next match
Banks and wallets it reads
469 institutions across nine groups, matched by their own notification wording in Arabic and English. Masareef is an independent app: it is not affiliated with or endorsed by any institution named here, and it never asks for a banking password.
Egypt (64) — National Bank of Egypt · البنك الاهلي, Banque Misr · بنك مصر, Commercial International Bank · التجاري الدولي, QNB Alahli · كيو ان بي, Banque du Caire · بنك القاهرة, Arab African International Bank · العربي الأفريقي, Bank of Alexandria · بنك الاسكندرية, HSBC Egypt · اتش اس بي سي, Crédit Agricole Egypt · كريدي اجريكول, ADIB Egypt · مصرف أبوظبي الإسلامي, Faisal Islamic Bank of Egypt · بنك فيصل, Al Baraka Bank Egypt · البركة, Housing & Development Bank · بنك التعمير والاسكان, Egyptian Gulf Bank · البنك المصري الخليجي, Al Ahli Bank of Kuwait – Egypt · البنك الأهلي الكويتي, National Bank of Kuwait – Egypt · بنك الكويت الوطني, SAIB Bank · المصرف العربي الدولي, Emirates NBD Egypt · بنك الامارات دبي الوطني, Mashreq Bank Egypt · بنك المشرق, Attijariwafa Bank Egypt · التجاري وفا بنك, Suez Canal Bank · بنك قناة السويس, aiBANK (Bank NXT) · المصرف العربي للاستثمار, Bank ABC Egypt · بنك المؤسسة العربية المصرفية, Industrial Development Bank · بنك التنمية الصناعية, Export Development Bank of Egypt · البنك المصري لتنمية الصادرات, Agricultural Bank of Egypt · البنك الزراعي المصري, ADCB Egypt · بنك أبوظبي التجاري, First Abu Dhabi Bank Egypt (FABMISR) · بنك أبوظبي الأول, Kuwait Finance House Egypt · بيت التمويل الكويتي, Arab International Bank · البنك العربي الدولي, The United Bank · البنك المتحد, Arab Bank · البنك العربي, Vodafone Cash · فودافون كاش (wallet), Orange Cash · اورنج كاش (wallet), e& Cash · اتصالات كاش (wallet), WE Pay · المصرية للاتصالات (wallet), NBE PhoneCash · فون كاش (wallet), BM Wallet · محفظة بنك مصر (wallet), InstaPay · تحويل لحظي (wallet), Fawry · فوري (wallet), ValU · فاليو (wallet), Telda · تيلدا (wallet), Contact · كونتكت (wallet), Premium Card · بريميوم كارد (wallet), MNT-Halan · حالان (wallet), Khazna · خزنة (wallet), Kashat · كشات (wallet), OPay Egypt · او باي (wallet), Damen Cash · ضامن كاش (wallet), NowPay (wallet), Sympl (wallet), Blnk (wallet), Aman (wallet), Lucky (wallet), Basata (Bee) · بسطة (wallet), Meeza · ميزة (wallet), Money Fellows · موني فيلوز (wallet), Kiwe (wallet), Sahl (wallet), MIDBANK · بنك مصر ايران للتنمية, Egyptian Arab Land Bank · البنك العقاري المصري العربي, Nasser Social Bank · بنك ناصر الاجتماعي, Souhoola · سهولة (wallet), Egypt Post Easy Pay · البريد المصري (wallet).
Saudi Arabia (19) — Al Rajhi Bank · الراجحي, Saudi National Bank · البنك الأهلي السعودي, Riyad Bank · بنك الرياض, Saudi Awwal Bank · البنك السعودي الأول, Alinma Bank · مصرف الإنماء, Bank Albilad · بنك البلاد, Arab National Bank · البنك العربي الوطني, Banque Saudi Fransi · البنك السعودي الفرنسي, Bank Aljazira · بنك الجزيرة, The Saudi Investment Bank · البنك السعودي للاستثمار, meem (Gulf International Bank) · بنك الخليج الدولي, D360 Bank · بنك دي 360, STC Bank (STC Pay) · اس تي سي باي (wallet), urpay · يورباي (wallet), Barq · تطبيق برق (wallet), mada Pay (wallet), Emkan Finance · إمكان (wallet), Tweeq · طويق (wallet), Nayifat Finance · النايفات (wallet).
United Arab Emirates (13) — Emirates NBD · بنك الإمارات دبي الوطني, Abu Dhabi Commercial Bank · بنك أبوظبي التجاري, First Abu Dhabi Bank · بنك أبوظبي الأول, Mashreq · بنك المشرق, Dubai Islamic Bank · بنك دبي الإسلامي, Abu Dhabi Islamic Bank · مصرف أبوظبي الإسلامي, RAKBANK · بنك رأس الخيمة الوطني, Commercial Bank of Dubai · بنك دبي التجاري, Emirates Islamic · الإمارات الإسلامي, Wio Bank · بنك ويو, Liv., e& money · اي اند موني (wallet), Sharjah Islamic Bank · مصرف الشارقة الإسلامي.
Middle East, North Africa & Türkiye (110) — National Bank of Kuwait · بنك الكويت الوطني, Kuwait Finance House · بيت التمويل الكويتي, Boubyan Bank · بنك بوبيان, Gulf Bank · بنك الخليج الكويتي, Burgan Bank · بنك برقان, Al Ahli Bank of Kuwait · البنك الأهلي الكويتي, Warba Bank · بنك وربة, Commercial Bank of Kuwait · البنك التجاري الكويتي, Kuwait International Bank · بنك الكويت الدولي, KNET · كي نت (wallet), QNB · بنك قطر الوطني, Qatar Islamic Bank · مصرف قطر الإسلامي, Commercial Bank of Qatar · البنك التجاري القطري, Doha Bank · بنك الدوحة, Masraf Al Rayan · مصرف الريان, Qatar International Islamic Bank · بنك قطر الدولي الإسلامي, Ooredoo Money (wallet), National Bank of Bahrain · بنك البحرين الوطني, BBK · بنك البحرين والكويت, KFH Bahrain · كويت فاينانس هاوس البحرين, Bahrain Islamic Bank · بنك البحرين الإسلامي, BenefitPay (wallet), stc pay Bahrain (wallet), Bank Muscat · بنك مسقط, Bank Dhofar · بنك ظفار, National Bank of Oman · البنك الوطني العماني, Sohar International · بنك صحار, Ahli Bank Oman · البنك الأهلي العماني, Oman Arab Bank · البنك العربي العماني, Thawani (wallet), Arab Bank · البنك العربي, Housing Bank · بنك الإسكان, Jordan Islamic Bank · البنك الإسلامي الأردني, Cairo Amman Bank · بنك القاهرة عمان, Jordan Ahli Bank · البنك الأهلي الأردني, Bank al Etihad · بنك الاتحاد, Zain Cash Jordan · زين كاش (wallet), eFAWATEERcom · إي فواتيركم (wallet), Bank Audi · بنك عودة, BLOM Bank · بنك لبنان والمهجر, Byblos Bank · بنك بيبلوس, Fransabank · بنك فرنسبنك, Credit Libanais · الاعتماد اللبناني, Whish Money (wallet), OMT (wallet), Rafidain Bank · مصرف الرافدين, Rasheed Bank · مصرف الرشيد, Trade Bank of Iraq · المصرف العراقي للتجارة, Qi Card · كي كارد (wallet), ZainCash Iraq · زين كاش العراق (wallet), FastPay · فاست باي (wallet), AsiaHawala · اسيا حوالة (wallet), Bank of Palestine · بنك فلسطين, Jawwal Pay · جوال باي (wallet), Attijariwafa Bank · التجاري وفا بنك, Bank of Africa, Banque Populaire · البنك الشعبي, CIH Bank, CashPlus (wallet), Banque Nationale d Algerie · البنك الوطني الجزائري, Credit Populaire d Algerie · القرض الشعبي الجزائري, BADR Bank · بنك الفلاحة والتنمية الريفية, BaridiMob · بريدي موب (wallet), BIAT, STB Bank, Attijari Bank Tunisie, D17 (wallet), Flouci (wallet), Jumhouria Bank · مصرف الجمهورية, Sahara Bank · مصرف الصحاري, Wahda Bank · مصرف الوحدة, Bank of Khartoum · بنك الخرطوم, Bankak · بنكك (wallet), Omdurman National Bank · بنك أم درمان, Commercial Bank of Syria · المصرف التجاري السوري, Syriatel Cash · سيرياتيل كاش (wallet), Al-Kuraimi Islamic Bank · الكريمي, CAC Bank · كاك بنك, Ziraat Bankasi, Turkiye Is Bankasi, Garanti BBVA, Yapi Kredi, Akbank, VakifBank, Halkbank, DenizBank, Papara (wallet), Enpara, Bank of Jordan · بنك الأردن, Jordan Kuwait Bank · البنك الأردني الكويتي, Weyay Bank · وياي, Dukhan Bank · بنك دخان, Ahlibank Qatar · الأهلي قطر, ila Bank · بنك إيلا, Al Salam Bank · بنك السلام, Bank Nizwa · بنك نزوى, OMPAY · أوم باي (wallet), Saham Bank · بنك سهام, Al Barid Bank · البريد بنك, BMCI · بي ام سي اي, Banque Extérieure d'Algérie · البنك الخارجي الجزائري, CNEP-Banque · الصندوق الوطني للتوفير, Société Générale Algérie · سوسيتيه جنرال الجزائر, Bank of Beirut · بنك بيروت, First Iraqi Bank · المصرف العراقي الأول, Bank of Baghdad · مصرف بغداد, e-Dinar (Poste Tunisienne) · البريد التونسي (wallet), QNB Finansbank, Kuveyt Türk, Paycell (wallet).
Asia (93) — HBL, UBL, MCB Bank, Meezan Bank, Bank Alfalah, JazzCash (wallet), Easypaisa (wallet), NayaPay (wallet), SadaPay (wallet), Kotak Mahindra Bank, Punjab National Bank, Bank of Baroda, Yes Bank, IDFC FIRST Bank, BRAC Bank, Dutch-Bangla Bank, Islami Bank Bangladesh, bKash (wallet), Nagad (wallet), Commercial Bank of Ceylon, Sampath Bank, eSewa (wallet), Khalti (wallet), Nabil Bank, BCA, Bank Mandiri, BRI, BNI, GoPay (wallet), OVO (wallet), DANA (wallet), Maybank, CIMB, Public Bank, RHB Bank, Hong Leong Bank, Touch 'n Go eWallet (wallet), DBS, OCBC, UOB, GrabPay (wallet), BDO, BPI, Metrobank, UnionBank, GCash (wallet), Maya (wallet), Bangkok Bank, KBank (Kasikorn), SCB, Krungthai Bank, TrueMoney (wallet), Vietcombank, BIDV, Techcombank, VietinBank, MB Bank, MoMo (wallet), ICBC, Bank of China, China Construction Bank, Hang Seng Bank, MUFG, Mizuho Bank, SMBC, PayPay (wallet), Rakuten Pay (wallet), KB Kookmin Bank, Shinhan Bank, Woori Bank, Hana Bank, KakaoBank, Toss Bank, Kaspi, Halyk Bank, National Bank of Pakistan, Allied Bank, Bank AL Habib, Canara Bank, Union Bank of India, Federal Bank, IDBI Bank, AU Small Finance Bank, Bank Jago, ShopeePay (wallet), VPBank, ZaloPay (wallet), SeaBank, Boost (wallet), GXBank, Trust Bank, Agricultural Bank of China, Standard Chartered.
Africa (27) — GTBank, Zenith Bank, Access Bank, First Bank of Nigeria, UBA, Fidelity Bank Nigeria, Kuda, OPay (wallet), PalmPay (wallet), Moniepoint, M-PESA (wallet), Equity Bank, KCB, NCBA, MTN MoMo (wallet), Ecobank, Commercial Bank of Ethiopia, Telebirr (wallet), FNB, Standard Bank, Absa, Nedbank, Capitec, TymeBank, GCB Bank, EcoCash (wallet), Discovery Bank.
The Americas (43) — Truist, Ally Bank, Citizens Bank, Fifth Third Bank, KeyBank, Regions Bank, Huntington Bank, Navy Federal, USAA, SoFi, Apple Card / Apple Cash (wallet), Scotiabank, BMO, CIBC, Desjardins, Tangerine, Interac (wallet), Itaú, Bradesco, Banco do Brasil, Caixa Econômica, Banco Inter, C6 Bank, PicPay (wallet), PagBank, Mercado Pago (wallet), Banorte, Banamex, Banco Azteca, Bancolombia, Nequi (wallet), Davivienda, BCP, Interbank, Yape (wallet), Banco de Chile, BancoEstado, Banco Nación, Ualá (wallet), Banco Galicia, Spin by OXXO (wallet), Plin (wallet), Daviplata (wallet).
Europe (52) — Nationwide, TSB, Virgin Money, Metro Bank, AIB, Bank of Ireland, BBVA, CaixaBank, Banco Sabadell, Bankinter, Bizum (wallet), Millennium BCP, Caixa Geral de Depósitos, MB WAY (wallet), Sparkasse, DKB, Volksbank, comdirect, Erste Bank, UBS, PostFinance, Raiffeisen, TWINT (wallet), Crédit Agricole, La Banque Postale, BoursoBank (Boursorama), Crédit Mutuel, Intesa Sanpaolo, Postepay (wallet), ABN AMRO, Rabobank, bunq, KBC, Belfius, Nordea, Danske Bank, SEB, Swedbank, Swish (wallet), DNB, Vipps (wallet), MobilePay (wallet), Handelsbanken, PKO Bank Polski, mBank, BLIK (wallet), Česká spořitelna, Banca Transilvania, Piraeus Bank, Alpha Bank, PrivatBank, monobank.
Global banks, cards and wallets (48) — Wise (wallet), PayPal (wallet), Revolut (wallet), Payoneer (wallet), Skrill (wallet), Cash App (wallet), Venmo (wallet), Zelle (wallet), Chime (wallet), Google Pay (wallet), Paytm (wallet), PhonePe (wallet), Alipay (wallet), WeChat Pay (wallet), Nubank (wallet), Monzo (wallet), Starling Bank (wallet), N26 (wallet), Chase, Bank of America, Wells Fargo, Citibank, Capital One, U.S. Bank, PNC Bank, TD Bank, American Express, Discover, RBC Royal Bank, TD Canada Trust, HSBC, Barclays, Lloyds Bank, NatWest, Halifax, Santander, Deutsche Bank, Commerzbank, BNP Paribas, Société Générale, ING, UniCredit, HDFC Bank, ICICI Bank, State Bank of India, Axis Bank, Tabby · تابي (wallet), Tamara · تمارا (wallet).
If a bank is missing, send the wording of one of its alerts and it gets added.
Add in seconds
- A purpose-built numpad with categories, wallets, notes and any date, for expense, income or transfer
- Or just type it: "spent 250 coffee", in English or Arabic, posts instantly
- Reply straight from the notification to log without opening the app; paste or share a bank message in; attach a receipt photo
- Money is stored as integer minor units end to end, never floats, and rendered through one formatting path
Wallets & multi-currency
- Cash, bank accounts, cards and mobile wallets side by side, each with its own balance, colour and history
- Pick any country and currency and the app adapts symbols, formatting and the banks it recognizes
- Net worth across every wallet, with a trend for recent months
- Count your cash reconciles the notes in your pocket; balance check compares your bank's own figure and shows the drift
Budgets & planning
- Per-category monthly limits with on-track, near-limit and over states, and optional alerts before one tips over
- A monthly spending target that answers what is safe to spend today
- Cash flow that answers whether the balance lasts to payday, not just to month end
- A coming up list of everything scheduled in the next 30 days
Subscriptions, bills & debts
- Subscriptions detected from your own history, with brand logos and yearly cost; overdue and price-raised ones flagged
- Recurring rent, salary and bills that post on schedule, with catch-up, pause and skip
- Planned one-off income or expense held pending until you confirm it
- Debts you owe or are owed, and savings goals
Insights
- Where it went: a donut ranking every category, tappable straight through to its transactions
- Income vs. expense over six months, this month vs. last, and your biggest movers
- Spending calendar heatmap, day-of-week and time-of-day patterns
- Savings rate, financial health, and the share of spending that logged itself
- Price creep on your regular buys, no-spend streaks, top merchants, and a shareable money-story recap
Catch mistakes
- Double charges: the same amount hitting the same merchant twice
- Refunds matched back to the purchase they reversed
- Charges above what a merchant normally costs you
Trips & ATMs
- Track a trip in its own currency and exchange rate, without polluting your normal months
- Find cash machines around you on a map or list, sorted by distance, filtered by bank or exchange (keyless Overpass)
Backup & sync
- Sign in and your wallets, transactions, budgets, categories and trips follow you to a new phone
- Fully offline-first: edits happen locally and instantly, then reconcile in the background (last-write-wins by timestamp)
- Two clocks kept separate: a client clock decides who wins and a server cursor decides what's new, so sync is immune to device clock skew; deletes propagate as tombstones and the network layer never throws
- CSV export and a full JSON backup/restore (file pick, paste, or share intent)
How it's built#
Offline-first by design: the local SQLite database is the single source of truth for the UI, so every read and write is instant and works with no signal. Derived answers (totals, pace, runway, habits) come from a dedicated ledger layer that returns answers rather than rows, so no screen recomputes the same aggregate twice.
Cloud sync runs as a best-effort background reconciler against a Cloudflare Workers API (Hono + Drizzle over Neon Postgres, with D1 for the small hot tables): local changes are flagged and pushed, remote changes pulled page by page, and conflicts resolve last-write-wins by timestamp. Session lives in SecureStore via better-auth + @better-auth/expo, and key derivation runs on the device.
Auto-track is the part that can silently lose money, so it is the most defended piece of the app: a durable native spool that survives the process being killed, a bank-only allowlist, word-bounded matching for the Arabic drop markers, reference-based dedupe, and a trace log that records why any message was dropped or held.
Tech stack#
| Layer | Choice |
|---|---|
| Framework | Expo SDK 57 + Expo Router (typed routes, declarative auth guards) |
| Runtime | React Native 0.86 / React 19, React Compiler enabled |
| Language | TypeScript, strict mode |
| UI | NativeWind 4 (Tailwind) + a dual light/dark palette (usePalette()) |
| Client state | Zustand, persisted to MMKV |
| Local data | expo-sqlite, the offline-first source of truth |
| Cloud sync | custom last-write-wins engine over a typed pull/push contract |
| Backend | Cloudflare Workers + Hono + Drizzle · Neon (Postgres) + D1 |
| Server state | TanStack Query |
| Auth | better-auth + @better-auth/expo (session in SecureStore, Google Sign-In) |
| Auto-track | notification-listener bridge (Play-safe) + SMS parser + native spool |
| Maps | MapLibre + keyless Overpass for the ATM finder |
| Animation | react-native-reanimated · SVG via react-native-svg |
| Icons | lucide-react-native |
| i18n | i18next / react-i18next, English + Arabic with RTL |
| Analytics | PostHog (privacy-safe: no amounts, merchants, or balances) |
Non-negotiables#
- Offline-first. The local SQLite DB is the source of truth; the app is fully usable with no connection and sync is never a blocker.
- Money is integer minor units. Never floats, end to end, and one formatting path renders every amount.
- Auto-capture never silently loses a transaction. A dropped capture is the one outcome a user can't recover, so every drop rule is word-bounded, traced, and covered by a "never miss a real transaction" test.
- Analytics carry no financial data. Structural events only, never amounts, merchants, or balances.
Deep dive — reading bank messages automatically, on Android and on iOS#
Most expense trackers ask you to type. Masareef's auto-track reads the alert your bank already sent you and books it. This section is the whole design: what each platform actually permits, the rules that survived contact with real banks, and the failures that paid for them. There is no product source here, only the ideas and the shapes, which are the parts worth copying.
The one principle everything else follows from#
A wrong row is visible. A missing row is invisible.
If the app books something incorrectly, you see it and delete it. If the app silently refuses a real alert, there is no row, no trace, and no evidence anything ever arrived. You find out weeks later while reconciling by hand, and you cannot tell whether the bank failed or the app did.
So there are three outcomes, deliberately unequal:
| Outcome | When |
|---|---|
| Book it | a movement that can be verified: known sender and a money-shaped amount |
| Hold for review | anything ambiguous; you settle it in one tap |
| Drop | only what can never be a transaction, whatever else it says |
Every widening of drop is a candidate for silent loss and has to be argued on those terms. That single sentence decided almost every hard call below.
Android: you get the firehose, and you must not drink it#
Android offers a notification listener and, on a sideloaded build, the SMS receiver. The listener is the interesting one, and the first thing to understand is that you cannot subscribe to one app. Grant the permission and the OS hands you every notification on the device: chat apps, music, mail, all of it. There is no per-app opt-in to decline.
That single fact drives the privacy design. The pipeline:
notification / SMS
→ native pre-filter (cheap, in Java, before anything is stored)
→ durable spool on disk (survives the process being killed)
→ headless JS gate (the real rules)
→ parser
→ book · hold · dropDisk first, always. The native side commits the record synchronously and takes a wake lock before handing anything to JavaScript. Android is free to kill the process between the notification arriving and your code running, and anything held only in memory at that moment is money the user spent and will never see.
// Shape, not source. The ordering is the whole point.
fun onNotificationPosted(sb: StatusBarNotification) {
if (!allowedPackages.contains(sb.packageName)) return // refuse on package alone
val haystack = sb.title + "\n" + sb.text + "\n" + sb.bigText
if (!prefilter.matches(haystack)) return
spool.commitSync(haystack, sb.packageName, sb.postTime) // disk BEFORE anything else
wakeLock.acquire(TASK_BUDGET_MS)
HeadlessJs.start("autotrack") // only now wake JS
}A refusal must leave no record of any kind#
An early build kept a diagnostics tally of ignored apps ("Yahoo 1,114×, Spotify 664×") to prove the app ignored them. It read as surveillance, and it was right to. Keeping a running count of Spotify to prove you do not watch Spotify is a contradiction no caption can fix. The counter was deleted and the stored key purged on upgrade, and a test now asserts that an excluded source leaves a trace of length zero. Not a small record. Zero.
The native pre-filter, and the property that keeps it honest#
Running the full rules in JavaScript for every notification on the phone is a battery problem, so a compiled pattern runs natively first. The trap is obvious once stated: two filters, two sets of rules, and they drift.
Two things prevent it.
The pattern is generated from the JavaScript rule lists, never hand-copied. A smarter rule then reaches binaries already installed the next time the JS publishes a new pattern, with no native release.
And one property, pinned by a test:
Anything the JS gate would keep, the native filter must also keep. The reverse is fine: native is deliberately looser.
That asymmetry is the principle from the top, expressed as a rule about two layers. A native filter that is too generous costs a wasted wake-up. One that is too strict costs a transaction.
Two limits fell out of it, both easy to get wrong:
- Only unconditional rejections may be mirrored. "You have won a prize" is never a transaction, so refusing it earlier loses nothing. A conditional rule ("looks like an advert and the number is not money-shaped") depends on information the native side cannot see, so it stays in JS.
- The two sides do not read the same text. JS parses one line, the newest in a thread. Native scans the whole collapsed blob, because that is the only form in which a "2 new messages" rollup carries its payment at all. Banks post marketing and charge alerts from the same short code, so an unconditional ad refusal would let an unread advert veto a real charge posted after it. That is the invisible loss, on the bank most of the user's transactions come from.
The rules that real banks taught#
Marketing rides ordinary verbs. A promo filter that only rejects when no movement verb is present will not save you, because a bank pitches its own products in perfectly ordinary sentences: "exclusive perks when you purchase the …". The verb fires and the blast walks through every gate.
What works is an intersection, and neither half is safe alone:
// An advert that never prints money beside a figure, and carries no reference
// and no masked account, cannot be a receipt.
if (isAutomated && hasPromoMarkers && !amountIsMoneyShaped) return DROPPromo vocabulary on its own rides plenty of genuine receipts ("earn double reward points"). A non-money-shaped amount on its own is the ordinary hold case. Only the intersection is safe to drop. And isAutomated matters: a human who types "50 off at the offer" into the paste box means it.
A marker list is a spelling list. One campaign got through because the rule read t&c apply and the message ended T&Cs apply., missed by one character. Both spacings, both apostrophe habits and the plural are all in the wild. When a marker fails, suspect the spelling before the logic.
A deadline is not an amount. "available only until August 8, 2026" scored 2026 as the figure. Date-awareness has to look back across a day number, not only at the words immediately beside it.
The notification shade writes text the sender never sent. Android's own [2 unread] rollup gets prepended exactly where an amount scorer looks, and the "2" becomes a transaction. Strip the OS chrome before anything reads the body.
Name the half that actually failed. A hold chip reading "Unverified sender" fired whenever either the sender or the amount check failed, so a perfectly matched bank alert with an odd amount accused the user's own bank of being a stranger. They are separate questions and now produce separate reasons, from one helper that drives both the chip and the booking gate, so they can never disagree.
Held is not a bin. Every card you must dismiss trains you to stop reading the queue, which is how the next real capture gets swiped away unread. A queue is only worth having if everything in it deserves a decision.
"Is the app alive?" is not a question your state store can answer#
A message arriving while the app is not in front takes one of two routes: hand it to the foreground drain, or do the work in the background task. Choosing by reading the user out of the global store is wrong, and subtly so: module state outlives the React tree. Android destroys the Activity while keeping the process alive, and on a phone with spare RAM that is the ordinary "I closed the app" state. The store still names a signed-in user long after the component that owns the drain unmounted, so the task pushes to an inbox nobody is listening to and returns.
Nothing is lost (the next foreground recovers it), but the user spent money and got no notification, which is indistinguishable from the app having missed it.
Route on whether the drain is actually subscribed, never on whether a user is in memory:
// Refcount live subscribers per event label. A mounted diagnostics screen
// is not a working drain, so the label matters.
const canHandInline = hasListener(INBOX_DRAIN) && isHydrated()Undercounting is the dangerous direction: a double cleanup driving the count negative would route live captures down the killed-app path.
iOS: state the impossibilities first#
This is where most write-ups go vague. Concretely, on iPhone:
- There is no notification listener, and there will not be one. A bank's own app push is unreachable by any mechanism. Roughly half of what Android captures does not exist as a capture surface on iOS at all.
- There is no SMS-reading API. The message-filter extension sees SMS from unknown senders and then cannot tell you: it may not write to disk and may not write the shared container the app reads. Its only exit relays the sender and body to a URL fixed in
Info.plist, which means shipping raw bank SMS through your own server. Rejected: it inverts on-device redaction, and an extension that always returns.allowwhile forwarding everything is not a spam filter, which is a real review risk. - Shortcuts cannot read notifications, only messages and mail.
So iOS auto-track covers bank SMS and nothing else, and the setup screen says exactly that rather than implying parity. Under-promising here is a feature.
The one iOS route that works#
A Shortcuts personal automation ("When I get a message", set to run immediately) calling an App Intent the app publishes:
bank SMS
→ Shortcuts personal automation ("When I get a message", Run Immediately)
→ App Intent (openAppWhenRun = false)
→ append one raw line to a spool file on disk
→ one collapsed local notification, however many arrived
→ next foreground: drain the spool through the SAME gate and parser as AndroidFive things carry that design.
The Swift does not parse. The intent writes a raw record and stops. Every rule (the sender allowlist, the promo intersection, hold reasons, redaction) stays in the shared TypeScript, where the test suite already pins it. A Swift parser would be a second set of rules, and the first time the two disagreed the disagreement would be invisible.
The drain reuses the Android SMS entry point rather than reimplementing it. That function already takes exactly this shape (a raw body, a sender id, a post time), so iOS became a fourth caller of an existing route, not a parallel pipeline.
The intent lives in the main app target, not in a module. Xcode extracts App Intent metadata per target, and intents compiled into an autolinked pod routinely go missing from the Shortcuts action list. A second benefit falls out of it: running in the app's own process means the spool lands in the app's own container, so no App Group is needed.
The intent returns nothing. It once returned a Bool, and the first run outside the editor asked "Allow … to output 1 boolean?", a prompt a locked-phone automation can never answer, so the capture was swallowed. A failed write throws instead.
// Shape only. Returning a value is the bug; throwing is the contract.
func perform() async throws -> some IntentResult {
try spool.append(body: messageBody, sender: senderId, at: .now)
await notifier.postCollapsedBanner()
return .result() // nothing to confirm, nothing to prompt about
}Nothing is destructive before it is safe. A record is removed only after its handler returns, so a process death replays whatever was in flight, and a replay is a no-op, because the dedupe key derives from the message text and its original post time. The one hazard replay cannot survive is a record that kills the process every time, so each carries an attempt count written to disk before it is handled, and is dropped, loudly, after three.
A bug the tests caught and the comments did not: a failing record originally stayed at the head of the file, so the walk retried it instead of moving on and starved everything queued behind it. A failed record now rotates to the back and the pass stops when the head comes round again: one attempt per pass, not three in a row.
When the OS blanks the text#
A field report: notification access granted, background running allowed, the reader alive, a message received minutes ago, and the last captured transaction two days old. A real card charge never became a row. The parser was innocent; the same text books correctly through every delivery channel.
Android's classifier had decided the user's bank alerts looked like one-time codes and replaced every body with a placeholder before the listener ever saw it. There is no app-side read of that text in a store-distributed build: it is a system role a finance app cannot hold. Hundreds of alerts, blanked, in silence.
What was actually broken was ours, and it was the silence. The app counted hidden alerts and posted a notice once per device, ever, on the theory that repeating a persistent condition teaches people to ignore you. That theory is right for a condition that costs nothing and wrong for one that costs a transaction every time it fires.
The rules now:
- Blinded is a health state of its own, ranked below "nothing is arriving at all" and above "at risk", and it is live-gated: a phone hit once last spring reports healthy without the user clearing anything.
- The notice returns weekly while it is still happening, from the background gate and from the app root, sharing one slot so they cannot double-post.
- Daily, not weekly, once capture is producing nothing at all. Weekly is the pace for a degraded app; a device blanking everything is not degraded, it is dead, and weekly means six-day stretches with every charge dropped in silence. "Dead" is measured by money-shaped messages seen and not captured since the last capture, so it clears itself the moment capture works again.
- The explanation books the money it cannot read. Naming the cause does nothing for the charge that was blanked an hour ago. The message is still on the phone, and two taps is the difference between a lost transaction and a booked one.
The capture route that starts outside the app#
Every import route began with "be in the app": open it, find the paste screen, get sent to the SMS app, find the thread, long-press, select, share, find the app in the sheet. Seven steps, and the first two are the ones a person reading a bank SMS has not taken.
Android's process-text action inverts it. An app that declares it is offered by name in the floating toolbar the OS draws over any selected text: Messages, WhatsApp, mail, the bank's own app, a browser. Select the message, tap the app. The text lands in the same importer and runs the same parser as everything else, and ends where every capture ends.
Three things about it fail silently, which is exactly why they are worth writing down:
- Read the extra as a
CharSequence, never aString. A styled selection (a linkified amount, a bolded merchant) arrives as a span, and the string getter returns null. The feature would die on precisely the messages worth capturing. CATEGORY_DEFAULTis not optional. An implicit intent never matches a filter without it, and the toolbar entry simply does not appear, against a manifest that reads perfectly correctly.- Alternate launcher icons disable the main activity. A disabled component resolves no intent, so choosing a different app icon silently removed the app from the share sheet. Every icon alias has to carry the same intent filter, checked by deep-equality against the real one so that narrowing one can never pass.
It is also policy-safe by construction: it reads text the user selected and acted on, never touches the SMS store, and needs no permission.
What I would carry to any project like this#
- Rank your failure modes before writing a single rule. Wrong-and-visible beats missing-and-invisible, and once that is settled most arguments answer themselves.
- Commit to disk before you do anything clever. Parsing, waking JS and the network can all be interrupted. Disk first, then everything else.
- One set of rules. If a second layer must exist for performance, generate it from the first and pin the superset property with a test.
- Never route on state that outlives the tree. Ask whether the listener is subscribed, not whether a value is still in memory.
- Say what the platform cannot do, inside the product. iOS covers SMS only, and the setup screen says so. A feature that quietly does half of what its name implies is worse than one that names its own limits.
- A refusal should leave no trace. If you would not want a screenshot of your diagnostics posted on a forum, do not collect it.
- Silence is a bug, and it has a severity. Anything that can fail invisibly needs a health state, an escalation, and a way for the user to recover the specific thing that was lost.
Refunds#
Bought through a store, refunded by that store. Google Play takes self-service refund requests for about 48 hours after the purchase and decides the rest case by case: how to ask Google. Apple sets no fixed window and reviews every request: report a problem. If a store turns you down and you think it was wrong, write to us and we will put the request to them ourselves.
Bought directly from us rather than through a store: write within 14 days of the payment and you get all of it back, no reason needed. The money goes back to the card or wallet that paid it. Banks take up to 14 working days to show it, which is their clock rather than ours. A refunded lifetime unlock is switched off on the account it was bought for.
Cancelling a subscription is not a refund. It stops the next charge, and Pro keeps running until the end of the period you already paid for.
Support#
One address for all of it (a bug, a bank whose alerts are not being read, a refund, a question about your data): boogado@yahoo.com.
Masareef is built and sold by Mohamed Hamed Mohamed Gado, Egypt.
Terms#
Pro is a right to use the app, granted to your Masareef account. You are not buying the app itself, and the right cannot be resold or moved to someone else's account.
What you keep if you stop paying: everything. Every transaction, wallet, budget, goal and debt stays exactly where it is and the app keeps working. Automatic capture returns to its free monthly allowance and the Pro screens lock. Nothing is deleted and nothing is held hostage.
A lifetime unlock is not a subscription and never renews. If prices change later, one already bought is unaffected. A subscription renews at the price you agreed until you cancel it; if that price ever changes, the store tells you before it charges you and you can cancel first.
Masareef records money you have already spent. It is not financial advice, and automatic capture is a best-effort reading of the messages your bank sends. It can miss one, and your bank statement is the authority, not this app. Check anything that matters against it.
Your account and its data are yours to delete at any time, from inside the app or by asking us: delete your account. What we collect and why is in the Privacy Policy.
These terms are governed by the laws of the Arab Republic of Egypt.

