<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sam Smith</title>
    <description>The latest articles on DEV Community by Sam Smith (@sam_compsapi).</description>
    <link>https://dev.to/sam_compsapi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4174389%2F4b17ffaf-14dc-435e-867a-96a8e1d91d08.png</url>
      <title>DEV Community: Sam Smith</title>
      <link>https://dev.to/sam_compsapi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sam_compsapi"/>
    <language>en</language>
    <item>
      <title>I logged every status code scraping eBay sold listings. The block is not where you think</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 17 Oct 2026 18:29:56 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/i-logged-every-status-code-scraping-ebay-sold-listings-the-block-is-not-where-you-think-gb9</link>
      <guid>https://dev.to/sam_compsapi/i-logged-every-status-code-scraping-ebay-sold-listings-the-block-is-not-where-you-think-gb9</guid>
      <description>&lt;p&gt;Every tutorial for this is the same twelve lines. I ran them today, logged every status code, and the interesting part is not that it failed. It is &lt;em&gt;where&lt;/em&gt; it failed, which turns out to be a different place than the tutorials assume.&lt;/p&gt;

&lt;p&gt;All numbers below are from 17 October 2026, from one residential address, in the order shown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: the twelve lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.ebay.com/sch/i.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_nkw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incredible hulk 181&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LH_Sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LH_Complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/129.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# 403 1832
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;403, 1,832 bytes.&lt;/strong&gt; Not a captcha, not a login redirect. A page titled &lt;code&gt;Error Page | eBay&lt;/code&gt; carrying &lt;code&gt;&amp;lt;meta name="robots" content="noindex,nofollow"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The useful control: the same request with the sold flags removed, against the ordinary active search, also returned 403, at 1,831 bytes.&lt;/p&gt;

&lt;p&gt;So the first block has nothing to do with sold data. It is the request not looking like a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: look like a browser
&lt;/h2&gt;

&lt;p&gt;Copy a real request's headers. &lt;code&gt;accept&lt;/code&gt;, &lt;code&gt;accept-language&lt;/code&gt;, the &lt;code&gt;sec-ch-ua&lt;/code&gt; client hints, the &lt;code&gt;sec-fetch-*&lt;/code&gt; set, &lt;code&gt;upgrade-insecure-requests&lt;/code&gt;. This is where it gets interesting, because the same headers produced different outcomes for the two URLs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User agent only, active search&lt;/td&gt;
&lt;td&gt;403, 1,831 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User agent only, sold search&lt;/td&gt;
&lt;td&gt;403, 1,832 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full header set, active search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200, 198,706 bytes, 67 listings&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full header set, sold search&lt;/td&gt;
&lt;td&gt;302, empty body&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Identical headers. The active search handed over a real page with 67 listing ids in it. The sold search did not.&lt;/p&gt;

&lt;p&gt;That is the actual shape of the problem. Active listings are public and indexable. Completed sales are the dataset eBay sells research subscriptions against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: it stops working at all
&lt;/h2&gt;

&lt;p&gt;Retrying the sold URL a minute later, following redirects this time, returned &lt;strong&gt;403&lt;/strong&gt; rather than the 302.&lt;/p&gt;

&lt;p&gt;Then I ran ten more trials, alternating active and sold, ten seconds apart. &lt;strong&gt;403 on all ten, including the active search that had worked minutes earlier.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Total requests before the address was finished: about fifteen.&lt;/p&gt;

&lt;p&gt;So there are two findings here, not one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sold search is gated harder than active search.&lt;/li&gt;
&lt;li&gt;The dominant effect over any real workload is address reputation, not headers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 2 is the one that kills projects. Your scraper works on your laptop for an afternoon and dies the moment it does anything useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The selector in every tutorial is also wrong
&lt;/h2&gt;

&lt;p&gt;Worth knowing if you do get a page back. Essentially every tutorial and Stack Overflow answer parses &lt;code&gt;.s-item__price&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the 200 response above, that selector matched &lt;strong&gt;zero&lt;/strong&gt; elements. The prices were in &lt;code&gt;.s-card__price&lt;/code&gt;, 70 of them.&lt;/p&gt;

