- Role: creator and sole developer
- Timeline: since November 2024
- Platforms: Android, iPhone and iPad (available in Brazil, in Portuguese)
- By the numbers: 545 users, 554 receipts processed and 5,961 prices published
- Available on: Google Play · App Store · buscapreco.net
It started with the price of coffee
How it works
- Scan the receipt. The app reads the QR code and the backend fetches the items from the SEFAZ portal. Each item becomes a recorded price, with a date and a store.
- Look up a product. By name or by barcode, you can see its price at nearby stores and how it has changed over time.
- Compare stores. Build a shopping list and the app estimates the total at each store in the area. After you shop, it shows how much you saved compared with the others.
Architecture
The challenges
Reading receipts without an API
A search that understands "leite"
| Query | Top result (before) | Top result (after) |
|---|---|---|
| leite (milk) | Creme de leite (heavy cream) | Leite integral (whole milk) |
| papel higiênico (toilet paper) | Tapete higiênico (pet pad) | Papel higiênico (toilet paper) |
- Full-text search with a Portuguese dictionary and accent stripping: "higiênico" and "higienico" become the same term, and results containing every word in the query rank above the rest.
- Per-field weights: a match in the product name counts more than a match in the brand.
- Word position: "Leite integral" starts with the query term and "Creme de leite" doesn't. That was the signal that fixed the milk case.
- Trigrams as a fallback, when nothing else matches, to keep tolerating typos.
- Popularity as a tiebreaker, based on how many recent prices each product has.
Products sold by weight: one cut, ten names
From Android to iOS without rewriting the app
| Feature | Android | iOS |
|---|---|---|
| Camera and barcode scanning | CameraX + ML Kit | AVFoundation + Vision |
| Subscriptions | Google Play Billing | StoreKit 2 |
| Location | Google Play Services | CoreLocation |
Other problems worth mentioning
- Product data. In Brazil, barcode databases are mostly closed. The app first queries Open Food Facts, which is openly licensed, and gives proper credit. When a product isn't there, it is created from the receipt itself (where it shows up as "LEITE UHT INTEGRAL ITAMBE 1L"), and the community can complete the entry with a photo and details. Submissions go through automatic screening for inappropriate images and then manual moderation. The category is assigned automatically from the NCM code, the tax classification that already comes with every item on the receipt.
- Where is this store? The receipt carries the store's tax ID, legal name and address, but no coordinates, and not the name people actually know it by. The backend geocodes the address, searches nearby places and picks the candidate by street number and name similarity. When it gets it wrong, the user who registered the store can pick another candidate, and I can fix it on a map in the dashboard.
- The chicken-and-egg problem. An empty crowdsourced app has no value, and without value nobody contributes. That's why looking up prices costs credits: some renew every day, for people who just want to use the app, and others are earned by contributing, with receipts that have more items earning more. Experience levels give a sense of progress, and the subscription removes the limits. There are also anti-abuse protections, which, for obvious reasons, I won't detail here.
Where the project stands
- 545 users
- 554 receipts processed
- 5,961 prices published
What I learned
- Record where your data comes from, from day one. In early versions, I also used other product data sources. When I reviewed their licensing, I decided to stick to open sources and community submissions, even at the cost of coverage. The migration re-queried the whole catalog and removed the images whose license wasn't compatible. It would have been much simpler if I had known where each record came from: two thirds of the catalog had no such information, and the only safe way out was to reprocess all of it.
- A human in the loop is a feature, not a hack. Stores, products sold by weight, user submissions: in all of these cases, the best design was for the algorithm to suggest and a person to confirm, with reversible decisions.
- Use the database you already have until it truly can't keep up. PostgreSQL handled full-text search, typo tolerance and ranking without adding another system to operate.
- Isolate what's fragile. The most unstable part of the system, reading the portals, lives in its own service and doesn't take the rest down when it fails.
- Write before you code. Before every significant feature, I write a short design document with the context, the decisions and what's out of scope. There are more than 60 of them now, and they are what made this write-up possible.
Tech stack
- Mobile: Kotlin Multiplatform, Compose Multiplatform, Ktor, Koin
- Backend: Kotlin, Spring Boot 3, Spring Security (OAuth2), JPA, Flyway, OpenFeign, Testcontainers
- Data: PostgreSQL (full-text search and pg_trgm), Redis, S3
- Receipt ingestion: Python, Flask, Selenium
- Moderation dashboard: React, TypeScript, Vite, Tailwind, TanStack Query
- Website: Astro
- Authentication: Keycloak (OIDC, social login)
- Infrastructure: self-managed VPS, Docker Swarm, Cloudflare, Prometheus
- Integrations: Open Food Facts, Google Places and Geocoding, Cloud Vision, Firebase Cloud Messaging, Google Play Billing, StoreKit 2
Links
- Website: buscapreco.net
- Google Play: Busca Preço
- App Store: Busca Preço: Compras



