# Prisync Alternative — Competitor Price & Stock Monitor (`solutionssmart/solutionssmart-competitor-price-monitor`) Actor

Monitor competitor prices and stock across Shopify stores and product pages. Track price and availability changes with SKU matching, change-only runs, datasets, schedules, webhooks, and pay-per-event pricing.

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

## Pricing

from $0.00005 / actor start

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

## Prisync Alternative: competitor price and stock monitor

Monitor competitor prices and stock on Shopify and product pages. Pay per check, get change webhooks, and keep the results in your Apify account. The Actor extracts product identity, price, currency, image, and availability, then compares each check with the previous snapshot.

Use it for scheduled competitor price monitoring, stock alerts, product catalog checks, and webhook-based pricing workflows. There is no monthly seat. Enter product URLs or a Shopify store domain, choose the output mode, and run the Actor.

The extraction harness was checked against [20 real product URLs across 10 domains](TEST.md), with 100% parse success and 100% usable-price recovery in the recorded run.

### What the Actor does

- Checks individual product pages or discovers a Shopify catalog from `/products.json` and product sitemaps.
- Extracts product data from JSON-LD, Shopify data, Open Graph and other metadata, then uses DOM signals as a last resort.
- Matches products across runs by SKU, MPN, GTIN/EAN, or canonical URL.
- Reports `PRICE_DROP`, `PRICE_RISE`, `OOS`, `BACK_IN_STOCK`, `NEW`, and `UNCHANGED` events.
- Records `BLOCKED`, `LOGIN_REQUIRED`, `EMPTY`, and `PARSE_ERROR` failures instead of inventing product data.
- Stores snapshots in the default key-value store so scheduled runs can compare against the previous check.
- Sends a batch of change rows to an optional webhook.
- Uses Apify Proxy and optional Unblocker fallback for sites that need a residential route or browser challenge recovery.
- Exposes results through the Apify dataset, API, schedules, integrations, and webhooks.
- Optionally uses Jev to classify ambiguous pages when deterministic extraction cannot identify a product.

### Prisync Alternative comparison

| | This Actor | Prisync-style SaaS |
|---|---|---|
| Billing | Pay per product check and change event on Apify | Monthly seat or plan subscription |
| Monitoring | Your product URLs or a Shopify catalog | Vendor and product watchlists |
| Delivery | Dataset, API, schedules, webhooks, and integrations | SaaS dashboard and plan-dependent exports |
| Data ownership | Results and snapshots stay in your Apify account | Data stays in the vendor workspace |

This Actor is a fit when you want programmable output, scheduled runs, and direct access to the collected dataset.

### Quick start

1. Add one or more product URLs in the `startUrls` field, or enter a Shopify domain in `catalogMode`.
2. Choose `ALL` to receive every check or `CHANGES_ONLY` to receive only changes and failures.
3. Set `persistState` to `true` for comparisons between runs.
4. Set a fixed `apifyProxyCountry` when prices or currencies depend on the visitor's market.
5. Run the Actor once, then schedule it to run again at the interval you need.

The Apify Console uses request objects for `startUrls`:

