← back to Handbag Auth Nextjs

python-matcher/scripts/ebay_sold_scraper.py

188 lines

"""
eBay Sold Listings Scraper
Fetch real sales history for handbag valuation and price charts
"""
import os
import sys
import requests
import time
from datetime import datetime, timedelta
from typing import List, Dict, Optional

# eBay Finding API (no auth required for sold listings search)
EBAY_FINDING_API = "https://svcs.ebay.com/services/search/FindingService/v1"
EBAY_APP_ID = os.environ.get("EBAY_APP_ID", "")  # Get free API key from eBay Developers

def search_sold_listings(
    query: str,
    category_id: Optional[str] = None,
    min_price: Optional[float] = None,
    max_price: Optional[float] = None,
    days_back: int = 90,
    max_results: int = 100
) -> List[Dict]:
    """
    Search eBay sold/completed listings

    Args:
        query: Search keywords (e.g., "Hermes Birkin 35")
        category_id: eBay category (e.g., "63852" for Women's Handbags)
        min_price: Minimum price filter
        max_price: Maximum price filter
        days_back: How many days back to search (max 90)
        max_results: Max results to return (max 100 per request)

    Returns:
        List of sold listings with price, date, condition
    """

    if not EBAY_APP_ID:
        raise ValueError("EBAY_APP_ID environment variable not set")

    # Calculate date range
    end_date = datetime.now()
    start_date = end_date - timedelta(days=days_back)

    params = {
        "OPERATION-NAME": "findCompletedItems",
        "SERVICE-VERSION": "1.0.0",
        "SECURITY-APPNAME": EBAY_APP_ID,
        "RESPONSE-DATA-FORMAT": "JSON",
        "REST-PAYLOAD": "",
        "keywords": query,
        "paginationInput.entriesPerPage": min(max_results, 100),
        "itemFilter(0).name": "SoldItemsOnly",
        "itemFilter(0).value": "true",
        "itemFilter(1).name": "EndTimeFrom",
        "itemFilter(1).value": start_date.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
        "itemFilter(2).name": "EndTimeTo",
        "itemFilter(2).value": end_date.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
    }

    # Add category filter
    filter_idx = 3
    if category_id:
        params[f"categoryId"] = category_id

    # Add price filters
    if min_price:
        params[f"itemFilter({filter_idx}).name"] = "MinPrice"
        params[f"itemFilter({filter_idx}).value"] = str(min_price)
        filter_idx += 1

    if max_price:
        params[f"itemFilter({filter_idx}).name"] = "MaxPrice"
        params[f"itemFilter({filter_idx}).value"] = str(max_price)
        filter_idx += 1

    # Make request
    response = requests.get(EBAY_FINDING_API, params=params, timeout=30)
    response.raise_for_status()

    data = response.json()

    # Parse results
    sold_listings = []

    try:
        search_result = data["findCompletedItemsResponse"][0]["searchResult"][0]

        if search_result["@count"] == "0":
            return []

        items = search_result["item"]

        for item in items:
            # Only include sold items (not just ended)
            selling_status = item["sellingStatus"][0]
            selling_state = selling_status["sellingState"][0]

            if selling_state != "EndedWithSales":
                continue

            listing = {
                "item_id": item["itemId"][0],
                "title": item["title"][0],
                "price_usd": float(selling_status["convertedCurrentPrice"][0]["__value__"]),
                "currency": selling_status["convertedCurrentPrice"][0]["@currencyId"],
                "condition": item.get("condition", [{}])[0].get("conditionDisplayName", ["Unknown"])[0],
                "end_time": item["listingInfo"][0]["endTime"][0],
                "location": item.get("location", [""])[0],
                "shipping_type": item.get("shippingInfo", [{}])[0].get("shippingType", [""])[0],
                "url": item["viewItemURL"][0],
                "image_url": item.get("galleryURL", [""])[0],
                "category_name": item.get("primaryCategory", [{}])[0].get("categoryName", [""])[0],
            }

            sold_listings.append(listing)

    except (KeyError, IndexError) as e:
        print(f"Error parsing eBay response: {e}")
        return []

    return sold_listings


def get_sales_statistics(sold_listings: List[Dict]) -> Dict:
    """
    Calculate statistics from sold listings

    Returns:
        Dict with avg, min, max, median prices and sale counts
    """
    if not sold_listings:
        return {
            "count": 0,
            "avg_price": 0,
            "min_price": 0,
            "max_price": 0,
            "median_price": 0,
        }

    prices = [item["price_usd"] for item in sold_listings]
    prices_sorted = sorted(prices)

    median_idx = len(prices_sorted) // 2
    median_price = prices_sorted[median_idx] if len(prices_sorted) % 2 == 1 else (prices_sorted[median_idx - 1] + prices_sorted[median_idx]) / 2

    return {
        "count": len(sold_listings),
        "avg_price": sum(prices) / len(prices),
        "min_price": min(prices),
        "max_price": max(prices),
        "median_price": median_price,
        "listings": sold_listings,
    }


if __name__ == "__main__":
    # Test scraper
    print("Testing eBay sold listings scraper...\n")

    test_query = "Hermes Birkin 35"

    print(f"Searching for: {test_query}")
    print(f"Date range: Last 90 days\n")

    try:
        results = search_sold_listings(test_query, days_back=90, max_results=50)

        print(f"Found {len(results)} sold listings")

        if results:
            stats = get_sales_statistics(results)
            print(f"\n📊 Sales Statistics:")
            print(f"   Count: {stats['count']}")
            print(f"   Average: ${stats['avg_price']:,.2f}")
            print(f"   Median: ${stats['median_price']:,.2f}")
            print(f"   Min: ${stats['min_price']:,.2f}")
            print(f"   Max: ${stats['max_price']:,.2f}")

            print(f"\n📦 Recent sales:")
            for item in results[:5]:
                print(f"   • ${item['price_usd']:,.2f} - {item['condition']} - {item['end_time'][:10]}")
                print(f"     {item['title'][:80]}")

    except Exception as e:
        print(f"❌ Error: {e}")