If you sell a product through other shops, you need to know which of them list it below your minimum advertised price (MAP), and you need to know it the day it happens, not at the end of the month.

This is part 3 of a small series. In part 1 I compared one product's price across stores, and in part 2 I added a change mode that returns only offers whose price or stock moved. This post adds the MAP check I built on top of it this week: give it your MAP and it flags every offer below it, with the gap in money and in percent. Real output from today (2026-11-14) is below, plus the limits.

Why the usual approach hurts

Most small brands check MAP like this:

  1. Someone opens 20 to 200 reseller pages in a browser, once a week if there is time.
  2. They copy prices into a spreadsheet, some in USD, some in EUR or GBP.
  3. They compare each one with the MAP column by hand.
  4. A reseller drops the price on Monday, and the brand finds out on Friday, after other resellers have already matched it.

A script helps, but then you own the scraper, the currency conversion, the per-product MAP table and the "what changed since yesterday" logic. That last part is the one that breaks.

What the tool does

I built the Cheapest Price Finder on Apify. You give it product page URLs from any stores. It reads the real price from each page's structured data (JSON-LD offers, Shopify product data, meta price tags), converts everything to one currency with daily ECB rates and sorts the offers from cheapest to most expensive.

The MAP check adds three inputs:

  • mapPrice: your MAP, in the convertTo currency.
  • mapPrices (optional): a different MAP per product URL, as {url: price}. It overrides mapPrice for that URL.
  • onlyChanged + monitorId (from part 2): return only new or changed offers, so a daily run is quiet unless something moved.

Every row then gets mapPrice, belowMap (true/false), mapGap (how far below MAP, in money; negative means above MAP) and mapGapPercent. RUN_SUMMARY.mapCheck lists belowMapCount and belowMapUrls, so one key tells you whether today needs attention.

Step 1: get a token

Create a free Apify account and copy your token from Console (Settings, API and Integrations):

export APIFY_TOKEN=your_token_here 

Step 2: one run with curl

For the demo I used the AeroPress Original at four coffee shops. I am not the brand and I do not know AeroPress's real MAP, so I picked a made-up MAP of $42, and for one store a separate MAP of $49.95 to show the per-URL override:

curl -X POST "https://api.apify.com/v2/acts/martin10291~cheapest-price-finder/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"productUrls": [ "https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.intelligentsia.com/products/aeropress", "https://www.espressoparts.com/products/aeropress-coffee-maker", "https://www.stumptowncoffee.com/products/aeropress"], "convertTo": "USD", "mapPrice": 42, "mapPrices": {"https://www.stumptowncoffee.com/products/aeropress": 49.95}}' 

Real output (2026-11-14, run took 4.6 s)

store price availability mapPrice belowMap mapGap mapGapPercent
vervecoffee.com 39.95 USD InStock 42 true 2.05 4.9
intelligentsia.com 40.00 USD OutOfStock 42 true 2.00 4.8
espressoparts.com 44.95 USD InStock 42 false -2.95 -7.0
stumptowncoffee.com 46.00 USD InStock 49.95 true 3.95 7.9

And the summary from the same run:

"mapCheck": { "belowMapCount": 3, "belowMapUrls": [ "https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.intelligentsia.com/products/aeropress", "https://www.stumptowncoffee.com/products/aeropress" ], "mapPrice": 42.0, "perUrlPrices": 1, "currency": "USD" } 

Note the Intelligentsia row: it is below MAP but out of stock. Whether that counts as a violation is your policy, so the tool reports availability and leaves the decision to you.

Step 3: the same in Python, as a daily watch

import os import requests ACTOR = "martin10291~cheapest-price-finder" URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items" RESELLERS = [ "https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.intelligentsia.com/products/aeropress", "https://www.espressoparts.com/products/aeropress-coffee-maker", "https://www.stumptowncoffee.com/products/aeropress", ] def map_violations(map_price, per_url=None): body = { "productUrls": RESELLERS, "convertTo": "USD", "mapPrice": map_price, "mapPrices": per_url or {}, "onlyChanged": True, # only new or changed offers "monitorId": "aeropress-map", # keeps yesterday's prices for you } r = requests.post(URL, params={"token": os.environ["APIFY_TOKEN"]}, json=body, timeout=300) r.raise_for_status() return [row for row in r.json() if row.get("belowMap")] if __name__ == "__main__": for row in map_violations(42): print(f'{row["store"]}: {row["price"]} {row["currency"]} ' f'({row["mapGapPercent"]}% below MAP, {row["availability"]}, ' f'change: {row["changeType"]})') 

With onlyChanged, the first run returns every offer once as new. I ran it twice today with MAP $39.95 and the same monitorId:

  • Run 1 (6.5 s): 4 rows, all changeType: "new", belowMapCount: 0.
  • Run 2 (5.2 s): 0 rows saved, summary "unchanged": 4.

So on a normal day your script prints nothing. On the day a reseller cuts the price, you get one row with changeType: "priceDrop", previousPrice, priceChange and the MAP gap together.

Step 4: schedule it

The simplest way, no server:

  1. Open the actor page, paste your reseller URLs, set mapPrice, tick "Only changed offers" and type a Monitor ID.
  2. Save it as a Task and add a schedule, e.g. every day at 07:00.
  3. Optional: connect the dataset to Slack, email or Google Sheets with Apify integrations, or add a webhook on "run succeeded".

Or run the Python file from your own cron:

0 7 * * * /usr/bin/python3 /home/me/map_watch.py >> /home/me/map.log 2>&1 

Without an Apify account

The MAP check (not the change mode) also works through plain HTTP APIs with one key:

  • API.market, free plan to test.
  • radarbg.com, my own site, 100 free calls a month with one key for all my APIs.
curl -X POST https://radarbg.com/v1/cheapest-price-finder \ -H 'X-API-Key: YOUR_KEY' -H 'Content-Type: application/json' \ -d '{"productUrls": ["https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.espressoparts.com/products/aeropress-coffee-maker"], "convertTo": "USD", "mapPrice": 42}' 

I tested this path today too: the same two rows came back with belowMap true and false. onlyChanged and monitorId are switched off on these APIs on purpose: the remembered prices would live in my account and be shared by every caller with the same URLs. Keep yesterday's rows yourself there, or use the Apify actor for the built-in change feed.

Honest limits

  • Big marketplaces often block automated requests. Amazon, Walmart and Best Buy frequently answer with a bot-check page. The tool does not bypass that; such pages land in RUN_SUMMARY.failed and are not charged. Brand stores and independent shops (Shopify, WooCommerce, Magento, BigCommerce) work best.
  • It reads the listed price only: no coupons, cart discounts or shipping. A reseller who hides the real price in the cart will not show up.
  • You choose the URLs. It does not find your resellers for you or match products; gtin, sku and brand in each row help you check.
  • Pages that build the price only with JavaScript and publish no product data return "no price found".
  • It does not contact anyone. It tells you who is below MAP; what you do next is up to you.

Price

On Apify it costs $3.00 per 1,000 saved results. With onlyChanged, unchanged offers are not saved and not billed, so 50 resellers checked daily cost nothing for results on a quiet day. Apify's free plan includes a small monthly credit, enough to try it. On API.market and radarbg.com there are free plans to test first.

Wrap-up

I built this because MAP checks are the same price scrape plus one comparison, and the comparison is what people end up doing by hand. Now one field does it.

If you manage a brand with resellers, try it on Apify with 3 to 5 of your reseller pages. How many reseller pages would your daily list have? If a page returns "no price found", post the URL in the comments and I will look at it.