# Minimum Advertised Price Monitor - Evidence Rows (`steadyworks/minimum-advertised-price-monitor`) Actor

Flag advertised prices below MAP across retailers with per-row evidence URLs, checked timestamps and seller identity.

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

## Pricing

from $60.00 / 1,000 violations

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

## Minimum Advertised Price Monitor - Evidence Rows

Flag advertised prices below MAP across up to 200 retailer pages per run, and get one evidence row per below-minimum price with URL, checked timestamp, and seller identity. This Minimum Advertised Price Monitor checks the product URLs you list against the minimum advertised prices you set under your unilateral policy — one list in, MAP evidence rows out, no API key needed. Click **Try for free** — input opens prefilled. Start as-is, check rows, then raise the cap.

### What it does / What MAP data you get

You set the minimum advertised price per product under your unilateral policy. A dealer advertises below it and you hear about it from a customer — every day it stays live, the discount comes out of your margin. This Actor checks each URL you list and returns one row per below-minimum advertised price with evidence fields. Review-only: forward rows only after you verify.

- **Below-MAP evidence rows** — one row per advertised price below your minimum, with `evidenceUrl`, `checkedAt`, seller identity, shortfall in USD and percent, and a severity grade.
- **Quiet when compliant** — run 1 seeds the baseline and previews current rows; later runs emit only changes (`new` / `deeper-cut`), so a compliant portfolio stays silent instead of spamming rows.
- **Recovery rows on request** — set `alertMode` to `violations-and-recoveries` and SKUs back at or above MAP emit `recovery` rows.
- **Retailer and portfolio rollups** — one compliance row per retailer hostname plus a single-row portfolio summary, alongside the full `MAP_COMPLIANCE_REPORT` JSON.
- **Spend caps you control** — per-run `maxSpendUsd` plus the platform max-charge limit; a capped run lands counts plus the report instead of a surprise bill.
- **Verify-before-enforce on every row** — each violation and recovery row carries the unilateral-policy notice; nothing is edited, nothing is sent, nothing is guessed.

### Quick start

1. Click **Try for free** — the input opens prefilled with 2 demo products and `storeName: demo-store` (a seconds-long run under 1¢: 2 checks).
2. Keep the prefill for your first run, or swap in 1–2 of your own public HTTPS product page URLs with their minimum advertised prices.
3. Keep `maxSpendUsd: 2` for the first run — the run stops before billing past your cap.
4. Click **Save & Start**, then open the **Dataset** tab (`default` holds the violation rows) and the `MAP_COMPLIANCE_REPORT` record.
5. Once the row shape looks right, paste your full list (up to 200 URLs) — the spreadsheet prompt under Input converts sheet rows to JSON in about a minute.

Trial billing on step 1 is honest: `$0` on paid-plan trial (5 runs / 7 days); on free plans the prefilled run bills prepaid credit from run 1 (platform rule, still under 1¢ for the demo).

### Input

Minimal JSON (required fields only, byte-exact with the form prefill — copy, paste, start):

```json
{
  "targets": [
    { "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "mapPrice": 60, "sku": "attic" },
    { "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html", "mapPrice": 40, "sku": "sharp" }
  ],
  "storeName": "demo-store"
}
```

Full JSON (every default shown):

```json
{
  "targets": [
    { "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "mapPrice": 60, "sku": "attic" },
    { "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html", "mapPrice": 40, "sku": "sharp" }
  ],
  "authorizedSellers": [],
  "alertMode": "violations-only",
  "maxSpendUsd": 2,
  "capMode": "capped",
  "storeName": "demo-store"
}
```

