# Marketplace Listing Change Detector (`mehdi_badawi/marketplace-listing-truth`) Actor

Compare marketplace listing snapshots and get field-level new, changed, removed, unchanged, or unknown results. Failed or incomplete evidence never becomes a false removal.

- **URL**: https://apify.com/mehdi\_badawi/marketplace-listing-truth.md
- **Developed by:** [Mehdi Badawi](https://apify.com/mehdi_badawi) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 resolved listing comparisons

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

## Marketplace Listing Change Detector

Compare marketplace listing snapshots and get field-level `new`, `changed`,
`removed`, `unchanged`, or `unknown` results. Failed or incomplete evidence
never becomes a false removal.

### Start in 30 seconds

1. Select **Try for free** and run with no input for a labeled demo.
2. Supply current and prior listing snapshots with source coverage details.
3. Schedule repeat runs after your collector produces each new snapshot.

**Price:** $0.002 per resolved listing comparison, plus a $0.00005 start event.
Unknown, failed, incomplete-evidence, and demo results are free.

This Actor compares supplied snapshots. It does not collect Amazon, Walmart,
Etsy, or eBay listings or claim live status without complete upstream evidence.

### Why Listing-Truth Diff Matters

E-commerce sellers face recurring silent failures where platforms suppress, alter, or delist items without timely notification:

- **Etsy:** Silent search invisibility incidents where shops disappear from search rankings.
- **Walmart:** Items unpublished with specific reason codes (`INCORRECT_PRICE`, `POLICY_VIOLATION`) surfaced via the Unpublished Items API.
- **Amazon:** Buy Box hijacking, title overwrites, and bot detection blocking visibility checks.
- **eBay:** Silent delistings or VeRO actions where notifications arrive after the fact.

Generic diff tools and price monitors fail because they infer that an item missing from a failed or partial scrape has been "removed", or that a failed fetch means "unchanged". **This Actor enforces honest failure semantics:** when observation evidence is missing, incomplete, or blocked, it reports `unknown` and **never** false `unchanged` or `removed`.

***

### Core Architecture & Truth Semantics

The Actor follows a clean separation between pure deterministic rules and platform plumbing:

- `src/core/diff.mjs`: Pure evaluation core with no I/O, no wall clock, and no network dependencies.
- `src/core/canonical.mjs`: Stable listing identity resolution, fact normalization, and deterministic deep equality.
- `src/main.mjs`: Thin platform adapter that maps inputs from Apify into datasets and key-value store records.

#### Stable Listing Identity

Listing identity is canonicalized as:

```
<marketplace>/<listingId>
```

Examples: `walmart/55885233`, `amazon/B08N5WRWNW`, `etsy/1596891234`, `ebay/123456789012`. Marketplace names are normalized to lowercase trimmed strings; listing IDs support aliases (`listingId`, `asin`, `itemId`, `id`).

#### The 5 Diff Statuses

| Status | Definition |
|---|---|
| `new` | Listing present in current snapshot, absent in prior snapshot (with successful fetch). |
| `unchanged` | Listing present in both snapshots; all normalized facts are identical. |
| `changed` | Listing present in both snapshots; one or more facts differ (emits field-level before/after diffs). |
| `removed` | Listing present in prior snapshot, absent in current snapshot, **and current snapshot is complete and verified**. |
| `unknown` | Current snapshot or listing fetch failed or was incomplete; truthful status cannot be determined. |

#### Honest Failure Semantics

1. **Incomplete Current Snapshot (`complete: false`):** If a current snapshot is marked partial or incomplete, any listing in the prior snapshot not observed in current emits `unknown` with reason `snapshot-incomplete`. It is **never** emitted as `removed`.
2. **Failed Current Snapshot (`fetchStatus: "failed"`):** If the current snapshot fetch failed, absent listings emit `unknown` with reason `snapshot-fetch-failed`.
3. **Per-Listing Fetch Failure (`fetchStatus: "blocked"`, etc.):** If an anti-bot challenge or 403 blocks a listing fetch in the current snapshot, its verdict is `unknown` with reason `listing-fetch-failed`.

#### Exact Source Provenance

Every emitted row retains complete provenance for both current and prior observations:

- `sourceId`: Identifier of the source or connector.
- `mechanism`: Extraction method (`api`, `scrape`, `feed`, `manual`).
- `uri`: Endpoint or public listing URL.
- `fetchedAt`: Exact ISO-8601 timestamp of extraction.
- `fetchStatus`: `ok`, `failed`, `blocked`, `timeout`, etc.
- `httpStatus`: HTTP response code when applicable.
- `error`: Upstream error payload or message when fetch failed.

***

### Input Schema (`.actor/input_schema.json`)

```json
{
  "contractVersion": "1.0.0",
  "evaluationTime": "2026-09-21T16:00:00Z",
  "currentSnapshot": {
    "snapshotId": "snap-20260921-current",
    "timestamp": "2026-09-21T12:00:00Z",
    "complete": true,
    "source": {
      "id": "walmart-items-api",
      "mechanism": "api",
      "fetchStatus": "ok"
    },
    "listings": [
      {
        "marketplace": "walmart",
        "listingId": "55885233",
        "title": "Water Bottle 32oz",
        "price": { "amount": 24.99, "currency": "USD" },
        "status": "unpublished",
        "reasonCodes": ["POLICY_VIOLATION"]
      }
    ]
  },
  "priorSnapshot": {
    "snapshotId": "snap-20260920-prior",
    "timestamp": "2026-09-20T12:00:00Z",
    "complete": true,
    "listings": [
      {
        "marketplace": "walmart",
        "listingId": "55885233",
        "title": "Water Bottle 32oz",
        "price": { "amount": 19.99, "currency": "USD" },
        "status": "active"
      }
    ]
  }
}
```

***

### Dataset Item Shape (`.actor/dataset_schema.json`)

Each dataset item represents a single listing diff:

```json
{
  "contractVersion": "1.0.0",
  "key": "walmart/55885233",
  "marketplace": "walmart",
  "listingId": "55885233",
  "status": "changed",
  "changes": [
    {
      "field": "price",
      "before": { "amount": 19.99, "currency": "USD" },
      "after": { "amount": 24.99, "currency": "USD" }
    },
    {
      "field": "status",
      "before": "active",
      "after": "unpublished"
    }
  ],
  "facts": {
    "title": "Water Bottle 32oz",
    "price": { "amount": 24.99, "currency": "USD" },
    "status": "unpublished",
    "reasonCodes": ["POLICY_VIOLATION"]
  },
  "priorFacts": {
    "title": "Water Bottle 32oz",
    "price": { "amount": 19.99, "currency": "USD" },
    "status": "active"
  },
  "reasons": [],
  "provenance": {
    "current": { "sourceId": "walmart-items-api", "fetchStatus": "ok" },
    "prior": { "sourceId": "walmart-items-api", "fetchStatus": "ok" }
  },
  "evaluatedAt": "2026-09-21T16:00:00Z"
}
```

***

### Local Development & Testing

#### Running Tests

Tests use Node.js built-in test runner (`node:test`) and require no external dependencies, cloud accounts, or network access:

```bash
npm test
```

#### Running the Actor (Credential-Free)

Running the Actor without credentials or input executes built-in multi-marketplace reference snapshots demonstrating all 5 statuses:

```bash
npm start
```

#### Docker

Build and run using the standard Apify Node.js 22 runtime:

```bash
docker build -t marketplace-listing-truth .
docker run --rm marketplace-listing-truth
```

Listing snapshots may contain nonpublic prices or inventory. Minimize supplied
fields, preserve observed-at and evaluated-at timestamps, and delete datasets
under the customer's retention policy. Upstream authentication, proxy cost,
and collection rights remain outside this Actor. Support owner: Mehdi Badawi
through the Apify Store support channel, with an initial-response target of two
business days.

# Actor input Schema

## `contractVersion` (type: `string`):

Contract version (default: 1.0.0).

## `evaluationTime` (type: `string`):

ISO-8601 instant treated as evaluation time. If omitted, uses current run start time.

## `currentSnapshot` (type: `object`):

The current listing snapshot with optional snapshotId, timestamp, source attestation, and listings array.

## `priorSnapshot` (type: `object`):

The prior listing snapshot to compare against. If omitted, valid current listings are treated as new.

## `config` (type: `object`):

Optional diff knobs: priceTolerance, ignoreFields, strictProvenance.

## Actor input object example

```json
{
  "contractVersion": "1.0.0"
}
```

# Actor output Schema

## `diffRows` (type: `string`):

One dataset row per listing diff with status, changes, facts, and exact provenance.

## `runOutput` (type: `string`):

Aggregate run counts, per-status breakdown, and evaluation metadata.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("mehdi_badawi/marketplace-listing-truth").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("mehdi_badawi/marketplace-listing-truth").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 '{}' |
apify call mehdi_badawi/marketplace-listing-truth --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mehdi_badawi/marketplace-listing-truth"
        }
    }
}
```

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/KGNnjM4rD2GPjXx9t/builds/MR13fBFuBzBVpuAC2/openapi.json
