# Supplier Stock & Price Monitor (Public Pages) (`tomwang/supplier-stock-monitor`) Actor

Watches your own suppliers' public product pages and reports price and stock changes between runs. One plain GET per URL, no CAPTCHA bypass, snapshots persisted in key-value store.

- **URL**: https://apify.com/tomwang/supplier-stock-monitor.md
- **Developed by:** [Tom](https://apify.com/tomwang) (community)
- **Categories:** E-commerce, Automation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 change detecteds

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

## Supplier Stock & Price Monitor (public pages)

Watches a small list of **your own suppliers' public product pages** and
reports price and stock/availability changes between runs. Built for
dropshippers and small retailers whose suppliers have no API - the people
currently hand-writing scraper scripts or catching supplier price rises days
late. This is operational continuity ("did MY supplier's SKU go out of stock /
did MY cost change"), not competitor price intelligence.

### What it does

- One plain HTTP GET per watched URL per run. No crawling, no retries, no
  JavaScript rendering, **no CAPTCHA or bot-check bypass**.
- Fetch order: (1) Shopify storefronts - the public
  `/products/<handle>.js` endpoint (verified live 2026-10-31 on a public
  Shopify store: returns per-variant `sku`, `available`, and integer-cent
  `price`); (2) any other site - schema.org `Product`/`Offer` JSON-LD
  (`price`, `priceCurrency`, `availability`) embedded in the HTML
  (https://schema.org/Product, https://schema.org/Offer).
- Diffs against the previous run's snapshot and emits deterministic events:
  `product_added`, `variant_added`, `variant_removed`, `price_changed`
  (from/to in minor units + currency), `out_of_stock`, `back_in_stock`,
  `status_changed`.
- Blocked pages (401/403/429/503) are reported as `status: "blocked"` -
  never worked around. Verified live: repeated polling of one storefront
  within minutes returns HTTP 429, so schedule conservatively (hourly at
  most per URL; less for small watchlists) and expect some sites
  (e.g. gymshark.com) to refuse plain fetchers entirely.

### Safety guards (v2, after review)

- **SSRF protection:** only http/https, no credentials in URLs, ports 80/443
  only, and every hostname (including redirect targets, validated at redirect
  time) must resolve to public IPs - loopback/private/link-local/multicast/
  reserved are refused before any request leaves the box. Residual limit:
  stdlib-only cannot pin DNS against rebinding between check and connect;
  documented, not hidden.
- **Body cap:** responses over 5MB are rejected.
- **Baseline preservation:** a failed run (429, timeout, parse failure) never
  overwrites the last-known-good snapshot for that URL, so transient failures
  cannot manufacture false change events on recovery (tested).
- **Unknown stays unknown:** a missing `available` field is unknown and fires
  no stock event - it is never treated as out-of-stock (tested).
- **Currency precision:** minor units per currency (JPY 0, BHD-type 3, else
  2\); prices with more precision than the currency supports are rejected,
  never rounded (tested).

### Honest limits

- Availability is only what the page publicly exposes (usually a per-variant
  boolean). Stock quantities are typically not public and are not invented.
- Shopify `.js` prices carry no currency field; currency comes from the
  watchlist entry's optional `currency` hint, else null.
- Sites without Shopify `.js` or JSON-LD return `parse_failed`.
- No login-walled supplier portals. Public pages only.

### Input

JSON watchlist on stdin / input field:
`[{"url": "https://supplier.example/products/some-item", "currency": "USD"}, ...]`

CLI: `python3 supplier_monitor.py --snapshot snap.json < watchlist.json`
(snapshot optional; without it every URL reports `product_added`).
For the Apify wrapper: input schema `{watchlist: [...]}`; persist the
snapshot in the Actor's default key-value store (`STATE`/`snapshot.json`),
push results + events to the dataset, log counts only (never full URLs
with customer context or variant detail).

### Fixtures and tests

- `fixtures/shopify_product.js` - synthetic, modeled on the verified live
  Shopify `.js` shape (integer-cent prices)
- `fixtures/product_page.html` - synthetic JSON-LD page incl. a malformed
  block (skipped) and InStock/OutOfStock variants
- `test_supplier_monitor.py` - 38 deterministic tests, all passing
  (`python3 test_supplier_monitor.py`), incl. a two-pass run with snapshot
  proving price-change and restock events fire exactly once

No third-party dependencies. No code or data copied from external sources.

### Demand evidence (gathered 2026-10-31)

- r/dropshipping "How do you track supplier stock and price changes?"
  (Jun 2026): users hand-write scraper scripts; "price increases can slip
  through for days... margin damage is already done."
  https://www.reddit.com/r/dropshipping/comments/1u9gt9c/
- r/dropshipping: "I lost two sales this week because an item was out of
  stock and I didn't catch it in time."
  https://www.reddit.com/r/dropshipping/comments/1my2wr4/

### Competition and pricing (ESTIMATE ONLY, Tom decides)

- On-store: https://apify.com/zentrafoundry/supplier-stock-price-monitor -
  fetched 2026-10-31: "2 total users, 1 monthly users, no ratings", priced
  $43.00 per 1,000 change alerts, scoped to "approved public supplier
  product URLs". Same wedge, verified near-zero traction.
- Suggested event pricing: per `change-detected` event at ~$0.002-0.005
  (i.e. ~$2-5 per 1,000 alerts vs competitor's $43), plus free first-run
  baseline (`product_added` events uncharged). Run cost is one GET per URL
  (~$0.001 for a small watchlist), so margin is ample. Charge only real
  change events; `status_changed` to blocked stays free.

### Privacy

Watchlists are user-provided public URLs. Runs process public page data
only. When deployed on Apify, run inputs/datasets/logs persist on the
platform by default - do not claim otherwise; log counts, not payloads.

# Actor input Schema

## `watchlist` (type: `array`):

JSON array of {"url": ..., "currency": "USD"?} for YOUR suppliers' public product pages (max 50 per run; one plain GET per URL; blocked pages are reported, never bypassed; schedule conservatively, hourly at most per URL). Entries with url "demo:shopify" or "demo:jsonld" use built-in synthetic sample pages and fetch nothing - the default demo input is also what the daily automatic Store test runs, so it generates no third-party traffic.

## Actor input object example

```json
{
  "watchlist": [
    {
      "url": "demo:shopify",
      "currency": "USD"
    },
    {
      "url": "demo:jsonld",
      "currency": "USD"
    }
  ]
}
```

# Actor output Schema

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

Default dataset: one item with events and observations for the watchlist.

# 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 = {
    "watchlist": [
        {
            "url": "demo:shopify",
            "currency": "USD"
        },
        {
            "url": "demo:jsonld",
            "currency": "USD"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tomwang/supplier-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 = { "watchlist": [
        {
            "url": "demo:shopify",
            "currency": "USD",
        },
        {
            "url": "demo:jsonld",
            "currency": "USD",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tomwang/supplier-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 '{
  "watchlist": [
    {
      "url": "demo:shopify",
      "currency": "USD"
    },
    {
      "url": "demo:jsonld",
      "currency": "USD"
    }
  ]
}' |
apify call tomwang/supplier-stock-monitor --silent --output-dataset

```

## MCP server setup

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