| field | type | required | default | units | limits |
|---|---|---|---|---|---|
| `targets` | array of objects | yes | — | 1–200 items per run | each item: `url` required string, `^https://`, 1–2048 chars, public host only (`http://`, IP literals, localhost / private hosts, and userinfo rejected at input); `mapPrice` required number, `0.01`–`1000000`, plain number, no currency symbols; `sku` optional string, 1–256 chars, `^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$`, defaults to the URL — reuse identically every run (case-sensitive: `Boots` and `boots` are different products; without a stable `sku`, below-MAP prices cannot be confirmed and count as `unmatched`); `currency` optional string, default `USD`, `^[A-Z]{3}$` |
| `authorizedSellers` | array of strings | no | `[]` | at most 50 hostnames, each at most 253 chars | plain non-empty hostnames, case-insensitive exact match — list subdomains explicitly (e.g. `shop.example.com`); trailing dots ignored; matching hosts emit no violation or recovery rows (checks still bill) |
| `alertMode` | string enum | no | `violations-only` | — | `violations-only` | `violations-and-recoveries` — anything else rejected; recoveries emit only in the second mode |
| `maxSpendUsd` | number | no | `2` | USD per run | `0.05`–`50`; the run stops before billing past the cap, publishing counts plus the report instead of evidence rows |
| `capMode` | string enum | no | `capped` | — | `capped` | `complete-input` — capped bills up to `maxSpendUsd`, then lands counts-only; `complete-input` processes the whole list in one run with the worst case quoted before anything bills (never above $0.08 snapshots / $20.00 violations / $4.00 recoveries per run); schedules and task runs always run capped |
| `storeName` | string | yes | — (prefills `demo-store`) | portfolio key, max 63 chars | `^[a-z0-9][a-z0-9-]{1,62}$`; the same name plus the same targets reuses one baseline — a different name forks a separate baseline |

Mode rules: duplicate `url`s (after canonicalization) collapse to one check; a duplicated `sku` fails the run; a currency mismatch against the baseline is treated as no change and never flagged; split portfolios above 200 pages across scheduled runs.

Job-titled recipes (one line each): `single-dealer check` (minimal JSON above) · `reseller-list audit` (up to 200 targets with `sku` on each) · `recovery watch` (add `"alertMode": "violations-and-recoveries"`) · `authorized-seller exclusion` (add `"authorizedSellers": ["shop.example.com"]`) · `tight-budget probe` (add `"maxSpendUsd": 0.5`).

<details>
<summary>Spreadsheet → JSON in 60 seconds</summary>

Paste this prompt plus your rows into any AI chatbot (ChatGPT, Claude, Gemini), then paste its answer straight into this Actor's input:

> Turn these rows into Minimum Advertised Price Monitor input JSON. Each row is a product URL plus its
> minimum advertised price under my unilateral policy. Output ONLY this shape, no
> other text: {"targets": \[{"url": "...", "mapPrice": 0}], "storeName": "..."}.
> Rules: every url starts with https://; every mapPrice is a plain number, no
> currency symbols; storeName is lowercase letters, numbers and dashes only,
> max 63 chars, built from my brand: ACME; add "sku" per target: 1-3 distinctive words from the product name, lowercase letters, numbers and dashes only, no spaces, reuse identically every run (case-sensitive: Boots and boots count as different products) — without it, below-MAP prices cannot be confirmed and count as unmatched; Rows:
> `<paste your URL + price rows here>`

Worked example — these 3 sheet rows:

```text
https://shop.example.com/boots, 120
https://shop.example.com/jacket, 89.99
https://other.example.net/cap, 25
```

become:

```json
{
  "targets": [
    { "url": "https://shop.example.com/boots", "mapPrice": 120, "sku": "boots" },
    { "url": "https://shop.example.com/jacket", "mapPrice": 89.99, "sku": "jacket" },
    { "url": "https://other.example.net/cap", "mapPrice": 25, "sku": "cap" }
  ],
  "storeName": "acme-1"
}
```

URLs only, no prices yet? Paste the URLs alone — the chatbot fills mapPrice with 0
and you MUST replace every 0 with the real minimum advertised price before running
(0 is rejected at input).

</details>

### Output

Violations land in the default dataset, rollups in three named datasets, and the run summary in the key-value store — every buyer-facing row goes to a dataset; run state stays in KV:

| surface | key | content |
|---|---|---|
| dataset `default` | violations | one row per advertised price below MAP |
| dataset `recoveries` | recoveries | one row per SKU back at or above MAP (only when `alertMode` is `violations-and-recoveries`) |
| dataset `retailer_summary` | retailer summary | one row per retailer hostname: `retailer`, `complianceRate`, `violationCount`, `riskLevel` (`High` / `Medium` / `Low`) |
| dataset `category_summary` | category summary | single-row rollup: `violationCount`, `mostViolatedSku` (null when zero violations) |
| key-value store | `MAP_COMPLIANCE_REPORT` | run report: `storeName`, `checkedAt`, `notice`, `violations`, `retailerSummary`, `categorySummary`, `recoveryCount`, `spendUsd`, `chargeId`, `blockCounts`, `allFail`, `baselineReset`, `candidateCapHit`, `lockedCount`, `llmCalls` |

