Detecting MAP Violations Across Marketplaces

You have a batch of observed competitor offers, already matched to your catalog, and a table of Minimum Advertised Price (MAP) floors your brand publishes. The job is to flag which offers advertise below the floor, classify how badly, attribute each violation to a seller and marketplace, and emit an evidence record you can act on — without firing false positives on currency artifacts, bundles, or cart-only discounts. This recipe implements that join end to end. It sits under MAP violation & price-war detection, and the severity it emits is meant to be consumed by the guardrail logic in price floors and guardrails in repricing.

Prerequisites & Input Contract

Offers must arrive already resolved to a canonical product_id with a match confidence, and prices must already be converted to a single base currency — do that with currency conversion and exchange-rate sync upstream, never inside this stage. Python 3.11+ is assumed; the recipe uses only the standard library so it runs anywhere.

# One observed offer. `cart_price` is None when the crawler could not
# reach the cart. `qty` normalizes multipacks to a per-unit basis.
offer = {
    "product_id": "p-44182",
    "match_confidence": 0.97,
    "seller_id": "mkt:acme-electronics",
    "marketplace": "amazon",
    "advertised_price": 179.00,
    "cart_price": None,
    "qty": 1,                       # units in the listing (3 for a 3-pack)
    "currency": "USD",              # already base currency
    "observed_at": "2026-07-15T09:14:00Z",
    "listing_url": "https://example.com/dp/B0CXYZ",
}

The MAP table is keyed on product_id with an effective window and a per-row tolerance. Keep MSRP separate and never enforce against it — MAP governs the advertised price only.

map_table = {
    "p-44182": {
        "map_price": 199.99, "currency": "USD", "tolerance_pct": 0.02,
        "effective_from": "2026-07-01T00:00:00Z",
        "effective_to": None, "policy_version": "map-2026Q3-v2",
    }
}

Step-by-Step Implementation

Step 1 — Gate on match confidence and currency

An offer built on an uncertain match, or in the wrong currency, is not evidence. Reject both before any price comparison so no false accusation can be produced.

MIN_CONFIDENCE = 0.90

def eligible(offer, map_row):
    """Return (True, None) if enforceable, else (False, reason)."""
    if offer["match_confidence"] < MIN_CONFIDENCE:
        return False, "low_match_confidence"
    if offer["currency"] != map_row["currency"]:
        return False, "currency_mismatch"      # normalize upstream, not here
    return True, None

Step 2 — Select the MAP row whose effective window contains the observation

MAP floors change; comparing against a stale floor is a classic false-positive source. Pick the row active at observed_at.

from datetime import datetime

def _ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))

def active_map(offer, map_row):
    obs = _ts(offer["observed_at"])
    if obs < _ts(map_row["effective_from"]):
        return None
    if map_row["effective_to"] and obs > _ts(map_row["effective_to"]):
        return None
    return map_row

Step 3 — Normalize to a per-unit basis and classify severity

Reduce multipacks to a per-unit price so a legitimately-priced 3-pack is not flagged, then compare advertised (and cart) price against the tolerance-adjusted floor.

def classify(offer, map_row):
    floor = map_row["map_price"]
    grace = floor * (1.0 - map_row["tolerance_pct"])   # soft-band lower edge
    qty = max(1, offer.get("qty", 1))
    adv = offer["advertised_price"] / qty              # per-unit advertised
    if adv < grace:
        sev, price = "hard", adv
    elif adv < floor:
        sev, price = "soft", adv                       # inside tolerance
    else:
        sev, price = "compliant", adv
    # Cart-price evasion: advertised passes, cart is below floor.
    cart = offer.get("cart_price")
    if sev == "compliant" and cart is not None:
        if (cart / qty) < grace:
            sev, price = "cart_evasion", cart / qty
    shortfall = max(0.0, (floor - price) / floor)
    return sev, round(price, 2), round(shortfall, 4)

Step 4 — Emit an attributed evidence record

Tie the verdict to seller and marketplace and stamp it with both policy and detector versions so it is defensible later.

DETECTOR_VERSION = "detector-v4"

def detect_violation(offer, map_table):
    row = map_table.get(offer["product_id"])
    if row is None:
        return {"status": "no_map", "product_id": offer["product_id"]}
    ok, reason = eligible(offer, row)
    if not ok:
        return {"status": "skipped", "reason": reason,
                "product_id": offer["product_id"]}
    active = active_map(offer, row)
    if active is None:
        return {"status": "no_active_map", "product_id": offer["product_id"]}
    sev, price, shortfall = classify(offer, active)
    return {
        "status": "violation" if sev != "compliant" else "compliant",
        "product_id": offer["product_id"],
        "seller_id": offer["seller_id"],
        "marketplace": offer["marketplace"],
        "severity": sev,
        "observed_price": price,
        "map_price": active["map_price"],
        "shortfall_pct": shortfall,
        "policy_version": active["policy_version"],
        "config_version": DETECTOR_VERSION,
        "listing_url": offer["listing_url"],
        "observed_at": offer["observed_at"],
    }

