# Shopify Reseller Price & Stock Evidence Monitor (`felixgoldwin/shopify-reseller-price-stock-monitor`) Actor

Compare exact SKUs and barcodes across supplied Shopify dealer URLs. Capture price, currency, published availability and snapshot changes per catalog item and dealer context. Unofficial; no dealer discovery, exact stock quantities or MAP verdicts.

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

## Pricing

from $1.00 / 1,000 catalog-dealer evaluations

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 Reseller Price & Stock Evidence Monitor

Turn an explicit list of Shopify dealer product pages and a nonsecret SKU/barcode catalog into repeatable price and purchasability evidence. Each dataset row represents **one catalog item × one dealer host/locale context**, with exact candidate variants aggregated across the supplied product URLs. No credentials are needed.

This adapter supports Shopify-hosted theme storefronts exposing the documented Ajax Product API. It does not discover dealers, crawl collections, search a store, support arbitrary commerce platforms, or claim coverage of headless Shopify stores.

### Input and a first run

Use [examples/input.json](examples/input.json) for a small public Shopify Hardware example. Product availability and price may change. The example is a technical API check, not evidence that Shopify Hardware is an independent reseller or that any brand authorizes a dealer.

```json
{
  "dealerHosts": ["hardware.shopify.com"],
  "productUrls": ["https://hardware.shopify.com/products/shopify-pos-go"],
  "catalog": [{"id":"shopify-pos-go","sku":"887675103492","barcode":"887675103492"}],
  "maxRequests": 4,
  "maxResults": 5,
  "maxBudgetUsd": 0.05
}
```

```sh
npm ci --ignore-scripts
npm test
npm run validate:schema
npm run smoke
```

For the actual SDK entrypoint, put the input at `storage/key_value_stores/default/INPUT.json`, then run `npm start`. Local smoke uses no account or paid Apify run. See [PUBLISHING.md](PUBLISHING.md) for deployment checks and [QA.md](QA.md) for tested scope.

`dealerHosts` is an exact lowercase hostname allowlist (1–10). `productUrls` accepts 1–50 canonical HTTPS URLs in `/products/handle` or `/locale/products/handle` form. Queries, fragments, credentials, ports, percent escapes and custom headers are rejected. The Actor derives only those product `.js` endpoints and a same-host/same-locale `cart.js` GET for currency. It never mutates a cart or carries user cookies.

The catalog accepts 1–100 items with a unique `id`, and `sku`, `barcode`, or both. Identifiers are exact and case sensitive; leading zeros are preserved. Titles and vendor names never create a match. A single exact match is labeled `unique_exact_match`; more than one candidate is `ambiguous_exact_matches`. If one identifier matches but another supplied, published identifier disagrees, the row is `identifier_conflict` and preserves both values. A no-match row is `not_matched_in_supplied_urls`. Missing other identifiers remain unknown; they are not conflicts.

### Evidence and changes

Each matching variant carries product/API URL, product and variant IDs/titles, vendor label, published-date validity, source SKU/barcode, matched/conflicting fields, raw price and compare-at price, normalized amounts, discount percentage, currency and currency-source URL, `purchasable` and `requiresSellingPlan`.

Amounts divide Shopify's integer representation by 100, including JPY/KRW; ISO minor-digit rules must not be substituted. Currency is read separately from anonymous same-host/locale `cart.js`. Cookies and custom market/currency query parameters are not supported, so evidence describes this anonymous server-side presentment context. Taxes, shipping, checkout, customer-specific discounts and selling-plan allocations are not evaluated. A positive compare-at spread is the published display discount, not proof of historical selling price. Null means missing or unknown.

`purchasable` is the published variant `available` flag. It is **not an exact stock quantity**, purchase/delivery guarantee, tested checkout, or legal MAP verdict. The Actor makes no requests that test inventory by adding quantities to a cart.

`OUTPUT` contains diagnostics, response SHA-256 hashes and observation times, exact normalized before/after changes, and a reusable `previousSnapshot`. Pass that snapshot explicitly on the next run with exactly the same catalog and URLs. There is no global or cross-user snapshot state. Input is limited to 1.5 MB, snapshots to 1 MB and strict known fields. Caller snapshots are structurally checked but are caller-provided evidence, not signed attestations.

