lesecuritae/Korbunio
lesecuritae/Korbunio has 51 GitHub stars and 4 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-15 08:56 UTC. Last push 2026-11-13.
Measured star growth
- in 24h
- 0
- in 7 days
- Collecting history
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
About
Self-hosted shopping intelligence platform with native serverless Android app and optional Docker web service.
Self-hostedREADME.en.md
OriginalKorbuino
Android app
The published APK is the native Kotlin/Jetpack Compose app in android/. It fetches retailer offers directly and stores them offline in Room. Users can optionally connect their own Korbuino Docker instance; credentials stay in the Android Keystore. See android/README.md for details and APK build instructions.
Loyalty programs: tested behavior and source limitations.
← Language selection · Deutsch
Native Android app
The Kotlin/Jetpack Compose client in android/ uses a
serverless, offline-first architecture. It stores normalized data in Room,
uses WorkManager for opt-in background work, keeps credentials in the Android
Keystore, and fetches retailer sources directly without a Korbuino server.
The native registry currently covers REWE, GLOBUS, ALDI Nord, ALDI Süd,
Kaufland, Rossmann, Müller, HOL'AB!, both Netto variants and dm, plus
regional Marktguru sources for famila Nordwest, Lidl and PENNY. The existing Flutter client in
app/ remains the compatibility client while further provider flows
are migrated.
Korbuino is a self-hosted service for comparing current regional supermarket offers in Germany. In normal use, the user only enters a German postal code. Korbuino discovers matching retailers and stores, retrieves current weekly offers, normalizes product names and package sizes, keeps package size separate from unit-price reference quantities, and compares identical or meaningfully comparable products. Loyalty programs can be included optionally, and results are presented in an interactive web interface.
It does more than list leaflet prices: the comparison engine cleans inconsistent source data and presents price, actual package size, and unit price separately so differences between retailers and package formats remain understandable.
How the project started
Korbuino grew out of vibe coding for a specific personal use case. The original idea was to have a local LLM run the supermarket comparison automatically and provide or post the result through Conduit on Monday mornings, once the new weekly offers were available.
During development, it became clear that the comparison logic made more sense as a standalone service. In my view, that made Korbuino faster, more flexible, easier to automate, and independent of any particular LLM model or frontend. Browsers, REST clients, scripts, Conduit, and LLMs can all use the same service.
An LLM is therefore optional and is not a runtime requirement. The original idea of an automatically generated Monday report using a local LLM and Conduit remains a possible use case.
Quick start
You need Docker Engine, Docker Compose v2, Git, and internet access from the container.
git clone https://github.com/lesecuritae/Korbunio.git
cd Korbunio
docker compose pull
docker compose up -d --no-build
This uses the prebuilt ghcr.io/lesecuritae/korbunio:latest image published through GitHub Container Registry. The default setup does not require a .env file.
Local source build
This separate development path builds the image from the checked-out Dockerfile:
docker compose build
docker compose up -d --no-build
The default port is 8000. Open:
http://SERVER-IP:8000
Normal browser use requires no account, no LLM, no API key, and no mandatory .env file.
Before starting a search, users can choose the current offer week or the next week. A preview is used only where the retailer has already published it; otherwise current offers remain visible with a transparent notice. Persistent product-name keyword filters use OR semantics and can be backed up or restored as versioned JSON.
Running with Docker
The Compose project, service, and container are all named korbunio; persistent data is stored in the korbunio-data volume.
docker compose logs -f korbunio
curl http://127.0.0.1:8000/health
docker compose down
docker compose down preserves the volume. If host port 8000 is occupied, select another host port with the still-current internal variable SUPERMARKT_PORT; the container continues to listen on port 8000:
SUPERMARKT_PORT=8080 docker compose up -d --no-build
The interface is then available at http://SERVER-IP:8080. The value may also be stored in an optional .env file.
What happens during a search
postal code
↓
discover regional retailers and stores
↓
+retrieve current offers
↓
normalize product, price, and quantity data
↓
determine unit prices and comparable offers
↓
assign loyalty prices and quantified benefits
↓
store a snapshot in SQLite
↓
display the results
The browser interface and REST API use the same comparison engine. Retailer adapters, normalization, loyalty logic, and price comparison are not reimplemented in the frontend.
Supported retailers and data paths
Korbuino currently supports REWE, EDEKA, Marktkauf, ALDI Nord, ALDI Süd, Kaufland, Lidl, PENNY, Netto Marken-Discount, Netto schwarz, GLOBUS, HOL’AB!, Rossmann, Müller, famila Nordwest and trinkgut.
REWE, EDEKA, Marktkauf, Kaufland, GLOBUS, and the applicable ALDI region are loaded preferentially from direct retailer sources. ALDI Süd uses its structured official weekly publication as its complete primary source. ALDI Nord takes price, unit price, explicitly published deposit, and product image from its official offer data. If the ALDI region cannot be determined unambiguously and no explicit selection was made, ALDI is omitted and a warning is shown.
For trinkgut, the store list is downloaded from the official trinkgut website (or taken from cached data) and the store with the postal code, or else the nearest one, is used. A store further away than SUPERMARKT_TRINKGUT_MAX_DISTANCE_KM (default 40 km) is not presented as local: the official source reports that there is none nearby and the regional Marktguru data is used instead. Where the listing cuts the deposit off, the product page is read for it (at most SUPERMARKT_TRINKGUT_DEPOSIT_FETCH_WORKERS at a time) and the value is remembered.
Lidl, PENNY, Netto Marken-Discount, and famila Nordwest are loaded from regional Marktguru data. Netto schwarz, Rossmann, Müller, and HOL’AB! use separate source-specific data paths. Korbuino combines a broad regional search with supplementary retailer-name searches where applicable; the name queries alone are never treated as a complete catalogue.
When a postal code has multiple exact store matches, users can select a specific REWE or Netto Marken-Discount store. REWE offers are loaded for that store. Netto Marken-Discount currently remains a regional catalogue, so Korbuino displays the selected official store transparently without claiming store-specific prices.
famila Nordwest only trades in north-western Germany and is therefore optional in the same sense as Marktkauf and GLOBUS: an empty regional result is not reported as a connection error. Korbuino shows only the regional catalogue actually available and does not substitute data from another area.
famila Nordwest and famila Nordost are separate, unrelated retail groups. Only famila Nordwest is matched; famila Nordost is explicitly excluded so its offers can never appear under the Bünting brand.
If a direct adapter fails or returns no offers for the target week, Marktguru may act as a fallback for that retailer only. A successful direct catalogue is never mixed with a second complete Marktguru catalogue. Actual availability depends on postal code, region, and reachable sources; retailers without results are not shown as empty filters.
docker exec korbunio python -m supermarkt.diagnostics 12345
The command above runs the current source diagnostic for a postal code inside the container.
Results interface
The interface includes:
- retailer filtering
- combined product and brand search
- sorting by price, unit price, retailer, or product
- regular price and unit-price comparisons
- a view containing only the cheapest safe comparison matches
- an all-results view including more expensive duplicates
- product images through the local image proxy
- loyalty prices and specifically quantified euro benefits
- selection of multiple loyalty programs
- warnings for failed or incomplete sources
- automatic loading of additional results while scrolling
When exactly one retailer is selected, the redundant retailer column is hidden.
Package sizes and data cleaning
Package size and the reference quantity used for a unit price are handled separately. A can containing 330 ml with a unit price per 1 litre remains a 330 ml package. The litre reference appears only with the unit price and is not combined into an incorrect quantity.
Alternative sizes are not added together. Source text such as 85 g or 100 g describes two possible packages and must not become 85 g + 100 g or 185 g. A + appears only when the source actually describes a combined pack.
Product names, brands, prices, and quantities are cleaned before comparison groups are built. Placeholders such as This is no brand are treated as missing brand data and are not displayed.
Loyalty programs
The current code recognizes:
- REWE Bonus
- Lidl Plus
- PENNY App
- Netto plus App
- Kaufland Card XTRA
- EDEKA App
- MARKTKAUF App
- mein GLOBUS
- PAYBACK at supported retailers
Multiple programs may be selected together. Korbuino only applies product prices or euro-denominated benefits explicitly present in the offer data. Direct loyalty prices may reduce the checkout price, while a specifically stated euro credit is applied as a benefit.
Points, personal coupons, status benefits, percentage promotions without a concrete resulting price, and unknown discounts are never estimated or converted into invented euro amounts.
No mandatory LLM
Korbuino is a standalone service:
Browser / script / REST client / LLM
↓
Korbuino
↓
retailer sources
A local LLM can still use Korbuino, for example for an automatic Monday report through Conduit, natural-language queries, or summaries. The price comparison itself requires no LLM and remains independent of any model, agent, or frontend.
MCP server for AI assistants
The server ships an MCP server at /mcp (Streamable HTTP). An assistant can ask "where is Hochland cream cheese on offer?" and gets the retailer, the price without a loyalty programme, the price with one (where the data quantifies a benefit) and one image (up to three with max_images). Read-only.
- Tools:
find_offers,list_retailers,list_bonus_programs. - Client configuration:
https://<your-server>/mcp. IfSUPERMARKT_API_KEYis set, the client must sendAuthorization: Bearer <key>; otherwise access is open like the rest of the server. - Locally without HTTP:
python -m supermarkt.mcp_server(stdio). - Waiting: a new postal code loads every retailer (usually 10 to 20 seconds). Loading continues in the background, long calls report progress, and after 45 seconds (
SUPERMARKT_MCP_DEADLINE_SECONDS) the server asks the client to repeat the question shortly. The default postal code and recently used ones are kept fresh. - Shopping list (optional): once KitchenOwl is set up,
add_to_shopping_listis offered too. Set it up on the/settingspage (address, token, pick a list); the token stays on the server in the data folder (kitchenowl.json, mode 0600) and is never shown again. WithSUPERMARKT_API_KEYset, the page asks for the admin key.SUPERMARKT_KITCHENOWL_URL,..._TOKENand..._LIST_IDwork as an alternative. It puts an item with retailer and price as a note on the KitchenOwl list and skips duplicates. It is the only writing tool and exists only with a token; use a token that is good for this list only and keep the server on your own network. - Price history: on every fresh load the server records the lowest daily price per retailer and product (SQLite
price-history.sqlite3in the data folder, one year).price_historyshows whether an offer is really cheap. History starts with this version. - Watching:
watch_product,list_watches,remove_watch(up to 20). Offers only change weekly, so the server looks once a day (reloading only when the cache has expired) and reports each new match once, optionally only below a maximum price. Messages go to an address entered on/settings(an ntfy topic or a webhook, via POST); without it these tools do not exist. check_shopping_listshows which items on the KitchenOwl list are on offer now.add_to_shopping_listis limited to 30 new items per hour (SUPERMARKT_MCP_SHOPPING_ADDS_PER_HOUR).- Open WebUI: Admin panel → Settings → External Tools → add, type "MCP (Streamable HTTP)", URL
https://<your-server>/mcp, authentication "Bearer" if a key is set. From inside a Docker container the address must be reachable (notlocalhost). - Clients that only speak stdio:
python -m supermarkt.mcp_bridge https://your-server/mcp(key inKORBUINO_MCP_KEY). Ready-made setup lines are on/settings. - Disable with
SUPERMARKT_MCP=0.
Operations: backup and protection
- Backup: The data folder (
/data, thekorbunio-datavolume in Docker) holds the price history, watches, the KitchenOwl setting with its token and the notification address. Include it in your backups. - Set a key: Without
SUPERMARKT_API_KEY,/settingsis open like the rest of the server, and anyone with access could change the KitchenOwl target or the notification address. Set the key as soon as you use the MCP server with its writing tools. - Watching sources: On
/settingsa table shows when the server last saw offers per retailer (JSON:/health/sources).
REST API
External clients and automations use:
POST /api/v1/compare
Minimal request:
{
"postal_code": "01067"
}
Optional fields include offer week, persistent product-name keywords, loyalty programs, retailer, pagination, view, sorting, ALDI region, and forced refresh:
{
"postal_code": "01067",
"offer_week": "next",
"keywords": ["Milka", "coffee"],
"loyalty_programs": ["rewe_bonus", "lidl_plus", "kaufland_xtra", "payback"],
"view": "best_only",
"sort": "unit_price"
}
offer_week accepts current (the default) or next. Retailers without an
available preview fall back to their current catalogue. keywords are cleaned,
deduplicated, and matched against product names using OR semantics.
Browser access remains unauthenticated. If SUPERMARKT_API_KEY is set, bearer
authentication protects the REST and app endpoints. The Android app can
exchange this administrator key once for a dedicated app token. The server
stores only its SHA-256 digest in /data/access-tokens.json; the clear token is
kept in Android secure storage and the administrator key is discarded. A
private server without an administrator key remains usable without a token. See
.env.example for current settings.
Cache and data
No external database server is required. SQLite stores offer snapshots so filtering, sorting, and incremental result loading do not repeatedly query retailer sources.
The persistent data path holds offer snapshots, an automatically generated signing key, the local image cache, and cached REWE and Kaufland store mappings. A complete snapshot of the current week stays fresh until the next offer change, Thursday or Sunday at midnight Berlin time (the server already selects the coming week on Sundays), because leaflets do not change in between (SUPERMARKT_CACHE_WEEKLY=1; set it to 0 for a fixed TTL). Snapshots with a failed source and next-week previews are fresh for 30 minutes (SUPERMARKT_CACHE_TTL_MINUTES=30), and refresh=1 always re-queries. Up to 100 snapshots are retained (SUPERMARKT_CACHE_MAX_SNAPSHOTS=100), and result links remain available for 168 hours or seven days (SUPERMARKT_RESULT_RETENTION_HOURS=168). REWE and Kaufland store mappings are cached for 86,400 seconds or one day; this does not replace fresh offer retrieval.
The image cache defaults to 604,800 seconds or seven days, 512 MiB total, and 4 MiB per file. These values are configurable.
Image proxy and security
Product images are delivered through a local cache. The image service accepts only HTTP and HTTPS targets, resolves target hosts, and blocks private, loopback, link-local, multicast, reserved, and unspecified addresses. Redirect targets are validated again, providing SSRF protection.
Common tracking pixels, logos, placeholders, loyalty badges, and unsupported image types are rejected. Downloads have timeout, file-size, and cache-size limits. Result and image links are HMAC-signed. Korbuino generates and persistently stores a random signing key on first start unless an explicit key is configured.
Configuration
The default setup needs no .env. .env.example documents every current variable:
SUPERMARKT_PORTSUPERMARKT_DEFAULT_POSTAL_CODESUPERMARKT_DEFAULT_RETAILERSSUPERMARKT_API_KEYSUPERMARKT_DATA_DIRSUPERMARKT_CACHE_DBSUPERMARKT_SIGNING_SECRET_FILESUPERMARKT_ACCESS_TOKENS_FILESUPERMARKT_SIGNING_SECRETSUPERMARKT_IMAGE_CACHE_DIRSUPERMARKT_KAUFLAND_CACHE_DIRSUPERMARKT_REWE_CACHE_DIRSUPERMARKT_CACHE_TTL_MINUTESSUPERMARKT_CACHE_WEEKLYSUPERMARKT_CACHE_MAX_SNAPSHOTSSUPERMARKT_RESULT_RETENTION_HOURSSUPERMARKT_TIMEOUT_SECONDSSUPERMARKT_MARKTGURU_PAGE_SIZESUPERMARKT_MAX_WORKERSSUPERMARKT_USER_AGENTSUPERMARKT_KAUFLAND_STORE_CACHE_TTL_SECONDSSUPERMARKT_REWE_STORE_CACHE_TTL_SECONDSSUPERMARKT_IMAGE_CACHE_TTL_SECONDSSUPERMARKT_IMAGE_CACHE_MAX_BYTESSUPERMARKT_IMAGE_MAX_FILE_BYTESSUPERMARKT_TRINKGUT_MAX_DISTANCE_KMSUPERMARKT_TRINKGUT_DEPOSIT_FETCH_WORKERSSUPERMARKT_TRINKGUT_DEPOSIT_CACHE_TTL_SECONDSSUPERMARKT_TRINKGUT_STORE_CACHE_TTL_SECONDSSUPERMARKT_TRINKGUT_CACHE_DIR
The historical internal prefixes remain part of the current technical interface. .env.example is authoritative for meanings, defaults, and Docker paths.
The web interface stores the last postal code and retailer selection locally in the browser. A shared Docker instance can instead provide initial values through SUPERMARKT_DEFAULT_POSTAL_CODE and the comma-separated SUPERMARKT_DEFAULT_RETAILERS. Personal browser values take precedence; without either setting the regular selection page remains unchanged.
Running without Docker
Korbuino requires Python 3.12 or newer:
python -m venv .venv
. .venv/bin/activate
pip install -e .
uvicorn supermarkt.asgi:app --host 0.0.0.0 --port 8000
The application is then available at http://127.0.0.1:8000. By default, runtime data is kept in the local user state directory.
Development and tests
python -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'
pytest -m 'not live'
Live retailer tests are deliberately opt-in:
RUN_LIVE_TESTS=1 pytest -m live
The offline suite covers postal-code validation, browser and API routes, cache behavior, package-size and unit-price normalization, comparison groups, loyalty combinations, retailer filters, the image proxy, SSRF protection, and release cleanliness. Live tests exercise changing external retailer paths and require internet access.
Architecture
src/supermarkt/
├── sources/ retailer adapters and Marktguru client
├── models.py data models and retailer definitions
├── common.py normalization and shared helpers
├── region.py regional ALDI mapping
├── compare.py mapping, deduplication, and price comparison
├── loyalty.py loyalty programs and quantified benefits
├── cache.py SQLite snapshot cache
├── service.py source orchestration and comparison service
├── presentation.py response fields
├── images.py downloads, image cache, and SSRF protection
├── security.py signatures and optional API key
├── access.py access and request helpers
├── api_routes.py REST routes
├── browser_routes.py browser routes
├── media_routes.py image and media routes
├── health_routes.py health check
├── runtime.py shared runtime objects
└── static/ interface HTML, CSS, and JavaScript
The internal Python package name supermarkt remains for technical reasons. Adapters in sources/ understand their sources, service.py orchestrates direct sources and fallbacks, and compare.py builds comparisons. The browser and REST API share the same runtime; the frontend does not calculate prices independently.
Limitations
Retailer websites and undocumented interfaces may change at any time. An individual adapter may fail temporarily without making Korbuino unusable as a whole. Where suitable regional fallback data exists, it can replace only the affected retailer; other reachable sources remain usable.
Korbuino does not invent missing prices or estimate unknown loyalty benefits. Completeness and freshness depend on reachable regional source data.
Shopping list and export
The Einkauf area stores the personal list exclusively in IndexedDB in the current browser profile. There are no accounts, server-side personal lists, trackers, or automatic device synchronisation. Offers and manual items can be added, edited, checked, and grouped by retailer. Goods and deposits are calculated separately; when prices are missing, Korbuino shows only the known total.
General transfer options include text copy, Web Share, TXT, and a versioned JSON backup with a local import preview. The browser UI has no app-specific Bring connection. The native Android app additionally provides the direct KitchenOwl integration described below.
Roadmap
Future versions may add REST or OpenAPI connections for local automations. The current release does not connect or synchronise with an external shopping-list application. The existing REST API can already support custom automations.
Support the project
Korbuino remains free, ad-free, and without user tracking. Every feature remains available regardless of whether a user donates. There is no paywall and no restriction for users who do not donate.
Anyone who voluntarily wants to support ongoing development, new retailer adapters, and maintenance of data sources can send Monero to this public project address:
83WjjKs4ijKChStc9GPrpZYa9DXYpHmbSeVipJrQSzMnRdmYtFE4K5D7ff7BsrTDa8TTZvJmAWivgWLEcJpULQ79KpRX8ik
Donations are entirely optional and do not influence the feature set, access to Korbuino, or prioritisation of individual users.
License and trademarks
The source code is available under the BSD 3-Clause License.
Copyright © 2026 lesecuritae for Tarnkappe.info.
Korbuino is independent and is not affiliated with any retailer or loyalty program mentioned. Brand, retailer, and product names belong to their respective owners.
KitchenOwl (native Android)
The native Android app connects directly to KitchenOwl with username/email and password or an existing token. Choose the HTTPS URL of the official service, AdminForge or your own instance; SSO-only accounts can use a KitchenOwl token. Setup and security.