Violation row fields:

| field | type | notes |
|---|---|---|
| `sku` | string | your stable product identifier |
| `retailer` | string | hostname of the listing URL |
| `advertisedPrice` | number | current advertised price only, never was-now strike-through |
| `mapPrice` | number | your unilateral policy floor from input |
| `shortfallUsd` / `shortfallPct` | numbers | how far below MAP, in USD and percent |
| `severity` | enum `minor` / `moderate` / `severe` | minor under 5%, moderate 5–10%, severe over 10% |
| `seller` | string | seller identity where the page states one |
| `evidenceUrl` | string | the exact page checked |
| `checkedAt` | string | ISO timestamp of the check |
| `notice` | string | `verify-before-enforce: advertised price only under a unilateral minimum advertised price policy; independently verify before any action.` |

Illustrative row shape on the prefill host (shortened for viewing — field names are exact, values illustrate the shape, not a live finding):

```json
{
  "sku": "attic",
  "retailer": "books.toscrape.com",
  "advertisedPrice": 51.77,
  "mapPrice": 60,
  "shortfallUsd": 8.23,
  "shortfallPct": 13.7,
  "severity": "severe",
  "seller": "books.toscrape.com",
  "evidenceUrl": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
  "checkedAt": "2026-09-14T00:00:00.000Z",
  "notice": "verify-before-enforce: advertised price only under a unilateral minimum advertised price policy; independently verify before any action."
}
```

<details>
<summary>Second example: retailer rollup row (shortened for viewing)</summary>

```json
{ "retailer": "books.toscrape.com", "complianceRate": 50.0, "violationCount": 1, "riskLevel": "Medium" }
```

</details>

Partial-failure semantics (never a guessed price): unreachable or oversize pages skip for that URL with `$0` charge; login-walled pages yield `requiresAuth` (`$0`, never a guessed price); a run that trips `maxSpendUsd` publishes counts plus the report with no evidence rows — raise the cap and re-run to collect rows; `unmatched` counts below-MAP prices whose identity could not be confirmed — add `sku` values to convert them into evidence rows; if a run halts mid-charge, billed checks stand and a retry attempts only the remainder (billed charges stand, no void API).

Canonical identifiers: events `map-snapshot`, `map-violation`, `map-recovery`; datasets `default`, `recoveries`, `retailer_summary`, `category_summary`; key-value store record `MAP_COMPLIANCE_REPORT`; every violation and recovery row carries `evidenceUrl` + `checkedAt` + `notice`. (`map-start` is retired — no charge call; `requiresAuth` is a `$0` reason label on misses + report, never a billing event.)

### How much will it cost?

`from $0.32 / 1,000 URL checks` (checks = `map-snapshot` events, Gold tier and above). The billable noun is the event: `map-snapshot` fires once per URL **iff a price literal was actually read** (`S` = prices actually read); walled (`requiresAuth`), failed, and skipped pages bill `$0`. `map-violation` fires per emitted violation row, `map-recovery` per emitted recovery row (only in `violations-and-recoveries` mode). There is no per-run start charge — including all-fail runs. `authorizedSellers` exclusions emit no row and bill `$0` beyond their check. This meter is under review — the table below is the current live meter stated with its quiet costs, and it makes no frequency or revenue promises.

| plan | checks / 1k (`map-snapshot`) | violations / 1k (`map-violation`) | recoveries / 1k (`map-recovery`) |
|---|---|---|---|
| Free (= base) | $0.40 | $100.00 | $20.00 |
| Bronze (= base) | $0.40 | $100.00 | $20.00 |
| Silver (−10%) | $0.36 | $90.00 | $18.00 |
| Gold (−20%) | $0.32 | $80.00 | $16.00 |
| Platinum (−20%) | $0.32 | $80.00 | $16.00 |
| Diamond (−20%) | $0.32 | $80.00 | $16.00 |