Only complete scans return a reusable snapshot or changes. Source failures, unknown currency, exhausted limits, malformed responses, possible variant truncation, or incomplete dataset delivery suppress **all** comparison. A product HTTP 404 is a failed source read, not a removal. A matched candidate becoming absent from a complete response is a change in observed evidence, never a dealer-wide delisting assertion. Observed timestamps and response hashes are outside comparisons.

### Coverage and limits

Shopify Product API returns at most **250 variants**. A response containing 250 variants is conservatively marked possibly truncated. The single-product endpoint does not paginate; the Actor does not invent pagination or use undocumented bulk listings. Maximum 100 exact candidates per row; reaching a larger candidate set makes the run incomplete. Catalog rows are still emitted as current incomplete evidence when possible, with `change.type: "not_compared"`.

| Limit | Default | Allowed maximum |
| --- | ---: | ---: |
| Dealer hosts | explicit | 10 |
| Product URLs | explicit | 50 |
| Host/locale contexts | derived | 20 |
| Catalog items | explicit | 100 |
| Result rows | 100 | 500 |
| HTTP attempts, including retries | 30 | 150 |
| Retries per request | 2 | 3 |
| Scan seconds | 90 | 300 |
| Bytes per response | 2,000,000 | 5,000,000 |
| Aggregate response bytes | 20,000,000 | fixed |
| Event budget USD | 0.25 | 20 |

Requests run serially. HTTP 429 and selected transient 5xx responses retry within the request/time bounds, honoring Retry-After. Access denials and redirects are never bypassed. All DNS answers must be public unicast addresses; the selected address is pinned for the TLS connection, preventing a second DNS lookup from rebinding to a private address. IPv4, IPv6, mapped addresses, reserved/transition ranges, host syntax and redirects are checked. No proxy configuration or browser is used. Per-response streams are destroyed on overflow; an arriving network chunk can exceed the byte threshold before destruction, but no subsequent request starts after the aggregate threshold.

### Approved listing rates

The user-approved listing rates are **$0.005 automatic `apify-actor-start` per started 1 GB memory unit** plus **$0.001 per `reseller-evidence` dataset row**. These rates are configured for this listing. They are not an earnings forecast. Each catalog/dealer-context row counts once, whether matched, ambiguous, conflicting, unmatched or incomplete. Candidate variants inside a row are not charged separately. `OUTPUT` diagnostics do not trigger events. Example: 25 delivered rows at 256 MB would be $0.030 in event charges in customer event charges; platform usage is included in the configured pay-per-event pricing.

On pay-per-event runs the entrypoint reads actual configured event prices and existing counts, reserves automatic starts, checks remaining capacity before source work and each push, and refuses a positive synthetic dataset-item price or an unexpected paid event. It never manually charges `apify-actor-start`. Outside pay-per-event mode `maxBudgetUsd` uses an estimate based on the approved rates only; compute/storage costs are outside that input cap. An automatic platform start can occur before input processing and cannot be undone by a zero input budget. Set platform max total charge appropriately as well.

### Hosted verification — 2026-10-11 UTC

Apify build 0.0.1 succeeded using the supplied Node 22 Dockerfile. Two owner-account runs at 256 MB and a 180-second timeout completed against the public Shopify Hardware example: two HTTP 200 requests, one exact-match dataset row, complete delivery, and a reusable snapshot. The second run reported `unchanged` using the first run's snapshot. Both used a $0.006 event cap. Runtime confirmed pay-per-event mode with the configured $0.005 automatic start and $0.001 custom evaluation price; the code verifies one custom event-count increase per delivered row. The owner's Console reported usage costs rounded to $0.000 for each run. This is not a customer invoice test. Peak memory was 58.9 MB across these small tests, not a maximum-load guarantee.

The static `diagnostics.platform` sentence in this source still describes the builder's earlier local-only verification. For actual hosted execution, use the dated record above and current `completeScan`, `deliveryComplete`, `billingMode`, source-response and comparison fields. Failed/denied-source and budget boundary behavior also has offline fixture coverage; no source-denial bypass was used.

This Actor is unofficial and is not affiliated with Shopify.

### Sources

