grocer-nz · diff

git:20260723.a282dc5 to git:20260726.346f2a3

32 added, 0 removed. Audit A to A.

---
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:
```bash
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 the public DuckDB/parquet download URLs so another agent can download or analyse the raw files itself.
- 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:
```bash
GROCER=skills/grocer-nz/scripts/cli.py
```
+ ### Get raw asset download URLs
+
+ Use `assets` when the caller wants the source files rather than parsed rows. This
+ command only constructs links from Grocer's fixed public asset layout. It makes
+ no network request, starts no DuckDB process, and does not download or parse any
+ file.
+
+ ```bash
+ # Base catalogue DuckDB URL (also the default with no selectors)
+ python3 $GROCER assets --base --json
+
+ # Current-price parquet URLs for selected stores
+ python3 $GROCER assets --store-id 230 --store-id 307 --json
+
+ # Per-product history parquet URLs
+ python3 $GROCER assets --product 5452 --product 5461 --json
+
+ # Combine all three asset types
+ python3 $GROCER assets --base --store-id 230 --product 5452 --json
+ ```
+
+ The result contains `delivery: external_url` and a `resources` array. Each
+ resource has a declared `format`, filename, and public `url`. Through The Colab
+ MCP, supported resources are also emitted as native MCP `resource_link` content,
+ so the client downloads directly from Grocer rather than through the MCP server.
+ The URLs are deliberately not probed; a nonexistent store or product id can
+ therefore produce a link that returns 403/404 when downloaded. At most 50 links
+ may be requested in one call.
+
### List stores
```bash
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:
```bash
python3 $GROCER search "milk" --limit 5
```
Search plus current prices in matching stores:
```bash
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:
```bash
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
1. Search Grocer by product description and select the right brand/name/size candidate.
2. Copy a returned `retailer_search_terms` value.
3. Search each retailer skill with that value:
```bash
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
```bash
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:
```bash
python3 $GROCER prices 5461 --all-stores --limit 30
```
### Price history for a product
```bash
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
```bash
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).
```bash
# cheapest blue-top milk (product 5452) across Papakura stores
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
# count products by vendor that stock a given product's price rows
python3 $GROCER query "with p as (select id,name,brand from products where lower(brand)='anchor') select * from p" --limit 20
# resolve compact retailer barcodes against Grocer's canonical catalogue
python3 $GROCER query "select b.barcode, p.* from barcodes b join products p on p.id=b.product_id where b.barcode='00000094152210'" --json
# price history rows for a product
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
1. **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.
2. **`--all-stores` can be noisy/slow.** It may download hundreds of current price parquet files. Prefer targeted store ids for fast checks.
3. **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.
4. **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.
5. **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.
6. **Historical availability varies.** Some products/stores have no history parquet or no rows for a selected store.
7. **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.
8. **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.
9. **This is public read-only data.** Do not try to bypass sign-in or scrape private Grocer user/list/pro features.
10. **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.
+ 11. **`assets` constructs but does not validate links.** Use it for low-overhead handoff of raw public files. Use the parsed commands when the agent needs verified rows immediately.
## Verification Checklist
Run these before relying on a fresh install:
```bash
python3 $GROCER stores --query Papakura --json
+ python3 $GROCER assets --base --store-id 230 --product 5452 --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.