Base single-run prices: check `$0.0004`, violation `$0.10`, recovery `$0.02`. Formula: `cost = S×0.0004 + V×0.10 + R×0.02` (`S` = prices actually read). The $5 trial credit covers about 12,500 quiet URL checks (≈ 125 daily 100-URL runs) or, for example, ~49 violations plus 100 checks (~$5.00). A quiet portfolio of 100 URLs checked daily runs about $1.20/mo on base (3,000 checks); paid tiers bring the same portfolio to about $1.08 (Silver) or $0.96 (Gold and above).

Worked math (base tier): demo (2 URLs, 0 violations) = 2×0.0004 = **$0.0008** — under a cent. Small dealer list (20 URLs, 1 violation) = 20×0.0004 + 1×0.10 = **≈$0.11** per run, ≈$3.24/mo on a daily schedule plus any further violations. Spike month (100 URLs/day, 20 violations) = 3,000×0.0004 + 20×0.10 = **≈$3.20/mo**. All-walled month (no price read on any page) = **$0.00**. Capped run (200 checks + 30 violations) = 200×0.0004 + 30×0.10 = **$3.08** — set `maxSpendUsd` to 3.09 or higher to keep the evidence rows.

Completeness mode (one click, no billing field): keep `capMode: capped` (default) for the cap behavior above, or set `capMode: complete-input` to process your entire input list in one run instead of splitting it. The run quotes its worst case from YOUR input before anything bills (`Y = N×$0.0004 snapshots + N×$0.10 violations + N×$0.02 recoveries` for N targets) and never bills above the frozen per-event ceilings ($0.08 snapshots / $20.00 violations / $4.00 recoveries for the 200-target schema max; the $24.08 sum is informational, never a gate). The buyer max-charge pin still lowers any run; schedules and task runs always run capped; lifted trips land counts-only SUCCEEDED (`lifted-cap-trip`). Store promise: Max $2/run default (`maxSpendUsd` 0.05–50) for capped runs — lands SUCCEEDED at the cap; you pay more only if you turn on Complete my input (worst case computed from your input, never above $0.08/$20.00/$4.00 per event).

Cap, floor, and quiet-cost disclosure: `maxSpendUsd` (`0.05`–`50`, default `2`) is your per-run cost ceiling; a buyer-set platform max total charge (`ACTOR_MAX_TOTAL_CHARGE_USD`) is respected the same way, and charging stops on `eventChargeLimitReached` — capped runs land counts plus the report, never an abort. Quiet costs stated plainly: steady-state re-checks bill checks while emitting zero new rows by design; `requiresAuth` pages bill `$0` with no price delivered. The listed event prices are the whole meter — no separate compute, proxy, or AI add-on is asserted; the Console Pricing tab price is authoritative.

```text
event:map-snapshot | $/1k per tier Free/Bronze/Silver/Gold/Platinum/Diamond 0.40/0.40/0.36/0.32/0.32/0.32 | includes_usage:false | billed_on_empty:false | caps:[targets-200,maxSpendUsd] | gate:price-read-only
event:map-violation | $/1k per tier Free/Bronze/Silver/Gold/Platinum/Diamond 100.00/100.00/90.00/80.00/80.00/80.00 | includes_usage:false | billed_on_empty:false | caps:[targets-200,maxSpendUsd]
event:map-recovery | $/1k per tier Free/Bronze/Silver/Gold/Platinum/Diamond 20.00/20.00/18.00/16.00/16.00/16.00 | includes_usage:false | billed_on_empty:false | caps:[targets-200,maxSpendUsd,alertMode]
note:requiresAuth | $0 reason-label-only (misses + report; never billed) | map-start retired (no charge call)
```

### Trial / Free tier

$5 of credit covers roughly 12,500 quiet URL checks — enough to prove the loop before spending anything real. The Actor enforces its own trial in code: **5 runs within 7 days, $0 on paid plans** — each trial run covers up to 100 pages (500 checks total); run 1 shows up to 4 current rows plus `lockedCount` (rows beyond the first 4 are counted and unlock on paid runs); trial data is kept 14 days after the trial ends. On free plans, runs bill per event from prepaid credit from run 1 (platform rule, not ours) — the prefilled 2-product demo still costs under a cent. Click **Try for free** — input opens prefilled. Start as-is, check rows, then raise the cap. No card is needed for the trial beyond an Apify account; paid plans unlock higher caps plus the tier discounts above.

