# Exact-Variant Competitor Price & Stock Monitor (`plusonets/exact-variant-price-stock-monitor`) Actor

Check an approved competitor URL against one catalog variant. Pack size is normalized, price and stock changes are reconfirmed, and a URL that no longer matches the variant is flagged instead of compared.

- **URL**: https://apify.com/plusonets/exact-variant-price-stock-monitor.md
- **Developed by:** [Olalekan Johnson](https://apify.com/plusonets) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 offer observeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Exact-Variant Competitor Price & Stock Monitor

A paid check for brands, specialist retailers, and ecommerce agencies that already know which competitor URL belongs to which sellable unit.

You supply the catalog item (GTIN, MPN, or SKU, plus pack size) and the competitor product URL you approve. The Actor reads the selected variant and tells you whether that offer is the same unit, what it costs per unit, whether it is publicly in stock, and whether a price or stock change survived a second fetch. A URL that no longer points at the same variant is flagged as mapping drift. The previous baseline stays in place.

Unofficial, and not affiliated with Shopify, Allbirds, Death Wish Coffee, or Brooklinen. Those names appear because the built-in demo reads their public product pages. Product text and prices remain the stores' data. This Actor stores a short evidence line and a hash, not a copy of the page.

### Who it is for

Use it when a similar title is not good enough. Replacement filters, coffee bags, shoes, and bedding regularly share a name across sizes and pack counts. Comparing a 5-bag price with a 1-bag price as if they were the same offer is a bad purchasing decision. This Actor will not do that.

It is a monitor for a list you already trust. It does not discover competitor sites, place orders, or change your prices.

### What you provide

| Input | Required | What it means |
| --- | --- | --- |
| Catalog items | Yes, unless you use the built-in demo | `catalogItemId`, `title`, `brand`, `gtin`, `mpn`, `sku`, `packQuantity` |
| Catalog CSV or catalog file URL | No | The same columns, as pasted CSV or an HTTPS JSON/CSV file |
| Approved competitor URLs | Yes, unless you use the demo | `url`, `catalogItemId`, optional `expectedVariantId` and `expectedGtin` |
| Your currency | No | ISO code used only to flag a mismatch. It never fills in a missing page currency |
| Market label | No | Stored on the row. It does not switch the storefront country |
| Output | No | `all` (default) or `changes_only` |
| Reconfirm changes | No | On by default |
| State store name | No | Named key-value store for baselines. Default `exact-variant-monitor-state` |
| Maximum offers | No | Default 50, maximum 500 |

A variant id in the URL (`?variant=`) selects that variant. If the id is gone, the Actor falls back to GTIN, MPN, SKU, and pack size.

Leave the input empty and the run uses five public product URLs: two Allbirds shoes, one Death Wish Coffee product checked as both a 1-bag and a 5-bag, and one Brooklinen sheet set. The coffee product publishes the same GTIN on three pack sizes, which is the case this Actor is built to separate.

You can also paste a catalog:

```csv
catalogItemId,title,brand,gtin,mpn,sku,packQuantity
filter-6,Acme filter 6 pack,Acme,012345678905,AC-6,AC-6,6
```

### What you get

Each dataset row is one flat observation. The important fields:

- `matchStatus` and `matchMethod` say whether the row is a GTIN, MPN, or SKU match, a title candidate, an ambiguous page, or mapping drift.
- `acceptedForComparison` is true only when the identifier matches and the pack context can be compared.
- `price` is the absolute sticker price of the selected variant. `perUnitPrice` is that price divided by the observed pack. It is null when the pack was not stated.
- `absolutePricesComparable` is true only when your pack quantity and the page pack quantity are known and equal. A 1-pack and a 5-pack never get an absolute `priceDelta`.
- `availability` is the public stock state. `visibleInventoryQuantity` is included only when the storefront publishes a number. `inventoryCountExact` is always false.
- `shippingQualifier` and `shippingPrice` stay empty unless the page states them. `taxIncluded` stays null unless the page says tax is included or excluded. A Shopify `taxable: true` flag is not treated as tax-included.
- `changeStatus` is `baseline_established` on the first good read, `unchanged` when the baseline still holds, `change_confirmed` when a second fetch agrees, and `change_unconfirmed` when it does not. The saved baseline is not replaced on an unconfirmed change or a failed fetch.
- `alert` is true only for a reconfirmed price, stock, or pack change, or for a newly confirmed mapping drift.
- `evidenceSnippet`, `evidenceHash`, `sourceUrl`, and `observedAt` are the evidence for the row.
- `charged` is true when that delivered row was billed as one `offer-observed` event.

The key-value record `OUTPUT` is the run summary: coverage counts, alerts, baselines kept, and `chargedUnits`.

#### Example row

Illustrative numbers, not a live quote:

```json
{
  "observationId": "obs_0123456789abcdef01234567",
  "catalogItemId": "coconut-caramel-5-bag",
  "sourceUrl": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
  "observedAt": "2026-10-23T12:00:00Z",
  "extractor": "shopify_product_json",
  "coverageStatus": "complete",
  "confidence": 0.97,
  "confidenceLabel": "high",
  "matchStatus": "matched",
  "matchMethod": "gtin",
  "acceptedForComparison": true,
  "mappingDrift": false,
  "changeStatus": "baseline_established",
  "alert": false,
  "charged": true,
  "productTitle": "Coconut Caramel Coffee",
  "variantTitle": "5 bags",
  "seller": "Death Wish Coffee Company",
  "gtin": "810063344313",
  "sku": "LGCCML45K",
  "packQuantity": 5,
  "catalogPackQuantity": 5,
  "packMismatch": false,
  "price": 69.95,
  "currency": "USD",
  "perUnitPrice": 13.99,
  "absolutePricesComparable": true,
  "perUnitComparable": true,
  "availability": "in_stock",
  "visibleInventoryQuantity": null,
  "inventoryCountExact": false,
  "shippingQualifier": null,
  "shippingPrice": null,
  "shippingKnown": false,
  "taxIncluded": null,
  "taxKnown": false,
  "priceDelta": null,
  "compareNote": "GTIN 810063344313 matches and SKU LGCCML45K matches. Pack quantity 5 selects this variant among others with the same identifier. Pack quantities match, so the absolute price refers to the same sellable unit. No shipping price or qualifier was stated. Shipping stays unknown. Tax treatment was not stated and stays unknown."
}
```

A title-only row is `matchStatus: candidate_for_review`, `acceptedForComparison: false`, and it is not charged. An ambiguous pack, a blocked store, or an empty page is delivered so you can see the gap, and it is not charged.

### How a match is decided

1. GTIN / UPC / EAN, compared after normalizing GTIN-8, GTIN-12, GTIN-13, and GTIN-14.
2. MPN, then SKU. Codes are compared without spaces or hyphens.
3. Pack quantity, when one identifier matches several variants. "5 bags" and "6-pack" are read from the variant title. A bare size such as "8" or "Queen" is one sellable unit, not an 8-pack.
4. The variant id in the URL, when you supplied one.

If the SKU and the pack size point at different variants, no price is selected. Title similarity is only a candidate for a person to review. It never becomes an accepted comparison, even when the titles are close and the brand matches.

If your GTIN is on the page but the pack size is not stated, the absolute price is reported as evidence and then withheld from comparison. No per-unit price is invented.

### State, reconfirm, and drift

Baselines live in the named key-value store, one record per catalog item and URL. The first good, comparable observation is `baseline_established`. It is not an alert.

On the next run, a change in price, per-unit price, currency, pack, or availability is fetched a second time before any alert. The two reads must agree. If they do, the baseline updates and `alert` is true. If they disagree, or the second fetch fails, the old baseline stays and `changeStatus` is `change_unconfirmed`.

Mapping drift is separate from a price change. It is raised when the GTIN, variant id, or (when there is no GTIN) SKU no longer matches the saved baseline, or when the page contradicts the GTIN you approved. The new price is shown so you can see it. It is not written over the approved baseline, and no price delta is calculated. The same drift on later runs is reported without a new alert.

A blocked, empty, or crashed URL does not write the baseline. An errored run cannot replace a good record with an empty one, because each mapping is its own key and failed mappings are left untouched.

`outputMode: changes_only` writes baselines, confirmed changes, unconfirmed changes, and new drift. Unchanged checks are left out of the dataset and are not charged. Use `all` when you want every check on the record and on the bill. Blocked and ambiguous rows are still written in both modes.

Scheduled runs should keep the same `stateStoreName`. Different clients or markets should use different store names.

### Scope and limits

Reads public pages with a normal HTTP client. No browser and no login.

Supported sources, in order:

- Shopify product JSON at `/products/{handle}.js` (price, variant, SKU, barcode, and availability).
- JSON-LD `Product` / `Offer` on the HTML page.
- Microdata.
- Open Graph product price tags, which are partial: they rarely identify a variant.

A low-to-high `AggregateOffer` is not treated as a selected price.

Other limits:

- You must supply the URL. There is no site-wide crawl.
- Pack size comes from the variant and product titles. A pack that is only in a photograph will be unknown.
- The currency is the currency this fetch actually received. Shopify can show a different presentment currency based on where the run is located. The row records that currency. It does not convert it, and your `expectedCurrency` is only a mismatch flag.
- Shipping text is a phrase seen on the page, such as "Free shipping" or "Shipping calculated at checkout". It is not a checkout quote. If the phrase is missing, shipping stays unknown.
- Public availability is not a warehouse count, even when a quantity is visible.
- Condition stays unknown unless the page says new, used, or refurbished.
- Requests are sequential, with about one second between calls to the same host, a 25 second timeout, and retries on 429 and 5xx. Private IPs, localhost, and `file://` URLs are rejected.
- At most 500 mappings per run.

### Pricing

This Actor uses pay-per-event pricing. You pay **$0.01** per delivered, identifier-backed offer (`offer-observed`), plus **$0.005** once per run (`apify-actor-start`).

You are charged once per delivered matched or mapping-drift row. Blocked, empty, ambiguous, unverified, and title-only rows are not charged. The same observation is not charged again on a retry inside one run, and the second fetch used to reconfirm a change is part of that one observation, not a second event.

This version does not render pages in a browser. There is no separate per-result charge.

| Run | What happens | Offer events | Offer total | With the $0.005 start event |
| --- | --- | ---: | ---: | ---: |
| Demo | 5 public URLs, all matched | 5 | $0.05 | $0.055 |
| Weekly watch | 40 mappings, 4 weeks, output `all` | 160 | $1.60 | about $1.62 |
| Daily agency list | 200 mappings every day for 30 days | 6,000 | $60.00 | about $60.15 |
| One capped run | `maxOffers` 500, every row matched | 500 | $5.00 | $5.005 |

Set the run's maximum charge in Apify. When that limit is reached the Actor stops, writes the rows already billed, and still saves `OUTPUT`. A 200-URL daily list is the workload behind the $60 example. It is not a forecast of how many URLs a buyer will submit.

`changes_only` is cheaper because unchanged rows are not delivered and not billed. The audit trail is the `all` mode.

### Integrations

Rows are flat, so a spreadsheet, a webhook, and an automation can all read them without unpacking nested objects.

- **Webhook.** Schedule the Actor. On the run-succeeded webhook, read `OUTPUT` for `chargedUnits` and `alerts`, then read the dataset. Post to Slack or email when `alert` is true or `coverageStatus` is `blocked`.
- **n8n.** Use the Apify node to run the Actor, then an HTTP Request node on the dataset items URL. A Filter node keeps `alert` equals true. Map `catalogItemId`, `perUnitPrice`, `availability`, and `compareNote` into the rest of the workflow.
- **Make.** The Apify module runs the Actor and watches the dataset. Route on `changeStatus`.
- **Google Sheets.** Export the dataset as CSV, or append dataset items with the columns above. `compareNote` is the cell a buyer can read without opening the source page.

### Run it locally

```bash
pip install -r requirements.txt
python -m exact_variant
pytest
```

An empty input uses the built-in demo. Results land in `storage/datasets/default` and the summary in `storage/key_value_stores/default/OUTPUT.json`. Baselines land in `storage/key_value_stores/exact-variant-monitor-state`.

Deploy with `apify push` from this directory when you are ready. The Actor asks for 512 MB and will not start above 1024 MB. No proxy and no API key are required for the demo.

### FAQ

**Why was a price change not alerted?** The second fetch did not match the first, or the pack size made the absolute prices incomparable. `change_unconfirmed` means the old baseline was kept on purpose.

**Why is `perUnitPrice` empty?** The page did not state a pack quantity. The absolute price is still in `price`, and it is not compared.

**The same GTIN is on three variants. Which price did you use?** The SKU, MPN, or pack quantity has to select one. If they disagree, the row is `ambiguous`, the price is empty, and the row is not charged.

**Is the stock count exact?** No. `availability` is the public state. A visible quantity is the figure the storefront chose to print.

**Why is tax or shipping null?** The page did not say. Unknown stays unknown.

**The product at that URL changed.** `mappingDrift` is true and the approved baseline is still the previous variant. Approve the new mapping before those prices are compared.

**Does it work on any store?** It reads Shopify product JSON and pages that publish JSON-LD, microdata, or Open Graph. A store that only paints the price with JavaScript after load will come back empty or partial. That is a blocked or empty result, not a guessed price.

**Can I send a private URL?** No. Localhost, private networks, link-local addresses, and non-HTTP URLs are rejected.

### Support

Questions and reproducible reports: plusonecares@gmail.com. Include the input mapping (without secrets), the `observationId`, and whether `coverageStatus` was complete, partial, empty, blocked, or unsupported.

# Actor input Schema

## `catalog` (type: `array`):

Your sellable units. GTIN and MPN are the match keys. packQuantity is the unit you sell, so a 5-pack is not compared with a single as if the sticker prices meant the same thing.

## `catalogCsv` (type: `string`):

Optional CSV with headers catalogItemId,title,brand,gtin,mpn,sku,packQuantity. Combined with Catalog items.

## `catalogUrl` (type: `string`):

Optional HTTPS URL of a JSON array or CSV catalog. Private hosts, localhost, and file URLs are rejected.

## `mappings` (type: `array`):

Product URLs you have already approved. Each one points at a catalog item. Put the variant id in the URL query (?variant=) when the page has more than one variant.

## `mappingsCsv` (type: `string`):

Optional CSV with headers url,catalogItemId,expectedVariantId,expectedGtin.

## `expectedCurrency` (type: `string`):

ISO currency you sell in. Used only to flag a mismatch. If the page does not state a currency, the result stays unknown rather than using this value.

## `country` (type: `string`):

Two-letter market label stored with each row. It does not change the storefront country or currency.

## `outputMode` (type: `string`):

All observations writes every check. Changes only writes baselines, reconfirmed changes, unconfirmed changes, and new mapping drift. Unchanged rows are omitted and are not charged.

## `reconfirmChanges` (type: `boolean`):

Fetch the URL a second time before alerting on a price, stock, pack, or identity change. If the two fetches disagree, the previous baseline is kept.

## `stateStoreName` (type: `string`):

Named key-value store that keeps confirmed baselines between runs. A failed URL does not replace a good baseline.

## `maxOffers` (type: `integer`):

Stop after this many mappings. The run ends cleanly and still writes the summary.

## Actor input object example

```json
{
  "catalog": [
    {
      "catalogItemId": "tree-runner-jet-black-8",
      "title": "Men's Tree Runner Jet Black size 8",
      "brand": "Allbirds",
      "gtin": "843416184854",
      "mpn": "TR3MJBW080",
      "sku": "TR3MJBW080",
      "packQuantity": 1
    }
  ],
  "mappings": [
    {
      "url": "https://www.allbirds.com/products/mens-tree-runners?variant=33179624669264",
      "catalogItemId": "tree-runner-jet-black-8"
    }
  ],
  "expectedCurrency": "USD",
  "country": "US",
  "outputMode": "all",
  "reconfirmChanges": true,
  "stateStoreName": "exact-variant-monitor-state",
  "maxOffers": 50
}
```

# Actor output Schema

## `offers` (type: `string`):

Flat offer rows: selected variant, per-unit price, stock, match, and change status.

## `summary` (type: `string`):

Coverage counts, alerts, baselines kept, and offer-observed units charged for this run.

# 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 = {
    "catalog": [
        {
            "catalogItemId": "tree-runner-jet-black-8",
            "title": "Men's Tree Runner Jet Black size 8",
            "brand": "Allbirds",
            "gtin": "843416184854",
            "mpn": "TR3MJBW080",
            "sku": "TR3MJBW080",
            "packQuantity": 1
        },
        {
            "catalogItemId": "strider-explore-natural-black-10",
            "title": "Men's Strider Explore Natural Black size 10",
            "brand": "Allbirds",
            "gtin": "196942208595",
            "mpn": "A11768M100",
            "sku": "A11768M100",
            "packQuantity": 1
        },
        {
            "catalogItemId": "coconut-caramel-1-bag",
            "title": "Coconut Caramel Coffee 1 bag",
            "brand": "Death Wish Coffee",
            "gtin": "810063344313",
            "mpn": "LGCCML09",
            "sku": "LGCCML09",
            "packQuantity": 1
        },
        {
            "catalogItemId": "coconut-caramel-5-bag",
            "title": "Coconut Caramel Coffee 5 bags",
            "brand": "Death Wish Coffee",
            "gtin": "810063344313",
            "mpn": "LGCCML45K",
            "sku": "LGCCML45K",
            "packQuantity": 5
        },
        {
            "catalogItemId": "classic-percale-queen-white",
            "title": "Classic Percale Core Sheet Set Solid White Queen",
            "brand": "Brooklinen",
            "gtin": "840302715352",
            "mpn": "COR-Q1",
            "sku": "COR-Q1",
            "packQuantity": 1
        }
    ],
    "mappings": [
        {
            "url": "https://www.allbirds.com/products/mens-tree-runners?variant=33179624669264",
            "catalogItemId": "tree-runner-jet-black-8"
        },
        {
            "url": "https://www.allbirds.com/products/mens-strider-explore?variant=41334294020176",
            "catalogItemId": "strider-explore-natural-black-10"
        },
        {
            "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
            "catalogItemId": "coconut-caramel-1-bag"
        },
        {
            "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
            "catalogItemId": "coconut-caramel-5-bag"
        },
        {
            "url": "https://www.brooklinen.com/products/classic-core-sheet-set?variant=41405019586650",
            "catalogItemId": "classic-percale-queen-white"
        }
    ],
    "expectedCurrency": "USD",
    "country": "US",
    "outputMode": "all",
    "stateStoreName": "exact-variant-monitor-state",
    "maxOffers": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("plusonets/exact-variant-price-stock-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 = {
    "catalog": [
        {
            "catalogItemId": "tree-runner-jet-black-8",
            "title": "Men's Tree Runner Jet Black size 8",
            "brand": "Allbirds",
            "gtin": "843416184854",
            "mpn": "TR3MJBW080",
            "sku": "TR3MJBW080",
            "packQuantity": 1,
        },
        {
            "catalogItemId": "strider-explore-natural-black-10",
            "title": "Men's Strider Explore Natural Black size 10",
            "brand": "Allbirds",
            "gtin": "196942208595",
            "mpn": "A11768M100",
            "sku": "A11768M100",
            "packQuantity": 1,
        },
        {
            "catalogItemId": "coconut-caramel-1-bag",
            "title": "Coconut Caramel Coffee 1 bag",
            "brand": "Death Wish Coffee",
            "gtin": "810063344313",
            "mpn": "LGCCML09",
            "sku": "LGCCML09",
            "packQuantity": 1,
        },
        {
            "catalogItemId": "coconut-caramel-5-bag",
            "title": "Coconut Caramel Coffee 5 bags",
            "brand": "Death Wish Coffee",
            "gtin": "810063344313",
            "mpn": "LGCCML45K",
            "sku": "LGCCML45K",
            "packQuantity": 5,
        },
        {
            "catalogItemId": "classic-percale-queen-white",
            "title": "Classic Percale Core Sheet Set Solid White Queen",
            "brand": "Brooklinen",
            "gtin": "840302715352",
            "mpn": "COR-Q1",
            "sku": "COR-Q1",
            "packQuantity": 1,
        },
    ],
    "mappings": [
        {
            "url": "https://www.allbirds.com/products/mens-tree-runners?variant=33179624669264",
            "catalogItemId": "tree-runner-jet-black-8",
        },
        {
            "url": "https://www.allbirds.com/products/mens-strider-explore?variant=41334294020176",
            "catalogItemId": "strider-explore-natural-black-10",
        },
        {
            "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
            "catalogItemId": "coconut-caramel-1-bag",
        },
        {
            "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
            "catalogItemId": "coconut-caramel-5-bag",
        },
        {
            "url": "https://www.brooklinen.com/products/classic-core-sheet-set?variant=41405019586650",
            "catalogItemId": "classic-percale-queen-white",
        },
    ],
    "expectedCurrency": "USD",
    "country": "US",
    "outputMode": "all",
    "stateStoreName": "exact-variant-monitor-state",
    "maxOffers": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("plusonets/exact-variant-price-stock-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 '{
  "catalog": [
    {
      "catalogItemId": "tree-runner-jet-black-8",
      "title": "Men'\''s Tree Runner Jet Black size 8",
      "brand": "Allbirds",
      "gtin": "843416184854",
      "mpn": "TR3MJBW080",
      "sku": "TR3MJBW080",
      "packQuantity": 1
    },
    {
      "catalogItemId": "strider-explore-natural-black-10",
      "title": "Men'\''s Strider Explore Natural Black size 10",
      "brand": "Allbirds",
      "gtin": "196942208595",
      "mpn": "A11768M100",
      "sku": "A11768M100",
      "packQuantity": 1
    },
    {
      "catalogItemId": "coconut-caramel-1-bag",
      "title": "Coconut Caramel Coffee 1 bag",
      "brand": "Death Wish Coffee",
      "gtin": "810063344313",
      "mpn": "LGCCML09",
      "sku": "LGCCML09",
      "packQuantity": 1
    },
    {
      "catalogItemId": "coconut-caramel-5-bag",
      "title": "Coconut Caramel Coffee 5 bags",
      "brand": "Death Wish Coffee",
      "gtin": "810063344313",
      "mpn": "LGCCML45K",
      "sku": "LGCCML45K",
      "packQuantity": 5
    },
    {
      "catalogItemId": "classic-percale-queen-white",
      "title": "Classic Percale Core Sheet Set Solid White Queen",
      "brand": "Brooklinen",
      "gtin": "840302715352",
      "mpn": "COR-Q1",
      "sku": "COR-Q1",
      "packQuantity": 1
    }
  ],
  "mappings": [
    {
      "url": "https://www.allbirds.com/products/mens-tree-runners?variant=33179624669264",
      "catalogItemId": "tree-runner-jet-black-8"
    },
    {
      "url": "https://www.allbirds.com/products/mens-strider-explore?variant=41334294020176",
      "catalogItemId": "strider-explore-natural-black-10"
    },
    {
      "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
      "catalogItemId": "coconut-caramel-1-bag"
    },
    {
      "url": "https://www.deathwishcoffee.com/products/coconut-caramel-coffee",
      "catalogItemId": "coconut-caramel-5-bag"
    },
    {
      "url": "https://www.brooklinen.com/products/classic-core-sheet-set?variant=41405019586650",
      "catalogItemId": "classic-percale-queen-white"
    }
  ],
  "expectedCurrency": "USD",
  "country": "US",
  "outputMode": "all",
  "stateStoreName": "exact-variant-monitor-state",
  "maxOffers": 50
}' |
apify call plusonets/exact-variant-price-stock-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plusonets/exact-variant-price-stock-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/ZnBvWf2buomc03ul2/builds/tDsn02HieaeTZhFAh/openapi.json
