Back to blog

Build a Trading-Card Pricing Pipeline Without Mixing Grades and Variants

By SoldFetch Engineering7 min read

Turn eBay card-search results into reviewable cohorts by preserving set, card number, parallel, language, edition, grader, and grade before calculating a median.

Build a Trading-Card Pricing Pipeline Without Mixing Grades and Variants

A card-search result can look precise while combining several different products. The same character or player may appear in several sets, languages, parallels, and editions. A raw card and two graded copies can share most of their title words while belonging in different comparison groups.

For a trading-card pricing pipeline, the difficult step is deciding which observations belong together. Searching eBay and calculating a median comes later. A defensible report keeps the matching decisions visible and carries uncertainty through the calculation.

This guide uses SoldFetch to discover candidate listings, then adds an application-owned identity schema and human review. All card names, listing records, and amounts in the example are fictional. They demonstrate the workflow, not a current card valuation.

Define the exact card before searching

Create a target record that describes one comparison cohort. At minimum, record the set, card number, parallel, language, edition, grader, and grade. Add the character or player, release year, and raw condition when they matter for the product being researched.

Here is a deliberately fictional target:

{
  "set": "Garden Creatures",
  "cardNumber": "017/100",
  "parallel": "standard foil",
  "language": "English",
  "edition": "unlimited",
  "grader": "Example Grading Co",
  "grade": "9"
}

These are your application's fields, not native SoldFetch search parameters. A grading company is part of identity; a grade number alone is insufficient. Treat raw cards as their own cohort, with an explicit raw state and condition review instead of pretending they have a numeric slab grade.

Preserve meaningful distinctions such as first edition versus unlimited, standard foil versus a named parallel, or an autograph versus an unsigned card. Keep the source wording alongside normalized values. If the listing does not establish a field, mark it unknown and leave it out of the accepted cohort until reviewed.

Retrieve candidates with a bounded query

Build a keyword query from the target's strongest identifiers. Include the set and full card number before adding common title language. Use the relevant ebaySite, sold=true, a bounded count, and a finite page budget. The endpoint is GET /v1/scrape; see the search reference for the full contract.

Broad condition choices such as itemCondition=used can help describe a request, but they are not card-grade controls. SoldFetch does not provide native filters for grader, numeric grade, parallel, edition, or verified card identity. aspectFilter, conditionId, and unavailable enrichment flags should not be added to imitate those features.

Keep exactMatch=true if you want explicitly marked loose matches excluded. It still does not verify the card, bundle, language, or grade. Treat every returned row as a candidate. Stop pagination on empty or repeated pages, deduplicate marketplace/item-ID pairs, and retain any warnings.

Create a review queue before a pricing table

Copy the raw title, URL, item ID, marketplace, source retrieval time, displayed price, currency, and quality fields into your candidate store. Add your own review status, identity fields, reviewer, review time, and reason.

A useful queue has three outcomes: accepted, rejected, and needs review. Rejected rows remain auditable. Reasons might include wrong set, different parallel, card lot, raw card, unclear label, missing currency, or an amount that represents a range. “Needs review” is appropriate when a truncated title or missing image prevents a decision.

Inspect images and available item details for the small subset that needs enrichment. The item endpoint can return itemSpecifics and images when available; it does not guarantee certification verification or complete card metadata. An empty specifics object is possible. Keep enrichment time separate from the original search time.

Do not let a high keyword-match score automatically approve a listing. A title containing two grade numbers may describe a comparison or mixed lot. A photographed slab can still have a different edition from the target. If the evidence is incomplete, keep the row pending.

Separate identity from evidence about price

Once identity matches, review the price basis. Sold search rows use soldPrice and soldCurrency. Their quality object distinguishes displayed_listing_price from range_lower_bound; a range lower bound should not silently become a single-card price.

Displayed prices are not independently verified accepted-offer amounts. bestOfferAccepted and the accepted-offer amount may be unknown. Keep that limitation on the resulting summary even after a reviewer accepts the card's identity. Identity approval does not turn an uncertain amount into a verified transaction.

