# Etsy Search Monitor - New Listings, Price & Rank Changes (`kaz_kakyo/etsy-search-delta`) Actor

Monitor Etsy searches and categories: new listings, disappeared listings, price changes, rank movement between runs. Persistent per-monitor snapshots. HTTP-only, no browser, no API key.

- **URL**: https://apify.com/kaz\_kakyo/etsy-search-delta.md
- **Developed by:** [Heim AI](https://apify.com/kaz_kakyo) (community)
- **Categories:** Automation, Agents, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 change events

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Etsy Search Monitor — New Listings, Price & Rank Changes

**Search/category in → change events out.** Monitor Etsy market keywords and category pages between runs; get new listings, disappearances from the watched window, price changes, and rank moves. HTTP-only, no browser. Built for **MCP agents, API clients, and scheduled monitors**.

| | |
|---|---|
| **Actor id** | `kaz_kakyo/etsy-search-delta` |
| **Minimal input** | `{ "queries": ["ceramic mug"], "monitorId": "default" }` |
| **Cost** | **$0.002 / observation** + **$0.002 / change event** |
| **Output** | Dataset rows: `observation`, `change-event`, `error`, `summary` |

### Call it (MCP / API / schedule)

#### MCP (agents)

```json
{
  "actor": "kaz_kakyo/etsy-search-delta",
  "input": {
    "queries": ["ceramic mug"],
    "monitorId": "default"
  }
}
```

Category window (≈64 listings/page):

```json
{
  "categoryUrls": ["https://www.etsy.com/c/jewelry/necklaces"],
  "monitorId": "necklaces-daily",
  "maxPagesPerTarget": 2,
  "rankMoveThreshold": 5
}
```

After the run, read the default dataset. Every row has a `type` discriminator — filter on `"change-event"` / `"observation"`; treat `"error"` as per-target failure. Bad hosts, empty input, and missing market pages become error rows and the run still **SUCCEEDS** so agent mistakes do not look like platform outages.

#### API / `apify-client`

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('kaz_kakyo/etsy-search-delta').call(
  { queries: ['ceramic mug'], monitorId: 'default' },
  { maxTotalChargeUsd: 1.0 }, // hard budget for this run
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const changes = items.filter((i) => i.type === 'change-event');
const observations = items.filter((i) => i.type === 'observation');
```

Same shape via REST: `POST /v2/acts/kaz_kakyo~etsy-search-delta/runs` with your token, then poll or attach a webhook.

#### Schedule recipe (the core loop)

This actor is a **monitor**: one run = one sample of the watched window. Diffs appear only when the same `monitorId` runs again against the stored snapshot.

1. **Save a Task** with fixed `queries` / `categoryUrls`, a stable `monitorId`, and your `rankMoveThreshold` / `maxPagesPerTarget`.
2. **Schedule the Task** (e.g. hourly or daily). Same `monitorId` every tick — that is what links runs.
3. **Webhook on `SUCCEEDED`** → your endpoint / Zapier / Make. Process `type === "change-event"` rows; ignore or log `error` / `summary`.
4. **Cap spend** with `maxTotalChargeUsd`. When the cap hits, remaining work becomes uncharged `type: "error"` skipped rows — no surprise bill.
5. First scheduled tick establishes the **baseline** (observations only, no change events). From the second tick onward you get deltas.

Long runs checkpoint finished targets and aggregates. A platform migration resumes without double-billing: completed targets are skipped, and a migration that hits mid-delivery is resolved in the never-double-bill direction (rows for that target in that run may be missing — an uncharged error row says so). Overlapping runs with the same `monitorId` are lease-guarded: the second run soft-skips (uncharged) instead of billing the same delta twice.

### Input reference

| Field | Type | Default | Notes |
|---|---|---|---|
| `queries` | string\[] | — | Keywords → `https://www.etsy.com/market/<slug>`. Covers Etsy's **market** page (~8 results/page — top-of-search window), not the full `/search` backend. Prefill: `["ceramic mug"]`. |
| `categoryUrls` | string\[] | — | `/c/...` or `/market/...` URLs. Locale prefixes (`/sg-en/`) stripped. `https://www.etsy.com/search?q=X` is **silently converted** to the market page for X. Other hosts → uncharged error row. |
| `monitorId` | string | `"default"` | Snapshot namespace. Display form sanitized to `[a-zA-Z0-9-_]` (max 60); storage keys also hash the original id, so distinct ids never collide after sanitization. |
| `maxPagesPerTarget` | int 1–5 | `1` | Category ≈ 64 listings/page; market ≈ 8/page. |
| `rankMoveThreshold` | int ≥1 | `5` | Emit rank-move only when |Δrank| ≥ threshold. |
| `maxObservationsPerRun` | int 1–5000 | `2000` | Fetch stops before exceeding. |

At least one of `queries` / `categoryUrls` is required at runtime (schema marks both optional so the Console form stays flexible). Max **25 targets** per run; overflow → uncharged error rows.

### Output contract

Every dataset row includes `type`. Filter on it.

#### `type: "change-event"` (charged)

```json
{
  "type": "change-event",
  "changeType": "new | disappeared | price-change | rank-move",
  "monitorId": "default",
  "targetKey": "q:ceramic_mug",
  "listingId": "1234567890",
  "title": "…",
  "url": "https://www.etsy.com/listing/1234567890",
  "image": "https://…",
  "shopName": null,
  "shopId": "21401620",
  "price": 24.0,
  "currency": "USD",
  "inStock": null,
  "rank": 3,
  "oldPrice": 22.0,
  "newPrice": 24.0,
  "priceDelta": 2.0,
  "priceDeltaPct": 9.09,
  "oldRank": 10,
  "newRank": 3,
  "rankDelta": -7
}
```

One listing may emit several change events (e.g. price-change + rank-move). `rankDelta` negative = improved (moved up). Price fields only on `price-change`; rank fields only on `rank-move`.

#### `type: "observation"` (charged)

```json
{
  "type": "observation",
  "monitorId": "default",
  "targetKey": "q:ceramic_mug",
  "listingId": "1234567890",
  "title": "…",
  "url": "https://www.etsy.com/listing/1234567890",
  "image": "https://…",
  "shopName": null,
  "shopId": "21401620",
  "price": 24.0,
  "currency": "USD",
  "inStock": null,
  "rank": 3,
  "fetchedVia": "datacenter"
}
```

#### `type: "error"` (never charged)

```json
{ "type": "error", "monitorId": "default", "targetKey": "…", "url": "…", "error": "…" }
```

#### `type: "summary"` (never charged)

```json
{
  "type": "summary",
  "monitorId": "default",
  "runId": "…",
  "targets": 1,
  "targetsProcessed": 1,
  "observations": 8,
  "changeEvents": { "new": 0, "disappeared": 0, "price-change": 0, "rank-move": 0 },
  "baselineTargets": 1,
  "errors": 0,
  "chargeLimitReached": { "observation": false, "change-event": false },
  "residentialFetches": 0,
  "runAt": "2026-…"
}
```

### Delta semantics

- **Baseline**: first **complete** fetch per `(monitorId, target)` writes a snapshot and emits **no** change events (`baselineTargets` in summary). Incomplete fetches never baseline.
- **new**: listing id present now, absent before.
- **disappeared**: present before, absent now — means **left the monitored window**, not necessarily delisted.
- **price-change**: same listing, same currency, |Δprice| > 0.009. Currency mismatch → skip that listing's price compare (counted in summary). A transiently missing price/title does **not** erase the previously known value, so recovery never fabricates a change.
- **rank-move**: |Δrank| ≥ `rankMoveThreshold`. Rank is 1-based across fetched pages in order.
- **Coverage guard**: change events are emitted only when the window fetch was complete **and** at least as broad as the previous snapshot. If a page fails, degrades, or coverage shrinks (including lowering `maxPagesPerTarget`), the run delivers observations only, keeps the prior snapshot, and explains in an uncharged error row — no false disappearances, ever.
- **All-or-nothing billing per target**: if the remaining `maxTotalChargeUsd` budget cannot cover a target's whole batch, that batch is withheld uncharged (and re-detected next run) rather than partially billed.
- Window sizes: category ≈ **64**/page; keyword market ≈ **8**/page.

### Honest limits

- Top-of-window monitoring only — **not** the full Etsy `/search` backend (hard-blocked). Keyword monitoring uses `/market/<slug>`.
- `/search?q=…` URLs are auto-converted to market pages.
- Currency is whatever the US datacenter response serves (normally USD).
- **shopName** is typically `null` on platform runs (US-served cards do not render shop names). Use **`shopId`** (numeric string, stable, public) as the reliable shop identity key. Shop name/id are public shop identifiers — no seller personal data.
- **inStock** is `null` when the served page variant lacks availability data (typical on the platform). Boolean only when Etsy serves rich JSON-LD (some non-US egress).
- **price** is the current (sale) price shown on the card; discounted cards expose sale first, original second — we keep sale.
- Listing-detail fields (description, variants, reviews) are out of scope — SERP/window cards only.
- **disappeared ≠ delisted** — the listing may have moved below your page window.
- Occasional stray 403s are retried (fresh headers, then optional residential). Cookie jars are never used (a blocked DataDome cookie would poison later requests). Shell/challenge pages (200 HTML with no parseable ItemList) are treated as fetch failures — they can never fabricate change events or overwrite snapshots.
- Nonsense market slugs: true HTTP 404 and an empty first page produce the "no market page" error. Etsy sometimes soft-200s a nonsense slug with a small fallback shelf of real listings; those are monitored as served (they are real listings on that market URL).
- HTTP redirects are followed only to `https://(www.)etsy.com` — anything else is blocked (SSRF guard).
- Error rows echo user-supplied URLs only after conservative redaction — userinfo removed, every query value and the fragment replaced, and any path segment that follows a credential word (`/reset/…`, `/token/…`) or is an opaque high-entropy blob masked. Credentials or tokens pasted into a URL never reach the dataset, and the same secrets are scrubbed out of free-text error messages.
- Keywords keep letters and digits of every script (`猫 mug` stays `猫 mug`); punctuation and symbols are dropped. A keyword is never silently replaced by a subset of itself, so you are never billed for a query you did not ask for.
- If a previous run's delivery was interrupted and it cannot be established whether its change events were billed, this actor neither suppresses nor re-bills them: the window is re-detected and its change events are delivered **free of charge**, with an error row saying so.
- One run per `monitorId` at a time (lease-guarded); overlapping runs soft-skip uncharged.
- If your window genuinely shrinks (e.g. you lower `maxPagesPerTarget`), the old wider snapshot is kept and change events pause until coverage matches it again — start a fresh `monitorId` to re-baseline at the narrower window.

### Pricing

| Event | Price | When |
|---|---|---|
| Listing observation | **$0.002** | One listing captured in the monitored window this run |
| Change event | **$0.002** | One detected change vs the previous snapshot |
| Actor start | platform default | Per run |

Error / summary rows are **never** billed. Cap spend with `maxTotalChargeUsd` on the run or task. Charging is atomic charge-on-write: a row is billed exactly when it is written, and any ambiguity (migration or write failure mid-delivery) is always resolved in your favor — never a double charge.

**Runs always SUCCEED on bad input** — check `type: "error"` rows.

***

*If this saved you time, a Store review on the [actor page](https://apify.com/kaz_kakyo/etsy-search-delta) helps a solo dev. Hit a problem? [Open an issue](https://apify.com/kaz_kakyo/etsy-search-delta/issues).*

# Actor input Schema

## `queries` (type: `array`):

Keywords to monitor. Each maps to Etsy's market page (`/market/<slug>`) — the top-of-search window (~8 results/page), not the full `/search` backend. Spaces become underscores. Provide at least one of `queries` or `categoryUrls`.

## `categoryUrls` (type: `array`):

Etsy category (`/c/...`) or market (`/market/...`) URLs. Locale prefixes (e.g. `/sg-en/`) are normalized away; query params are stripped (pagination is handled by the actor). `https://www.etsy.com/search?q=X` is accepted and silently converted to the market page for X. Any other host/path yields an uncharged error row; the run still succeeds.

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

Snapshot namespace. Runs that share the same monitorId diff against each other. Non-alphanumeric characters (except `-` `_`) are stripped; max 60 chars.

## `maxPagesPerTarget` (type: `integer`):

Pages to fetch per target (sequential). Category ≈ 64 listings/page; market/keyword ≈ 8/page.

## `rankMoveThreshold` (type: `integer`):

Emit a rank-move change event only when |Δrank| is at least this value.

## `maxObservationsPerRun` (type: `integer`):

Hard cap on listing observations this run. Fetching stops before exceeding it.

## Actor input object example

```json
{
  "queries": [
    "ceramic mug"
  ],
  "monitorId": "default",
  "maxPagesPerTarget": 1,
  "rankMoveThreshold": 5,
  "maxObservationsPerRun": 2000
}
```

# Actor output Schema

## `changes` (type: `string`):

New listings, disappeared listings, price moves and rank moves since the previous run — the delta this actor bills for. Change rows carry changeType and the old/new/delta columns; the run's snapshot rows share the view with those columns empty.

## `observations` (type: `string`):

Every listing captured in the monitored window this run, with price, shop, rank and image — the full snapshot the changes above were computed against.

# 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 = {
    "queries": [
        "ceramic mug"
    ],
    "monitorId": "default"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kaz_kakyo/etsy-search-delta").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 = {
    "queries": ["ceramic mug"],
    "monitorId": "default",
}

# Run the Actor and wait for it to finish
run = client.actor("kaz_kakyo/etsy-search-delta").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "ceramic mug"
  ],
  "monitorId": "default"
}' |
apify call kaz_kakyo/etsy-search-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kaz_kakyo/etsy-search-delta",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/aEX9MC2PMj8B8Hxw4/builds/LfTzLTbUT7y1WEsdx/openapi.json