&lt;p&gt;eBay reworked the search markup. The tutorials did not. If your scraper quietly returns empty lists, check the class names before you check your parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there is no API to fall back to
&lt;/h2&gt;

&lt;p&gt;The usual next question is why not use eBay's own API. The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;findCompletedItems&lt;/code&gt;, the Finding API call that returned sold listings, is retired.&lt;/li&gt;
&lt;li&gt;The Browse API is open to everyone and returns &lt;strong&gt;active&lt;/strong&gt; listings only. There is no sold filter, because sold items are not in the index it reads.&lt;/li&gt;
&lt;li&gt;The Marketplace Insights API does carry sold data. It is a limited release restricted to approved partners, and it caps at 90 days.&lt;/li&gt;
&lt;li&gt;Terapeak, now Product Research in Seller Hub, has three years of it and no API, no export, and requires a signed in seller session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole reason a third party market exists for this data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs
&lt;/h2&gt;

&lt;p&gt;The parser is an afternoon. The real cost is sessions. eBay has required a signed in account to see sold search results since around the middle of 2026, so the job becomes account management: real accounts, keeping them signed in, addresses that hold up, handling challenges, re-auth when they fail. That is an operations problem with a monthly bill, and no tutorial covers it because it is not fun to write about.&lt;/p&gt;

&lt;p&gt;I work on an API that runs that layer, so take the conclusion with the appropriate amount of salt. But the status codes above are reproducible in about five minutes, and I would rather publish the control case (active search, same headers, 200) than imply eBay blocks everything, because it does not.&lt;/p&gt;

&lt;p&gt;If you are starting this project: budget for the session layer rather than the parser, and test from the address your code will actually run on, not from your laptop.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>ebay</category>
    </item>
    <item>
      <title>I changed one parameter and the median moved 10x</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 17 Oct 2026 16:07:44 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/i-changed-one-parameter-and-the-median-moved-10x-1kam</link>
      <guid>https://dev.to/sam_compsapi/i-changed-one-parameter-and-the-median-moved-10x-1kam</guid>
      <description>&lt;p&gt;I went looking for one thing, failed to find it, and measured something more useful on the way. Both results are below, including the hypothesis that did not survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;60 specific products across ten categories: graded cards, retro games, sneakers, watches, lenses, coins, tools, collectibles, consumer electronics. For each one, the same query twice, once over 90 days and once over 1,095 days, reading cleaned server side statistics rather than raw listings. 120 requests, paced.&lt;/p&gt;

&lt;p&gt;The question: eBay's public sold filter shows about 90 days. How much of an item's sales record does that window hide?&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis failed
&lt;/h2&gt;

&lt;p&gt;I expected thin 90 day windows and rich three year ones. Instead, &lt;strong&gt;58 of 59 comparable queries already had eight or more clean sales inside 90 days&lt;/strong&gt;. For the typical product, the 90 day window held essentially the whole visible record.&lt;/p&gt;

&lt;p&gt;Only one query was genuinely starved: a complete copy of Earthbound on SNES, 6 sales in 90 days against 228 over three years.&lt;/p&gt;

&lt;p&gt;The reason is not that eBay shows more than I thought. It is that &lt;strong&gt;a keyword search is broad&lt;/strong&gt;. Ask for &lt;code&gt;seiko skx007 dive watch&lt;/code&gt; and the window fills easily, because it fills with straps, bezels, crystals and cases as well as watches.&lt;/p&gt;

&lt;p&gt;Breadth hides depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What showed up instead
&lt;/h2&gt;

