grocer-nz · git:20260723.a282dc5 · 2026-07-23 · sha256 36509a8fe7a67013

grocer-nz git:20260723.a282dc5A

Immutable. This exact content never changes and is served at /api/v1/blob/36509a8fe7a67013.

---
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 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
```

### 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.

## Verification Checklist

Run these before relying on a fresh install:

```bash
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.