### Compatibility

Works with any server-rendered structured-data shop page. Filed-test coverage (L0 — no payers yet; paid test meter, Oct 2026):

- **Shopify page shape** — filed test, Oct 2026 (Allbirds sample; not a live-buyer finding)
- **WooCommerce page shape** — filed test, Oct 2026 (Porter & York sample; not a live-buyer finding)
- **BigCommerce page shape** — filed test, Oct 2026 (Mountain Crest Gardens sample; not a live-buyer finding)
- **Demo fixture** — books demo shop catalogue pages (below-MAP row flagged, clean product correctly quiet; fixture-class demo shop, not a retailer checkout)

Not covered: marketplace and big-box bot walls, JavaScript-rendered prices, login-walled pages. These return `requiresAuth` or `fetch-failed` — never a guessed price.

*Store names belong to their owners; no affiliation, endorsement, or sponsorship implied.*

### What people use it for + What it does NOT do

- Weekly dealer MAP sweep: schedule the same dealer list and wake up only to change rows.
- Reseller-list audit before a review: bulk-check up to 200 URLs and forward verified rows.
- Authorized-seller exclusion review: keep approved dealers out of violation rows while still checking them.
- Recovery confirmation after a price fix: re-run in `violations-and-recoveries` mode and confirm the SKU is back at or above MAP.
- Retailer risk ranking: sort `retailer_summary` by `violationCount` and `riskLevel` to see which dealer needs attention first.
- Portfolio rollup for category review: one `category_summary` row per run for the shelf-level view.

What this Actor does NOT do: it does not edit listings or prices — read-only checks of public pages. It does not read login-walled, marketplace bot-wall, or JavaScript-rendered prices — those return `requiresAuth` or `fetch-failed`, never a guessed price. It does not send legal notices or take action — every row is verify-before-enforce, forward only after you verify. Evidence is the row fields (`evidenceUrl` + `checkedAt` + `notice`) — no screenshots.

### Use with schedules / webhooks / API / MCP

Schedule (daily quiet-portfolio cadence): Console → Schedules → Add Actor → `steadyworks/minimum-advertised-price-monitor`, cron daily, with input-JSON overrides carrying only `targets` plus a distinct `storeName` per schedule (baselines are per-portfolio; per-run `maxSpendUsd` times cadence is the spend bound — schedules themselves are free).

Webhook (alert only when rows exist): Actor Detail → Integrations → webhook on `ACTOR.RUN.SUCCEEDED`, POST JSON with a payload template surfacing `{{resource.defaultDatasetId}}`, then filter buyer-side on `violationCount > 0` (webhooks fire per run status, not per violation — severe-only filtering lives in the receiver or an iPaaS step such as Watch-runs → Get-Dataset-Items → filter `severity == severe` → Slack/Sheets; always async, never sync Run against the ~30s iPaaS timeout).

MCP (agent calling this Actor through the Apify MCP client):

```json
{ "mcpServers": { "apify": { "command": "npx", "args": ["-y", "@apify/mcp-server"], "env": { "APIFY_TOKEN": "<your-token>" } } } }
```

Then `call-actor` on `steadyworks/minimum-advertised-price-monitor` with the Input JSON above, and poll `get-actor-run` (or a webhook) for completion — `call-actor` waits at most ~45s, so never block on it; fetch rows with `get-dataset-items`.

API one-liners (start, poll, fetch — replace `$APIFY_TOKEN`):

```bash
curl -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" -d '{"targets":[{"url":"https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html","mapPrice":60,"sku":"attic"}],"storeName":"demo-store","maxSpendUsd":2}' "https://api.apify.com/v2/acts/xeupuwWOQlX587MoH/runs?waitForFinish=45"
```

```javascript
const run = await apifyClient.actor('xeupuwWOQlX587MoH').call({ targets: [{ url: 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', mapPrice: 60, sku: 'attic' }], storeName: 'demo-store', maxSpendUsd: 2 });
const { items } = await apifyClient.dataset(run.defaultDatasetId).listItems({ limit: 50 });
```

