# GPSR Storefront Monitor - Product Safety Visibility & Changes (`mrkrokko/gpsr-storefront-monitor`) Actor

Check how product-safety information appears on public product pages in desktop and mobile. Field-level evidence and change detection across scheduled runs. Technical screening, not legal certification.

- **URL**: https://apify.com/mrkrokko/gpsr-storefront-monitor.md
- **Developed by:** [Alex O](https://apify.com/mrkrokko) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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

## GPSR Storefront Monitor

Technical visibility check for product-safety information on public product pages — desktop and mobile request profiles, field-level evidence, change detection across scheduled runs.

> Technical screening only. No legal advice, no certification, no guarantee of legal conformity.
> `OBSERVED` explicitly does not mean "GPSR-compliant".

### What it does

- Checks **one shop origin per run**: direct product URLs (up to 500) or bounded sitemap discovery.
- Observes ten fields per product and viewport: manufacturer name / postal / electronic contact, responsible-person name / postal / electronic contact, product identifier, type/description, image, safety warning text.
- Distinguishes `VISIBLE`, `REVEALED`, `LINK_ONLY`, `HIDDEN_ONLY`, `NOT_DETECTED`, `AMBIGUOUS`, `NOT_ASSESSED` — with text evidence excerpts, selectors and content hashes.
- Compares monitor runs against immutable snapshots in a per-customer named key-value store (`REGRESSION` / `IMPROVEMENT` / `CONTENT_CHANGED` / `VISIBILITY_CHANGED`), aggregates desktop/mobile differences.
- Writes one dataset row per technically completed product × viewport, plus `REPORT.html`, `SUMMARY.json`, `ERRORS.json`, `UNASSESSED.json` into the default key-value store. Technical errors are free records, never billable rows.

### Quick start

1. Open the Actor, set **shopOrigin** (exact HTTPS origin, e.g. `https://shop.example`).
2. Add product URLs **or** switch to sitemap discovery with path filters.
3. Start. Results appear in the **Dataset** tab, the report under **Key-Value Store → REPORT.html**.
4. For repeat checks: set `mode=monitor` + stable `monitorId`, save as a **Task**, attach a **Schedule** (e.g. weekly). One monitor = one shop/language/config fingerprint.

### Examples

Direct URLs (audit):

```json
{
  "mode": "audit",
  "shopOrigin": "https://shop.example",
  "discoveryMode": "urls",
  "productUrls": [{ "url": "https://shop.example/products/arbeitsleuchte", "externalId": "SKU-1001" }],
  "maxProducts": 25,
  "locale": "de-DE",
  "reportLanguage": "de"
}
```

Sitemap discovery (audit):

```json
{
  "mode": "audit",
  "shopOrigin": "https://shop.example",
  "discoveryMode": "sitemaps",
  "sitemapUrls": ["https://shop.example/sitemap.xml"],
  "productPathIncludes": ["/products/"],
  "maxProducts": 25,
  "locale": "de-DE",
  "reportLanguage": "de"
}
```

Monitor task (re-run via Schedule):

```json
{
  "mode": "monitor",
  "monitorId": "kunde-produktseiten-de",
  "shopOrigin": "https://shop.example",
  "discoveryMode": "urls",
  "productUrls": [{ "url": "https://shop.example/products/arbeitsleuchte" }],
  "retainedSnapshots": 12,
  "maxRuntimeSecs": 900
}
```

### Rendering engines

- **static** (default): fast HTTP observation of the served HTML. Cheap, low memory.
- **browser**: real Chromium with JavaScript rendering, bounded scrolling, safe
  disclosure clicks (`summary`/`details`, `button[type=button]`, labelled tabs —
  never links, carts, forms or logins) and one compressed viewport screenshot per
  auffälligem profile (`SHOT-<resultId>.jpg` in the key-value store, WebP preview
  embedded in `REPORT.html`). Fields that appear only after a click are marked
  `REVEALED` with the click logged in `interactions`.
- Browser runs need **4096 MB run memory** (`memoryMbytes=4096` on the run) and take
  longer (up to 45 s per product × viewport). Monitor histories are kept strictly
  separate per engine (different comparison fingerprints).

### Billing

Actor is **public** (`mrkrokko~gpsr-storefront-monitor`). Planned store price:
**0.025 USD per completed product × viewport** (25 USD per 1,000 profile checks;
desktop + mobile = 2 checks per product). Billed via the native synthetic dataset
event — errors, summaries, aliases and unassessed units are never dataset rows.
To activate monetization: Apify Console → Actor → Monetize → Pay per event →
price the `apify-default-dataset-item` event at 0.025 USD, then publish to the
Store. Measured infra (Sept 2026): static ~0.00032 CU/profile, browser
\~0.0016–0.003 CU/profile — re-validate the margin against the current CU rate
before pricing (see `BENCHMARKS.md`).

### Storage & retention

- Default dataset: billable profile rows only.
- Default key-value store: `REPORT.html`, `SUMMARY.json`, `ERRORS.json`, `UNASSESSED.json`, `RUN_STATE.json`, `RESULT-<id>.json` checkpoints, `EV-<id>.json` evidence files, `PUBLISH_JOURNAL.json`.
- Named monitor store `gpsr-v1-<hash>`: immutable `SNAP-*` snapshots + `OWNER.json`. Keeps the youngest `retainedSnapshots` plus the last 2 hours. Deleting the named store resets the monitor.
- No developer telemetry, no copies to other accounts. Delete run storages / the monitor store manually to erase data.

### Limits

See `LIMITATIONS.md` (exact supported scope) and `SECURITY.md` (network boundaries, sandbox). Key points: two rendering engines (static HTTP / Chromium); screenshots only in browser mode; HTTPS-only same-origin checks through a pinned local egress proxy; `robots.txt` respected; no logins, no carts, no form submissions; linked PDFs referenced but not read.

# Actor input Schema

## `mode` (type: `string`):

audit = one-off check. monitor = compare against history (requires monitorId, stable Task + Schedule).

## `monitorId` (type: `string`):

Stable name for the comparison history, 3-64 chars \[a-z0-9\_-]. Must be empty in audit mode.

## `shopOrigin` (type: `string`):

Exactly one allowed HTTPS origin without path, e.g. https://shop.example. www and non-www are NOT equated.

## `discoveryMode` (type: `string`):

urls = direct product pages. sitemaps = discover candidates from sitemaps.

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

Direct product pages (1-500 in urls mode). Each item: {url, externalId?}. Plain URL strings are also accepted.

## `sitemapUrls` (type: `array`):

Explicit sitemaps on shopOrigin (1-5 in sitemaps mode).

## `productPathIncludes` (type: `array`):

Required in sitemaps mode. Literal substrings in the URL path (no regex), max 10.

## `maxProducts` (type: `integer`):

Upper limit for selected product URLs (1-500). Both viewports are always checked per product.

## `locale` (type: `string`):

Browser locale for requests. The actual page language is recorded separately.

## `reportLanguage` (type: `string`):

Report language. Does not change the detection rules.

## `evidenceMode` (type: `string`):

issues = store text evidence excerpts for auffällige profiles. Text evidence in the dataset is always active.

## `rendering` (type: `string`):

static = fast HTTP observation (served HTML). browser = real Chromium with JS rendering, safe reveal clicks and screenshots (slower, needs 4096 MB run memory). Both viewports are always checked either way.

## `selectorProfile` (type: `object`):

Optional explicit mapping of product areas (standard CSS selectors only, max 300 chars each). A broken explicit selector is reported, never silently replaced.

## `expectations` (type: `array`):

User-declared product information per URL (scope, manufacturerJurisdiction, warningsRequired, expectedTexts). Max 500 entries.

## `retainedSnapshots` (type: `integer`):

Number of most recent monitor snapshots to keep (2-30). Snapshots from the last 2 hours are always kept.

## `maxRuntimeSecs` (type: `integer`):

Internal runtime limit (60-3600). The platform timeout may apply earlier.

## Actor input object example

```json
{
  "mode": "audit",
  "shopOrigin": "https://shop.example",
  "discoveryMode": "urls",
  "productUrls": [
    {
      "url": "https://shop.example/products/arbeitsleuchte",
      "externalId": "SKU-1001"
    }
  ],
  "maxProducts": 25,
  "locale": "de-DE",
  "reportLanguage": "de",
  "evidenceMode": "issues",
  "rendering": "static",
  "retainedSnapshots": 12,
  "maxRuntimeSecs": 900
}
```

# Actor output Schema

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

Structured profile results in the default dataset: one billable row per technically completed product URL x viewport (desktop + mobile).

## `report` (type: `string`):

Self-contained agency report (REPORT.html) with summary, coverage, findings, changes, viewport differences, errors and limitations.

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

Balanced run accounts (SUMMARY.json): selected/planned/completed/error/unassessed profiles, published rows, baselines, stop reason.

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

Inaccessible or unassessable pages (ERRORS.json). These are free technical errors, not billable results.

## `unassessed` (type: `string`):

Units excluded by the user, dropped by limits/redirects/robots, or cut by budget/time (UNASSESSED.json).

# 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 = {
    "shopOrigin": "https://shop.example",
    "productUrls": [
        {
            "url": "https://shop.example/products/arbeitsleuchte",
            "externalId": "SKU-1001"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mrkrokko/gpsr-storefront-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 = {
    "shopOrigin": "https://shop.example",
    "productUrls": [{
            "url": "https://shop.example/products/arbeitsleuchte",
            "externalId": "SKU-1001",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("mrkrokko/gpsr-storefront-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 '{
  "shopOrigin": "https://shop.example",
  "productUrls": [
    {
      "url": "https://shop.example/products/arbeitsleuchte",
      "externalId": "SKU-1001"
    }
  ]
}' |
apify call mrkrokko/gpsr-storefront-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mrkrokko/gpsr-storefront-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/7XGbK1nfau4CsvXTc/builds/AlUvhov2nOR259rof/openapi.json
