๐Ÿท๏ธ MAP Violation Detector - Reseller Price Enforcement avatar

๐Ÿท๏ธ MAP Violation Detector - Reseller Price Enforcement

Pricing

Pay per event

Go to Apify Store
๐Ÿท๏ธ MAP Violation Detector - Reseller Price Enforcement

๐Ÿท๏ธ MAP Violation Detector - Reseller Price Enforcement

๐Ÿท๏ธ Match retailer listings to your brand catalogue and flag Minimum Advertised Price (MAP) violations, with enforcement-ready evidence. โœ… Brand/model/size/pack hard disqualifiers block wrong-SKU matches. โš ๏ธ Low-confidence matches are reported unmatched, never accused.

Pricing

Pay per event

Rating

0.0

(0)

Developer

mohamed alaya

mohamed alaya

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Categories

Share

MAP Violation Detector

Brands set a Minimum Advertised Price (MAP); resellers sometimes advertise below it anyway. This actor matches observed retailer listings against your brand catalogue, compares the observed price to MAP with a configurable tolerance, and emits an evidence row per violation โ€” retailer, listing URL, observed price, MAP, delta, match confidence, matched-on fields, timestamp โ€” the kind of thing that goes straight into an enforcement letter. Brands pay real money for exactly this.

Why the matching step is the whole ballgame

Getting the match wrong means accusing a reseller of violating MAP on the wrong product. That is not a cosmetic bug, it is a legal and reputational risk. So this actor reuses the product-matcher actor's matching approach (adapted, not imported โ€” this actor has no cross-actor dependency): brand, model/part number, and GTIN identity, plus size and pack-count canonicalisation ("500ml" vs "0.5L" vs "16.9 fl oz" all compare correctly). Crucially, a different size or pack count is a HARD DISQUALIFIER โ€” it blocks a match even when the titles are near-identical, before any weighted scoring runs. A 500ml bottle and a 1L bottle of the exact same brand are NOT the same SKU, and this actor will never say they are.

Any listing that cannot be matched with at least matchThreshold confidence is reported as unmatched, never scored as a violation. Match confidence is included on every row so a human can audit the call.

What it does

  1. Input โ€” a brand catalogue (catalogue: product identity + MAP price per SKU) and observed listings (listings: retailer, title, price, url), inline and/or via Apify dataset IDs. Listing prices can also be scraped directly from listingUrls using the shared monitor-core engine's selector-free price detection (JSON-LD โ†’ microdata/OG โ†’ common price containers โ†’ text-scan fallback).
  2. Match each listing to its best-scoring catalogue candidate using brand/model/GTIN identity and canonicalised size/pack-count, with blocking so large catalogues stay fast.
  3. Compare price vs MAP with a configurable absolute and/or percent tolerance (toleranceAbsolute, tolerancePercent, toleranceMode), currency-aware (requireSameCurrency โ€” a currency mismatch is reported unmatched rather than silently mis-compared, since there is no live FX conversion here).
  4. Classify every listing as compliant, violation, below-threshold-but-within-tolerance, or unmatched.
  5. Severity on every violation: deltaAbsolute, deltaPercent, and a minor/moderate/ severe bucket (10% / 25% breakpoints).
  6. Per-retailer summary (RETAILER_SUMMARY in the key-value store, and inline in SUMMARY): violation count, worst offender (URL + delta), average discount below MAP.
  7. Optional repeat-offender tracking (trackRepeatOffenders) persists cumulative per-retailer violation counts in a named key-value store across runs.

Input

{
"catalogue": [
{ "sku": "ACME-500", "title": "Acme Hydro Flask 500ml", "brand": "Acme", "size": "500ml", "map": 24.99, "currency": "USD" }
],
"listings": [
{ "retailer": "discount-outlet.example", "title": "Acme Hydro Flask 500ml Bottle", "price": 17.99, "url": "https://discount-outlet.example/p/123", "currency": "USD" }
],
"matchThreshold": 80,
"tolerancePercent": 2
}

Output

One row per checked listing (unless filtered by includeCompliant/includeUnmatched): its classification, severity (violations only), observedPrice, mapPrice, deltaAbsolute, deltaPercent, matchConfidence, matchedOn (which fields agreed), and full per-field evidence. A SUMMARY key-value record with run stats and the per-retailer breakdown.

Honest limitations โ€” read before you act on this

  • Match confidence matters legally. This is text/structured-attribute matching, not image or barcode-verified identity. A confident-looking title match is still a probabilistic call. Anything below matchThreshold is reported unmatched, on purpose โ€” never guess your way into an accusation.
  • No live currency conversion. Cross-currency listings are reported unmatched (currency-mismatch) rather than compared using a stale or assumed rate.
  • Prices must share one unit convention. toleranceAbsolute is a whole number in the SAME currency-unit convention (dollars, or cents โ€” pick one) as your catalogue/listing prices; the catalogue/listing price fields themselves are plain numbers, not schema-restricted.
  • GTIN/EAN/UPC/ASIN are normalised but never checksum-validated.
  • Bare "oz" is treated as fluid ounce, matching common retail convention โ€” pass an explicit size field to avoid ambiguity for weight-in-ounces products.
  • Scraped listing prices depend on page structure. detectPrice's JSON-LD/microdata/selector fallback chain is reliable on most modern e-commerce pages but can miss on unusual layouts โ€” those rows report an explicit scrape error rather than a fabricated price.
  • Capped by the same blocking safety valve as product-matcher: oversized blocks are skipped rather than compared exhaustively.