Extract eBay search listings and product pages as clean, normalized JSON, using the Scrapeless cloud Scraping Browser — no local Chrome, no proxy fleet.
Four interchangeable surfaces (Python, Node.js, CLI, MCP) emit the same JSON shape, documented in DATA_MODEL.md.
eBay is where resale and secondary-market pricing actually happens, so this is the data layer for:
- Resale pricing — what a used model really sells for, not what a retailer lists it at.
- Sneaker, collectible and parts arbitrage — spot listings priced below the going rate.
- Competitor and seller monitoring — track a seller's catalogue, feedback rate and shipping terms.
- Condition-aware market research — eBay exposes
subtitleslike "Pre-Owned" and "Open Box" that retail sites do not.
Unlike the Walmart sibling repo, extraction here is CSS-selector based — eBay renders listings server-side. The selectors carry .s-card__* primary and .s-item__* fallback paths, because eBay ships both card layouts depending on the query.
- A Scrapeless API key — create a free account
- Python 3.10+ or Node.js 18+ (each surface stands alone)
git clone https://github.com/<owner>/ebay-scraper.git
cd ebay-scraper
cp .env.example .env # then fill in SCRAPELESS_API_KEY
export SCRAPELESS_API_KEY=your_key_herecd browser/python
pip install scrapeless playwright parsel loguru python-dotenv
python run.py # prints JSON to stdout
SAVE_TEST_RESULTS=true python run.py # writes results/*.json insteadcd browser/nodejs
pnpm install
node run.mjsPoint it at your own targets without editing code:
EBAY_SAMPLE_SEARCH_URL="https://www.ebay.com/sch/i.html?_nkw=nintendo+switch&_ipg=60" \
EBAY_SAMPLE_PRODUCT_URL="https://www.ebay.com/itm/177439887865" python run.py_ipg in the search URL controls page size (60 by default) and the scraper reads it to plan pagination — so change _ipg rather than looping pages yourself.
| Surface | Path | Built on |
|---|---|---|
| Python | browser/python |
official scrapeless SDK + Playwright over CDP |
| Node.js | browser/nodejs |
official @scrapeless-ai/sdk + puppeteer-core over CDP |
| CLI | browser/cli |
scrapeless-scraping-browser CLI with in-page eval |
| MCP | browser/mcp |
Scrapeless MCP server — conversational, no code |
| Python | Node.js |
|---|---|
scrape_search(url, max_pages) |
scrapeSearch(url, maxPages) |
scrape_product(url) |
scrapeProduct(url) |
A real run on 2026-08-12 (Python surface): 146 search listings for iphone 12 plus one product page, in 133 seconds. Committed samples in browser/python/results/ come from that run — untrimmed, because this scraper emits a normalized shape rather than a framework blob (58 KB for 146 listings).
search.json:
[
{
"url": "https://www.ebay.com/itm/116370846040",
"title": "Apple iPhone 12 Unlocked Verizon, T-Mobile, AT&T all Carriers",
"price": "$189.94",
"shipping": "Free delivery in 2-4 days",
"location": "United States",
"subtitles": "Pre-Owned",
"photo": "https://i.ebayimg.com/images/g/...",
"rating": "95%",
"rating_count": 128
}
]product.json adds id, seller_name, seller_url, photos[], a features key/value table, and variants[] — see DATA_MODEL.md.
Measured across the 146 listings in that run:
| Field | Empty |
|---|---|
url, title, price, shipping, photo |
0 |
location, subtitles, rating |
26 (18%) |
rating_count |
27 (18%) |
Sellers without feedback history have no rating, and listings without a stated condition have no subtitles. Treat those four as nullable — the schema in DATA_MODEL.md marks them so, and price is the only field the parser requires (an item without one is skipped as a layout row).
price arrives as "$189.94", and eBay also emits ranges ("US $10.00 to $20.00") on multi-variant listings. Parse before comparing:
import re
def to_amount(price: str | None) -> float | None:
if not price:
return None
match = re.search(r"([\d,]+\.\d{2})", price) # first amount of a range
return float(match.group(1).replace(",", "")) if match else NoneOn product pages there are two price fields: price_original in the listing's currency, and price_converted, eBay's approximate conversion for the viewer's region. Which currency price_converted shows depends on the proxy country of the browser session — DEFAULT_PROXY_COUNTRY = "US" in the scraper. Change the proxy country and that field changes with it, so store price_original as your source of truth.
cd browser/python && poetry install --with dev && poetry run pytest # needs pytest-asyncio + pytest-rerunfailures
cd browser/nodejs && node --test test.mjsThe Python suite validates each record against the documented schema and skips itself when SCRAPELESS_API_KEY is unset, so it is safe in CI without a key.
| Symptom | Cause and fix |
|---|---|
| Zero listings parsed | eBay served the alternate card layout. The selectors already carry .s-item__* fallbacks; if both miss, the markup changed — check .s-card__price first, since price gates the whole record. |
Hangs, then net:: or ERR_TUNNEL_CONNECTION_FAILED |
Transient CDN teardown; the code retries these. |
shipping holds a long blob instead of a delivery line |
Seen on older captures, not in the 2026-08-12 run. eBay sometimes renders a screen-reader span with the whole card summary; if it returns, take the shortest matching text node. |
price_converted is in an unexpected currency |
It follows the session's proxy country. See above. |
| Fewer pages than expected | eBay caps deep pagination; the scraper stops when a page yields no items. |
- Verified live on 2026-08-12 — the Python surface completed a full run in 133 s: 146 search listings plus one product page, no block and no CAPTCHA. The committed
browser/python/results/fixtures come from that run. - Not re-run in this pass: the Node.js, CLI and MCP surfaces. They were carried over unchanged from the upstream monorepo and share the data model, but their committed fixtures are older than the Python ones.
ebay-scraper/
├── DATA_MODEL.md # the JSON contract every surface emits
├── browser/
│ ├── python/ # SDK + Playwright over CDP, with pytest validators
│ ├── nodejs/ # SDK + puppeteer-core over CDP
│ ├── cli/ # scrapeless-scraping-browser CLI
│ └── mcp/ # Scrapeless MCP server config
├── .env.example
└── LICENSE
- Product: Scraping Browser · Scraping API
- Guide: eBay scraper with the Scrapeless Scraping Browser
MIT — see LICENSE.