```python
run = client.actor('xeupuwWOQlX587MoH').call(run_input={'targets': [{'url': 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', 'mapPrice': 60, 'sku': 'attic'}], 'storeName': 'demo-store', 'maxSpendUsd': 2})
items = client.dataset(run['defaultDatasetId']).list_items(limit=50).items
```

### Troubleshooting

| symptom | cause | fix |
|---|---|---|
| 0 rows on a run you expected to flag | portfolio already baselined, or prices at or above MAP (runs 2+ emit changes only) | lower one `mapPrice` below its advertised price and re-run to confirm a row appears |
| `requiresAuth` on marketplace / big-box URLs | login or bot wall — no readable price | swap in public structured-data shop URLs; the Actor never guesses a price |
| counts on the report but no evidence rows | `maxSpendUsd` cap tripped | raise `maxSpendUsd` and re-run to collect the rows |
| `unmatched` counts above 0 | below-MAP prices seen but identity unconfirmed | add a stable `sku` per target and re-run to convert them into evidence rows |
| a price fix never emits a recovery | `alertMode` is `violations-only` | set `alertMode` to `violations-and-recoveries` and re-run |
| run stops on the charge limit | buyer-set max total charge below the run cost | raise the binding cap and re-run — counts plus the report are preserved |
| oversize or unreachable pages | page skipped for that URL, `$0` charge | remove or replace the URL; billed checks stand and a retry attempts only the remainder |

### FAQ + legality

**Does this change my listings, prices, or data?** No — read-only checks of public retailer pages. Nothing is edited, logged into, or sent.

**How much will my dealer list cost?** See the worked math above: the 2-product demo is under a cent, 20 URLs with 1 violation is about $0.11 a run on base, and a quiet 100-URL daily portfolio is about $1.20/mo plus any violations.

**What happens on login-walled or bot-wall pages?** They yield `requiresAuth` (`$0`, no price) or `fetch-failed` (`$0`) — never a guessed price. Use public structured-data shop pages for coverable targets.

**Is a flagged row proof of a violation?** No — it is an advertised-price signal under your unilateral policy. Every row carries the verify-before-enforce notice: independently verify before any action, and forward rows only after you verify.

**Does it process personal data (GDPR)?** It reads only the public listing pages you submit (URL, advertised price, seller hostname) and returns price-comparison rows. It requests no personal data, opens no accounts, and touches no login-walled pages. You control the target list — submit only pages you may review.

**Which pages may I check (Terms)?** Proceed only for public HTTPS retailer pages you may review, under your own unilateral minimum advertised price policy. Respect the target sites' terms; this Actor is review-only tooling, not an enforcement service.

**Who do I contact about a charge?** Charge disputes go to Apify support within 15 days of the charge; billed charges stand (no void API).

### Other Actors you might need

| need | actor |
|---|---|
| full-site crawl to build your URL list before MAP targeting | `apify/web-scraper` |

Enforcement-letter workflows, screenshot evidence pipelines, and marketplace/big-box coverage live outside this Actor by design — the boundaries under Compatibility and What it does NOT do apply.

### Changelog + support

[Changelog](./CHANGELOG.md) (0.1 — 2026-10-02 adoption baseline: MAP evidence rows across 4 datasets + `MAP_COMPLIANCE_REPORT`, `map-*` events, 5-run/7-day trial, per-run `maxSpendUsd` cap; 0.2 — 2026-10-03 re-meter per ADR-008: snapshot $0.0004 read-only gate, `requiresAuth` $0, `map-start` retired). Report breakage via the Store discussion / Issues tab — replies and fixes stay visible there; charge disputes go to Apify support within 15 days of the charge; no SLA. Store page: https://apify.com/steadyworks/minimum-advertised-price-monitor

# Changelog

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

# Actor input Schema

## `targets` (type: `array`):

Products to check: each advertised price is compared against its minimum advertised price under your unilateral policy. Structured-data shops (Shopify, WooCommerce, BigCommerce, custom) only, up to 200 per run.

## `authorizedSellers` (type: `array`):

Retailer hostnames excluded from advertised price violation and recovery rows (case-insensitive; exact-match — list subdomains explicitly, e.g. shop.example.com; trailing dots ignored). Snapshots still bill; verify-before-enforce applies.

