<?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: Ahmed Jasarevic</title>
    <description>The latest articles on DEV Community by Ahmed Jasarevic (@ahmed_jasarevic_9fa29bf1d).</description>
    <link>https://dev.to/ahmed_jasarevic_9fa29bf1d</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%2F4006314%2Fe4e58d21-c50e-43bb-a2b8-231645214d3e.png</url>
      <title>DEV Community: Ahmed Jasarevic</title>
      <link>https://dev.to/ahmed_jasarevic_9fa29bf1d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_jasarevic_9fa29bf1d"/>
    <language>en</language>
    <item>
      <title>Buy in Poland, sell in France: a 110% margin that nets €4.61</title>
      <dc:creator>Ahmed Jasarevic</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:35:01 +0000</pubDate>
      <link>https://dev.to/apify/buy-in-poland-sell-in-france-a-110-margin-that-nets-eu461-2a0p</link>
      <guid>https://dev.to/apify/buy-in-poland-sell-in-france-a-110-margin-that-nets-eu461-2a0p</guid>
      <description>&lt;h2&gt;
  
  
  The 110% Vinted arbitrage margin is really 9%
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A four-market Vinted scan reported a 110.1% spread on one sneaker query. After five costs it was 9.2% — below the threshold I would act on, and one return eats two winners. Here is the arithmetic, the dataset, and the traps.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure: I built the Actor used in this post. The numbers are from a real run on 25 October 2026: 480 listings across PL, DE, IT and FR, ECB reference rates from 23 October. Everything below is recomputable from the dataset.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The number everyone screenshots
&lt;/h2&gt;

