# Vinted Underpriced Deals — Cheap and Wanted, in One Call (`jpmarketdata/vinted-underpriced-deals`) Actor

Which Vinted listings are cheap right now? Builds the asking-price distribution for your search on one country's site, scores every listing against it, and returns those below your percentile, ranked by favourite count. Promoted listings are kept out of the basis. A mixed search is refused.

- **URL**: https://apify.com/jpmarketdata/vinted-underpriced-deals.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 search scanned for deals

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 Underpriced Deals — Cheap and Wanted, in One Call

Every Vinted scraper on the Store will hand you a page of listings. None of them will tell you whether the listing in front of you is **cheap**, and none of them will tell you whether anyone else **wants it**. This one answers both in a single call: it builds the asking-price distribution of your search on one country's site, returns the listings below your percentile, and ranks them by demand.

### Read this before you read the numbers

**Vinted has no sold data.** Its catalog serves live, unsold listings only — there is no sold book to scrape, buy or infer. So the basis here is the **asking-price distribution of the search itself**, exactly as our AutoScout24 deal finder does for cars.

That means every number in the output answers *"cheap relative to what else is being asked right now"* and **not** *"cheap relative to what things sell for"*. `discountBasis` says so on the summary and on every single deal record. If you need realized sale prices, this is the wrong marketplace, not the wrong Actor.

### How a deal is picked

1. Page the catalog for your search term with `order=relevance`, pushing `priceMin` / `priceMax` to Vinted so the basis and the candidates are **the same sample**.
2. Drop the promoted placements, the duplicates and the priceless rows (see below) — what is left is the basis.
3. `dealCutoffPrice` = the price at your `dealPercentile` of that basis. `20` means "in the cheapest fifth of what is on offer".
4. Everything at or below the cutoff is a deal — then **ranked by `favouriteCount`, not by how cheap it is.**

**Cheap alone is worthless.** The cheapest listing in a fashion search is usually the least wanted one. Measured on `nike air max` / vinted.fr: the five cheapest "deals" in an unbounded search were a sports bra, a sweatshirt, a pair of shorts and two pairs of shoes; the five *most favourited* deals were all shoes. Demand is what separates a bargain from a thing nobody wants at any price.

The ranking is auditable, not asserted: `demand.favouriteCountBasis` and `demand.favouriteCountDeals` ship with every summary, and every deal carries its own `favouriteCount`, `favouritePercentile` and `demandRank`.

> `view_count` is returned by Vinted's catalog endpoint and is **always 0** — measured over ~1,800 items across seven searches and two country sites. It is therefore not part of the ranking, and `demand.viewCountAvailable` is `false` rather than the Actor quietly ranking on a field of zeroes.

### Two prices, and which one the percentile is on

Vinted quotes `price` (what the seller asks) and `total_item_price` (what the buyer pays, i.e. asking + Buyer Protection — measured affine, about `0.70 + 5%`). **The percentile, the cutoff and the spread guard are all computed on the asking price**, which is what the search page shows and what "underpriced" means. Both are emitted on every deal, together with `buyerProtectionFee`, so you can see the difference before you commit.

### Two guards, because a basis can lie in two ways

**Too thin.** `minComps` (default 30) refuses a search whose percentile would rest on a handful of prices.

**Too many kinds of thing.** `maxCompSpread` checks the **p90 / p10** ratio of the sampled asking prices and refuses a search that is mixing product tiers rather than describing one market. p90/p10 rather than p75/p25 because a fashion keyword is a wide market by nature and the mixing shows up in the tail. Measured on vinted.fr, 2026-08-10:

| search | p90 / p10 | what it is |
|---|---|---|
| `levis 501` | 3.3x | one product |
| `nike air max`, `priceMin: 30` | 2.5x | one product, tier-bounded |
| `nike air max`, unbounded | 5.3x | shoes **plus** tracksuits, leggings and bags |
| `zara robe` | 5.7x | many dress tiers |
| `chanel` | 15.0x | a keychain and a handbag in one median |
| `leica m6` | 204x | a lens cap and a camera in one median |

The fix is named in the refusal itself:

```json
{ "keywords": ["nike air max"], "domain": "fr", "priceMin": 30 }
```

The band is pushed to Vinted, so it bounds the basis and the candidates together.

In both cases the run returns `basisUsable: false` with the reason, the ratio and the input that fixes it — and **is not charged**.

### What the guards cannot fix is printed, not hidden