&lt;p&gt;Changing only the window changed the answer, often by a lot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change in median, 90d vs 3y&lt;/th&gt;
&lt;th&gt;Share of products&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10% or more&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25% or more&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The individual movers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;90 day median&lt;/th&gt;
&lt;th&gt;3 year median&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rolex datejust 16220&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$435.56&lt;/td&gt;
&lt;td&gt;$4,400.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seiko skx007 dive watch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$14.13&lt;/td&gt;
&lt;td&gt;$200.00&lt;/td&gt;
&lt;td&gt;14x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;marantz 2270 receiver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$99.00&lt;/td&gt;
&lt;td&gt;$710.00&lt;/td&gt;
&lt;td&gt;7.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mike trout bowman chrome 2009&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$4,750.00&lt;/td&gt;
&lt;td&gt;$897.50&lt;/td&gt;
&lt;td&gt;5.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;festool track saw ts 55&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$199.31&lt;/td&gt;
&lt;td&gt;$325.00&lt;/td&gt;
&lt;td&gt;1.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  I checked the biggest one, because 10x usually means bug
&lt;/h2&gt;

&lt;p&gt;Pulled both windows again and looked at listings instead of statistics.&lt;/p&gt;

&lt;p&gt;Both windows return the same kinds of thing. Bottom of the distribution: a sapphire crystal at $23, a bracelet link at $39, an engine turned bezel at $84. Top: real watches at $4,500, $5,100, $5,700. Identical price deciles in both.&lt;/p&gt;

&lt;p&gt;The three year set is the 90 day set &lt;strong&gt;plus 62 older sales&lt;/strong&gt;, and those added sales sit high in the distribution. That drags the midpoint from the parts end of the list to the watch end.&lt;/p&gt;

&lt;p&gt;So the market did not move. The &lt;strong&gt;composition of the match&lt;/strong&gt; moved. Nobody's Datejust changed price by 10x, and anyone comparing those two medians would have concluded it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the more useful result
&lt;/h2&gt;

&lt;p&gt;Price history gets sold on depth. More years, more data, better answers. Depth is real, and its actual value is sample size on scarce items.&lt;/p&gt;

&lt;p&gt;But depth does nothing if the query is matching the wrong population, and widening the window can quietly change which population you are measuring. Retrieval is the problem. The time window is just another way to perturb it.&lt;/p&gt;

&lt;p&gt;I measured the same failure from another angle recently: the same card search returned a $14.73 median raw and $545.00 once lots, proxies, accessories and "PSA 10 contender" listings were removed. &lt;a href="https://compsapi.com/blog/why-your-card-comps-are-wrong" rel="noopener noreferrer"&gt;That write up is here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rules if you work with marketplace price data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never compare medians across windows unless the filters are identical and tight.&lt;/strong&gt; If the only thing you changed is the date range and the number moved more than a few percent, suspect the match before you believe the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter on parsed structure before you widen.&lt;/strong&gt; Condition, grading fields, print run, category, and dropping lots and accessories. A tight match is stable across windows. A loose one is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the count alongside the median.&lt;/strong&gt; If the median moved and the count jumped, you are looking at a different set of sales, not a trend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use depth for sample size, not for trend.&lt;/strong&gt; Three years is the right tool for something that sells twice a quarter. For a trend, bucket sales by date and compare buckets. One long window median is a level, not a trend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;Numbers are from a run on 17 October 2026. Capped queries, where the result hit the page size, are marked and excluded from the volume claims. Two of 59 were capped.&lt;/p&gt;

&lt;p&gt;The free tier is 100 requests a month and no card, and &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;used_for_stats&lt;/code&gt;, &lt;code&gt;median&lt;/code&gt;, &lt;code&gt;p25&lt;/code&gt; and &lt;code&gt;p75&lt;/code&gt; come back on every response: &lt;a href="https://compsapi.com/docs" rel="noopener noreferrer"&gt;compsapi.com/docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>api</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Your card pricing API is returning the median of a junk drawer</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:58:11 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/your-card-pricing-api-is-returning-the-median-of-a-junk-drawer-101m</link>
      <guid>https://dev.to/sam_compsapi/your-card-pricing-api-is-returning-the-median-of-a-junk-drawer-101m</guid>
      <description>&lt;p&gt;Here is a measurement I ran on 17 October 2026 against my own API, which does not flatter it.&lt;/p&gt;

