# Real Estate Deal Score & Rental Yield (Germany & Austria) (`mocha_tassel/real-estate-deal-score`) Actor

Scrapes Immowelt, willhaben or Kleinanzeigen and scores every property against its local market: price per m², deviation from the postal-code median, a 0-100 deal score and gross rental yield from real local rents.

- **URL**: https://apify.com/mocha\_tassel/real-estate-deal-score.md
- **Developed by:** [Niko T.](https://apify.com/mocha_tassel) (community)
- **Categories:** Real estate, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 scored properties

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

## Real Estate Deal Score & Rental Yield — Germany & Austria

![real-estate-deal-score](https://api.apify.com/v2/key-value-stores/yfnvtbimUC1yUnStx/records/real-estate-deal-score.png?signature=1oMImVb9aluaQyNV4daCw)

Scraping property listings is easy. Knowing which of them is actually a good price is the hard part.

This Actor collects listings from Immowelt, willhaben or Kleinanzeigen and then **scores each one against its own local market**: what the median price per m² is in that postal code, how far this property deviates from it, and what it would yield if rented out.

### What makes the numbers trustworthy

**The comparison is local, not city-wide.** A flat in a prime district is not overpriced just because it costs more than the city average. The Actor builds medians per postal code first, falls back to the district, then the city, and records which level was used in `valuation.referenceLevel`.

**Outliers are removed** before the median is calculated, so a single mispriced listing does not distort the benchmark.

**Thin data is treated as thin data.** With few comparable properties the deal score is pulled towards the middle instead of pretending certainty, and `valuation.sampleSize` tells you how many properties it was based on. Properties without a usable price or area are returned with the rating `not comparable` rather than a guess.

**Rental yield comes from real local rents.** When enabled, the Actor runs a second search in the rental segment of the same area and derives the actual rent per m² for each postal code. Postal codes with fewer than three rental listings get no yield figure at all, instead of a made-up one.

### What you get

```json
{
  "url": "https://www.immowelt.de/expose/8ca0b750-a54e-4114-9bc7-0fabecfd199a",
  "title": "Wohnung zum Kauf — 2 Zimmer, 88,6 m²",
  "price": 170000,
  "livingArea": 88.6,
  "pricePerSqm": 1918.74,
  "postalCode": "04179",
  "city": "Leipzig",
  "imageUrl": "https://mms.immowelt.de/example/photo.jpg",
  "valuation": {
    "referenceLevel": "postalCode",
    "marketMedianPerSqm": 3120.5,
    "marketP25PerSqm": 2480.0,
    "marketP75PerSqm": 3890.0,
    "deviationPct": -38.5,
    "rating": "bargain",
    "dealScore": 93,
    "sampleSize": 34,
    "priceDeltaToMedian": -106500,
    "grossYieldPct": 6.8
  }
}
```

**Deal score** runs from 0 to 100, where 50 is exactly the local median and higher means cheaper. **Rating** is one of `bargain`, `below market`, `market price`, `above market`, `overpriced`, `not comparable`. The Output tab shows a photo next to each score.

### Typical uses

- **Finding underpriced property**: turn on *Return underpriced properties only* with a minimum deal score of 65 and get a shortlist instead of a spreadsheet.
- **Investment screening**: sort by gross yield to see where the rent-to-price ratio actually works.
- **Market reports**: run it monthly for the same city and track how medians move.
- **Feeding a model**: every field is numeric and normalised, ready for a notebook or a database.

### How to set it up

1. Choose a portal and a location, or paste a **Search URL** copied from the portal.
2. Set **Properties to score**. More is better for the statistics: below 50 the medians get thin, from 300 upwards you get solid per-postal-code figures.
3. Leave **Calculate gross rental yield** on unless you only want the price comparison.

### Pricing

You pay per scored property. If rental yield is enabled, each rental listing used as a comparable costs a small additional amount, because it takes the same work to fetch but does not appear in your results.

### Proxies

German and Austrian property portals reject data-centre requests with HTTP 403, so the Actor uses Apify residential proxies from the matching country by default. You can supply your own instead.

### Limitations

- The valuation is based on **asking prices**, not on recorded sale prices. It tells you how a listing compares to what else is on the market right now, which is not the same as an appraisal.
- Gross yield is gross: it does not deduct maintenance, management fees, vacancy or taxes.
- Data comes from public search result pages, so fields the portal only shows on the detail page are not included.

***

### Auf Deutsch

Dieser Actor sammelt Inserate von Immowelt, willhaben oder Kleinanzeigen und **bewertet jedes Objekt gegen seinen eigenen lokalen Markt**: Median-Preis je m² in der Postleitzahl, Abweichung davon, ein Deal-Score von 0 bis 100 und die Bruttomietrendite.

**Warum die Zahlen belastbar sind:** Verglichen wird zuerst innerhalb der Postleitzahl, erst danach auf Stadtteil- und Stadtebene — eine Wohnung in bester Lage ist nicht überteuert, nur weil sie mehr kostet als der Stadtdurchschnitt. Ausreißer fliegen vor der Median-Bildung raus. Bei dünner Datenlage wird der Score zur Mitte gezogen, statt Sicherheit vorzutäuschen; das Feld `valuation.sampleSize` zeigt, auf wie vielen Vergleichsobjekten die Aussage beruht.

**Die Rendite beruht auf echten Mieten** aus derselben Gegend: Der Actor führt dafür eine zweite Suche im Mietsegment durch. Postleitzahlen mit weniger als drei Mietobjekten bekommen bewusst gar keine Rendite statt einer geschätzten.

**Wichtige Einschränkung:** Grundlage sind **Angebotspreise**, keine beurkundeten Kaufpreise. Das Ergebnis sagt, wie ein Objekt im Vergleich zum aktuellen Angebot dasteht — es ersetzt kein Gutachten. Die Rendite ist eine Bruttorendite, ohne Instandhaltung, Verwaltung, Leerstand und Steuern.

**Kosten:** Sie zahlen je bewertetem Objekt. Ist die Renditeberechnung aktiv, kommt ein kleiner Betrag je herangezogenem Mietinserat hinzu — diese Inserate verursachen denselben Aufwand, erscheinen aber nicht im Ergebnis.

# Actor input Schema

## `portal` (type: `string`):

Which portal to collect and score properties from.

## `searchUrl` (type: `string`):

Paste any search URL from the portal. If set, the location and category fields below are ignored.

## `location` (type: `string`):

For Immowelt, a German city name such as Berlin, Munich (München) or Leipzig. For willhaben, an Austrian town or state such as Wien, Graz or Steiermark.

## `offerType` (type: `string`):

Whether to score properties for sale or for rent. Rental yield only makes sense for properties for sale.

## `estateType` (type: `string`):

Type of property. Applies to Immowelt only; willhaben and Kleinanzeigen use the category field instead.

## `category` (type: `string`):

For willhaben: eigentumswohnung, mietwohnung, haus-kaufen, haus-mieten, grundstuecke. For Kleinanzeigen: mietwohnung, eigentumswohnung, haus-mieten, haus-kaufen, grundstueck.

## `locationSlug` (type: `string`):

The location exactly as it appears in the Kleinanzeigen URL, for example karlsruhe.

## `locationCode` (type: `string`):

The code starting with l from the Kleinanzeigen URL, for example l9186 for Karlsruhe.

## `maxItems` (type: `integer`):

More properties make the market comparison more reliable. Below 50 the median gets thin; from 300 upwards you get solid per-postal-code medians.

## `calculateYield` (type: `boolean`):

Runs a second search in the rental segment of the same area and derives the local rent per m² for each postal code. Only meaningful for properties for sale.

## `rentSampleSize` (type: `integer`):

How many rental listings are used as the reference. Postal codes with fewer than three rentals are left without a yield figure rather than guessed.

## `onlyDeals` (type: `boolean`):

Keeps only properties priced clearly below their local market level.

## `minDealScore` (type: `integer`):

50 equals the market median, higher means cheaper. 65 is a good starting point for bargains. Only applies when the option above is on.

## `sortByDealScore` (type: `boolean`):

Puts the best-priced properties first.

## `proxyConfiguration` (type: `object`):

Property portals block requests from data centres. The default uses Apify residential proxies from the matching country — without them runs fail with HTTP 403. Your own proxies work too.

## Actor input object example

```json
{
  "portal": "immowelt",
  "location": "berlin",
  "offerType": "buy",
  "estateType": "apartment",
  "category": "eigentumswohnung",
  "maxItems": 300,
  "calculateYield": true,
  "rentSampleSize": 200,
  "onlyDeals": false,
  "minDealScore": 65,
  "sortByDealScore": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `properties` (type: `string`):

Each property with deal score, market deviation and optional rental yield.

## `summary` (type: `string`):

Counts, rating breakdown and best-priced property of this run.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("mocha_tassel/real-estate-deal-score").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("mocha_tassel/real-estate-deal-score").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 '{}' |
apify call mocha_tassel/real-estate-deal-score --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mocha_tassel/real-estate-deal-score"
        }
    }
}

```

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/yCVjpPT6GZVpTwz38/builds/CxYVaF1qforcFsCts/openapi.json