Every summary carries a `limitations` array. It will tell you, in the run's own numbers:

- **`totalFound` saturates at 960.** Vinted stops counting and paging at 10 pages of 96 (page 11 is an HTTP 400). `nike air max` reports 960 and so does `chanel`, while `leica m6` reports 218. **960 is a ceiling, not a count** — `totalFoundCapped: true` means the real supply is unknown and larger, and your sample is the first pages by relevance rather than a random draw.
- **Promoted listings are excluded, and how many.** Measured over all 880 distinct `nike air max` listings on vinted.fr: promoted median **EUR 40** (n=281) against organic median **EUR 20** (n=599) — and 66% of the first three relevance pages were promoted, against 32% over all ten. Sampling paid placements into the basis lifts the median and manufactures discounts out of nothing, so they are dropped from both sides and counted in `excluded.promoted`. The sampler pages deeper to make the shortfall up.
- **Duplicates, malformed rows and priceless rows**, each counted separately in `excluded`. A filter whose count you cannot see makes the remainder look like the whole.
- **A pushed price band truncates the basis**, so listings sitting on the floor of the band read as the 0th percentile.
- **A keyword is not a product.** `sampleMix` shows the brands and conditions the percentile was actually built on.

### Output

One `deal_summary` per search term — the basis (`price` with p10/p25/median/p75/p90, plus `totalItemPrice`), the guards and their verdict, the sample and everything excluded from it, the cutoff, the deal count, the demand statistics behind the ranking, `bestDeal` (most wanted) and `cheapestDeal` (lowest price — deliberately a different field). Optionally one `deal` record per listing under the cutoff, ranked by demand.

Country sites: `vinted.com` (USD), `.fr` `.de` `.es` `.it` `.nl` (EUR), `.co.uk` (GBP), `.pl` (PLN). **The domain decides both the market and the currency, so a basis is never built across countries.** USD figures are added alongside the native currency.

### Pricing

From **$0.02** per search term, $0.002 per individual deal. **A search with an unusable basis is never charged.**

Read-only public catalog data over HTTP/JSON — no login, no browser, throttled to one page per 1.5 s. Seller identities are never emitted. Nothing is stored between runs.

# Actor input Schema

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

What to search for, e.g. "nike air max". Each term gets its own price basis.

## `domain` (type: `string`):

Which Vinted site to search. The domain decides both the market and the currency, so a basis is only ever built within one country.

## `dealPercentile` (type: `integer`):

A listing counts as a deal when its price sits at or below this percentile of the sampled asking prices. 20 means "in the cheapest fifth of what is on offer".

## `maxListings` (type: `integer`):

How many listings to sample for the basis and for scoring. Vinted stops counting and paging at 960 hits, so this cannot exceed that ceiling.

## `minComps` (type: `integer`):

Refuse to score a search with fewer sampled listings than this. A percentile built on a dozen prices is noise.

## `maxCompSpread` (type: `string`):

Coherence ceiling for the basis, as the p90/p10 ratio. Measured on vinted.fr 2026-08-10: a bounded single-product search runs 2.3-3.3x, while "chanel" (keychain and handbag in one median) runs 7.4-15x and "leica m6" 204x. A search above the ceiling is mixing product tiers and is refused rather than scored - bound it with priceMin / priceMax.

## `priceMin` (type: `integer`):

Pushed to Vinted, so the basis and the candidates cover the same tier. In the country site's own currency.

## `priceMax` (type: `integer`):

Pushed to Vinted, like the minimum.

## `includeIndividualDeals` (type: `boolean`):

Also return each listing under the cutoff (charged per deal).

## `convertToUsd` (type: `boolean`):

Add USD figures alongside the native currency.

## Actor input object example

```json
{
  "keywords": [
    "nike air max"
  ],
  "domain": "fr",
  "dealPercentile": 20,
  "maxListings": 300,
  "minComps": 30,
  "maxCompSpread": "4.0",
  "includeIndividualDeals": true,
  "convertToUsd": true
}
```

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jpmarketdata/vinted-underpriced-deals").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"] }

# Run the Actor and wait for it to finish
run = client.actor("jpmarketdata/vinted-underpriced-deals").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"
  ]
}' |
apify call jpmarketdata/vinted-underpriced-deals --silent --output-dataset

```

## MCP server setup

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

```

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/gUOxhNZpgi9Li9XsM/builds/kIbAPSoVt9It4a5so/openapi.json