&lt;p&gt;Search three years of completed eBay sales for &lt;code&gt;charizard base set psa 10&lt;/code&gt;. Take the raw feed of sold listings, keep only the rows that sold a single unit, take the median:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$14.73&lt;/strong&gt;, over 239 listings, with individual sales running from $1.30 to $30,000.&lt;/p&gt;

&lt;p&gt;Run the same words through an endpoint that drops lots, mystery packs, accessories, replicas and keyword stuffed titles before it calculates anything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$545.00&lt;/strong&gt;, over 60 matching sales, middle half between $236.77 and $2,861.08.&lt;/p&gt;

&lt;p&gt;Same words. Same window. Same marketplace. A 37x difference in the answer.&lt;/p&gt;

&lt;p&gt;If you are building anything that prices collectibles, this is the whole problem in one example, and it is not specific to my data. Any tool reading the same eBay search sees the same listings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the first number happens
&lt;/h2&gt;

&lt;p&gt;eBay matches on words in the title, and it matches generously. A search for &lt;code&gt;charizard base set psa 10&lt;/code&gt; returns every listing whose title holds those words in any arrangement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a card sleeve titled "fits PSA 10 Charizard Base Set"&lt;/li&gt;
&lt;li&gt;a bulk lot of commons advertised as "Charizard Base Set PSA 10 chase"&lt;/li&gt;
&lt;li&gt;a raw card described as a "PSA 10 contender"&lt;/li&gt;
&lt;li&gt;a custom fan art proxy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those is the card. All of them are cheap, so they drag the middle of the distribution to the floor.&lt;/p&gt;

&lt;p&gt;The difference between pricing tools is entirely in what they throw away after retrieval. Most throw away nothing and show you a median anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that looks like the fix
&lt;/h2&gt;

&lt;p&gt;The obvious response is to put more words in the query. Add the card number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;q=charizard base set psa 10 4/102   -&amp;gt;  median $555.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like it worked. It did not work, it moved. The range is still $4.00 to $35,000, which tells you the set is still full of things that are not the card.&lt;/p&gt;

&lt;p&gt;Two things go wrong when you push identity into the query string:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;More words makes eBay widen, not narrow.&lt;/strong&gt; You get a different population of wrong listings rather than a clean population of right ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card numbers are reused across sets.&lt;/strong&gt; &lt;code&gt;4/102&lt;/code&gt; is Base Set Charizard and it is also a later anniversary reprint. Pinning the number without pinning the set pins nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What actually works: filter on structure, not words
&lt;/h2&gt;

&lt;p&gt;Parse the title into fields, then filter the fields. Grading company, grade, graded status and a junk classifier do almost all of the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; https://api.compsapi.com/v1/comps &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"q=charizard base set"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"graded=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"grader=PSA"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"grade=10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"days=1095"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"used_for_stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"median"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;545.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p25"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;236.77&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p75"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2861.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outliers_removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classifier flags that matter in practice, each one a listing class that will quietly wreck a median:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;More than one card sold together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mystery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repacks and "chance to win a PSA 10" listings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accessory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cases, sleeves, binders, display stands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replica&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Customs, proxies, fan art&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyword_stuffing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Several graders named with no grade, to catch searches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not_graded_claim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw cards described as a "PSA 10 contender"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To pin a specific card inside a set, filter after retrieval rather than before it. A post-search filter can only ever remove rows, so the effect is auditable. Text in the query changes which rows come back at all, so it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules if you are building this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never ship a median without its sample size.&lt;/strong&gt; A median of three sales is an anecdote. Return &lt;code&gt;count&lt;/code&gt; next to it and let the caller decide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return the spread.&lt;/strong&gt; In the cleaned set above the middle half ran $236.77 to $2,861.08. That is not noise to average away, it is the market. A nominally identical slab does not fetch the same price in a quiet week as it does with good photos and a seller with feedback. &lt;code&gt;p25&lt;/code&gt; and &lt;code&gt;p75&lt;/code&gt; are cheap to compute and they stop your users trusting a point estimate they should not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat an empty window as missing data, not a low price.&lt;/strong&gt; eBay's public sold filter reaches back about 90 days. For anything scarce that window is often empty, and the reflex is to loosen the query until something appears. What appears is a different item.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody can see at all
&lt;/h2&gt;

