# UK Grocery Price Monitor - Sainsbury's & Asda Price Drop Feed (`datadeltas/uk-grocery-price-monitor`) Actor

Scheduled price-drop and promo change feed for UK supermarkets: what dropped price, went on Rollback or Nectar Price, or came back in stock at Sainsbury's and Asda since your last run. Emits only changes - not full catalogue dumps.

- **URL**: https://apify.com/datadeltas/uk-grocery-price-monitor.md
- **Developed by:** [Daniel Matthee](https://apify.com/datadeltas) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 change detecteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## UK Grocery Price Monitor — Sainsbury's & Asda Price-Drop Feed

A scheduled **change feed** for UK supermarket prices: every run tells you
what **dropped price, went on Rollback or promotion, rose in price, or came
back in stock** at Sainsbury's and Asda since your last run — and nothing
else. No full-catalogue dumps to diff yourself: you pay per change detected,
not per page scraped.

Point it at the search terms or category pages you care about, schedule it
(daily works well — grocery prices move on overnight repricing), and read
the dataset as an alerts-ready diff.

### Why a monitor and not a scraper

Plenty of actors will dump Sainsbury's or Asda product pages. None of them
tells you *what changed*. If you are:

- a **repricing / price-intelligence team** benchmarking against the UK
  mults ("alert me when Asda drops any SKU in fresh milk by 5%+"),
- an **FMCG brand or category manager** watching shelf price and promo
  compliance for your products ("did our promotion actually start today?"),
- a **deal-hunter app, cashback or affiliate site** that needs fresh
  price-drop content the moment it happens ("Rollback feed for the snacks
  aisle"),

then the diff *is* the product, and this actor emits exactly that.

**Tesco is deliberately not covered.** Tesco raw data is already saturated
on the Apify store (the leading Tesco scraper has 400+ users) — if you want
Tesco dumps, they exist. This monitor covers the two big chains where
change-feed coverage did not exist at all, and watches them properly.

### What one change record looks like

```json
{
  "store": "asda",
  "productId": "4836254",
  "name": "Vintage Dundee Orange Marmalade 340g",
  "brand": "Mackay's",
  "priceNow": 1.47,
  "priceWas": 1.90,
  "dropPercent": 22.63,
  "unitPrice": "£4.32/KG",
  "changeType": "price-drop",
  "promoText": "Rollback",
  "categoryPath": "Food Cupboard > Jams & Spreads > Marmalade",
  "productUrl": "https://www.asda.com/groceries/product/4836254",
  "imageUrl": "https://asdagroceries.scene7.com/is/image/asdagroceries/0637793000919",
  "detectedAt": "2026-08-31T04:00:00Z"
}
```

`changeType` is one of `price-drop`, `price-rise`, `promo-started`,
`promo-ended`, `back-in-stock`. Sainsbury's promo text includes Nectar
member prices (e.g. `"Nectar Price £2.00"`) alongside offer strap lines.

### First run seeds — it emits nothing (by design)

The first run over a watch set stores a **baseline** of every product it
sees and pushes **zero records** — there is nothing to compare against yet.
The log says so explicitly. From the second run on, you get only changes.
Keep the same watch set between runs; that is what "since last run" means.

### Input options

| Field | Default | What it does |
|---|---|---|
| `queries` | `["milk"]` | Search terms watched on each selected store. Every product they return is monitored. |
| `categoryUrls` | `[]` | Sainsbury's category URLs (`.../c:1019823`) and/or Asda aisle URLs (contain a numeric taxonomy id). Routed to the right store automatically. |
| `stores` | `["sainsburys", "asda"]` | Which chains to run. |
| `changeTypes` | all five | Emit only the change kinds you want. |
| `minDropPercent` | `0` | Suppress price-drops smaller than this %. Rises, promos and stock changes are never filtered by it. |
| `maxItems` | `1000` | Cap on products tracked per store per run and on emitted records. |
| `proxyConfiguration` | Apify residential GB | Sainsbury's blocks non-UK traffic; keep GB residential proxies on. |

### How it behaves when things go wrong

- One chain failing (blocked, changed, down) **degrades**: the other chain
  still runs, and the failed chain's baseline is untouched — no false
  "back in stock" storms when it recovers.
- Both chains failing **fails the run loudly** instead of pretending it was
  a quiet day.
- 429/blocked responses back off exponentially and rotate to a fresh proxy
  session without losing state.
- A schema change at a chain logs one clear warning naming the field and
  the run continues.

### Pricing

Pay-per-event: a flat fee per run start ($0.005), a small per-product
tracking fee ($0.0002 — this is what a quiet run costs), and $0.01 per
change record emitted. A daily watch of ~1,000 products with typical
grocery churn lands in the low dollars per month.

### Update promise

Endpoint schemas are re-checked when drift warnings appear in run logs;
the field set above is stable and versioned. Found a gap or want another
chain (Morrisons is next on the list)? Open an issue on the actor page.

### Changelog

- **0.1** — Initial release: Sainsbury's + Asda, five change types,
  per-chain state isolation, GB proxy support.

# Actor input Schema

## `queries` (type: `array`):

Keywords to watch on each selected store, e.g. "milk", "heinz beans". Every product these searches return (up to Max items) is monitored for changes.

## `categoryUrls` (type: `array`):

Sainsbury's category URLs (https://www.sainsburys.co.uk/gol-ui/groceries/.../c:1019823) and/or Asda aisle/category URLs containing a taxonomy id (e.g. .../1215339434886). Each URL is routed to its store automatically.

## `stores` (type: `array`):

Which supermarket chains to monitor.

## `changeTypes` (type: `array`):

Which kinds of change to push to the dataset. Default: all of them.

## `minDropPercent` (type: `integer`):

Suppress price-drop records smaller than this percentage (0 = report every drop). Only price drops are filtered - rises, promos and stock changes are unaffected.

## `maxItems` (type: `integer`):

Hard cap on products tracked per store per run (and on emitted change records). Keeps runs cheap and fast.

## `proxyConfiguration` (type: `object`):

Sainsbury's (Akamai) geo/bot-blocks non-UK traffic - Apify residential GB proxies are required for it. Asda's search API is public and works without a proxy.

## Actor input object example

```json
{
  "queries": [
    "milk"
  ],
  "stores": [
    "sainsburys",
    "asda"
  ],
  "changeTypes": [
    "price-drop",
    "price-rise",
    "promo-started",
    "promo-ended",
    "back-in-stock"
  ],
  "minDropPercent": 0,
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}
```

# Actor output Schema

## `records` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "milk"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datadeltas/uk-grocery-price-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["milk"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("datadeltas/uk-grocery-price-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "milk"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call datadeltas/uk-grocery-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datadeltas/uk-grocery-price-monitor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8pgzRLKSd4gAAAzWh/builds/qAGvd8n7QDa2EJnHl/openapi.json
