# Changelog of eBay Listings Scraper & Price Tracker (`myth84/ebay-search-scraper`) Actor

- **URL**: https://apify.com/myth84/ebay-search-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/myth84/ebay-search-scraper.md

### Changelog

History of `ebay-search-scraper` (folder `actors/ebay-search`). Versions are the Apify build numbers.

#### Unreleased (repair: prices in the marketplace currency)

- **Symptom** (brief `docs/cloud-tasks/2026-10-21_ebay-currency-proxy-country.md`, 2026-10-21): in the control run `CpWgj7LRcws7BC7oZ` of `marketplace-price-compare-scraper` (country US, currency USD), the 100 eBay rows of "wireless earbuds" on www.ebay.com were in KRW (e.g. `800578188879` at 39,960.18 KRW), the two other eBay searches in USD. eBay converts the prices of a site into the currency of the visitor's country, and the default proxy (Apify residential, no country) had left from South Korea for that search. This Actor had the same default: its controls passed by chance, and a monitoring run could have compared two currencies.
- **Exit country = marketplace country**: when the proxy is Apify `RESIDENTIAL` without `apifyProxyCountry`, the run's proxy gets the country of the marketplace of the first search (`MARKETPLACES[code].country`: US, GB, DE, FR, IT, ES, NL, CA, AU; a URL-only input on ebay.de exits in DE), through `withProxyCountry` in the runner's `parseInput` hook (`packages/core` unchanged; `parseActorInput` itself is unchanged). A country, proxy URLs, another group or no proxy set by the customer are kept as they are. The start log line gains `proxyCountry`. With search URLs of several eBay sites in one run, the exit country follows the first search, like the Accept-Language header.
- **Currency guard** in the crawler: a result page whose listings are mostly priced in another currency than the marketplace's (read as the rows read it, `pageCurrency`) is asked again on a new session (new proxy session, new cookie jar), 3 times at most (`MAX_CURRENCY_ROTATIONS`), after `delayMs`; the search goes on with the session that answered. If the page stays in another currency, it is kept, its rows carry the currency they show, a warning says so once per search, and the next pages of that search are kept without new sessions (at most 3 extra requests per search). No new session when the proxy exits in another country set by the customer, or without a proxy. The extra requests and their bytes enter `RUN_STATS` (requests, bytes, blocked), `maxRequests` is respected, the guard reuses its parse of the page (no second parse). New counters `RUN_STATS.extra.currencyRotations` and `RUN_STATS.extra.pagesInOtherCurrency` (0 when every page is in the right currency).
- **Unchanged when the currency is right**: the digests of the default input, the 3 committed inputs and the example tasks are the same (new counters and the currency warning left out of the digest: the test serves the US capture to the UK, DE and FR tasks without a proxy).
- Tests: country of every eBay site in the proxy the runner builds, country / URLs / group / no proxy of the customer kept, first-search rule, committed inputs exit in the US; `pageCurrency` on every capture and on a **derived KRW page** (`derivedKrwPage` in `src/test-utils.ts`: the US capture with every dollar amount × 1,400 in KRW, not a capture); KRW page then USD page after one new session (USD rows identical to a run that got dollars at once, page 2 on the new session); KRW pages that persist (3 new sessions, KRW rows, one warning per search); exit country fixed elsewhere, no proxy, the marketplace's own country; an unusable new session counted as blocked; `maxRequests`.
- README (step 2 of *How to scrape*, `proxyConfiguration` row, new *Prices in the marketplace currency* limit, proxy FAQ) and the `proxyConfiguration` description of the input schema: "leave the proxy country empty, the proxy exits in the marketplace's country" replaces "do not set a proxy country, eBay serves any site from any country".

#### Unreleased (quality score: input documented for AI agents)