&lt;p&gt;One more thing that silently biases every price model built on eBay sold data: when a sale completes as an accepted Best Offer, the public sold page keeps showing the &lt;strong&gt;asking&lt;/strong&gt; price, not what the buyer paid.&lt;/p&gt;

&lt;p&gt;I matched 5,859 accepted offer sales against what the public page displayed for the same listing. 99.0% read high, by a median of 14.9%, with the top tenth over 40%. Full numbers and method: &lt;a href="https://compsapi.com/best-offer-gap" rel="noopener noreferrer"&gt;compsapi.com/best-offer-gap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That error is one directional. It never cancels out, and the data itself will never show it to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The free tier is 100 requests a month, no card, every filter on. The numbers above reproduce: &lt;a href="https://compsapi.com/docs" rel="noopener noreferrer"&gt;compsapi.com/docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you only take one thing from this: ask any pricing tool how many sales its number rests on, over what window, and what it discarded. If it cannot answer all three, it is showing you the median of a junk drawer.&lt;/p&gt;

</description>
      <category>api</category>
      <category>datascience</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your eBay sold-price data is 14.9% too high and the response looks fine</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:42:41 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/your-ebay-sold-price-data-is-149-too-high-and-the-response-looks-fine-2b92</link>
      <guid>https://dev.to/sam_compsapi/your-ebay-sold-price-data-is-149-too-high-and-the-response-looks-fine-2b92</guid>
      <description>&lt;p&gt;If your code reads eBay sold prices, it has a bug you cannot see. Not a parsing bug. The number comes back well formed, plausible, in the right currency, and wrong.&lt;/p&gt;

&lt;p&gt;I run CompsAPI, so I hold both the asking price and the accepted price on the same sale. That let me measure the gap instead of guessing at it. Here is what came out, and why no amount of better scraping fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;When an eBay listing sells through an accepted Best Offer, eBay's public sold page shows &lt;strong&gt;the asking price&lt;/strong&gt;. The amount the buyer actually paid is not on that page. There is no field holding it, no query parameter that reveals it, and no header that hints the row is affected.&lt;/p&gt;

&lt;p&gt;So the sale closed at $15,500, the page says $24,995, and your parser returns $24,995 with no error. Every tool reading that page returns the same wrong number, which is worse than disagreement, because when two sources agree you stop checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Over the 90 days to 10 October 2026: &lt;strong&gt;5,859 completed Best Offer sales across 5,840 distinct listings&lt;/strong&gt;, from 93 searches in 12 categories. Every pair is matched on listing id, so the asking price and the accepted price belong to the same item on the same sale. No row was discarded.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;99.0% read high&lt;/strong&gt; on the public sold page. 0.7% match within half a percent, 0.4% read low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median overstatement 14.9%.&lt;/strong&gt; A typical Best Offer sale shows about a seventh above what it closed at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p90 40.8%.&lt;/strong&gt; One sale in ten is overstated by more than two fifths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money weighted 14.2%&lt;/strong&gt;, across every dollar that changed hands, so this is not an artifact of cheap items.&lt;/li&gt;
&lt;li&gt;Median accepted price &lt;strong&gt;$240&lt;/strong&gt;, against a median asking price of &lt;strong&gt;$284&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How the tail behaves, which matters more than the median if you are setting a ceiling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Overstated by more than&lt;/th&gt;
&lt;th&gt;Share of accepted Best Offer sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;89.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;68.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;23.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  By category
&lt;/h2&gt;