print(detect_violation(offer, map_table))

Running against the sample offer (advertised 179.00 against a 199.99 floor with 2% tolerance) prints:

# {'status': 'violation', 'product_id': 'p-44182',
#  'seller_id': 'mkt:acme-electronics', 'marketplace': 'amazon',
#  'severity': 'hard', 'observed_price': 179.0, 'map_price': 199.99,
#  'shortfall_pct': 0.105, 'policy_version': 'map-2026Q3-v2',
#  'config_version': 'detector-v4',
#  'listing_url': 'https://example.com/dp/B0CXYZ',
#  'observed_at': '2026-07-15T09:14:00Z'}

The severity tiers and their downstream handling:

SeverityCondition (per unit)Attribution capturedTypical action
compliantadv >= floor and cart clearsNone
softgrace <= adv < floorseller, marketplaceWarn / watch
hardadv < graceseller, marketplaceEnforce, notify
cart_evasionadvertised OK, cart < graceseller, marketplaceEnforce, re-crawl cart

Verification & Testing

Cover one offer per severity plus the guard cases, so loosening a threshold or breaking the window join fails a test rather than shipping silently.

def test_detect():
    base_map = {"p-44182": map_table["p-44182"]}
    # hard violation
    assert detect_violation(offer, base_map)["severity"] == "hard"
    # compliant at floor
    ok = dict(offer, advertised_price=199.99)
    assert detect_violation(ok, base_map)["status"] == "compliant"
    # low confidence is skipped, never enforced
    weak = dict(offer, match_confidence=0.70)
    assert detect_violation(weak, base_map)["reason"] == "low_match_confidence"
    # 3-pack priced above per-unit floor is NOT a violation
    pack = dict(offer, advertised_price=630.0, qty=3)   # 210/unit
    assert detect_violation(pack, base_map)["status"] == "compliant"
    # cart evasion: advertised at floor, cart far below
    sneaky = dict(offer, advertised_price=199.99, cart_price=170.0)
    assert detect_violation(sneaky, base_map)["severity"] == "cart_evasion"
    print("all severity paths covered")

test_detect()   # -> all severity paths covered

Beyond unit coverage, sample real soft and hard verdicts weekly and have an analyst confirm the listing truly advertised below floor at capture time; that confirmation rate is your live precision and tells you whether tolerance or matching needs adjustment.

Edge Cases & Gotchas

  • Cart-price MAP evasion. The advertised price is pinned exactly at MAP and the discount appears only in the cart or via an auto-applied coupon. Step 3 catches it when cart_price is present; when the crawler cannot reach the cart, flag listings clustered exactly at the floor for targeted cart re-crawl rather than assuming compliance.
  • Bundles and multipacks. A “3-pack” fuzzy-matches the single unit and its total price looks far below floor. Normalize to per-unit with qty before comparison, and where a bundle is not cleanly divisible (mixed contents), exclude it from enforcement instead of guessing.
  • Currency and tax basis. A EUR price compared to a USD floor, or tax-inclusive against tax-exclusive, manufactures violations. Step 1 rejects a currency mismatch outright; tax basis must be reconciled upstream with tax and shipping normalization so both sides are like-for-like.
  • Stale or unbounded MAP rows. An old row with effective_to = None silently enforces a dead policy. Maintain effective windows and treat a missing active row as no_active_map, not as compliant.

Performance Notes

The whole path is O(1) per offer — a dictionary lookup, a couple of timestamp parses, and arithmetic — so a single worker clears well over 50k offers/second. The only real cost is the upstream match and normalization, which happen before this stage. For a full feed, vectorize the classification as boolean masks over columns in polars and implement Step 2 as an as-of join on effective_from; keep the object-level path shown here for streaming and for the low-volume evidence-emission tail. Do not batch the confidence gate away — it is a compliance control, not a hot-loop cost.

Frequently Asked Questions

Should I enforce MAP against a low-confidence product match? No. If match_confidence is below your enforcement floor, skip the offer with a reason of low match confidence and route it to review. A violation record built on an uncertain match is not evidence and exposes you to a wrongful-enforcement dispute.

Is a price below MAP always illegal for the seller? No. MAP restricts the advertised price only; a seller may lawfully sell below MAP as long as they did not advertise that lower price. That is why the recipe compares against the advertised price first and treats cart-only discounts as a separate evasion tier rather than a plain violation.

How much tolerance should the grace band allow? Set tolerance to your price-capture noise floor — typically one to three percent — not to a negotiated discount. Tolerance absorbs rounding and rendering artifacts; widening it to reduce alert volume just hides real violations and weakens any enforcement notice you later issue.