- Brief `docs/cloud-tasks/2026-10-20_studio-quality-score-insights.md` (2026-10-21): the input schema passes the shared checks of `@scraper-studio/core/testing` (`runInputQualityChecks`, `runSearchTextChecks`), the rules of Apify's quality score and of what an AI agent receives through the Apify MCP server.
- Descriptions rewritten so that each one states its default and the reason: `searchQueries` (the default only serves runs started without any keyword or URL), `startUrls`, `marketplace`, `maxResults` (also its range and the form prefill), `sortBy`, `condition`, `buyingFormat`, `freeShippingOnly`, `excludeWords`, `itemLocation`, `includeSponsored`, `resultsPerPage` (fewer requests and a faster run, no longer "costs less"), `maxPages`, `onlyChanges`, `monitorStoreName` (with an example), `maxRequests`, `delayMs`, `debugStoreHtml`; `priceMin` and `priceMax` gain an example and eBay's parameter (`_udlo`, `_udhi`), `priceMax` at least `priceMin`.
- Bounds added to the form: `maximum` 1,000,000,000 on `priceMin` and `priceMax` (the code accepts any non-negative amount; no realistic value reaches the bound), `maximum` 999,999,999 on `categoryId` (the code's own bound).
- `maxResults` is prefilled with 20 in the form (the default stays 100): the prefilled input Apify tests daily is exactly `inputs/daily-check.json` (20 listings, wireless earbuds, ebay.com, residential proxy), which is unchanged, so the live daily-check task needs no update.
- `store.json` gains `searchSynonyms` (eBay scraper, eBay price tracker, eBay search results, competitor price monitoring), all already within the first 3,000 README characters; no change to the title, the descriptions or the README.
- **Example task** `ebay-wireless-earbuds-headphones-category-us`: SEO title rewritten as the long-tail search it targets ("eBay wireless earbuds category, no cases – US prices"); to republish with `scripts/publish-tasks.mjs --actor actors/ebay-search --test --publish --send` (Mac).
- No code change: rows, logs and charges are identical.

#### Unreleased (filters and sold listings)

- **Three search filters** (brief `docs/cloud-tasks/2026-10-20_ebay-search-extend-filters.md`, 2026-10-20), translated into parameters of the search page already read (`/sch/i.html`), off by default: `categoryId` (integer, eBay's numeric category id) → `_sacat`; `excludeWords` (list of words or phrases) → appended to `_nkw` with eBay's minus sign (`-case`, `-"ear tips"`; a leading minus or quotes typed by the customer are removed, duplicates and exclusions already in a pasted URL's words are not repeated); `itemLocation` (`any` default, `domestic`, `continent`, `worldwide`) → `LH_PrefLoc=1|5|2`. On a pasted URL, its own `_sacat` and `LH_PrefLoc` are kept unless the input sets them, and the exclusions are added to its words. None of these parameters is in the captures: they are marked "not verified yet" in the input form, the README and the example task, and "to confirm at the first run" in the start log line (shown only when set). The brief named the list `excludeKeywords`; it is **`excludeWords`** because the Store quality score reads field names containing "key" as secrets (`SENSITIVE_INPUT_NAME` of the Store checks, the AliExpress `monitorKey` recommendation); the README switching table maps `excludeKeywords`, `category` and `locatedIn` to the new fields.
- **Sold and completed listings** (audit `docs/audits/ebay-sold.md`, NO-GO: sign-in wall): a start URL with `LH_Sold` or `LH_Complete` set (any case, any value but `0`) is refused when the input is read, with "eBay shows sold and completed listings only to signed-in users; this Actor never signs in" and the advice to remove the filters; a response that ends on `signin.ebay.<tld>` or carries `sgfl=srch` (also through the `ru` return URL of a page in between) stops that search with the same message: one request only (the session rotation is not followed by a second request), no `_ipg` fallback, no monitor record update (fail-closed, also when earlier pages were read), the next searches run as usual, `RUN_STATS.extra.signInWalls` counts the stopped searches, and a run that pushed nothing ends with that message instead of the generic "blocked" one.
- **Unchanged without the new options**: SHA-256 digests of the request URLs, rows, charged batches, log lines, counters and monitor records of the default input, the 3 committed inputs and the 9 example tasks of 0.1.1, computed on the base commit before the change, are checked by a test; the monitor record keys of earlier searches are unchanged (the new filters enter the search identity only when set). `RUN_STATS.extra` gains the counter `signInWalls` (0); the task building moved from `main.ts` to `searchTasks` in `input.ts` (tested).
- README: input table, *Limits* (sold items, what is not verified yet), FAQ *Can I filter by category or excluded words, or use my own eBay search URL?* (replaces *Can I use my own eBay search URL with filters?*) and **Can I get sold listings?**; the export / support FAQ entry moves to a **Support** line under *Integrations and API* to keep 10 questions. Example task `ebay-wireless-earbuds-headphones-category-us` (category 112529, the leaf category of the ebay.com captures, cases and ear tips excluded, items located in the US; 10 tasks, the platform limit). Tests: translation of each field, combination with a pasted URL and with the other filters, input normalization, refusal of sold URLs, sign-in redirect on the first and a later page (flagged or not by the core's login-redirect detection), run failure message, monitor fail-closed.

#### Unreleased (repair: condition and sold counts)

- **Condition read by vocabulary, not by position** (brief `docs/cloud-tasks/2026-10-20_ebay-search-repair-condition.md`, 2026-10-20). The agent panel of 2026-10-20 (scenarios s11 "used iPhone 13" and s12 "nintendo switch") got `attributes.condition` = "Apple ·" or "Sony PlayStation 5 ·": the first subtitle part was taken as the condition, while phone and console cards open their subtitle with catalogue aspects (brand, model, storage). The subtitle is now split on its separators (`|`, `·`) and every part, then every whole attribute line, is matched against eBay's condition labels (en-US / en-GB / en-AU, de, fr as captured, plus the usual it, es, nl labels). **`attributes.condition` changes meaning**: it now holds the normalized grade, `new`, `open_box` ("Open Box", "New (Other)", "Neu (Sonstige)", "Opened – never used", "Ouvert (jamais utilisé)": eBay's condition 1500), `used` (including the used grades "Like New", "Very Good"…), `refurbished` (every refurbished grade, tried first) or `for_parts`; the label as displayed moves to the new key `attributes.conditionText`. Both keys are absent when no part is a condition label: never a brand or a model.
- **`brand`** (always null before): the first part of an aspect subtitle ("Apple · iPhone 13 · 128 GB") when it is one word of letters starting with a capital, not a condition, seller kind or generic aspect ("Unlocked", colours), and repeated as a word in the title; null otherwise ("Sony PlayStation 5" names a platform). eBay prints member names in lower case, so a pseudonym never passes.
- **`soldCount`**: the "N sold" / "N verkauft" / "N vendus" motif is read anywhere in an attribute line ("Buy It Now · 1,498 sold"), also in a price or badge row (text read with a space between nodes, so "eBay Refurbished" + "2,597 sold" does not glue into "…d2,597"), the number glued to no letter or digit; a count of recent sales ("12 sold in last 24 hours") is not taken for the total. On the 7 committed captures every motif was already read (74 of 240 rows); the panel's 1 of 20 on "nintendo switch" (single used consoles) is to be compared at the first run.
- Tests: every condition label of the fixtures and of the brief, non-conditions (brands, models, "New Balance", "Used Cars", seller kinds), phone and console cards in the markup of the captures (not captured: synthetic), pseudonym guard, one `soldCount` per motif on each fixture with the same numbers, and SHA-256 digests of every fixture's rows without `attributes.condition`, `attributes.conditionText` and `brand`, computed on 0.1.1 before the repair (every other column unchanged). README (columns, input table, sample output, FAQ on price, condition, units sold and shipping from abroad) and dataset field descriptions updated; the dataset schema is regenerated (descriptions and example only).

#### Unreleased (README visibility)

- **README first screen and links** (brief `docs/cloud-tasks/2026-10-20_studio-readme-visibility.md`, 2026-10-20). The Store search indexes only the first ~3,000 README characters, so they now hold: an opening sentence with the phrase agents search and its synonyms, a "No login, no cookies, no personal data" line, the Free-plan price in figures in *How much* ($4.32 per 1,000 listings; Apify's $5 monthly credit covers about 1,157), computed from `pricing/2026-10-20_pay-per-event.autopilot.json` (Free tier of the primary event, one `apify-actor-start` event deducted), and a real output snippet (10 key columns of the README example row). Further down: *Use it from your AI assistant* (MCP URL `https://mcp.apify.com?tools=myth84/<name>` and a ready-to-copy prompt with a minimal input), *Alternatives and switching* (official API, home-made scraper, input-name mapping table, no competitor named), *More scrapers by Flatrow Data Studio* (Store links to the other Actors of the studio) with an invitation to rate the Actor (no counterpart), and the non-affiliation line at the very bottom. The cost section and the MCP cost bullet quote the same figures; the line 1 heading is unchanged. Checked by `runStoreChecks` (`readmeVisibility`): keyword in the opening prose, snippet equal to the example row and within 3,000 characters, figures equal to the pricing file in force (a new grid fails the test until the README follows), MCP block with a valid input, links to existing Actors of the same studio only, no `fpr=`. No code change: rows, logs and charges are identical.

#### Unreleased

- **Price monitoring** (brief `docs/cloud-tasks/2026-10-20_ebay-search-extend-monitor.md`, 2026-10-20, modelled on the AliExpress Actor): `monitorPrices` (off by default), `onlyChanges` (off by default) and `monitorStoreName` (default `ebay-price-monitor`); previous prices kept in the customer's named key-value store, one record per search identity (keywords or URL, marketplace, sort, condition, buying format, free shipping, price range; not the page size), a listing identified by its item id and marketplace; `changeType` / `previousPrice` / `priceDelta` / `priceDeltaPercent` filled from the comparison (same currency only, price as displayed); `onlyChanges` returns and charges only the new listings and the price changes, otherwise every compared listing with its `changeType`; first run = reference (`RUN_STATS.monitorBaseline`), `RUN_STATS.monitor` summary; records written only after a search that ended cleanly (fail-closed: not after a blocked, failed, empty, budget-cut or charge-cut search; a search that fell back to the default page size counts once); a monitoring run without any change succeeds with an empty dataset; `maxResults` = listings compared per search. With monitoring off the output and the log lines are those of 0.1 (non-regression test); `RUN_STATS.extra` gains four zero counters and `RUN_STATS.monitor` is null.
- README: *Price monitoring* section, input table, FAQ entry; input form section *Price monitoring*; dataset view **Changes**; example task `ebay-monitor-wireless-earbuds-prices-us` (9 in total); tests on the monitor module, the crawler pass, the input and the Store files.
- **Store texts** (brief `docs/cloud-tasks/2026-10-20_studio-promote-store-texts.md`, 2026-10-20): README opens with what you get / for whom / how much, shows the real input of the validation run, a pricing table that points to the Pricing tab (no figure in the text), a *For AI agents (MCP)* section with a minimal input, the key output fields, the cost per 1,000 results and the typical time; FAQ kept to 8–10 questions; no proxy or anti-bot detail left in the texts visible on the Store. Tests on the Store lengths (`seoTitle` ≤ 60, `seoDescription` ≤ 155, `description` ≤ 300), the README structure and the forbidden mentions. No code change.

#### 0.1.1 — 2026-10-20

- **Release fix** (brief `docs/cloud-tasks/2026-10-19_ebay-fix-release.md`): the first release stopped at the metadata step because `store.json › seoDescription` had 178 characters (limit 155 in `scripts/release-actor.mjs`); the text is shortened to 153 characters without losing the keyword or the "No personal data" promise. New tests on `store.json` (SEO lengths, categories, daily-check schedule, inputs mapping) and on every file of `inputs/` (valid, non-empty, residential proxy, dateless) so that the release script's checks are caught by `npm run check` before the Mac runs it. No code change: the build stays the one of 0.1.

#### 0.1 — 2026-10-20

- First version, written offline on the committed fixtures of the audit (`docs/audits/ebay.md`, brief `docs/cloud-tasks/2026-10-20_ebay-build.md`, 7 pages captured by the probe on ebay.com, ebay.co.uk, ebay.de and ebay.fr): server-rendered search page `/sch/i.html` read with Cheerio (`ul.srp-results > li.s-card[data-listingid]` only; the two "Shop on eBay" decoy cards and the carousels, which carry seller pseudonyms, are never read), optional tracking block `mainSearchContext` found by key search and brace balancing (sponsored flag, variation id, leaf category; absent on ebay.fr, where `isSponsored` is null), `_pgn` pagination until the page has no next link / `maxResults` / two pages without a new listing, global deduplication by item id (the sponsored cards repeat from one page to the next), one sticky session per search, one rotation on an unusable page then give-up, pay-per-event `result` charging, `RUN_STATS`, explicit failure on empty output.
- **Transport**: `impit` with the iOS 18 Safari profile on **Apify residential proxies by default** (`proxyConfiguration` default `RESIDENTIAL`): the only combination eBay's Akamai let through in the audit (13/13; got-scraping and the Chrome / Firefox profiles 0/47, datacenter 2/12). The generic crawler's `Accept` header is removed (the probe sent only `Accept-Language`); `debugSendAccept` sends it for a test. Anti-bot pages (`Error Page | eBay`, `reference-id`, `/splashui/challenge`, `loadArgon2WasmModule`) are block markers; a proxy authentication error (`ProxyAuthRequired` / 407, e.g. a country set on the datacenter tier) fails the run with an explicit message instead of being retried as a block.
- **Page size**: `resultsPerPage` default `240` (`_ipg=240`, 207 listings per page in the audit, validated once in datacenter: to confirm on residential at the first run), `120`, `60` or `default` (32 listings per page, 13/13). When the first page of a search with `_ipg` is unusable twice, the search is queued again once without `_ipg` (`RUN_STATS.extra.ipgFallbacks`).
- Output schema `ecommerce-product/1.1`: `price` / `currency` from a localised parser ("$17.99", "EUR 14,99", "£2.10", "10,95 EUR", "C $19.99", "AU $29.95"; ranges → lower bound + `attributes.priceMax`), `originalPrice` from the strike-through price or the "UVP" list price, `discountPercent` computed, `soldCount` as a lower bound ("1,000+ sold", "verkauft", "vendus"), `rating` / `reviewCount` from the star block, `shippingFree` / `shippingCost` / `deliveryEstimate` from the shipping lines (a dated "Get it by …" line wins over a window), `shipsFrom` from "Located in…" / "from…" / "de…" / "aus…" lines with a country-name table in 6 languages, `storeType` from "Gewerblich" / "Privat" / "Pro" / "Particulier" (never a name), `inStock` always true, `attributes` flat (`condition`, `listingFormat`, `bids`, `timeLeft`, `priceMax`, `watchers`, `scarcity`, `multiBuyDiscount`, `importFeesIncluded`, `freeReturns`, `priceWithCoupon`, `brandBadge`, `authorizedDealerBadge`, `refurbishedProgram`, `topRatedBadge`, `dealBadge`, `leafCategoryId`), `imageUrl` upgraded to the 500-px rendition (null for eBay's stock placeholder), `url` reduced to `https://www.ebay.<domain>/itm/<id>`; `stripPersonalData` on every row, no store column ever filled.
- **Decisions**: `searchRank` is the card's position counted across pages (the tracking block's `rank` equals it on every captured page and restarts on each page); an unknown program badge is never exported (it could name a seller); `sortBy` values other than `best-match` and `price-asc` (`_sop=15`), `condition: used`, `buyingFormat: auction`, `freeShippingOnly`, `priceMin` / `priceMax` and the IT / ES / NL / CA / AU marketplaces follow eBay's public URL conventions and are marked "not verified" in the input form, the README and the example tasks until the first runs (`_sop=16` highest price first, `_sop=10` newly listed, `_sop=1` ending soonest: the brief suggested 12 and 10 for the time sorts, eBay's documented desktop values were preferred); the default keywords (`wireless earbuds`) apply only when neither keywords nor URLs are given; a search URL's own domain decides its marketplace.
- Known limits (audit §3, §9): 13 residential requests only in the audit (the ≥ 90 % on ≥ 20 requests threshold is confirmed at the validation run), eBay's anti-bot policy moved during the audit evening, pagination depth unknown (`maxResults` capped at 1,000), ratings on about one card in four, one `Accept-Language` per run (the first search's marketplace) when searches mix marketplaces.
- Store readiness: README, input schema, dataset schema with Overview / Prices / Shipping / Ranking views, output schema, 8 example tasks (residential proxy, one page each, unverified filters flagged in their description), consistency tests (README ⇔ schemas ⇔ code, Ajv replay of the 240 fixture rows), crawler tests on a fake HTTP client (transport, headers, sticky session, pagination, deduplication, rotation, `_ipg` fallback, proxy error), seller-pseudonym injection test.