&lt;p&gt;Every one of the 12 categories sits above 11% money weighted. The ones where sellers list high and expect to negotiate are the worst.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Sales matched&lt;/th&gt;
&lt;th&gt;Read high&lt;/th&gt;
&lt;th&gt;Median gap&lt;/th&gt;
&lt;th&gt;p90&lt;/th&gt;
&lt;th&gt;Money weighted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto parts&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;96.9%&lt;/td&gt;
&lt;td&gt;12.2%&lt;/td&gt;
&lt;td&gt;51.3%&lt;/td&gt;
&lt;td&gt;23.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sneakers&lt;/td&gt;
&lt;td&gt;857&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;td&gt;22.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graded trading cards&lt;/td&gt;
&lt;td&gt;390&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;td&gt;15.7%&lt;/td&gt;
&lt;td&gt;40.0%&lt;/td&gt;
&lt;td&gt;17.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jewellery&lt;/td&gt;
&lt;td&gt;652&lt;/td&gt;
&lt;td&gt;99.1%&lt;/td&gt;
&lt;td&gt;17.6%&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;td&gt;16.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Designer handbags&lt;/td&gt;
&lt;td&gt;723&lt;/td&gt;
&lt;td&gt;98.9%&lt;/td&gt;
&lt;td&gt;17.5%&lt;/td&gt;
&lt;td&gt;46.7%&lt;/td&gt;
&lt;td&gt;15.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video games and consoles&lt;/td&gt;
&lt;td&gt;446&lt;/td&gt;
&lt;td&gt;99.8%&lt;/td&gt;
&lt;td&gt;14.7%&lt;/td&gt;
&lt;td&gt;36.4%&lt;/td&gt;
&lt;td&gt;15.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phones and tablets&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;td&gt;35.9%&lt;/td&gt;
&lt;td&gt;15.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Golf clubs&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;td&gt;13.7%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;14.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Musical instruments&lt;/td&gt;
&lt;td&gt;365&lt;/td&gt;
&lt;td&gt;98.4%&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coins and bullion&lt;/td&gt;
&lt;td&gt;362&lt;/td&gt;
&lt;td&gt;98.1%&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Luxury watches&lt;/td&gt;
&lt;td&gt;655&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;11.2%&lt;/td&gt;
&lt;td&gt;31.2%&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cameras and lenses&lt;/td&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;td&gt;97.9%&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;11.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why a better scraper does not help
&lt;/h2&gt;

&lt;p&gt;A Best Offer is a private negotiation. eBay publishes that the item sold and what it was listed at, and keeps the accepted amount off the public record. This is not a rendering quirk or a lazy loaded element. The number is not in the page, so there is nothing to select.&lt;/p&gt;

&lt;p&gt;That is the part worth internalising if you maintain a pricing pipeline: this is not a coverage problem you can close with more requests. It is a field that does not exist on the surface you are reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it does not average out
&lt;/h2&gt;

&lt;p&gt;The instinct with noisy data is that errors cancel. These do not. A Best Offer is almost never accepted &lt;strong&gt;above&lt;/strong&gt; the asking price, so the error is one sided and every affected comp pushes your estimate the same direction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The quiet version of this bug in a repricing loop.
&lt;/span&gt;&lt;span class="n"&gt;comps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_sold_comps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# some read the public sold page
&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;comps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;list_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;               &lt;span class="c1"&gt;# "just under market"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a third of those comps were accepted offers reading a median 14.9% high, &lt;code&gt;target&lt;/code&gt; is not the market, it is above it, and &lt;code&gt;0.97&lt;/code&gt; does not save you. You sit high, the item does not move, and nothing in the data looks wrong. Four places it bites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repricing.&lt;/strong&gt; You list against a level nobody paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buying.&lt;/strong&gt; You pay to a ceiling that was never real, and the margin you modelled was not there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valuation and insurance.&lt;/strong&gt; Values come out high in exactly the categories people insure: graded cards 17.3% and jewellery 16.8% money weighted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything trained on the data.&lt;/strong&gt; A model fed asking prices learns a price level that never cleared.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the accepted figure
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;price&lt;/code&gt; is what the sale closed at, and &lt;code&gt;best_offer&lt;/code&gt; flags which rows were negotiated, so you can measure your own category rather than taking my median for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; https://api.compsapi.com/v1/sold &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"q=charizard psa 10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"days=1095"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"best_offer=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;best_offer=only&lt;/code&gt; returns negotiated sales alone, which is the query that produced the tables above. In Python, the gap on your own data is about six lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.compsapi.com/v1/sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                 &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charizard psa 10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best_offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;negotiated sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also an opt in &lt;code&gt;best_offer=verify&lt;/code&gt; mode that returns a &lt;code&gt;best_offer_confidence&lt;/code&gt; of &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;likely&lt;/code&gt;, &lt;code&gt;unlikely&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt; per row, plus the &lt;code&gt;asking_price&lt;/code&gt; and &lt;code&gt;asking_gap_pct&lt;/code&gt; it reasoned from, so you can set your own threshold instead of inheriting mine. It costs extra upstream reads, so it is off by default.&lt;/p&gt;