- [Official Shopify Product API](https://shopify.dev/docs/api/ajax/reference/product)
- [Official Ajax requirements and limitations](https://shopify.dev/docs/api/ajax#requirements-and-limitations)
- [Official Cart API](https://shopify.dev/docs/api/ajax/reference/cart)
- [Official variant price representation, including JPY/KRW](https://shopify.dev/docs/api/liquid/objects/variant#variant-price)

Read [NOTICE.md](NOTICE.md) for attribution and source restrictions.

# Actor input Schema

## `dealerHosts` (type: `array`):

1–10 exact lowercase public hostnames. No wildcard, protocol, port or credentials. Only explicitly named public Shopify storefronts are supported.

## `productUrls` (type: `array`):

1–50 unique HTTPS URLs on dealerHosts: /products/handle or /locale/products/handle. No queries, fragments, encoded paths or collection listings.

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

1–100 JSON objects with unique id and sku and/or barcode strings. Exact case-sensitive matching; preserve leading zeros. No secrets or personal data.

## `previousSnapshot` (type: `object`):

Paste OUTPUT.previousSnapshot for the exact same hosts, product URLs and catalog. Maximum total input 1.5 MB. Never paste secrets.

## `maxResults` (type: `integer`):

Hard runtime limit. Any incomplete scan suppresses changes and reusable snapshots.

## `maxRequests` (type: `integer`):

Hard runtime limit. Any incomplete scan suppresses changes and reusable snapshots.

## `maxRetries` (type: `integer`):

Hard runtime limit. Any incomplete scan suppresses changes and reusable snapshots.

## `maxSeconds` (type: `integer`):

Hard runtime limit. Any incomplete scan suppresses changes and reusable snapshots.

## `maxResponseBytes` (type: `integer`):

Hard runtime limit. Any incomplete scan suppresses changes and reusable snapshots.

## `maxBudgetUsd` (type: `number`):

Caps local estimated or configured event charges including automatic start reserve. Does not cap compute/storage. No start event is manually charged.

## Actor input object example

```json
{
  "dealerHosts": [
    "hardware.shopify.com"
  ],
  "productUrls": [
    "https://hardware.shopify.com/products/shopify-pos-go"
  ],
  "catalog": [
    {
      "id": "shopify-pos-go",
      "sku": "887675103492",
      "barcode": "887675103492"
    }
  ],
  "maxResults": 1,
  "maxRequests": 4,
  "maxRetries": 1,
  "maxSeconds": 40,
  "maxResponseBytes": 500000,
  "maxBudgetUsd": 0.02
}
```

# Actor output Schema

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

No description

## `diagnostics` (type: `string`):

No description

# 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 = {
    "dealerHosts": [
        "hardware.shopify.com"
    ],
    "productUrls": [
        "https://hardware.shopify.com/products/shopify-pos-go"
    ],
    "catalog": [
        {
            "id": "shopify-pos-go",
            "sku": "887675103492",
            "barcode": "887675103492"
        }
    ],
    "maxResults": 1,
    "maxRequests": 4,
    "maxRetries": 1,
    "maxSeconds": 40,
    "maxResponseBytes": 500000,
    "maxBudgetUsd": 0.02
};

// Run the Actor and wait for it to finish
const run = await client.actor("felixgoldwin/shopify-reseller-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 = {
    "dealerHosts": ["hardware.shopify.com"],
    "productUrls": ["https://hardware.shopify.com/products/shopify-pos-go"],
    "catalog": [{
            "id": "shopify-pos-go",
            "sku": "887675103492",
            "barcode": "887675103492",
        }],
    "maxResults": 1,
    "maxRequests": 4,
    "maxRetries": 1,
    "maxSeconds": 40,
    "maxResponseBytes": 500000,
    "maxBudgetUsd": 0.02,
}

# Run the Actor and wait for it to finish
run = client.actor("felixgoldwin/shopify-reseller-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 '{
  "dealerHosts": [
    "hardware.shopify.com"
  ],
  "productUrls": [
    "https://hardware.shopify.com/products/shopify-pos-go"
  ],
  "catalog": [
    {
      "id": "shopify-pos-go",
      "sku": "887675103492",
      "barcode": "887675103492"
    }
  ],
  "maxResults": 1,
  "maxRequests": 4,
  "maxRetries": 1,
  "maxSeconds": 40,
  "maxResponseBytes": 500000,
  "maxBudgetUsd": 0.02
}' |
apify call felixgoldwin/shopify-reseller-price-stock-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,felixgoldwin/shopify-reseller-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/gBGZMZzqdtmh0fXUp/builds/geDBl2VBWsHqgATLF/openapi.json