&lt;p&gt;A Vinted scan returns a report that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;================ CROSS-COUNTRY ARBITRAGE REPORT ================
1. PL  162.99 PLN  (avg 37.09 EUR / median 37.89 EUR)  range 12-300  n=120
2. DE  71.27 EUR   (avg 71.27 EUR / median 50 EUR)     range 5-300   n=120
3. IT  76.92 EUR   (avg 76.92 EUR / median 50 EUR)     range 5-300   n=120
4. FR  77.92 EUR   (avg 77.92 EUR / median 55 EUR)     range 5-300   n=120
===============================================================
BUY in  PL  37.09 EUR
SELL in FR  77.92 EUR
Gross margin: 110.1% (before fees, shipping and Vinted's buyer protection)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;110.1%. Buy for €37, sell for €78, more than double your money.&lt;/p&gt;

&lt;p&gt;The last line is the honest one, and it is the line everyone skips: &lt;em&gt;before fees, shipping and buyer protection&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This post is the rest of the calculation. Not to kill the idea, but because I would rather have ten trades that make 25% than a spreadsheet full of trades that make minus six.&lt;/p&gt;

&lt;h2&gt;
  
  
  First problem: the report compares averages, and the average is not the market
&lt;/h2&gt;

&lt;p&gt;Here is the same run at the median instead of the average.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;th&gt;Average (EUR)&lt;/th&gt;
&lt;th&gt;Median (EUR)&lt;/th&gt;
&lt;th&gt;Range (EUR)&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PL&lt;/td&gt;
&lt;td&gt;PLN&lt;/td&gt;
&lt;td&gt;37.09&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.89&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12 - 300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;EUR&lt;/td&gt;
&lt;td&gt;71.27&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 - 300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IT&lt;/td&gt;
&lt;td&gt;EUR&lt;/td&gt;
&lt;td&gt;76.92&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 - 300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FR&lt;/td&gt;
&lt;td&gt;EUR&lt;/td&gt;
&lt;td&gt;77.92&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;55.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 - 300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 110.1% comes from comparing the Polish average (37.09) to the French average (77.92). The medians tell a different story: 37.89 versus 55.00. That is a &lt;strong&gt;45.2% spread, not 110.1%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The gap between average and median is the shape of the distribution, not free money:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In PL the average sits &lt;strong&gt;below&lt;/strong&gt; the median (37.09 vs 37.89), meaning a tail of very cheap listings drags the mean down.&lt;/li&gt;
&lt;li&gt;In FR the average sits &lt;strong&gt;above&lt;/strong&gt; the median (77.92 vs 55.00), meaning a tail of expensive listings pulls the mean up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That €22.92 gap between the French mean and the French median represents &lt;strong&gt;€2,750 of value spread across 120 listings&lt;/strong&gt;. It is a distribution, not a data error, and you cannot buy a distribution. You buy one item at one price, and you sell it for one price.&lt;/p&gt;

&lt;p&gt;Notice something else in the table: DE and IT have identical medians (€50.00). The "third cheapest market" in the report is an artefact of the average, not a real price level. The report sorts markets by mean, which is exactly the statistic most distorted by outliers at both ends. Use it to pick candidates, never to size a trade.&lt;/p&gt;

&lt;p&gt;Rule one: compare medians first, then pull P25 and P75 from the dataset. If the spread only exists between the means, it does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five costs that eat the spread
&lt;/h2&gt;

&lt;p&gt;Here is the same trade with the costs that Vinted, shipping and reality add. Everything below is in EUR, using the medians above and assumptions you can edit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Who pays&lt;/th&gt;
&lt;th&gt;Size in this example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Vinted fee on the buy leg&lt;/td&gt;
&lt;td&gt;Buyer (you)&lt;/td&gt;
&lt;td&gt;~5% + fixed, so €2.59 on €37.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Inbound shipping to you&lt;/td&gt;
&lt;td&gt;Buyer (you)&lt;/td&gt;
&lt;td&gt;€4.50 (EU parcel, tracked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Currency conversion&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;~1.2% per converted leg, €1.11 total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Packaging + handling&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;€0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Risk reserve&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;10% of buy price (returns, deadstock, price drift), €3.79&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few notes on each, because this is where most spreadsheets quietly cheat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Vinted fee.&lt;/strong&gt; There are no selling fees on Vinted, which is why the fee is easy to forget: the &lt;em&gt;buyer&lt;/em&gt; pays it. When you source, you are the buyer. Vinted describes it as typically a percentage plus a fixed amount, roughly 5% + a small fixed fee, varying by market. On a €38 item it is not the main cost. On a €380 item it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Inbound shipping.&lt;/strong&gt; You pay to get the item to you. The buyer of your resale pays the label on the way out, so outbound shipping is not your cost, but packaging is. This asymmetry is why cheap, light items can work and bulky ones rarely do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. FX.&lt;/strong&gt; The ECB reference rate is a mid-market rate. Your payment provider's rate is not. Vinted documents a 1.2% conversion fee on the USD/GBP pairs it handles directly; for other pairs the fee depends on your provider. Your bank may add its own on top. Converting in and out means you pay it twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Packaging.&lt;/strong&gt; Small, real, and it compounds. A €0.50 mailer on a €38 trade is 1.3% of your capital, every cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Risk reserve.&lt;/strong&gt; This is the one people leave out because it does not appear on any invoice. Some items arrive not as described. Some do not sell for eight weeks while your money sits in them. Some sell after the market has moved down. A 10% reserve is not pessimism; it is the cost of doing this more than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fee        = 37.89 * 0.05 + 0.70        =  2.59
fx         = (37.89 + 55.00) * 0.012    =  1.11
risk       = 37.89 * 0.10               =  3.79
packaging  =                               0.50
shipping   =                               4.50

all-in cost = 37.89 + 2.59 + 1.11 + 3.79 + 0.50 + 4.50 = 50.39
sell price  =                                            55.00
net         =                                             4.61
net margin  =                                             9.2%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade that showed a 110.1% gross margin nets &lt;strong&gt;€4.61, or 9.2%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is not a catastrophe. It is also not a business. Two numbers fall out of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Break-even sell price: €50.39.&lt;/strong&gt; You already need to clear the French median (€55.00) just to make €4.61. If the item sells at the median price of the &lt;em&gt;cheapest&lt;/em&gt; French listing band, you lose money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 25% net margin needs about €62.99.&lt;/strong&gt; In this run, that is above the median and somewhere in the upper quartile of the French market. If your "arbitrage" only exists at the median, you are buying a lottery ticket with extra steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then the return. One SNAD return — item not as described, refund, return shipping, packaging, your time — costs roughly €9 on a trade this size. That is two winners to pay for one loser. At a 10% return rate, the expected net per trade drops from €4.61 to about €3.25. At 20%, it is under €2. The margin is not just thin; it is one bad batch away from zero.&lt;/p&gt;

&lt;p&gt;Here is the same model as code, so you can drop your own numbers in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEE_PCT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEE_FIXED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FX_LEG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.012&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PACKAGING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RISK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;netMargin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inboundShipping&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;FEE_PCT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;FEE_FIXED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;FX_LEG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;RISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inboundShipping&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PACKAGING&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sell&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;netPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sell&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;breakEvenSell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sellFor25Pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;netMargin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;37.89&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;55.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inboundShipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.50&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="c1"&gt;// cost 50.39 | net 4.61 | netPct 0.092 | breakEven 50.39 | 25% target 62.99&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The real killer is not the fee. It is the border.
&lt;/h2&gt;

&lt;p&gt;The model above assumes you can buy in Poland and sell in France. That assumption is doing more work than any of the five costs.&lt;/p&gt;

&lt;p&gt;Your Vinted account is registered in one market. What you can buy and sell across borders depends on Vinted's international connections, the shipping options available to that pair of countries, item restrictions, weight limits and customs. Some connections are rolled out gradually and limited. On the connections Vinted documents publicly, electronics, cosmetics, jewellery and watches are commonly excluded or restricted, and those are exactly the categories with the widest spreads.&lt;/p&gt;

&lt;p&gt;So in practice, "sell in FR" often means: have a French account, a French delivery address, French shipping options, and the ability to handle returns in French. That is an operations problem, not a data problem. It is also why the gross-margin number should be treated as a &lt;em&gt;screening&lt;/em&gt; signal, not a business plan.&lt;/p&gt;

&lt;p&gt;There is a second border that is not geographic: DAC7. If you complete 30 sales or earn more than €2,000 on Vinted in a calendar year, the platform reports your seller data to tax authorities. That does not automatically mean tax is due, but it does mean the hobby has a paper trail. Once you are buying to resell, you are probably a business, and the EU margin scheme for second-hand goods is the thing to read before you scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually survives the model
&lt;/h2&gt;

&lt;p&gt;Running this across a few categories, four patterns hold up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same-market mispricing beats cross-border arbitrage.&lt;/strong&gt; A mispriced listing in your own market has no customs, no FX, no cross-border shipping and no language problem. The scanner finds those too; you just have to stop filtering for a country flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High value-to-weight wins.&lt;/strong&gt; Sneakers, jackets, small designer accessories. A €200 item that weighs 800g absorbs the same €4.50 shipping that destroys a €38 item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P25 buy to P75 sell.&lt;/strong&gt; If the spread only exists median-to-median, the trade dies at the first cost. Require the sell-side target to sit above P75 before you even look at the item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundles amortise everything.&lt;/strong&gt; One shipment, one fee, one packaging cost, several items. This is the only structural way to make low-priced categories work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the conclusion I did not expect: &lt;strong&gt;the data is worth more than the trades.&lt;/strong&gt; If you already sell on Vinted, the same scan tells you which market your item is priced highest in, and what the top quartile looks like. You do not have to ship anything across a border to profit from that. Price at P75 in the market with the highest median, and you have captured most of the spread without the operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five data traps to check before you trust a scan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Asking prices are not sold prices.&lt;/strong&gt; Vinted does not publish a public sold-price feed. Everything you scan is a listing, including the ones that will never sell. This is the single biggest limitation of any Vinted price dataset, mine included. Treat medians as an upper bound on clearing prices, not a forecast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Averages hide the tails.&lt;/strong&gt; This run's headline 110.1% was mean-to-mean. The medians said 45.2%. Always recompute before you believe the report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates across markets.&lt;/strong&gt; The same listing can surface in several markets. &lt;code&gt;itemId&lt;/code&gt; is stable; dedupe on it before you compute anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition mix.&lt;/strong&gt; A market with more "satisfactory" items will look cheaper. In this run the French range starts at €5, which is either a different product, a damaged item, or a placeholder price. Filter by condition (&lt;code&gt;status_ids&lt;/code&gt;) before comparing, or you are comparing two different products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample size.&lt;/strong&gt; n=120 is enough to see a shape and not enough to trust a percentile. If the difference between two markets is smaller than the spread of either, you have measured noise.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to run the scan
&lt;/h2&gt;

&lt;p&gt;The Actor takes &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;countries&lt;/code&gt; (up to five per run), &lt;code&gt;maxItems&lt;/code&gt; per market, and optional filters. &lt;code&gt;CROSS_COUNTRY&lt;/code&gt; mode returns the listings plus the per-market report.&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CROSS_COUNTRY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Nike Jordan 4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"countries"&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="s2"&gt;"pl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"de"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"it"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"maxPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status_ids"&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="s2"&gt;"new_with_tags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"very_good"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"good"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceBandPartitioning"&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;Two things matter for data quality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price bands.&lt;/strong&gt; Vinted caps how many results a single search pass returns. Setting &lt;code&gt;maxPrice&lt;/code&gt; enables &lt;code&gt;priceBandPartitioning&lt;/code&gt;, which splits the range into bands and sweeps each one, so a large query does not silently truncate at the cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five markets per run.&lt;/strong&gt; Run a second pass for the remaining countries and merge the datasets; &lt;code&gt;itemId&lt;/code&gt; is your join key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApifyClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apify-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIFY_TOKEN&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ahmed_jasarevic/vinted-cross-country-arbitrage-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CROSS_COUNTRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nike Jordan 4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;maxItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;listItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// quartiles per market. Replace RATES with the ECB reference rates for your run date.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EUR&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="na"&gt;PLN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2275&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;GBP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;CZK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;SEK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0895&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;DKK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.134&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byMarket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byMarket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;p25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="na"&gt;median&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="na"&gt;p75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At $1.50 per 1,000 results, 480 rows costs about $0.72. That is cheap enough to run weekly, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  A seven-day protocol
&lt;/h2&gt;

&lt;p&gt;Do not act on a single scan. Do this instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1.&lt;/strong&gt; Baseline scan, fixed query, fixed filters, five markets. Save the raw dataset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Days 2-7.&lt;/strong&gt; Same input, once a day. Track the median per market, not the average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 7.&lt;/strong&gt; Keep only pairs where the P75 sell side clears your all-in cost by 25%, and where the gap has held for at least three days. Gaps that appear and vanish in 24 hours are usually one seller, not a market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then verify by hand.&lt;/strong&gt; Open the top five candidates, check condition, photos, size, seller feedback and whether the item is still live. The scanner gives you candidates; it does not give you judgement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would not do
&lt;/h2&gt;

&lt;p&gt;I would not run this as a dropshipping business. The cross-border operational layer, the return risk and the sold-price blindness are all working against you, and no amount of scraping fixes any of them.&lt;/p&gt;

&lt;p&gt;I would run it as a pricing tool. It answers a question every reseller actually has: &lt;em&gt;what does this thing clear for, in which market, at the top of the range?&lt;/em&gt; That answer is worth more than a 110% screenshot.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Actor used here is &lt;a href="https://apify.com/ahmed_jasarevic/vinted-cross-country-arbitrage-scraper" rel="noopener noreferrer"&gt;Vinted Cross-Country Arbitrage&lt;/a&gt; — 26 European markets, ECB-normalised prices, per-market medians and pairwise gross margins. The fee and FX assumptions above are documented in the post; change them before you make a decision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want the version of this analysis with your own category, run one scan and drop the numbers into &lt;code&gt;netMargin()&lt;/code&gt;. The result is usually more useful than the headline.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>ecommerce</category>
      <category>data</category>
      <category>api</category>
    </item>
    <item>
      <title>Top 10 Real Estate Portals in 2026 — and What Data You Can Actually Extract</title>
      <dc:creator>Ahmed Jasarevic</dc:creator>
      <pubDate>Tue, 13 Oct 2026 09:29:34 +0000</pubDate>
      <link>https://dev.to/apify/top-10-real-estate-portals-in-2026-and-what-data-you-can-actually-extract-3j04</link>
      <guid>https://dev.to/apify/top-10-real-estate-portals-in-2026-and-what-data-you-can-actually-extract-3j04</guid>
      <description>&lt;p&gt;Zillow gets &lt;strong&gt;247 million visits a month&lt;/strong&gt;. Rightmove effectively &lt;em&gt;is&lt;/em&gt; the UK house-price index. Idealista is where price-per-m² in Madrid, Milan and Lisbon gets checked.&lt;/p&gt;

&lt;p&gt;Treat them as what they are: the biggest structured property databases on the planet — updated daily, no official public API.&lt;/p&gt;

&lt;p&gt;Here are 10 of the largest property portals, what each one actually exposes, and how to get it out without writing a scraper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list (ranked by monthly traffic, Aug 2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Portal&lt;/th&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Visits/mo&lt;/th&gt;
&lt;th&gt;Standout data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Zillow&lt;/td&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;247M&lt;/td&gt;
&lt;td&gt;Zestimate + rent estimates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Redfin&lt;/td&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;64M&lt;/td&gt;
&lt;td&gt;Price history, walk/transit scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Rightmove&lt;/td&gt;
&lt;td&gt;UK&lt;/td&gt;
&lt;td&gt;57M&lt;/td&gt;
&lt;td&gt;Sold prices, tenure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Idealista&lt;/td&gt;
&lt;td&gt;ES/IT/PT&lt;/td&gt;
&lt;td&gt;51M&lt;/td&gt;
&lt;td&gt;Price/m², energy rating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Apartments.com&lt;/td&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;47M&lt;/td&gt;
&lt;td&gt;Rent ranges, leasing phones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;ImmobilienScout24&lt;/td&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;39M&lt;/td&gt;
&lt;td&gt;Energy class, construction year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Homes.com&lt;/td&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;34M&lt;/td&gt;
&lt;td&gt;Agent name + phone per listing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Funda&lt;/td&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;Neighbourhood hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Trulia&lt;/td&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;21M&lt;/td&gt;
&lt;td&gt;Neighborhood context, price history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Bayut&lt;/td&gt;
&lt;td&gt;UAE/Gulf&lt;/td&gt;
&lt;td&gt;Gulf #1&lt;/td&gt;
&lt;td&gt;Off-plan payment plans, AED prices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;General classifieds (Craigslist, Leboncoin) excluded — they're not dedicated property portals. Bayut is ranked by regional leadership, not global traffic.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get per portal
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zillow&lt;/strong&gt; — price, address components, beds/baths, living + lot area, Zestimate and rent estimates, lat/lng, property type, status, photos, stable property ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redfin&lt;/strong&gt; — price &lt;em&gt;and&lt;/em&gt; price history, AVM (Redfin Estimate) history, sqft, MLS IDs, days-on-market, HOA, walk/transit/bike scores, agent + broker metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rightmove&lt;/strong&gt; — price/currency, beds/baths, property type + &lt;strong&gt;tenure&lt;/strong&gt;, coordinates, price-change history, branch/estate-agent info, Land Registry-backed sold prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idealista&lt;/strong&gt; — price, &lt;strong&gt;price/m²&lt;/strong&gt;, area, rooms, floor, GPS, &lt;strong&gt;energy rating&lt;/strong&gt;, year built, fees/taxes, price history, agency contact info.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apartments.com&lt;/strong&gt; — property name, street/city/state/ZIP, rent ranges, beds/baths, sqft, &lt;strong&gt;leasing-office phone&lt;/strong&gt;, URL, GPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ImmobilienScout24&lt;/strong&gt; — price + type (rent/sale), rooms, m², address, &lt;strong&gt;energy efficiency class&lt;/strong&gt;, construction year, agent contacts, amenities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Homes.com&lt;/strong&gt; — address + geocoordinates, price, beds/baths, sqft, status, descriptions, &lt;strong&gt;listing agent name, phone and title&lt;/strong&gt;, brokerage, open-house/sold flags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Funda&lt;/strong&gt; — street/postcode/city/province/neighbourhood/municipality, numeric + formatted price, size, status, buy/rent, agent contacts, coordinates, floor plans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trulia&lt;/strong&gt; — price, beds/baths, interior + lot area, address + neighborhood, coordinates, agent/broker name + phone, per-property price history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bayut&lt;/strong&gt; — AED asking prices, coordinates, beds/baths, amenities, &lt;strong&gt;off-plan payment-plan milestones&lt;/strong&gt;, unit breakdowns, developer + agency profiles, completion status (UAE, KSA, Qatar, Jordan, Oman).&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema overlap (why merging portals is easy)
&lt;/h2&gt;

&lt;p&gt;Every one of them resolves to roughly the same record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;bedrooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;bathrooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;property&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;agent&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;scraped&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Portal-specific extras are additive: Zestimate (Zillow), walk scores (Redfin), tenure (Rightmove), €/m² + EPC (Idealista), payment plans (Bayut). Merge three portals into one table and you lose nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pull the data without writing a scraper
&lt;/h2&gt;

&lt;p&gt;Each portal above has a maintained &lt;a href="https://apify.com/ahmed_jasarevic" rel="noopener noreferrer"&gt;Apify&lt;/a&gt; Actor that queries the portal's &lt;strong&gt;internal JSON endpoints&lt;/strong&gt; — not rendered HTML — so output is structured on arrival:&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;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;425000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Queens Head Street, London, N1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bedrooms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bathrooms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propertyType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apartment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;51.5366&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.0958&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agentName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Agency"&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.rightmove.co.uk/properties/173742266"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scrapedAt"&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-13T09:00:00Z"&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;Workflow: &lt;strong&gt;1)&lt;/strong&gt; set a search (city, ZIP, filtered URL) → &lt;strong&gt;2)&lt;/strong&gt; run → &lt;strong&gt;3)&lt;/strong&gt; export JSON/CSV/Excel → &lt;strong&gt;4)&lt;/strong&gt; schedule it (daily for price-drop monitoring, weekly for market snapshots).&lt;/p&gt;

&lt;p&gt;You can also drive it from code — the same Actors are callable over the Apify REST API with a single &lt;code&gt;POST /v2/acts/{id}/runs&lt;/code&gt;, so a cron job + webhook gives you a property data feed.&lt;/p&gt;

&lt;p&gt;Actor links: &lt;a href="https://apify.com/ahmed_jasarevic/zillow-scraper" rel="noopener noreferrer"&gt;Zillow&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/redfin-scraper" rel="noopener noreferrer"&gt;Redfin&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/rightmove-scraper" rel="noopener noreferrer"&gt;Rightmove&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/idealista-property-scraper" rel="noopener noreferrer"&gt;Idealista&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/apartments-scraper" rel="noopener noreferrer"&gt;Apartments.com&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/immobilienscout24-scraper-pro" rel="noopener noreferrer"&gt;ImmobilienScout24&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/homes-scraper" rel="noopener noreferrer"&gt;Homes.com&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/funda-scraper" rel="noopener noreferrer"&gt;Funda&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/trulia-scraper" rel="noopener noreferrer"&gt;Trulia&lt;/a&gt; · &lt;a href="https://apify.com/ahmed_jasarevic/bayut-scraper" rel="noopener noreferrer"&gt;Bayut&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there a real estate API alternative to Zillow?&lt;/strong&gt;&lt;br&gt;
Zillow's official API is closed to new sign-ups and Rightmove never had a public one. Scraper-as-API is the practical alternative: pay per result, no data contract, same fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is scraping property portals legal?&lt;/strong&gt;&lt;br&gt;
Listings are public pages. The risks live elsewhere: bypassing access controls, republishing photos/text, and outreach under GDPR/CCPA/CAN-SPAM. Respect each portal's ToS and robots.txt — this isn't legal advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the best data source for property prices?&lt;/strong&gt;&lt;br&gt;
Idealista for €/m² (ES/IT/PT), Rightmove sold prices (UK), Funda (NL), ImmobilienScout24 (DE), Zillow/Redfin (US). Schedule recurring runs — trends need a time dimension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I monitor new listings and price drops automatically?&lt;/strong&gt;&lt;br&gt;
Yes — schedule the Actor (daily/weekly) or call it from the API and attach a webhook. Results land in your DB, Sheet or CRM.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full breakdown with per-portal analysis: &lt;a href="https://medium.com/@jasarevicahmed999/the-10-biggest-property-portals-in-the-world-and-how-to-pull-clean-data-from-each-bd2e18adeacf?source=friends_link&amp;amp;sk=938bf4e9cfe6535a7d26709971802251" rel="noopener noreferrer"&gt;The 10 Biggest Property Portals in the World&lt;/a&gt; (Medium). I build the scrapers linked above — &lt;a href="https://apify.com/ahmed_jasarevic" rel="noopener noreferrer"&gt;230+ actors on Apify&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>dataengineering</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Linktree, Beacons &amp; Bio Email Scraper with Apify</title>
      <dc:creator>Ahmed Jasarevic</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:42:11 +0000</pubDate>
      <link>https://dev.to/apify/how-i-built-a-linktree-beacons-bio-email-scraper-with-apify-oo0</link>
      <guid>https://dev.to/apify/how-i-built-a-linktree-beacons-bio-email-scraper-with-apify-oo0</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Collecting emails from Linktree, Beacons, and other bio links is often a tedious and time-consuming task. As a developer and freelancer working with lead generation projects, I faced this problem firsthand: manually navigating hundreds of profiles just to extract a few emails is inefficient and error-prone.&lt;br&gt;
To solve this, I built a Linktree, Beacons &amp;amp; Bio Email Scraper Actor using Apify. It automates the process, reliably collects emails from multiple platforms, and provides structured outputs for marketing, data engineering, and business intelligence purposes.&lt;br&gt;
This article details the journey: the challenges I faced, the design of the Actor, lessons learned, and tips for anyone looking to build production-grade scraping solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Problem Context &amp;amp; Motivation
&lt;/h2&gt;

&lt;p&gt;Manual email collection from bio links presents several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic pages: Platforms like Linktree often use JavaScript to load content asynchronously. Simple HTML scraping fails here.&lt;/li&gt;
&lt;li&gt;Anti-scraping measures: Repeated requests can trigger rate limits or captchas.&lt;/li&gt;
&lt;li&gt;Scale: For real lead-generation projects, you often need hundreds or thousands of emails daily—manual collection is not an option.
I needed a solution that was fast, scalable, and reliable, and that could handle these technical challenges without breaking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Actor Overview
&lt;/h3&gt;

&lt;p&gt;Architecture and Workflow&lt;br&gt;
The Actor is built on Apify and Crawlee, combining headless browser automation with structured data extraction. Here’s a high-level workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Input: A list of Linktree, Beacons, or other bio URLs.&lt;/li&gt;
&lt;li&gt;Navigation: Actor launches a headless browser to visit each URL.&lt;/li&gt;
&lt;li&gt;Email Extraction: Uses DOM selectors and regex patterns to identify valid email addresses.&lt;/li&gt;
&lt;li&gt;Anti-blocking: Rotates proxies and applies request throttling to avoid detection.&lt;/li&gt;
&lt;li&gt;Output: Saves results in JSON or CSV with details: URL, email, timestamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Input, Output, and Configuration Options&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: List of profile URLs (CSV, JSON, or manually typed).&lt;/li&gt;
&lt;li&gt;Output: JSON, CSV, or Google Sheets integration.&lt;/li&gt;
&lt;li&gt;Configurable Options:
Maximum pages per run
Timeout per request
Proxy rotation (on/off)
Output file format
This flexibility allows the Actor to handle both small projects and large-scale campaigns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xwioawnnjr99hm1xvbz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xwioawnnjr99hm1xvbz.jpg" alt="Image 1: Example of output data" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Implementation Details
&lt;/h2&gt;

&lt;p&gt;Handling Dynamic Content&lt;br&gt;
Linktree and Beacons often load content via JavaScript. Initially, I tried simple HTTP requests, but emails were missing in the HTML response. Switching to Crawlee with Playwright solved the issue:&lt;/p&gt;

&lt;p&gt;import { PlaywrightCrawler } from 'crawlee';&lt;/p&gt;

&lt;p&gt;const crawler = new PlaywrightCrawler({&lt;br&gt;
    requestHandler: async ({ page, request, enqueueLinks, log }) =&amp;gt; {&lt;br&gt;
        await page.goto(request.url);&lt;br&gt;
        const emails = await page.$$eval('a[href^="mailto:"]', els =&amp;gt; els.map(e =&amp;gt; e.href));&lt;br&gt;
        console.log(&lt;code&gt;Found emails for ${request.url}: ${emails}&lt;/code&gt;);&lt;br&gt;
    }&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;await crawler.run(['&lt;a href="https://linktr.ee/example'%5D" rel="noopener noreferrer"&gt;https://linktr.ee/example']&lt;/a&gt;);&lt;/p&gt;

&lt;p&gt;This approach ensures the Actor captures all visible emails, even on dynamic pages.&lt;br&gt;
Anti-Blocking Strategies&lt;br&gt;
During early tests, some accounts triggered rate limits. To fix this, I added:&lt;br&gt;
Proxy rotation&lt;br&gt;
Randomized delays between requests&lt;br&gt;
Error retries for failed pages&lt;br&gt;
These measures increased the success rate to 99.6% over thousands of URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Metrics &amp;amp; Results
&lt;/h2&gt;

&lt;p&gt;To evaluate the Actor in a real-world scenario, I ran it against a dataset of 1,800+ URLs. The results showed strong reliability while keeping the cost of each run predictable:&lt;/p&gt;

&lt;p&gt;99.6% success rate&lt;br&gt;
1,816 emails collected&lt;br&gt;
$14.47 total cost per run&lt;br&gt;
$23.76 profit generated per run from the collected leads&lt;/p&gt;

&lt;p&gt;These results demonstrated that the Actor could process large batches of URLs reliably while remaining cost-effective for lead-generation workflows.&lt;/p&gt;

&lt;p&gt;This shows the Actor is not only technically reliable but also economically valuable for lead-generation workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this Actor taught me several important lessons:&lt;br&gt;
JavaScript rendering matters: Always test pages in a headless browser when dealing with dynamic content.&lt;br&gt;
Anti-blocking is critical: Even simple rotation and throttling drastically improve success rates.&lt;br&gt;
First-person debugging insights: Logging actual page content during development helped identify hidden issues.&lt;br&gt;
Scalable design: Structuring input/output for batch processing makes the Actor production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Who Can Benefit
&lt;/h2&gt;

&lt;p&gt;This Actor is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers looking to automate repetitive data collection tasks&lt;/li&gt;
&lt;li&gt;Marketers and sales teams needing up-to-date email lists&lt;/li&gt;
&lt;li&gt;Data engineers building pipelines that integrate multiple data sources&lt;/li&gt;
&lt;li&gt;The architecture can also be adapted for other bio link platforms or email collection projects with similar challenges.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Conclusion
&lt;/h3&gt;

&lt;p&gt;Automating email extraction from Linktree, Beacons, and bio links is no longer a manual nightmare. By using Apify and Crawlee, I built a reliable, scalable, and cost-effective Actor that delivers real-world results.&lt;br&gt;
If you plan to build your own Actor, remember: focus on handling dynamic content, preventing blocks, and structuring your data pipeline. Sharing these lessons ensures other developers can build production-grade automation with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test the Actor on new bio platforms&lt;/li&gt;
&lt;li&gt;Add AI-powered validation to filter incorrect emails&lt;/li&gt;
&lt;li&gt;Explore integrations with CRMs and email marketing tools&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>data</category>
      <category>scraping</category>
    </item>
  </channel>
</rss>
