# Vinted Price Comparison — Cross-Country Arbitrage & Price Gap (`jpmarketdata/vinted-cross-country-price-gap`) Actor

Compare one keyword across 8 national Vinted marketplaces in one call: each country median in USD with its sample size, the cheapest and priciest market, the price gap %, and a cross-country arbitrage spread including Buyer Protection fees. From $0.02 — no subscription. Vinted 8カ国の価格差を1コールで比較。

- **URL**: https://apify.com/jpmarketdata/vinted-cross-country-price-gap.md
- **Developed by:** [h ichi](https://apify.com/jpmarketdata) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 cross-country price gaps

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Vinted Price Comparison — Cross-Country Arbitrage & Price Gap

**Where is this cheapest, where is it dearest, and can you actually trade the difference?** One call per keyword prices the same search on up to **8 national Vinted marketplaces at once**, converts every median into **one currency (USD)**, and returns the ranking, the gap, and the resale spread.

| | |
|---|---|
| Markets | vinted **.com** (USD) · **.fr** · **.de** · **.es** · **.it** · **.nl** (EUR) · **.co.uk** (GBP) · **.pl** (PLN) |
| Price basis | **asking** — Vinted's catalog serves live listings only. No number here is a sold price |
| Per country | median + quartiles (native **and** USD), sample size, hit count, Buyer Protection fee, demand heat, brand mix |
| Across countries | cheapest / priciest, `gapPct`, **inventory pools**, and a buy-here/sell-there margin that includes the buyer's fee |

### The one thing every other Vinted price comparison gets wrong

**The eight marketplaces are not eight inventories.** Measured on 2026-08-07 with `leica m6` across all eight: the five euro sites served **58–87% literally the same listing ids** — same item, same price, same currency, five hostnames — while **.com**, **.co.uk** and **.pl** overlapped everything by **0.0%**.

```
        US    FR    DE    GB    ES    IT    NL    PL
FR       0%    -    62%    0%   64%   78%   78%    0%
IT       0%   78%   71%    0%   71%    -    81%    0%
```

So a "price gap" between France and Italy is mostly **one pool sampled twice**. You cannot buy an item in Italy and sell it in France when it is the same listing. This Actor therefore:

1. measures the overlap from the sampled listing ids and groups the countries into **`inventoryOverlap.inventoryPools`**;
2. still ranks **every** country by price, because "where is it listed lowest" is a real question;
3. computes **`gap.arbitrage` only across different pools** — and says `available: false` when every selected country turns out to be one pool.

Measured example of the trap (`levis 501 vintage`, 8 countries): cheapest **ES $38.59** → priciest **DE $44.97** looks like a **16.5%** opportunity. ES and DE cross-list one inventory, so it is worth **nothing**. The honest cross-pool deal that run was **buy US $42.70 → sell DE $44.97 = 5.3%**.

### The other three numbers that keep it honest

- **Buyer Protection is priced in.** The buyer pays asking + fee, and the fee is not the same everywhere — measured medians: **GB 7.22% · PL 6.45% · US 6.07% · DE 6.01%**. `gap.arbitrage.buyCostUsd` is the buyer's real total, and `excludes` lists what is still missing (shipping, duty, seller fees, FX spread).
- **`totalFoundCapped`.** Vinted stops counting at **960** hits (10 pages × 96). A `totalFound` of 960 is a ceiling, not supply, and says so instead of pretending to be a market size.
- **`thinSample`.** A country with fewer than `minSampleSize` priced listings is reported in full but is **excluded from the cheapest/priciest selection**. A median over four listings is not an arbitrage case.

### Pricing — from $0.02 per keyword, no subscription

| Event | Price | When |
|---|---|---|
| Keyword analyzed across countries | **$0.02** | Per keyword, whatever the number of countries |
| Individual listing | **$0.002** | Only if you enable **Include individual listings** |

A default run (1 keyword, 4 countries, summary only) costs **$0.02** and took **8 seconds** measured. All 8 countries cost the same **$0.02**. Apify's **$5 monthly free credit covers ~250 runs**, and there is **no subscription**.

**A keyword is only charged when at least two countries returned a usable median** (or the single one, if you selected only one). A "cross-country" record with one country in it is not a comparison, so it is free.

### Input

| Field | Example | Notes |
|---|---|---|
| `keywords` | `["nike air max 90"]` | Narrow keywords give a real gap; broad ones compare product mixes. $0.02 each |
| `countries` | `["com","de","co.uk","pl"]` | Default = one market per currency, which is also the four pools that measurably do **not** cross-list. Add `fr`/`es`/`it`/`nl` for the full euro picture |
| `maxItemsPerCountry` | `96` | 30–288. 96 = one result page and is usually enough |
| `minSampleSize` | `10` | Below this a country is flagged `thinSample` and cannot be cheapest/priciest. `0` disables the gate |
| `priceMinUsd` / `priceMaxUsd` | `30` | Optional window **in USD**, applied after conversion, identically in every country |
| `includeIndividualItems` | `false` | Enable to also get every sampled listing, +$0.002 each |

### Output example (`type: "cross_country_price_gap"`, abridged, measured 2026-08-07)

```json
{
  "type": "cross_country_price_gap",
  "keyword": "nike air max 90",
  "priceBasis": "asking",
  "quoteCurrency": "USD",
  "countries": {
    "co.uk": {
      "domain": "vinted.co.uk", "country": "GB", "currency": "GBP",
      "available": true,
      "totalFound": 960, "totalFoundCapped": true,
      "sampleSize": 96, "pricedListings": 96, "thinSample": false,
      "listingPrice":    {"min": 6.0, "p25": 20.0, "median": 32.37, "p75": 62.0, "max": 180.0, "average": 48.67},
      "buyerTotalPrice": {"median": 34.69},
      "buyerProtectionFeePctMedian": 7.22,
      "exchangeRateToUsd": 1.345818,
      "listingPriceUsd":    {"median": 43.56},
      "buyerTotalPriceUsd": {"median": 46.69},
      "signals": {"favouriteCount": {"median": 11},
                  "topBrands": [{"brand": "Nike Air", "listings": 59, "sharePct": 61.5}],
                  "conditionBreakdown": {"Very good": 59, "Good": 16, "New with tags": 12}},
      "sourceUrl": "https://www.vinted.co.uk/catalog?search_text=nike+air+max+90"
    },
    "de": {"country": "DE", "currency": "EUR", "listingPrice": {"median": 69.47},
           "listingPriceUsd": {"median": 80.1}, "buyerProtectionFeePctMedian": 6.01},
    "com": {"country": "US", "listingPriceUsd": {"median": 65.5}},
    "pl":  {"country": "PL", "currency": "PLN", "listingPrice": {"median": 200.0},
            "listingPriceUsd": {"median": 53.63}}
  },
  "inventoryOverlap": {
    "thresholdPct": 20.0, "sampledPairs": 6, "maxOverlapPct": 0.0,
    "independentMarkets": 4,
    "inventoryPools": [["US"], ["DE"], ["GB"], ["PL"]],
    "pairs": [{"countries": ["US","DE"], "sharedListings": 0, "overlapPct": 0.0, "sharedInventory": false}],
    "note": "Every compared country serves its own inventory …"
  },
  "gap": {
    "quoteCurrency": "USD",
    "ranking": [
      {"country": "GB", "medianListingPriceUsd": 43.56, "medianBuyerTotalUsd": 46.69, "medianListingPriceNative": 32.37, "pricedListings": 96, "totalFound": 960, "totalFoundCapped": true, "thinSample": false, "vsCheapestPct": 0.0},
      {"country": "PL", "medianListingPriceUsd": 53.63, "vsCheapestPct": 23.1},
      {"country": "US", "medianListingPriceUsd": 65.5,  "vsCheapestPct": 50.4},
      {"country": "DE", "medianListingPriceUsd": 80.1,  "vsCheapestPct": 83.9}
    ],
    "comparable": true,
    "cheapestCountry": "GB", "cheapestMedianUsd": 43.56,
    "priciestCountry": "DE", "priciestMedianUsd": 80.1,
    "gapUsd": 36.54, "gapPct": 83.9,
    "cheapestAndPriciestShareInventory": false,
    "arbitrage": {
      "available": true, "crossPool": true,
      "buyCountry": "GB", "buyCostUsd": 46.69, "buyCostIncludesBuyerProtection": true,
      "sellCountry": "DE", "sellProceedsUsd": 80.1,
      "grossMarginUsd": 33.41, "grossMarginPct": 71.6,
      "excludes": ["cross-border shipping", "customs duty and import VAT",
                   "the destination marketplace's seller fees", "FX spread on the actual transfer"]
    },
    "note": "Both sides are asking prices, not sold prices …"
  },
  "mixDiagnostics": {
    "topBrandByCountry": {"US": "Nike", "DE": "Nike Air", "GB": "Nike Air", "PL": "Nike Air"},
    "topBrandSharePct": {"US": 52.1, "DE": 64.6, "GB": 61.5, "PL": 53.1},
    "sameTopBrandEverywhere": false, "itemLevelMatching": false,
    "note": "The countries did not sample the same product mix …"
  },
  "checkedAt": "2026-08-07T06:56:00+00:00"
}
```

With `includeIndividualItems: true` you additionally get one record per listing (`type: "listing"`), each tagged with `country`, `domain` and `priceBasis`, carrying both `listingPrice` and `buyerTotalPrice` in native currency and USD.

### Use cases

- **Cross-border sourcing** — `gap.arbitrage` is the only line that pairs two genuinely separate inventories, with the buyer's fee already inside the buy cost
- **Deciding which country to list in** — `ranking` plus `totalFound` is price against the supply you would compete with
- **Repricing** — schedule the Actor and watch a country's median move; `signals.favouriteCount` is the demand heat
- **Market research** — how much cheaper is Poland than Germany for a category, and is the euro zone one market or five
- **Reality-checking a supplier's "EU arbitrage" pitch** — `inventoryOverlap` answers it in one field

### Notes & limits

- **Asking prices only.** Vinted does not serve sold listings. A high median means sellers *ask* more there, not that buyers pay it
- **A broad keyword compares product mixes, not countries.** `mixDiagnostics` is the tell — check `topBrandByCountry` and `topBrandSharePct` before believing a large `gapPct`, and narrow the keyword or set `priceMinUsd`/`priceMaxUsd`
- **Vinted's own brand labels differ per market** (`Nike` on .com vs `Nike Air` on .de for the same shoe), so `sameTopBrandEverywhere` is conservative — a `false` is a prompt to look, not a verdict
- **`totalFound` saturates at 960.** Above that the field is a ceiling; `totalFoundCapped` tells you which it is
- **Condition labels are localized** (`Very good` / `Sehr gut` / `Bardzo dobry`) and are deliberately **not** translated into a shared scale — that would be a claim this Actor cannot back
- **The price window is USD.** A bound in native currency would mean EUR 20 in Germany and PLN 20 in Poland; `totalFound` is never rewritten by it
- **Exchange rates** come from `open.er-api.com`, once per currency per run. A country whose rate is unavailable is reported with `fxUnavailable: true` and excluded from the gap rather than converted at 1.0
- **A country that fails does not fail the run**: it comes back as `available: false` with an `error`, and the rest are still compared. Only a keyword where *every* country failed fails the run
- Read-only public catalog data, no login, no browser. Each country opens its own proxy session (random per run, rotated up to 4×) and pages are throttled to one per 1.5 s
- No seller personal data is collected

***

**日本語**: Vinted 8カ国の同一キーワード相場を1コールで比較し、USD 換算の中央値ランキング・国別サンプル数・価格差(%)、および「実際に取引できる国の組み合わせだけ」で計算した裁定利ざやを返します(ユーロ圏5サイトは同一在庫を共有しているため自動判定して除外します)。

**中文**: 一次调用即可比较 Vinted 八国同一关键词的行情,返回按美元折算的中位价排名、各国样本量与价差百分比,以及仅在真正独立库存之间计算的套利空间(欧元区五站共享同一库存,会被自动识别并排除)。

# Actor input Schema

## `keywords` (type: `array`):

One or more search queries, priced in every selected country (e.g. 'nike air max 90', 'levis 501', 'north face nuptse'). Narrow keywords give a real price gap; broad ones compare different product mixes. Each keyword costs $0.02, whatever the number of countries.

## `countries` (type: `array`):

Which national Vinted marketplaces to price. The domain decides the market AND the currency; every median is converted to USD so the countries are comparable. Four are selected by default (one per currency) — adding the other four costs nothing extra, only a few seconds.

## `maxItemsPerCountry` (type: `integer`):

How many live listings to sample per country before computing the statistics. 96 = one result page and is usually enough; deeper pages are less relevant to the keyword, not more. Pages are throttled to one per 1.5 s.

## `minSampleSize` (type: `integer`):

A country with fewer priced listings than this is still reported in full, but is flagged 'thinSample' and excluded from the cheapest/priciest selection — a median over four listings is not an arbitrage case. Set 0 to rank every country regardless of sample size.

## `priceMinUsd` (type: `integer`):

Drop listings below this before computing the statistics. The bound is in USD and is applied after conversion, identically in every country — a bound in native currency would mean EUR 20 in Germany and PLN 20 in Poland. Useful to cut accessories out of a keyword.

## `priceMaxUsd` (type: `integer`):

Drop listings above this before computing the statistics. Applied in USD, identically in every country. Useful to cut bundles and lot sales out of a keyword.

## `includeIndividualItems` (type: `boolean`):

Off by default: a run costs a flat $0.02 per keyword, whatever the number of countries. Enable to also get every sampled listing from every country (title, asking price, buyer total, brand, size, condition, favourites, URL) at +$0.002 per listing.

## Actor input object example

```json
{
  "keywords": [
    "nike air max 90"
  ],
  "countries": [
    "com",
    "de",
    "co.uk",
    "pl"
  ],
  "maxItemsPerCountry": 96,
  "minSampleSize": 10,
  "includeIndividualItems": false
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "nike air max 90"
    ],
    "countries": [
        "com",
        "de",
        "co.uk",
        "pl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jpmarketdata/vinted-cross-country-price-gap").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": ["nike air max 90"],
    "countries": [
        "com",
        "de",
        "co.uk",
        "pl",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("jpmarketdata/vinted-cross-country-price-gap").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "nike air max 90"
  ],
  "countries": [
    "com",
    "de",
    "co.uk",
    "pl"
  ]
}' |
apify call jpmarketdata/vinted-cross-country-price-gap --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jpmarketdata/vinted-cross-country-price-gap"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zGsWUMiCTOPGH5EYV/builds/9yP5po4hsYXAFXeN9/openapi.json