&lt;p&gt;Free key is 100 requests a month, no card. That is enough to reproduce this on one category.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is 93 searches in 12 categories over 90 days, not a census of eBay. Other categories will differ.&lt;/li&gt;
&lt;li&gt;It measures &lt;strong&gt;accepted Best Offer sales only&lt;/strong&gt;. It says nothing about what share of all eBay sales go through an offer, which varies enormously by category. The 14.9% applies to affected rows, not to your whole dataset.&lt;/li&gt;
&lt;li&gt;Three rows of 5,859 have an accepted price under $5 and are almost certainly an opening bid or a bad parse. Dropping every row under $10 moves the median from 14.9% to 14.8% and the money weighted figure from 14.2% to 14.0%, so nothing rests on them.&lt;/li&gt;
&lt;li&gt;0.4% of pairs read low. I report that rather than filtering it out.&lt;/li&gt;
&lt;li&gt;Where one listing sold several units in the window, the accepted figure is the average of that listing's accepted offers. In these categories almost every listing is a single item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full tables and method: &lt;a href="https://compsapi.com/best-offer-gap" rel="noopener noreferrer"&gt;compsapi.com/best-offer-gap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you maintain something that prices off eBay comps, the useful takeaway is not my number. It is that the error exists, it is one sided, and it is invisible in the response, so it is worth measuring on your own categories rather than assuming it is small.&lt;/p&gt;

</description>
      <category>ebay</category>
      <category>api</category>
      <category>python</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How to get eBay sold listings data in 2026 (findCompletedItems is dead)</title>
      <dc:creator>Sam Smith</dc:creator>
      <pubDate>Sat, 10 Oct 2026 01:19:48 +0000</pubDate>
      <link>https://dev.to/sam_compsapi/how-to-get-ebay-sold-listings-data-in-2026-findcompleteditems-is-dead-3l2j</link>
      <guid>https://dev.to/sam_compsapi/how-to-get-ebay-sold-listings-data-in-2026-findcompleteditems-is-dead-3l2j</guid>
      <description>&lt;p&gt;If you build anything that needs eBay sold prices, 2026 broke your stack twice. Here is the current state of every route to completed listings data, and working code for the one that still works.&lt;/p&gt;

&lt;p&gt;Full disclosure up front: I run CompsAPI, which is one of the options below. The rest of this post is accurate whether or not you use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;A short timeline, because half the tutorials online are now wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2020&lt;/strong&gt;: eBay deprecates &lt;code&gt;findCompletedItems&lt;/code&gt;, the Finding API call everyone used for sold listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 2025&lt;/strong&gt;: the whole Finding API is decommissioned. Code that called it gets security errors, not data.&lt;/li&gt;
&lt;li&gt;The official replacement, the &lt;strong&gt;Marketplace Insights API&lt;/strong&gt;, is a limited release. You apply with a business case, and approval is restricted to established eBay partners. Most applications go nowhere, and the developer forums are full of people stuck at the same wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid 2026&lt;/strong&gt;: eBay puts the public sold listings filter behind a login. The &lt;code&gt;LH_Sold=1&lt;/code&gt; URL that every scraper and browser extension relied on now redirects to a sign in page for most traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the two free routes, the official API and scraping the public page, are both gone. What is left:&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Terapeak, if a human is doing the looking
&lt;/h2&gt;