```json
{
  "startUrls": [
    { "url": "https://www.cotopaxi.com/products/down-mitten-3" },
    { "url": "https://www.kith.com/products/s57ws0538p7573t4027" },
    { "url": "https://www.uniqlo.com/de/de/products/E476766-000/00" }
  ],
  "matchKey": "AUTO",
  "emitMode": "CHANGES_ONLY",
  "currencyPreferred": "USD",
  "priceDropPct": 2,
  "alertOnOos": true,
  "alertOnBackInStock": true,
  "persistState": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

For a Shopify catalog, use `catalogMode` instead of `startUrls`:

```json
{
  "catalogMode": "cotopaxi.com",
  "maxItems": 400,
  "emitMode": "CHANGES_ONLY",
  "persistState": true
}
```

### Input settings

| Field | Description |
|---|---|
| `startUrls` | Product page request objects. Required unless `catalogMode` is set. |
| `catalogMode` | Shopify store domain for catalog discovery. |
| `matchKey` | `AUTO`, `SKU`, `MPN`, `EAN`, or `TITLE_FUZZY`. `AUTO` uses the strongest available identifier and falls back to URL identity when no identifier is available. `TITLE_FUZZY` uses title similarity for match confidence. |
| `emitMode` | `ALL` or `CHANGES_ONLY`. |
| `currencyPreferred` | Fills a missing currency. It does not convert prices. |
| `priceDropPct` | Minimum percentage drop needed for `PRICE_DROP`. |
| `priceRisePct` | Minimum percentage rise needed for `PRICE_RISE`. |
| `alertOnOos` | Enables `OOS` events. Defaults to `true`. |
| `alertOnBackInStock` | Enables `BACK_IN_STOCK` events. Defaults to `true`. |
| `maxItems` | Maximum product pages checked in one run. Defaults to `1000`. |
| `maxConcurrency` | Number of pages checked in parallel. Defaults to `3`. |
| `requestTimeoutSecs` | Per-page timeout. Defaults to `45` seconds. |
| `proxyConfiguration` | Apify Proxy, custom proxy URLs, or direct access. |
| `unblockerFallback` | Retries blocked pages through Unblocker. Requires Apify Proxy and a two-letter `apifyProxyCountry`. |
| `maxUnblockerRequests` | Maximum product URLs sent to the fallback. Defaults to `10`. |
| `useJev` | Optional paid Jev recovery for ambiguous pages after deterministic extraction fails. Defaults to `false`. |
| `webhookUrl` | Receives a JSON batch when the run emits change events. |
| `persistState` | Stores snapshots for future comparisons. Defaults to `true`. |
| `debug` | Enables additional diagnostic logging. |

### Output

The default dataset contains one row per product check or failure. A successful product row looks like this:

```json
{
  "stableId": "5c6b3dc939cff6e3a932",
  "anchor": "sku",
  "url": "https://www.cotopaxi.com/products/down-mitten-3",
  "canonicalUrl": "https://www.cotopaxi.com/products/down-mitten-3",
  "title": "Down Mitten",
  "brand": "Cotopaxi",
  "sku": "CMS12958",
  "price": 26,
  "salePrice": 26,
  "currency": "USD",
  "availability": "IN_STOCK",
  "changeType": "PRICE_DROP",
  "pricePrevious": 32.5,
  "priceDelta": -6.5,
  "priceDeltaPct": -20,
  "availabilityPrevious": "IN_STOCK",
  "matchConfidence": 1,
  "priceSource": "jsonld",
  "scrapedAt": "2026-09-15T10:12:00.000Z"
}
```

Failed checks are still emitted so a scheduled workflow does not look successful when a site becomes unavailable:

```json
{
  "stableId": "empty:https://example.com/product",
  "anchor": "url",
  "url": "https://example.com/product",
  "checkStatus": "FAIL",
  "changeType": "PARSE_ERROR",
  "emptyReason": "PARSE_ERROR",
  "errorMessage": "no product identifiers found; page is not a confirmed removal"
}
```

When `CHANGES_ONLY` finds no changes, the Actor emits one free summary row:

```json
{
  "changeType": "NO_CHANGES_SUMMARY",
  "runId": "HnoZxeqjqG4tFWwzA",
  "stateNamespace": "default",
  "emittedRows": 0,
  "productChecks": 12,
  "unchangedCount": 12,
  "changedCount": 0,
  "failedCount": 0,
  "urlCount": 12
}
```

### Webhook example

When a run detects changes, `webhookUrl` receives a JSON batch similar to this:

```json
{
  "type": "solutionssmart-price-monitor",
  "runId": "HnoZxeqjqG4tFWwzA",
  "stateNamespace": "default",
  "changedCount": 1,
  "scrapedAt": "2026-09-18T21:22:38.800Z",
  "items": [
    {
      "stableId": "5c6b3dc939cff6e3a932",
      "changeType": "PRICE_DROP",
      "title": "Down Mitten",
      "price": 26,
      "pricePrevious": 32.5,
      "currency": "USD",
      "url": "https://www.cotopaxi.com/products/down-mitten-3"
    }
  ]
}
```

### Change detection and state

The Actor writes a snapshot for each tracked product to the default key-value store. A snapshot contains the last known price, availability, title, identifiers, currency, canonical URL, and update time.

Price changes require matching, known currencies. The Actor does not convert USD, EUR, GBP, or other currencies. Stock changes can still be reported when currencies differ. A product with no price can still produce a valid availability check when stock evidence is clear.

The first successful check for a product is `NEW`. Later runs use the stable identity and previous snapshot to produce change events. Set `persistState` to `false` when you want every run to start with a fresh baseline.

<details>
<summary>Optional paid recovery: Jev page classification</summary>

Set `useJev` to `true` when you want an additional paid recovery step for ambiguous pages that have no usable product identifiers. Jev classifies the page as `PRODUCT_PAGE`, `BLOCKED`, `LOGIN_REQUIRED`, `NOT_FOUND`, `EMPTY`, or `UNCERTAIN`. It runs only after deterministic checks cannot identify a product, and it is disabled by default.

Jev does not provide prices, identifiers, or availability values. The Actor continues to validate and normalize those fields in code. Missing keys, failed requests, timeouts, and low-confidence answers fall back to the deterministic `PARSE_ERROR` path. The Actor charges one `jev-recovery` event only when a Jev request is actually attempted; users do not provide an API key.

Example:

```json
{
  "startUrls": [{ "url": "https://example.com/product" }],
  "useJev": true
}
```

Users do not provide a TypeSafe key. The Actor owner configures Jev privately, and the key is never part of Actor input or public source code.

</details>

### Pricing

The Actor uses Apify resources according to your account and configuration. A run can consume compute units, proxy traffic, browser requests, and Unblocker usage when enabled.

This Actor supports Pay per Event configuration for `product-check`, `change-event`, `jev-recovery`, and the run-start event. Failed checks, summaries, and `NO_CHANGES_SUMMARY` rows are not intended to be billed as dataset items. Jev is opt-in, so ordinary checks do not incur a Jev event. Review the Actor's Pricing tab and your Apify usage before running large catalogs.

Recommended event titles and descriptions:

| Event | Title | Description |
| --- | --- | --- |
| `product-check` | Product check | One successfully parsed product URL, including price, stock, identity, and comparison with stored state. |
| `change-event` | Price or stock change | One detected new product, price change, stock change, availability change, or product replacement. |
| `jev-recovery` | Jev page classification | One optional Jev request used to classify an ambiguous page after deterministic extraction could not identify a product. |

With low-cost beta pricing, a starting point is `$0.003` per `product-check`, `$0.001` per `change-event`, and `$0.005` per `jev-recovery`. Adjust after checking TypeSafe usage and Apify compute costs.

Unblocker is an optional paid fallback. It can make an additional browser request for a selected URL and may generate additional proxy usage. Keep `maxUnblockerRequests` low while testing.

### MAP and minimum-price monitoring

The Actor currently supports relative price thresholds through `priceDropPct` and `priceRisePct`. For an absolute MAP or minimum-price rule, run the Actor with `emitMode: "ALL"`, read the dataset through the Apify API, and filter rows where `price` is below your MAP value. This keeps the scrape and the business rule separate while preserving the raw observation.

```json
{
  "startUrls": [
    { "url": "https://www.cotopaxi.com/products/down-mitten-3" }
  ],
  "emitMode": "ALL",
  "persistState": true,
  "priceDropPct": 1
}
```

### Proxy and troubleshooting

#### The Actor stops with an Unblocker configuration error

Set both of these values when `unblockerFallback` is `true`:

```json
{
  "unblockerFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  }
}
```

If you do not need Unblocker, set `unblockerFallback` to `false` and use direct access or a regular proxy.

#### The output contains `UNKNOWN` availability

The page had price or product evidence but no reliable stock signal. This is intentional. The Actor avoids turning an ambiguous button or out-of-stock phrase into a false alert.

#### The output contains `PARSE_ERROR`

The page loaded, but the Actor could not find enough trusted product evidence. Check whether the site needs a residential proxy, whether the product URL is still live, and whether the page uses client-side data that is not exposed to the browser probe.

#### The output contains `BLOCKED` or `LOGIN_REQUIRED`

The site returned a bot challenge, access denial, or account wall. Use a permitted proxy configuration, enable the bounded Unblocker fallback when appropriate, or remove the URL. Do not use the Actor to bypass access controls without authorization.

### Compatibility and responsible use

The Actor works best with Shopify, JSON-LD product data, Open Graph metadata, or a visible product price and availability signal. It may need a residential proxy for Amazon, Walmart, eBay, Home Depot, ASOS, Target, Apple, and sites protected by Akamai or Cloudflare.

Check each website's terms, robots rules, and applicable law before monitoring it. Use an appropriate request rate and proxy configuration. Do not collect personal data or attempt to bypass authentication.

The extraction harness has been checked against 20 real product URLs across 10 domains. The recorded run reached 100% parse success and 100% usable-price recovery; see [TEST.md](TEST.md) for the URLs, source tiers, limits, and known failures.

### Related SolutionsSmart Actors

- [Business Brand DNA](https://apify.com/solutionssmart/business-brand-dna) for brand footprint and demographic research.
- [E-Commerce Checkout Auditor](https://apify.com/solutionssmart/e-commerce-checkout-auditor) for automated checkout-path checks.

### FAQ

#### Can I monitor a whole store?

Yes. Set `catalogMode` to a Shopify domain and limit the run with `maxItems`.

#### Does the Actor convert currencies?

No. It records the currency exposed by the page or uses `currencyPreferred` when the page omits one. Price comparisons require the previous and current currencies to match.

#### Does a failed URL stop the whole run?

Individual failures are emitted as rows. The run fails at the end when every requested URL fails, which prevents a completely empty or blocked run from appearing successful.

#### Can I use the dataset through an API?

Yes. Apify exposes the default dataset and run data through its API, SDKs, schedules, and integrations.

#### How do I report a problem?

Include the Actor run ID, input, failing URL, `emptyReason`, and the relevant log lines. Do not include proxy credentials, API keys, or private page content.

### Local development

Install dependencies and run the regression tests:

```bash
npm install
node --test test/changes.test.js test/retrieval.test.js
```

Run the Actor with the Apify CLI:

```bash
apify run --input-file examples/input-one-off.json
```

The falsification notes and representative URL checks are in [TEST.md](TEST.md).

# Actor input Schema

## `startUrls` (type: `array`):

Competitor product pages to monitor. One URL = one product check per run. Required unless catalogMode is set.

## `catalogMode` (type: `string`):

Shopify store domain (e.g. gymshark.com). Discovers the product catalog via /products.json (sitemap fallback), capped by maxItems, then checks each product page.

## `matchKey` (type: `string`):

What anchors stable product identity across runs: AUTO (best identifier found), SKU, MPN, EAN, or TITLE\_FUZZY (URL anchor + title similarity confidence).

## `emitMode` (type: `string`):

ALL: emit a row for every product check. CHANGES\_ONLY: emit only changed/new/failed rows (+ a NO\_CHANGES\_SUMMARY row when nothing changed) so scheduled runs never look empty.

## `currencyPreferred` (type: `string`):

ISO code used only when the page does not declare its own currency (e.g. EUR, USD).

## `priceDropPct` (type: `number`):

Minimum price decrease (percent) to report a PRICE\_DROP. Leave empty to alert on any decrease.

## `priceRisePct` (type: `number`):

Minimum price increase (percent) to report a PRICE\_RISE. Leave empty to alert on any increase.

## `alertOnOos` (type: `boolean`):

Report OOS as a change when a product goes out of stock.

## `alertOnBackInStock` (type: `boolean`):

Report BACK\_IN\_STOCK as a change when an out-of-stock product becomes available again.

## `maxItems` (type: `integer`):

Cap on product pages checked this run (also caps catalogMode discovery).

## `maxConcurrency` (type: `integer`):

Parallel browser pages while checking. Keep single digits on strict storefronts.

## `requestTimeoutSecs` (type: `integer`):

Per-page navigation + extraction timeout.

## `proxyConfiguration` (type: `object`):

Residential proxy recommended. Defaults to Apify Proxy RESIDENTIAL with no fixed country. For stable cross-run comparisons, set apifyProxyCountry (e.g. US) — without it, runs may land in different storefront markets with different currencies/prices.

## `unblockerFallback` (type: `boolean`):

Optional paid fallback after blocked pages or exhausted navigation retries. Requires Apify Proxy with a fixed country. Missing pages and login walls are not retried. Unblocker charges separately, including browser subrequests.

## `useJev` (type: `boolean`):

Optional paid recovery. When deterministic extraction cannot identify a product, Jev classifies whether the page is blocked, login-gated, unavailable, empty, or uncertain. Charged once per Jev request; requires the Actor owner to configure TYPESAFE\_API\_KEY.

## `maxUnblockerRequests` (type: `integer`):

Maximum product URLs sent to the fallback per run. This is not a billing cap: browser subrequests may also consume Unblocker units.

## `webhookUrl` (type: `string`):

Optional. A JSON batch of changed rows is POSTed once per run when at least one change is detected.

## `persistState` (type: `boolean`):

Store the last snapshot per product in the KV store, namespaced by Actor task id. Disable to always treat products as NEW.

## `debug` (type: `boolean`):

Verbose logging.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.allbirds.com/products/mens-strider-explore"
    }
  ],
  "matchKey": "AUTO",
  "emitMode": "CHANGES_ONLY",
  "alertOnOos": true,
  "alertOnBackInStock": true,
  "maxItems": 1000,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 45,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "unblockerFallback": false,
  "useJev": false,
  "maxUnblockerRequests": 10,
  "persistState": true,
  "debug": false
}
```