## `alertMode` (type: `string`):

Choose violations-only (default) or include recoveries alongside advertised price violations. verify-before-enforce applies.

## `maxSpendUsd` (type: `number`):

Spend protection for each run: the run stops before billing past this cap, so a violation spike can never surprise you. Set it high to let large spikes through, low for tighter control. A stopped run still reports counts plus spendUsd; raise the cap and rerun to collect the full evidence rows. Per run only - schedules multiply it, so cap multi-run spend with console max-cost and schedule quotas. Honest edges: walled/failed pages bill $0; billed charges stand (no void API). A sustained price drop re-trips the cap every run until raised. Run 1 shows at most 4 preview rows (billed on paid, $0 on trial).

## `capMode` (type: `string`):

capped (default) bills up to your maxSpendUsd cap, then lands counts-only SUCCEEDED. complete-input processes your entire input in one run: the worst-case cost is quoted from your input before anything bills (N targets x $0.0004 snapshots + N x $0.10 violations + N x $0.02 recoveries, never above $0.08 snapshots / $20.00 violations / $4.00 recoveries per run for 200 targets). One click, no billing field. A buyer-set platform max total charge still lowers any run; scheduled and task runs always use capped; increased-limit runs land counts-only SUCCEEDED. Billed charges stand (no void API); per-run scope, schedules multiply it.

## `storeName` (type: `string`):

Stable portfolio key; same store name plus same targets reuses the advertised price baseline. Run 1 shows up to 4 current rows + lockedCount (billed on paid, $0 on trial); runs 2+ show changes.

## Actor input object example

```json
{
  "targets": [
    {
      "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
      "mapPrice": 60,
      "sku": "attic"
    },
    {
      "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
      "mapPrice": 40,
      "sku": "sharp"
    }
  ],
  "authorizedSellers": [],
  "alertMode": "violations-only",
  "maxSpendUsd": 2,
  "capMode": "capped",
  "storeName": "demo-store"
}
```

# Actor output Schema

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

Advertised prices below minimum advertised price with URL+timestamp+notice. Empty when compliant (counts-only when capped). Capped at 4 rows on trial runs and paid run 1, remainder in report.lockedCount.

## `recoveries` (type: `string`):

SKUs returned to at-or-above minimum advertised price since last baseline. Only populated when input alertMode=violations-and-recoveries.

## `retailerSummary` (type: `string`):

One row per retailer hostname: complianceRate, violationCount, riskLevel (High/Medium/Low).

## `categorySummary` (type: `string`):

Single-row rollup: violationCount and mostViolatedSku (null when zero violations).

## `report` (type: `string`):

MAP_COMPLIANCE_REPORT from default key-value store: violations + summaries + recoveryCount, spendUsd, chargeId, blockCounts, allFail, baselineReset. Verify-before-enforce notice inside.

# 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 = {
    "targets": [
        {
            "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
            "mapPrice": 60,
            "sku": "attic"
        },
        {
            "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
            "mapPrice": 40,
            "sku": "sharp"
        }
    ],
    "authorizedSellers": [],
    "alertMode": "violations-only",
    "maxSpendUsd": 2,
    "capMode": "capped",
    "storeName": "demo-store"
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyworks/minimum-advertised-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 = {
    "targets": [
        {
            "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
            "mapPrice": 60,
            "sku": "attic",
        },
        {
            "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
            "mapPrice": 40,
            "sku": "sharp",
        },
    ],
    "authorizedSellers": [],
    "alertMode": "violations-only",
    "maxSpendUsd": 2,
    "capMode": "capped",
    "storeName": "demo-store",
}

# Run the Actor and wait for it to finish
run = client.actor("steadyworks/minimum-advertised-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 '{
  "targets": [
    {
      "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
      "mapPrice": 60,
      "sku": "attic"
    },
    {
      "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
      "mapPrice": 40,
      "sku": "sharp"
    }
  ],
  "authorizedSellers": [],
  "alertMode": "violations-only",
  "maxSpendUsd": 2,
  "capMode": "capped",
  "storeName": "demo-store"
}' |
apify call steadyworks/minimum-advertised-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyworks/minimum-advertised-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/xeupuwWOQlX587MoH/builds/ZEnaz5o2AfTrrlpEi/openapi.json