Preserve shipping separately as shippingPrice and shippingCurrency. Unknown shipping is not free shipping. Decide whether the report summarizes displayed item amounts or a complete, same-currency price-plus-shipping subset. Do not mix those definitions within one median.

Sale dates are also source-reported and unverified. Retrieval time is not sale time, and verified sale-date filters are unavailable. Label a snapshot by when the evidence was obtained rather than calling it a complete set of last week's sales.

Calculate only inside the accepted cohort

The following Python helper expects application review records. Each record nests an untouched SoldFetch row under listing; identity, reviewStatus, and ebaySite are fields your application supplies. It calculates a median of displayed item amounts after review, within one marketplace and currency.

from decimal import Decimal, InvalidOperation
from statistics import median

IDENTITY_FIELDS = (
    "set", "cardNumber", "parallel", "language",
    "edition", "grader", "grade",
)

def summarize_reviewed(rows, target, site, currency):
    if any(not target.get(k) for k in IDENTITY_FIELDS):
        raise ValueError("The target identity must be complete")
    values, seen = [], set()
    for row in rows:
        identity = row.get("identity", {})
        listing = row.get("listing", {})
        key = (row.get("ebaySite"), listing.get("itemId"))
        if row.get("reviewStatus") != "accepted" or key[0] != site:
            continue
        if not key[1] or key in seen:
            continue
        if any(identity.get(k) != target[k] for k in IDENTITY_FIELDS):
            continue
        if listing.get("soldCurrency") != currency:
            continue
        if listing.get("quality", {}).get("priceBasis") != "displayed_listing_price":
            continue
        try:
            value = Decimal(str(listing.get("soldPrice")))
        except InvalidOperation:
            continue
        if not value.is_finite() or value <= 0:
            continue
        seen.add(key)
        values.append(value)
    return {"count": len(values), "currency": currency,
            "medianDisplayedItemPrice": median(values) if values else None}

For three synthetic accepted rows with amounts 40.00, 42.00, and 44.00 USD, the median is 42.00 USD and the sample count is three. A fourth row at 95.00 with a different grade is excluded before arithmetic. Its higher amount is not an outlier to trim; it describes a different cohort.

The helper deliberately leaves an empty cohort as None, not zero. Keep the result as a decimal for calculations and convert it to a string when serializing JSON. The source timestamps and warnings should travel with the surrounding report even though they are not arithmetic inputs.

Show what was included and what was rejected

Put the complete target identity above the summary, followed by marketplace, currency, displayed-price basis, sample count, and observation timestamps. Link accepted source rows so a reviewer can inspect them. Add a separate rejected/pending view with reason codes.

Small cohorts deserve visible context. A median of three displayed amounts is a description of those three observations, not a verified market value. If grade or edition evidence is thin, show the shortfall and request review instead of widening the cohort automatically.

For duplicate retrievals, count a listing once within the report. Repeated cache hits do not represent new transactions. Sold searches currently have a six-hour cache duration, with shorter caching for empty results. Keep retrieval age visible and avoid manufacturing apparent activity by polling more frequently.

Expand coverage one cohort at a time

Start with one card whose identity a reviewer can establish consistently. Exercise the pipeline with a different language, a different grade, a lot, a price range, missing currency, and an empty accepted cohort using offline fixtures. Check that none enters the summary by accident.

Only then add related cohorts. Keep separate outputs for each grade, parallel, and edition instead of merging them into one headline number. Record changes to your matching rules so a later report can explain why membership changed.

Read the data-quality guide, inspect current capabilities, and create an API key to collect a small candidate sample. The useful first deliverable is a reviewable table with clear identity decisions; a price summary becomes meaningful once that table is trustworthy.

Download the offline example

Download the complete Python example. The companion includes synthetic fixtures and offline checks. It makes no network requests.

Topics

#ebay-api#trading-card-comps#data-quality#python

Related posts

AI agent or LLM? Read this page as Markdown