# Actor output Schema

## `results` (type: `string`):

Link to the default dataset containing the product check rows for this run.

## `errors` (type: `string`):

Link to the error row(s) pushed when a request failed or a page did not yield a usable product (PARSE\_ERROR / BLOCKED). Present only when at least one check failed.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.allbirds.com/products/mens-strider-explore"
        }
    ],
    "catalogMode": "",
    "currencyPreferred": "",
    "priceDropPct": 0,
    "priceRisePct": 0,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "webhookUrl": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("solutionssmart/solutionssmart-competitor-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 = {
    "startUrls": [{ "url": "https://www.allbirds.com/products/mens-strider-explore" }],
    "catalogMode": "",
    "currencyPreferred": "",
    "priceDropPct": 0,
    "priceRisePct": 0,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "webhookUrl": "",
}

# Run the Actor and wait for it to finish
run = client.actor("solutionssmart/solutionssmart-competitor-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 '{
  "startUrls": [
    {
      "url": "https://www.allbirds.com/products/mens-strider-explore"
    }
  ],
  "catalogMode": "",
  "currencyPreferred": "",
  "priceDropPct": 0,
  "priceRisePct": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "webhookUrl": ""
}' |
apify call solutionssmart/solutionssmart-competitor-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,solutionssmart/solutionssmart-competitor-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/lsnHRxcDUCE6ZDypz/builds/ankoDkEmIMbZl7DTa/openapi.json
