# Shared shopping list with automated price and offer scraping from Finland's main grocery stores @ www.groci.fi (pet project)

[Full CV index — Tony Miasoedov](https://cv.tony-m.dev/)

- Candidate: Tony Miasoedov, Senior Software Engineer
- Company: www.groci.fi (pet project)
- Period: 2026 — present
- Role: Founder, solo developer
- Team: Solo project
- Main result: Built groci.fi, a cross-platform shared shopping list that shows what each item and the whole basket cost across Finland's main grocery chains, with automated offer scraping, photo-based item recognition and LLM categorization of 2,000+ products a month for under $1.

## Context

The project solves 2 problems:
1. It's a shared shopping list that is cross-platform and syncs across all devices. A family with iOS and Android can use it, or a team at work with different devices.
2. It automatically scrapes products with prices from Finland's main grocery stores. So it's not just a shared shopping list: when users add an item, they immediately see what it costs in different stores and what their whole basket will cost.

There is also separate scraping of current store offers, and users can build a list based on these offers.

## Features in detail

### Authentication
- Built a reliable authentication system with Google, Magic Link and Apple sign-in. Authentication is covered by e2e tests with test datasets and email interception.
- App-level role management. The app has admins who can delete products from the shared catalog, add and edit categories, and see admin logging pages. These admins are known in advance, so there is dedicated testing and monitoring to make sure attackers can't change their own roles. All role changes are logged and checked automatically.

### Shared shopping list
- A shared shopping list with real-time database sync. When someone adds or changes an item, the change is instantly visible to every member of the list.
- Member role management lets you give users different roles in a list: who can invite new members, who can add and edit items, and who can only view the list. I added safeguards: users can't give themselves a role higher than their own, and the last list admin can't remove that role from themselves, so there is always at least one admin. These safeguards led to a zero-errors record in role management.
- Adding an item to the list is an especially complex and useful feature, described in "Adding an item to the list" below.
- Items can be added not only from the site's catalog but also from a store's catalog with a Chrome extension, described in "Adding an item with the Chrome extension" below.

## What I did well

### Adding an item to the list

Users can add an item by taking a photo, without searching the catalog. They photograph a barcode or just the product, and a recognition pipeline turns the photo into a list item. When it can, it links the item to a catalog product, so store prices show up straight away.

The pipeline tries strategies in a fixed priority order, from cheapest and most accurate to least:
1. **Barcode decoded in the browser** (`@zxing/browser`), matched exactly against our catalog. This path is free and costs no AI credits.
2. **Open Food Facts lookup** by barcode if the product isn't in our catalog. It has a 5-second timeout, and the user can add the item before the lookup finishes.
3. **Claude Vision** for photos without a readable barcode. It reads the digits under the barcode, or the product name and brand when there's no barcode. If it finds a barcode, the pipeline goes back to steps 1–2.
4. **Fuzzy catalog search** by name and brand (`pg_trgm`, up to 3 candidates). The user confirms a match or rejects it.
5. **Fallback**: a plain item named from the extracted name, the Open Food Facts name or the barcode itself. Adding an item never fails.

Challenges: Vision calls cost money, so I added per-user AI credits that are deducted atomically. The barcode path never uses them, so it keeps working when credits run out. The UI never waits on slow external sources. Results stream into a bottom sheet, and the "Add to list" button is available as soon as a barcode is known. Every run is logged with the input photo (kept in a separate S3 bucket), the extracted hints and the final result. A dev-only log viewer lets me trace why a photo was matched, or wasn't.

Grocy is developed with AI agents and my own [feature factory](https://cv.tony-m.dev/projects/feature-factory/).

On top of those, it has its own agents:
- An agent that updates the module docs and looks for the impact of changes. It keeps the docs up to date, so users and developer agents can always understand the intent of a feature. This cuts the tokens used in development and minimizes errors where an agent misreads how other modules work from the code. The agent also makes sure a feature's changes don't affect other modules, and that the docs and the code always stay consistent.
- A Q&A agent. It's a separate repository that runs every night and contains 10+ agents that test UX and user scenarios, find bugs and check that every Grocy feature works correctly. They record their results in a separate database, and another agent sends reports when an issue was reported by more than three agents. Those issues are worked on first.

### Adding an item with the Chrome extension

The extension adds an "Add to shopping list" button right on a product page in an online store. Clicking it adds the product to the shared shopping list automatically, with all the data it needs, such as name, price and category.

Challenges: the S-kauppa and K-market catalogs are SPAs. When you switch pages they don't fully update the DOM or the JSON data, so simple parsing doesn't work. I had to track DOM changes and check that the data on the page matched the title, but I automated all of it, and the extension works without errors. When adding a product, it also checks the EAN code and whether the product is in our catalog, and adds it together with the prices from other stores.

### Offer scraping

Offers are scraped automatically on a schedule. I set up a VPS with cron that runs the scraping script. It works "carefully" so it doesn't overload store servers and avoids getting blocked. It also collects only new offers, so existing data isn't duplicated.

My scraper recognizes 5 types of offers:
- A plain discount.
- Multi-buy for a set number of items.
- A discount on a bundle, when the store lists it as one product but our catalog has several, and we have to group them.
- A discount on a weighed product, when the price depends on weight but the store prices a weight that doesn't match other stores. Our scraper correctly recalculates the price per unit of weight.
- A discount on a product packed by the store. For example fish, priced per piece, where each fish weighs differently and the law requires comparing price per kg. The scraper correctly recalculates the price per unit of weight and accounts for the packaging.

**Challenges I faced**:
- Lidl has no catalog and no EAN codes that would identify products unambiguously. I had to use a combination of name, weight and other attributes to match products with other stores. Accuracy is currently 80%, and I'm collecting statistics: every mismatch is logged for further analysis.

- Automatic categorization with an LLM and vectors. S-kauppa has over 1,000 categories and K-ruoka over 1,200. On Grocy I kept 700 categories with 3 levels of nesting, but every new product has to be categorized automatically. I set up a system that saves costs and gives good accuracy. With Gemini's embedding model, I created a vector for each category and stored it in the database next to the category. When a product comes in, we create its vector, find the 10 closest categories by similarity, and only those 10 candidates go to the LLM for the final decision. So we don't send all 700 categories to the LLM, which saves budget and keeps the cost of categorizing 2,000+ products a month under $1. I chose Gemini because it's considered the best for multilingual search, but even it struggles with Finnish. I'm now collecting statistics to improve categorization.

## Results

## Stack & skills

- **Next.js**: Built the app on Next.js.
- **React**: Built the whole UI in React, including a real-time shared list and a bottom sheet that fills in as recognition results arrive.
- **TypeScript**: Wrote the whole codebase in TypeScript.
- **Server-side rendering**: Render pages on the server.
- **PostgreSQL**: Store the catalog, lists and prices in PostgreSQL, with pg_trgm fuzzy search over product names and category vectors stored next to each category.
- **AI native**: The project is developed by AI agents with my own feature factory.
- **AI agents**: Built project-specific agents — one keeps module docs in sync with the code and checks the impact of changes, and a nightly Q&A system of 10+ agents tests UX and user scenarios.
- **Feature factory**: Grocy is developed with my own feature factory.
- **Authentication**: Built sign-in with Google, Magic Link and Apple, covered by e2e tests with test datasets and intercepted emails.
- **Role-based access control**: Built app-level admin roles and per-list member roles, with safeguards against self-promotion and against removing the last list admin — zero errors in role management.
- **Real-time data**: Changes to a shared list show up instantly for every member on iOS, Android and desktop.
- **LLM integration**: Used Claude Vision to read barcodes and product names from photos, and an LLM to pick the final category from 10 candidates.
- **Vector search**: Created Gemini embeddings for 700 categories and send only the 10 nearest to the LLM, keeping categorization of 2,000+ products a month under $1.
- **Web scraping**: Scrape prices and offers from Finland's main grocery stores on a schedule, carefully enough to avoid overloading the stores or getting blocked, collecting only new offers.
- **Browser extensions**: Built a Chrome extension that adds products to the list straight from S-kauppa and K-market pages, handling their SPA navigation.
- **API integrations**: Look up unknown barcodes in Open Food Facts with a 5-second timeout and a fallback.
- **Cost optimization**: Charge AI credits only for paid Vision calls, keep the barcode path free, and use vector prefiltering to cut LLM categorization costs.
- **End-to-end testing**: Covered authentication with e2e tests, and run nightly agent-based testing of user scenarios.
- **Observability**: Log every recognition run, every role change and every product matching mismatch, with a dev-only viewer for recognition logs.
- **Automation**: Run the scraper on a VPS with cron.
- **Documentation**: An agent keeps module docs up to date, so people and developer agents understand the intent of each feature.
- **Ownership**: Own the product end to end, from idea to development and operations.

[Full CV index — Tony Miasoedov](https://cv.tony-m.dev/)
