# Shopify Variant Discount Drift Watch (`practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch`) Actor

Give one public Shopify store URL. Repeat runs flag variant compare-at discount drift and ignore invalid compare-at prices.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 variant drift scans

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

## Shopify Variant Discount Drift Watch

Give this Actor **one public Shopify store URL**. It reads the store's public product feed, saves a compact per-store snapshot, and on every later run reports which variants changed their compare-at ("was") discount. It also flags compare-at prices that are invalid, so they are never counted as discounts.

### Quick start (one URL)

1. Open the Actor and enter your store URL, for example `https://your-store.com`. Nothing else is required.
2. Run it. The first run has nothing to compare against, so it stores a snapshot and returns a single `BASELINE_CREATED` summary, a sample of the currently discounted variants, and any variants whose compare-at price is already invalid.
3. Run it again later. Each repeat run compares the new prices with the stored snapshot and returns one row per changed SKU.

Minimal input:

```json
{ "storeUrl": "https://www.allbirds.com" }
```

The example URL is only a sample. Run the Actor only against stores you are allowed to check, and replace it with your own.

#### Running it again later

The Actor does not run itself. To get repeat runs, create an Apify **Schedule** (Actor page > Schedules) that starts this Actor with the same input, for example once a day or once a week. No alerts are sent by this Actor; read the dataset, or attach your own Apify integration or webhook to the schedule if you want notifications.

Snapshots are kept in a **named key-value store** (default name `pal-variant-drift-state`, one entry per store domain), because a run's default store is not shared with later runs. Keep the same `stateStoreName` between runs. Deleting that store, or using the `resetBaseline` option, starts a fresh baseline.

### What gets reported

| Change type | Meaning |
| --- | --- |
| `DISCOUNT_STARTED` | A valid compare-at discount appeared. |
| `DISCOUNT_ENDED` | A discount that existed in the last snapshot is gone. |
| `DISCOUNT_DEEPENED` / `DISCOUNT_REDUCED` | Discount percentage moved by at least the configured number of points. |
| `REFERENCE_PRICE_RAISED` | The discount grew only because the compare-at price rose while the selling price stayed the same. |
| `NEW_VARIANT_DISCOUNTED` | A variant that was not in the last snapshot arrived with a valid discount. |
| `VARIANT_REMOVED_WHILE_DISCOUNTED` | A discounted variant left the feed (only reported when the feed was read completely). |
| `INVALID_COMPARE_AT_DETECTED` | The compare-at price cannot be trusted; no discount is counted. |

A compare-at price counts as a discount only when it is a number strictly above a positive selling price and implies a discount of at most 95%. Equal or lower compare-at values, non-numeric values, zero prices and extreme ratios are treated as invalid. The 95% limit is a heuristic of this Actor, not a Shopify rule. A compare-at of `0` or an empty value means "no compare-at".

Each drift row includes SKU, product and variant titles, previous and current price and compare-at price, discount percentages, the change in percentage points, a suggested action, and a product URL. Every run also writes:

- a `SUMMARY` row in the dataset (status, counts, sample discounted variants, caveats),
- `OUTPUT` in the run's key-value store (same summary as JSON),
- `DRIFT_CSV` in the run's key-value store (the drift rows as CSV). The dataset can also be exported as JSON or CSV from the Apify console.

### Inputs

| Field | Default | Notes |
| --- | --- | --- |
| `storeUrl` | required | Public HTTPS store URL. Only the domain is used. |
| `minDriftPercentagePoints` | 5 | Minimum discount movement to report for deepened/reduced/raised. |
| `maxPages` | 4 | Pages of up to 250 products. Maximum 10. |
| `requestTimeoutSecs` | 20 | Per request. |
| `resetBaseline` | false | Replace the saved snapshot with the current state. |
| `stateStoreName` | `pal-variant-drift-state` | Named store holding snapshots. |

### Access rules and safety

- Only the public `/products.json` feed and `robots.txt` of the given store are requested, nothing else. No login, cookies, proxies, or inventory endpoints.
- `robots.txt` is read first and checked, with the Actor's own user-agent (`PALVariantDriftWatch`), before every feed request. Wildcards and longest-match precedence are supported. A missing `robots.txt` (404/410) means no restrictions; a forbidden, erroring, or unreachable one makes the run stop. A crawl-delay above 10 seconds also stops the run.
- Only `https` store domains on the default port are accepted. IP addresses, `localhost`, credentials and internal-looking names are refused, and every host name is resolved and must point to public addresses only. The connection is pinned to the checked address. Redirects on product feed requests are not followed.
- At least one second passes between requests; response sizes, pages (10 max) and stored variants (20,000 max) are capped.

### Limitations and sample-size caveats

- **The first run cannot show drift.** Meaningful drift needs at least two runs, and any pattern in how often a store changes discounts needs many runs over time. Treat results from a handful of scans as anecdotes.
- Findings describe only what the public feed shows at the moment of each run. Changes that happen and revert between two runs are invisible.
- The feed has no currency; values are compared only within one store. Stores with multiple markets may show different prices by region.
- If a store has more products than `maxPages` allows, results are marked `feedIncomplete`, removals are not reported, and older snapshot entries are kept.
- Some stores disable or restrict the public product feed, or are protected against automation. Those runs end with a clear error and no results.
- A compare-at price is a merchant-set field; this Actor reports inconsistencies in it but does not judge legal or advertising compliance.
- Page-based feed paging may behave differently on very large catalogs.

### Billing

The code defines one pay-per-event event, `variant-drift-scan`, charged once after a scan has completed and its results and snapshot are stored. Failed runs (blocked robots, unsafe host, missing feed, network errors) push no dataset items and trigger no charge. No payable plan is configured in this repository; whether and how the event is priced is decided when the Actor is published.

### Development

Tests run fully offline and need no Apify SDK:

```
python -m unittest discover -s tests -t .
```

# Actor input Schema

## `storeUrl` (type: `string`):

Public HTTPS storefront URL of a Shopify store you are allowed to check, for example https://www.allbirds.com. Only the domain is used. This is the only required input.

## `minDriftPercentagePoints` (type: `number`):

A repeat-run discount change is reported only if the discount percentage moved by at least this many points. Discount started/ended and invalid compare-at findings are always reported.

## `maxPages` (type: `integer`):

Each page holds up to 250 products. The run stops after this many pages even if the store has more products (the result is then marked as incomplete).

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

Timeout for each HTTP request to the store.

## `resetBaseline` (type: `boolean`):

Discard the saved snapshot for this store and create a new baseline from the current prices.

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

Name of the named key-value store that keeps one snapshot per store domain between runs. Keep the default unless you need separate histories. Lowercase letters, digits and hyphens only.

## Actor input object example

```json
{
  "storeUrl": "https://www.allbirds.com",
  "minDriftPercentagePoints": 5,
  "maxPages": 4,
  "requestTimeoutSecs": 20,
  "resetBaseline": false,
  "stateStoreName": "pal-variant-drift-state"
}
```

# Actor output Schema

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

Download rows from the latest run in JSON/CSV.

# 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 = {
    "storeUrl": "https://www.allbirds.com"
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch").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 = { "storeUrl": "https://www.allbirds.com" }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch").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 '{
  "storeUrl": "https://www.allbirds.com"
}' |
apify call practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-shopify-variant-discount-drift-watch"
        }
    }
}
```

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/Rr0kXP4zOQNDq557o/builds/9kdnlrJT81K03dYa9/openapi.json