&lt;p&gt;eBay's own research tool (now called Product Research, inside Seller Hub) shows up to three years of sold data. If you price items by hand a few times a day, it is genuinely good and costs nothing extra with a seller account.&lt;/p&gt;

&lt;p&gt;It stops at the browser: no API, no export of research results, and sellers report daily caps on queries. The moment you need sold prices in a spreadsheet, a pricing model or an app, there is no door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: scrapers against the logged in page
&lt;/h2&gt;

&lt;p&gt;Some scrapers now run eBay accounts to get through the login wall. Two structural problems. First, the public sold page shows at most 90 days of history. Second, when a sale closes as an accepted Best Offer, the page shows the asking price, not what the buyer actually paid. In offer heavy categories (watches, cards, anything expensive) those numbers run high by 20 to 40 percent per affected row. A scraper inherits both limits because the page itself is the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: a sold listings API
&lt;/h2&gt;

&lt;p&gt;This is what I build. One GET request, JSON back, up to 1,095 days of history per search, and on accepted Best Offer sales the &lt;code&gt;price&lt;/code&gt; field is the real closing amount, flagged per row.&lt;/p&gt;

&lt;p&gt;Get a free key at &lt;a href="https://app.compsapi.com" rel="noopener noreferrer"&gt;app.compsapi.com&lt;/a&gt; (100 searches a month, no card), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://api.compsapi.com/v1/sold"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"q=charizard psa 10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"days=365"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"best_offer=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;239&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charizard PSA 10 Base Set Holo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1450.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"best_offer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"last_sold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-04 18:32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FIXED_PRICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"shipping_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;12.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.ebay.com/itm/..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;best_offer: true&lt;/code&gt; row is the point. On the eBay page this sale displays its asking price. Here it is the amount it closed at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python version
&lt;/h2&gt;

&lt;p&gt;There is a zero dependency client (&lt;a href="https://github.com/compsapi/compsapi-python" rel="noopener noreferrer"&gt;github.com/compsapi/compsapi-python&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/compsapi/compsapi-python.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;compsapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompsAPI&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompsAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iphone 14 pro 256gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sales, median $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three years of price history in 15 lines
&lt;/h2&gt;

&lt;p&gt;The thing you could never do with the public page, since it stops at 90 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;compsapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompsAPI&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CompsAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;monthly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sale&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_sold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iphone 14 pro 256gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_sold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sales)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is a monthly depreciation curve built from real completed sales. Each page of 240 sales costs one request, so a three year pull like this uses about a dozen requests.&lt;/p&gt;

&lt;p&gt;If you want a pre-aggregated answer instead of rows, &lt;code&gt;/v1/comps&lt;/code&gt; returns a cleaned median, the spread and the newest matches in one call, with lots, bundles and wrong model junk filtered before the math runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;eBay US is the primary marketplace, with matching sales from UK, DE, FR, IT, ES, CA and AU merged in or pinned with &lt;code&gt;site=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No seller information on rows.&lt;/li&gt;
&lt;li&gt;The free tier is 100 requests a month. Paid plans start at $9.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you came here from dead code
&lt;/h2&gt;

&lt;p&gt;Migrating from &lt;code&gt;findCompletedItems&lt;/code&gt;: your &lt;code&gt;keywords&lt;/code&gt; becomes &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;EndTimeFrom/To&lt;/code&gt; becomes &lt;code&gt;days&lt;/code&gt;, &lt;code&gt;entriesPerPage&lt;/code&gt; becomes &lt;code&gt;size&lt;/code&gt; (max 240 instead of 100), and the response is flat JSON instead of a SOAP envelope. There is a field by field mapping at &lt;a href="https://compsapi.com/replace/findcompleteditems" rel="noopener noreferrer"&gt;compsapi.com/replace/findcompleteditems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions welcome in the comments. If you hit something the API does not cover, tell me and there is a decent chance it ships.&lt;/p&gt;

</description>
      <category>ebay</category>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
