# Price Monitor & MAP Monitor for Online Stores (`tiger19935/price-map-monitor`) Actor

Price monitor and MAP monitor for lists of product URLs. Reads JSON-LD, schema.org and Open Graph data with no LLM, diffs against your previous run, flags MAP violations and posts changes to your webhook. Pay per event: $1 per 1,000 URLs, $0.50 per 1,000 changes.

- **URL**: https://apify.com/tiger19935/price-map-monitor.md
- **Developed by:** [Andrew Boiko](https://apify.com/tiger19935) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 url checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Price Monitor & MAP Monitor for Online Stores

Price Monitor & MAP Monitor checks a list of product page URLs and tells you what changed since the last run. It reads the structured data that stores already publish in their HTML (JSON-LD schema.org Product/Offer, microdata, Open Graph product tags), so it is fast, cheap and never "hallucinates" a price. It keeps the previous result in a named storage, flags prices below your minimum advertised price (MAP), and can POST the changes to your webhook.

There is no LLM, no browser and no subscription. You pay per URL checked and per change found.

### What does Price Monitor & MAP Monitor do?

- **Price tracking for any list of product URLs.** For every URL it returns price, currency, availability (in stock, out of stock, preorder, backorder, limited), product title, brand, SKU and GTIN when the page publishes them.
- **Diff against the previous run.** Every row says whether the product is `new`, `unchanged` or `changed`, with the previous price, the absolute change and the percentage change. Availability and currency changes are reported too.
- **MAP violation monitor.** Give it a MAP list (URL + minimum advertised price). Rows priced below MAP are flagged, with the amount below MAP, the date the violation was first seen and a `newViolation` flag. They are also written to a separate violations dataset and to a `VIOLATIONS` record.
- **Webhook alerts.** Set a webhook URL and the Actor sends one JSON POST at the end of the run when something changed or a MAP violation was found.
- **Polite and scalable.** Per-domain concurrency and delay, retries with backoff, Apify Proxy (datacenter by default) with an optional residential fallback for domains that block datacenter IPs.
- **Honest about what it cannot read.** If a page has no structured price data in its static HTML (for example a JavaScript-only shop), the row is returned with status `needs-javascript` and a clear reason. These rows are free.

How extraction works, in priority order: JSON-LD (including `@graph`, `@id` references, `AggregateOffer`, `priceSpecification` and `ProductGroup` variants), microdata, Open Graph / `product:price:*` meta tags, other price meta tags, and finally a few common CSS price selectors (marked `confidence: low`). The `source` and `confidence` fields tell you which one produced each price.

### Why use Price Monitor & MAP Monitor?

1. **No AI cost per page.** Extraction is deterministic code, so a check costs one HTTP request and about $0.001 instead of an LLM call per page. The same page gives the same answer every time, which is what a diff needs.
2. **Change tracking and MAP built in.** General purpose e-commerce scrapers give you today's price only. Here the state of the last run is kept for you (per `stateKey`), so a scheduled run produces change rows, violation rows and a webhook without any extra tooling.
3. **Pay per result instead of per seat.** Hosted price trackers are priced per month; Prisync, for example, lists plans from about $99 (October 2026, check current prices). Here 1,000 URLs checked costs $1.00 and each detected change adds $0.0005. A list you check once a week costs cents.

### How much does it cost to scrape price monitor data?

Pay per event. Prices per single event in USD:

| Event | What is billed | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|---|
| `url-check` | One URL fetched and a price found | $0.001 | $0.0009 | $0.0008 | $0.0007 |
| `change-event` | One URL whose price, availability or currency changed since the previous run | $0.0005 | $0.00045 | $0.0004 | $0.00035 |

Rows with status `needs-javascript`, `blocked`, `not-found` or `error` are never billed. The first run with a new State key is a baseline and has no change events.

Examples at FREE prices:

- 1,000 URLs, first run (baseline): 1,000 x $0.001 = **$1.00**
- 1,000 URLs, next run with 50 changes: $1.00 + 50 x $0.0005 = **$1.025**
- Daily check of 200 products for a month with about 5 changes a day: 6,000 x $0.001 + 150 x $0.0005 = **$6.08**

Apify Free plan users are limited to 25 URLs per run; the run then ends normally with the message "Free tier limit reached (25 results). Upgrade to a paid Apify plan for unlimited results." Set `maxTotalChargeUsd` on the run (or `maxItems` in the input) to cap spending; the Actor stops gracefully when the limit is reached. Platform usage (compute and proxy) is included in the event prices.

### Input

| Field | Type | Description |
|---|---|---|
| `urls` | array of strings | Product page URLs to check. |
| `mapList` | array of objects | `{"url", "minimumAdvertisedPrice", "label"?, "currency"?}`. These URLs are checked too; prices below MAP are flagged. |
| `stateKey` | string | Name of the watch list. Previous prices are stored in a named key-value store `pmm-state-<stateKey>` (lowercase letters, digits, hyphens). Use one key per list. Default `default`. |
| `resetState` | boolean | Forget the saved prices and start a new baseline. |
| `includeUnchanged` | boolean | If off, only new, changed, violating and failed rows are written. Checked URLs are still billed. Default on. |
| `priceChangeThresholdPercent` | number | Ignore price moves smaller than this percentage. Default 0. |
| `webhookUrl` | string (secret) | URL that receives a JSON POST when something changed. |
| `webhookOn` | string | `changes-and-violations` (default), `changes` or `violations`. |
| `maxItems` | integer | Maximum number of URLs to process in one run. |
| `maxConcurrency` | integer | Total parallel requests. Default 20. |
| `maxConcurrencyPerDomain` | integer | Parallel requests per website. Default 2. |
| `minDelayPerDomainMs` | integer | Minimum delay between request starts to one domain. Default 500. |
| `requestTimeoutSecs`, `maxRetries` | integer | Per-request timeout (30) and extra attempts on 429/5xx/network errors (2). |
| `proxyConfiguration` | object | Apify Proxy by default. Custom proxy URLs are supported. |
| `useResidentialFallback` | boolean | Retry a blocking domain through residential proxies. Off by default because residential traffic is expensive. |

Example 1, quick competitor price tracker:

```json
{
  "urls": [
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
    "https://scrapeme.live/shop/Bulbasaur/"
  ],
  "stateKey": "competitors-weekly"
}
```

Example 2, MAP violation monitor for a reseller list with a webhook:

```json
{
  "mapList": [
    { "url": "https://reseller-a.example/products/widget-pro", "minimumAdvertisedPrice": 49.99, "label": "WIDGET-PRO", "currency": "USD" },
    { "url": "https://reseller-b.example/p/widget-pro", "minimumAdvertisedPrice": 49.99, "label": "WIDGET-PRO", "currency": "USD" }
  ],
  "stateKey": "widget-map",
  "webhookUrl": "https://hooks.example.com/price-alerts",
  "webhookOn": "violations"
}
```

Example 3, large list, only changes, slow and polite:

```json
{
  "urls": ["https://shop.example/p/1", "https://shop.example/p/2"],
  "stateKey": "shop-example",
  "includeUnchanged": false,
  "priceChangeThresholdPercent": 1,
  "maxConcurrencyPerDomain": 1,
  "minDelayPerDomainMs": 2000
}
```

### Output example

Illustrative output (values are examples, not from a live run). One row per URL in the default dataset:

```json
{
  "url": "https://reseller-a.example/products/widget-pro",
  "domain": "reseller-a.example",
  "label": "WIDGET-PRO",
  "status": "ok",
  "title": "Widget Pro 3000",
  "brand": "Acme",
  "sku": "WP-3000",
  "price": 44.99,
  "currency": "USD",
  "availability": "in_stock",
  "listPrice": 59.99,
  "source": "json-ld",
  "confidence": "high",
  "change": "changed",
  "changeTypes": ["price-drop"],
  "previousPrice": 49.99,
  "priceChange": -5,
  "priceChangePct": -10,
  "minimumAdvertisedPrice": 49.99,
  "mapViolation": true,
  "mapDifference": 5,
  "mapDifferencePct": 10,
  "violationSince": "2026-10-19T08:00:12.000Z",
  "newViolation": true,
  "checkedAt": "2026-10-19T08:00:12.000Z"
}
```

Status values: `ok`, `needs-javascript` (no price in the static HTML), `blocked` (HTTP 403/429 or a captcha page), `not-found` (404/410) and `error`. Rows that are not `ok` carry a `reason` that says what to change.

Other outputs: the key-value store record `OUTPUT` (counts, billed events, per-domain stats, warnings), `VIOLATIONS` (the MAP violations of this run) and a named dataset `pmm-violations-<stateKey>` that collects violation rows run after run.

### How to use Price Monitor & MAP Monitor (tutorial)

1. **Collect the URLs.** Paste your competitor or reseller product URLs into `urls`. For MAP checks add them to `mapList` with the minimum advertised price.
2. **Pick a State key.** Use a short name such as `widget-map`. Runs with the same key are compared with each other.
3. **Run once to create the baseline.** Every row comes back as `new`. Check the `status` column: rows with `needs-javascript` mean that site publishes no price in static HTML.
4. **Save a Task and schedule it** (daily or weekly). From the second run on, rows show `changed` with the previous price, and MAP violations appear in the `map` dataset view.
5. **Connect alerts.** Add a `webhookUrl`, or attach an Apify integration (Slack, email, Google Sheets, Make) to the Task and filter the dataset by `change = changed` or `mapViolation = true`.

### Run via API

```bash
curl -X POST "https://api.apify.com/v2/acts/tiger19935~price-map-monitor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"],"stateKey":"api-demo"}'
```

Node.js:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('tiger19935/price-map-monitor').call({
  mapList: [{ url: 'https://reseller-a.example/products/widget-pro', minimumAdvertisedPrice: 49.99 }],
  stateKey: 'widget-map',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((r) => r.mapViolation));
```

MCP: the input is a plain array of URLs, so the Actor works as a tool in Apify MCP server for assistants (for example "check these 20 product URLs and tell me which ones dropped in price").

### Integrations

- **Webhook:** set `webhookUrl` to receive a JSON body with `changes`, `violations` and a summary (up to 200 rows each, with a `truncated` flag).
- **n8n, Make, Zapier:** use the Apify module, run the Actor from a schedule and branch on `change` or `mapViolation`.
- **Google Sheets:** attach the Apify Google Sheets integration to the Task to append rows each run.
- **Slack and email:** use Apify Task integrations or route the webhook through a Slack-compatible endpoint.
- **MCP:** call the Actor from any MCP client through the Apify MCP server.

### Legal / ethical use

The Actor reads publicly visible product pages and does not log in, use cookies or bypass paywalls. You are responsible for using it in line with the target websites' terms of service and applicable law. Check prices at a reasonable rate (the defaults are conservative), and do not use the data to collude on prices: monitoring a reseller for MAP compliance should follow your own agreements and local competition law. Product pages normally contain no personal data; if you store brand or seller information, handle it under GDPR or your local rules. This is not legal advice.

### FAQ and support

**Which stores does it support?** Any store whose product page publishes schema.org / JSON-LD, microdata or Open Graph price data in the HTML it sends. That covers a large share of Shopify, WooCommerce, Magento and custom stores. It does not run a browser, so shops that load prices only with JavaScript return `needs-javascript` rows (free) instead of a guess.

**How do MAP violations work?** Add URLs with `minimumAdvertisedPrice` in `mapList`. A row is a violation when the observed price is below MAP. If you also set `currency` and the page reports another currency, the row is not flagged (`mapCheck: currency-mismatch`). Violations appear in the main dataset, in the `VIOLATIONS` record and in the named dataset `pmm-violations-<stateKey>`.

**Can I schedule it?** Yes. Save the Actor as a Task, schedule it, and use the same `stateKey` every time. Connect the dataset or the webhook to Slack, Sheets or Make.

**Why was my URL billed even though the price did not change?** A `url-check` is billed for every URL where a price was found. `change-event` is billed only when something changed.

**Where is the previous state stored?** In a named key-value store `pmm-state-<stateKey>` in your Apify account. Delete it or set `resetState` to start over.

**What if a variant price is shown?** For product groups the Actor reports the lowest variant price (`priceMin`, `priceMax`, `variantCount`). Use a URL with a variant parameter (for example `?variant=123`) and, if the page publishes it, that variant's own price is used.

**A site returns `blocked`. What now?** Lower `maxConcurrencyPerDomain`, raise `minDelayPerDomainMs`, or enable `useResidentialFallback`.

Found a bug or a store that should work but does not? Open a ticket in the Issues tab with the URL and the run ID. We reply within 12 hours.

### More tools by the same author

- [Prozorro & TED Tenders Scraper](https://apify.com/tiger19935/prozorro-ted-tenders-scraper) - public tenders from Ukraine and the EU
- [Billing & Payment Stack Detector](https://apify.com/tiger19935/billing-stack-detector) - which payment gateway a website uses
- [Website Technology Detector](https://apify.com/tiger19935/website-technology-detector) - tech stack of any site
- [Decision-Maker Contacts](https://apify.com/tiger19935/decision-maker-contacts) - people and roles at a company

### Related searches

price monitor, competitor price tracker, map violation monitor, price tracking api, ecommerce price scraper, price change alert, json-ld product scraper, minimum advertised price, price comparison data, bulk product price checker, price monitoring tool, product price api.

# Changelog

This Actor's version history is a separate document: https://apify.com/tiger19935/price-map-monitor/changelog.md

# Actor input Schema

## `urls` (type: `array`):

Product page URLs to check, one per line. Each URL is checked once per run. The first run stores a baseline; later runs with the same State key report changes.

## `mapList` (type: `array`):

Optional. Array of objects: {"url": "https://...", "minimumAdvertisedPrice": 49.99, "label": "optional SKU or name", "currency": "USD (optional)"}. These URLs are checked too. Rows priced below the MAP are flagged as violations. If you set a currency and the page uses another one, the row is not flagged.

## `stateKey` (type: `string`):

Name of the monitoring list. The previous prices are kept in a named key-value store derived from this key (lowercase letters, digits and hyphens), so scheduled runs with the same key are compared with each other. Use a different key for every separate watch list.

## `resetState` (type: `boolean`):

Forget all previous prices for this State key and start a new baseline. Every row is then reported as new.

## `includeUnchanged` (type: `boolean`):

If off, only changed, new, violating and failed rows are written to the dataset. Every successfully checked URL is still billed as a url-check.

## `priceChangeThresholdPercent` (type: `number`):

Ignore price moves smaller than this percentage (0 = report every change). Availability and currency changes are always reported.

## `webhookUrl` (type: `string`):

HTTPS URL that receives one JSON POST at the end of the run when at least one price/availability change or MAP violation was found. Works with Slack-compatible endpoints, Make, Zapier, n8n and your own API. Treated as a secret.

## `webhookOn` (type: `string`):

Which findings trigger the webhook POST.

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

Stop after this many URLs have been processed. Free Apify plans are additionally capped at 25 URLs per run.

## `maxConcurrency` (type: `integer`):

Total number of simultaneous requests across all domains.

## `maxConcurrencyPerDomain` (type: `integer`):

Simultaneous requests to the same website. Keep low to be polite and avoid blocks.

## `minDelayPerDomainMs` (type: `integer`):

Minimum pause between two request starts to the same domain (a little random jitter is added).

## `requestTimeoutSecs` (type: `integer`):

Timeout for one page download attempt.

## `maxRetries` (type: `integer`):

Extra attempts on network errors, HTTP 429 and 5xx, with exponential backoff.

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

Apify Proxy (datacenter) is used by default. Custom proxy URLs are supported.

## `useResidentialFallback` (type: `boolean`):

If a domain blocks datacenter IPs (HTTP 403/429 or a captcha page), retry that domain through Apify RESIDENTIAL proxies. Residential traffic is much more expensive than datacenter traffic, so it is off by default.

## Actor input object example

```json
{
  "urls": [
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
    "https://scrapeme.live/shop/Bulbasaur/"
  ],
  "mapList": [
    {
      "url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
      "minimumAdvertisedPrice": 60,
      "label": "demo-sku-1"
    }
  ],
  "stateKey": "default",
  "resetState": false,
  "includeUnchanged": true,
  "priceChangeThresholdPercent": 0,
  "webhookOn": "changes-and-violations",
  "maxItems": 100,
  "maxConcurrency": 20,
  "maxConcurrencyPerDomain": 2,
  "minDelayPerDomainMs": 500,
  "requestTimeoutSecs": 30,
  "maxRetries": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "useResidentialFallback": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `overview` (type: `string`):

No description

## `mapView` (type: `string`):

No description

## `violations` (type: `string`):

No description

## `summary` (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 = {
    "urls": [
        "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
        "https://scrapeme.live/shop/Bulbasaur/"
    ],
    "mapList": [
        {
            "url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
            "minimumAdvertisedPrice": 60,
            "label": "demo-sku-1"
        }
    ],
    "stateKey": "default",
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tiger19935/price-map-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 = {
    "urls": [
        "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
        "https://scrapeme.live/shop/Bulbasaur/",
    ],
    "mapList": [{
            "url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
            "minimumAdvertisedPrice": 60,
            "label": "demo-sku-1",
        }],
    "stateKey": "default",
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("tiger19935/price-map-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 '{
  "urls": [
    "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
    "https://scrapeme.live/shop/Bulbasaur/"
  ],
  "mapList": [
    {
      "url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
      "minimumAdvertisedPrice": 60,
      "label": "demo-sku-1"
    }
  ],
  "stateKey": "default",
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call tiger19935/price-map-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tiger19935/price-map-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/PWBDS9JLhUmM4iE4H/builds/hZAUCieWHkaWbaP4W/openapi.json
