Etsy Search Monitor - New Listings, Price & Rank Changes
Pricing
from $2.00 / 1,000 change events
Etsy Search Monitor - New Listings, Price & Rank Changes
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.
Pricing
from $2.00 / 1,000 change events
Rating
0.0
(0)
Developer
Heim AI
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
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)
{"actor": "kaz_kakyo/etsy-search-delta","input": {"queries": ["ceramic mug"],"monitorId": "default"}}
Category window (≈64 listings/page):
{"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
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.
- Save a Task with fixed
queries/categoryUrls, a stablemonitorId, and yourrankMoveThreshold/maxPagesPerTarget. - Schedule the Task (e.g. hourly or daily). Same
monitorIdevery tick — that is what links runs. - Webhook on
SUCCEEDED→ your endpoint / Zapier / Make. Processtype === "change-event"rows; ignore or logerror/summary. - Cap spend with
maxTotalChargeUsd. When the cap hits, remaining work becomes unchargedtype: "error"skipped rows — no surprise bill. - 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)
{"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)
{"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)
{ "type": "error", "monitorId": "default", "targetKey": "…", "url": "…", "error": "…" }
type: "summary" (never charged)
{"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 (baselineTargetsin 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
maxTotalChargeUsdbudget 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
/searchbackend (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
nullon platform runs (US-served cards do not render shop names). UseshopId(numeric string, stable, public) as the reliable shop identity key. Shop name/id are public shop identifiers — no seller personal data. - inStock is
nullwhen 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 (
猫 mugstays猫 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
monitorIdat 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 freshmonitorIdto 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 helps a solo dev. Hit a problem? Open an issue.