| name | grocer-nz |
| description | Query grocer.nz public NZ supermarket data — canonical product and barcode/GTIN lookup, cross-retailer product matching, store lookup, current per-store prices, and historical price rows from public DuckDB/parquet assets. Use for exact NZ grocery product matching or pricing across Woolworths, New World, PAK'nSAVE, Fresh Choice, and related stores. Read-only; no login or private user data. |
| license | MIT |
| compatibility | Requires Python 3.10+ and network access for live data |
| metadata | {"thecolab.category":"food-and-fuel","thecolab.source_owner":"Grocer.nz","thecolab.source_type":"commercial","thecolab.auth":"none","thecolab.access_mode":"html-readonly","thecolab.data_class":"public","thecolab.writes":"false","thecolab.browser":"false","thecolab.risk":"low","thecolab.cache_ttl":"24h","thecolab.schema_version":"1","thecolab.skill_type":"html-readonly","thecolab.pack":"nz-commercial-web","thecolab.source_url":"https://assets-prod.grocer.nz/public","thecolab.allowed_domains":"assets-prod.grocer.nz,grocer.nz,meilisearch.grocer.nz","thecolab.last_verified":"2026-07-24","thecolab.health":"healthy","thecolab.maintainer":"@adam91holt"} |
Grocer.nz Public Price Data
Overview
This skill queries public data exposed by grocer.nz, a New Zealand grocery price comparison app. It is useful as a canonical product/GTIN crosswalk as well as for current supermarket price checks, per-store comparisons, and historical product price rows.
The CLI uses:
- Grocer public static assets:
https://assets-prod.grocer.nz/public/
- Public DuckDB base database:
base_v3.duckdb.br
- Per-store current price parquet files:
prices_per_store_v3/public_prices_<store_id>.parquet
- Per-product history parquet files:
price_history_v3/price_history_<product_id>.parquet
- Grocer frontend Meilisearch product index for search.
Script path:
skills/grocer-nz/scripts/cli.py
The script bootstraps duckdb via uv when the active Python does not already have it.
When to Use
- Adam asks for historic supermarket pricing in NZ.
- Adam wants to compare prices across Woolworths, New World, PAK'nSAVE, Fresh Choice, Super Value, The Warehouse, etc.
- Adam wants a store id for a known store, e.g. Papakura.
- Adam wants price history for a known Grocer product id.
- Adam wants current prices for one product across selected stores.
- Adam wants to identify the same exact product in the Woolworths, New World, or PAK'nSAVE skills.
- Adam has a UPC, EAN, or GTIN and needs the corresponding Grocer product id.
Do not use this for authenticated Grocer Pro features, private user lists, or anything requiring sign-in. This skill is read-only public data only.
Commands
Set a helper variable:
GROCER=skills/grocer-nz/scripts/cli.py
List stores
python3 $GROCER stores --query Papakura
python3 $GROCER stores --vendor "PAK" --limit 20 --json
Useful known Papakura ids from live smoke:
118 — Woolworths Papakura
206 — Fresh Choice Papakura
230 — PAK'nSAVE Papakura
307 — New World Papakura
Search products
Search only:
python3 $GROCER search "milk" --limit 5
Search plus current prices in matching stores:
python3 $GROCER search "milk" --store-query Papakura --limit 3
python3 $GROCER search "whittakers" --store-id 230 --store-id 307 --json
The search command returns Grocer product ids, canonical GTIN-14 barcodes, and compact retailer_search_terms. Use the product ids with prices and history; use the retailer search terms with the retailer-native skills.
Resolve a barcode
Grocer stores barcodes in zero-padded GTIN-14 form. The command accepts compact UPC/EAN forms too:
python3 $GROCER barcode 94152210
python3 $GROCER barcode 00000094152210 --json
For Anchor Blue Milk 2L, this resolves to Grocer product 5452, canonical GTIN 00000094152210, and retailer search term 94152210.
Match the exact product across retailers
- Search Grocer by product description and select the right brand/name/size candidate.
- Copy a returned
retailer_search_terms value.
- Search each retailer skill with that value:
python3 skills/woolworths-nz/scripts/cli.py search 94152210 --json
python3 skills/paknsave-nz/scripts/cli.py search 94152210 --json
python3 skills/newworld-nz/scripts/cli.py search 94152210 --json
Woolworths search can include loosely related results, so keep only rows whose barcode, normalised to GTIN-14, equals Grocer's canonical barcode. Foodstuffs barcode search returns the matching New World/PAK'nSAVE product id; both banners use the same Foodstuffs product id for the same product. If barcode search returns no exact result, do not silently substitute a similar size or variant—fall back to brand/name/size matching and label it as a probable match.
Current prices for a product
python3 $GROCER prices 5461 --store-query Papakura
python3 $GROCER prices 5461 --store-id 230 --store-id 307 --json
--all-stores is supported but can be slow because it downloads many per-store parquet files:
python3 $GROCER prices 5461 --all-stores --limit 30
Price history for a product
python3 $GROCER history 5461 --store-query Papakura --limit 20
python3 $GROCER history 5461 --store-id 230 --json
History files are per product and include store-level rows when available.
Raw product metadata
python3 $GROCER product 5461 --json
Ad-hoc SQL query (guarded, read-only)
Run a single SELECT/WITH statement over the dataset for analysis the fixed
commands don't cover (cheapest-across-stores, vendor trends, joins, aggregates).
Always-available relations: products, barcodes, stores, vendors. Load extra data with
flags: --store-id/--store-query/--all-stores populate a prices table;
--product populates a history table (with a product_id column added).
python3 $GROCER query "select s.name, p.original_price_cent from prices p join stores s on s.id=p.store_id where p.product_id=5452 order by 2" --store-query Papakura --json
python3 $GROCER query "with p as (select id,name,brand from products where lower(brand)='anchor') select * from p" --limit 20
python3 $GROCER query "select b.barcode, p.* from barcodes b join products p on p.id=b.product_id where b.barcode='00000094152210'" --json
python3 $GROCER query "select store_id, price_cent, updated_at from history order by updated_at desc" --product 5461 --limit 30 --json
Safety: the statement must be a single read-only SELECT/WITH (no ;, no
DDL/DML). Filesystem and network access are disabled (enable_external_access= false, lock_configuration=true) after the requested parquet are loaded, and
the base catalogue is attached READ_ONLY. An arbitrary query can therefore only
read the public grocery data exposed here — it cannot write, read other files,
reach the network, or change settings. Results are capped (--limit, max 5000).
Data Notes
See references/api-notes.md for the sniffed endpoints and file layout.
Important model fields:
- Products:
id, name, brand, unit, size, redirected_to
- Barcodes: zero-padded GTIN-14
barcode, product_id
- Stores:
id, vendor_id, name, is_enabled
- Current prices:
updated_at, store_id, product_id, original_price_cent, sale_price_cent, club_price_cent, online_price_cent, multibuy fields
- Price history:
updated_at, store_id, price_cent
Prices are stored in NZ cents. $5.50 is represented as 550.
Common Pitfalls
- No store selected = limited price context. Product search comes from Meilisearch. Current per-store pricing requires store ids or
--store-query so the CLI can download the relevant parquet files.
--all-stores can be noisy/slow. It may download hundreds of current price parquet files. Prefer targeted store ids for fast checks.
- The
.br DuckDB file may arrive already decompressed. The CLI treats Grocer's base_v3.duckdb.br CDN response as a DuckDB file directly because Cloudflare/Caddy often transparently decodes it.
- DuckDB DDL cannot use prepared parameters for
read_parquet(?). Quote local parquet paths explicitly before create view ... as select * from read_parquet('<path>'); parameter binding works for normal select filters but not this statement shape.
- History parquet schema is leaner than the app's local table. Per-product history files may only include
updated_at, store_id, and price_cent; get vendor_id by joining public_stores, and infer product id from the requested history file.
- Historical availability varies. Some products/stores have no history parquet or no rows for a selected store.
- Effective price is conservative. The CLI reports
effective_price_cent as the minimum of original/sale/club/online when present. Multibuy needs human interpretation because quantity matters.
- Live smoke tests depend on upstream stock/history. Product
5461 and Papakura stores were live-valid when added, but upstream catalogue changes can make the smoke fail without a code bug.
- This is public read-only data. Do not try to bypass sign-in or scrape private Grocer user/list/pro features.
- Retailer barcode search has different shapes. Search Foodstuffs with the compact barcode (leading zero padding removed). Woolworths returns its own
barcode; normalise and verify that field because its text search may include unrelated rows.
Verification Checklist
Run these before relying on a fresh install:
python3 $GROCER stores --query Papakura --json
python3 $GROCER search "milk" --store-query Papakura --limit 2
python3 $GROCER prices 5461 --store-query Papakura --limit 10
python3 $GROCER history 5461 --store-query Papakura --limit 5
Expected smoke signal: Papakura stores include Woolworths/New World/PAK'nSAVE, and product 5461 returns Anchor 2L milk pricing/history rows for at least PAK'nSAVE/New World/Woolworths Papakura when current Grocer data includes them.