# Israel Property Market Signals — Yad2 & Madlan Analytics (`competent_gatekeeper/israel-property-market-signals`) Actor

Turn Israeli property listings into market intelligence: days on market, price-drop history, motivated-seller scores and per-neighbourhood absorption. Works with any listings scraper.

- **URL**: https://apify.com/competent\_gatekeeper/israel-property-market-signals.md
- **Developed by:** [Amit Arie](https://apify.com/competent_gatekeeper) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

Turn a pile of **Israeli property listings** into market intelligence. Point this Actor at the dataset from any **Yad2**, **Madlan** or **Homeless.co.il** scraper and get back what no single scrape can tell you: **how long each flat has been on the market**, **how often and how deeply its price has been cut**, **which sellers look ready to negotiate**, and **how fast inventory is clearing** in each neighbourhood.

Sold prices in Israel are already free from the Tax Authority. Asking prices are on Yad2. Nobody publishes what happens in between — and that gap is where negotiating leverage lives.

### What you get that a listings scraper cannot give you

A scraper photographs the market. This Actor watches it.

| Question | In a scrape? | Here? |
|---|---|---|
| What is this flat asking? | Yes | Yes |
| How long has it been sitting? | No | **Yes — from run 1** |
| Has the seller cut the price, how often, how far? | No | **Yes** |
| Was it pulled and re-listed to reset its age? | No | **Yes** |
| Which sellers are ready to negotiate? | No | **Yes** |
| Is this priced above or below its own neighbourhood? | No | **Yes — from run 1** |
| How fast is this neighbourhood clearing? | No | **Yes** |
| What is the median ₪/m² here, and the agent/private split? | No | **Yes — from run 1** |

The rows marked "from run 1" work on your very first run. The rest appear as runs accumulate, because no listing carries its own price history — cuts can only be seen by watching.

### How to use it

**1. Get listings.** Run any Israeli property scraper from Apify Store and let it finish.

**2. Run this Actor.** Choose that run's dataset from the **Listings dataset** picker, set a **Tracker ID** for the market you are following (`tlv-sale`, `haifa-rent` — one per market, always the same value), and run. The Actor is granted read access to just that one dataset and nothing else in your account.

**3. Schedule it daily.** Console → Schedules, same Tracker ID every time. This is the important step: the Actor's whole value is accumulated observation.

**4. Read the first run straight away.** Days on market and the neighbourhood comparisons are meaningful immediately; price-cut history builds from run 2. See below.

You can also paste listings straight into the **listings** field as JSON, which is the quickest way to try it.

#### What the first run gives you

**The first run is useful immediately.** Two whole families of signal need no history at all:

- **Real days on market**, wherever your source stamps listings with a publication date — which Yad2 and Madlan both do. A flat listed 284 days ago reports 284 days on its very first run, not 0.
- **Every neighbourhood comparison**: median ₪/m², how each listing prices against its own area, which listings are sitting far longer than their neighbours, the agent/private split, and the inventory mix. These come from comparing listings to each other inside one snapshot.

So run 1 already ranks sellers by motivation and flags listings priced below their area median:

```
score  days  source     vs area   stale
   43   284  published    -7.3%   false
   41   208  published    -5.0%   false
   35   131  published    +4.1%   false
   25    74  published     0.0%   false
```

What genuinely needs time is **price-change history** — no listing carries its own past prices, so cuts can only be seen by watching. Those begin at run 2 and compound from there.

| After | What is added |
|---|---|
| Run 1 | Days on market, area comparisons, staleness, motivated-seller ranking |
| Run 2+ | Price cuts, depth and frequency of cuts, delistings |
| ~30 days | Absorption rates and median-days figures stabilise |

**Already have archived scrapes?** Use **Backfill: date this snapshot was taken** and replay them oldest first. The Actor folds each in as of its own date, so months of price history can be reconstructed in minutes rather than waited for.

If your source has no publication dates, `daysOnMarket` starts from today and is labelled `daysOnMarketSource: "observed"` so you always know which you are looking at. Everything else still works.

### Output

Four record types in one dataset, tagged by `recordType`, each with its own saved view.

**`listing`** — one row per active listing:

```json
{
  "recordType": "listing",
  "listingId": "yad2:8842011",
  "motivatedSellerScore": 64,
  "daysOnMarket": 284,
  "daysOnMarketSource": "published",
  "daysObserved": 55,
  "listedAt": "2025-11-30T00:00:00.000Z",
  "observedFrom": "2026-07-16T09:00:00.000Z",
  "pricePerSqmVsAreaMedianPct": -7.3,
  "areaMedianPricePerSqm": 17590,
  "belowAreaMedianPricePerSqm": true,
  "isStaleForArea": false,
  "daysOnMarketVsAreaMedian": 105,
  "priceDrops": 2,
  "totalChangePct": -8.34,
  "largestDropPct": -4.14,
  "daysSinceLastPriceChange": 0,
  "price": 1549000,
  "firstObservedPrice": 1690000,
  "pricePerSqm": 16305,
  "rooms": 4, "sqm": 95, "floor": 2,
  "city": "חיפה", "neighborhood": "הדר הכרמל", "street": "הרצליה",
  "isAgent": false, "agentInferred": false,
  "isNew": false, "isRelisted": false,
  "url": "https://www.yad2.co.il/item/7710334"
}
```

**`area`** — one row per neighbourhood: `medianPricePerSqm`, `medianDaysOnMarket`, `listingsWithPriceDropPct`, `absorptionRatePct`, `agentSharePct`, `newThisRun`, `delistedThisRun`.

**`delisted`** — listings that vanished this run, with their last asking price and how long they survived.

**`run_summary`** — counts for the run, so a scheduled job is easy to monitor.

#### The motivated-seller score

A 0-100 blend of time on market, how far the price has fallen, and how many separate cuts there have been. A listing at 90 days with three cuts totalling 9% scores far above one at 90 days that has never moved — patience is not the same as desperation.

The weights (staleness 45, price-drop depth 40, number of cuts 15) are exposed in the input so you can retune them for your own market, and **Dry run** lets you experiment without disturbing your history.

### Pricing

Pay per event, so your bill is predictable and you only pay for rows you actually receive:

- **`listing-signal`** — per listing row returned
- **`area-report`** — per neighbourhood row returned

Two things keep the cost down. Filtering with **Minimum motivated-seller score** bills you only for the rows above your threshold, not everything analysed. And this Actor never calls another Actor on your behalf, so nothing is charged twice and there is no hidden upstream bill — you pay for the scraper you chose, separately and visibly, and for this analysis.

Runs are light: the work is arithmetic over records you already have, not browsing.

### Works with any listing source

Field names are mapped, not assumed, and the mapping is verified against a real Yad2 scraper payload rather than assumed: `listingId` / `id` / `adNumber`, `price` / `priceIls`, `areaSqm` / `squareMeters` / `sqm`, `neighbourhood` / `neighborhood`, `publishedAt` / `createdAt`. Where a source gives both a Hebrew and an English city name, the Hebrew one is used, so area keys stay in one language.

Hebrew values carrying currency symbols and `מ״ר` are parsed, dates in `dd/mm/yyyy` are read day-first as Israeli sources write them, and the CSV export carries a BOM so Hebrew opens correctly in Excel.

Minimum per record: an identifier and a price. Everything else improves the output when present and is handled gracefully when absent — including a stated `hasAgent` or `adType`, which is trusted over guessing from description text.

**Attributes passed through when your source provides them:** `hasSecureRoom` (ממ"ד), `hasElevator`, `hasBalcony`, `hasParking`, `isRoommates`, `isExclusive`, `propertyType`. An attribute the source doesn't state is absent from the row rather than reported as `false`, so "no lift" and "unknown" never look the same.

### Honest limits

Read these before relying on the numbers. They are properties of the problem, not defects, and knowing them makes the output more useful.

- **`daysOnMarket` prefers the listing's own publication date and falls back to how long this Actor has watched.** `daysOnMarketSource` tells you which, and `daysObserved` always carries the watched figure. Where a source gives no date, the age starts from your first run and becomes accurate as runs accumulate.
- **Price-change history only exists from run 2 onward.** No listing carries its past prices, so cuts have to be observed. Backfill archived snapshots if you have them.
- **Some listings are years old and are not really for sale.** Real portal data contains ads nobody ever removed — publication dates five and seven years back. Anything over two years is flagged `ageLooksImplausible`, and age stops earning credit in the score past that point, so a dead record can never outrank a seller who has genuinely been trying for a year. Filter on that flag before showing anyone a shortlist.
- **Delisted is not the same as sold.** A seller who pulls a listing and one who sells look identical from outside. Treat absorption as a strong proxy, not a fact.
- **Small neighbourhoods are noise.** Anything below your sample threshold is flagged `lowConfidence`. Don't publish those medians without the flag.
- **The score's weights are reasoned defaults, not a fitted model.** They are exposed precisely so you can do better for your own market.
- **`isAgent` is sometimes inferred** from text when the source doesn't state it. `agentInferred` tells you which is which.
- **No personal data.** Seller names and phone numbers are deliberately not carried through, even when the source provides them. This Actor reports market statistics; it is not a contact database.

### FAQ

**Does this scrape Yad2 or Madlan?**
No. It analyses listings you already have, from whichever scraper you prefer. That keeps your bill transparent and means the Actor works with any source, including your own CRM export.

**Is the first run useful, or do I have to wait?**
Useful straight away. Where your source stamps a publication date — Yad2 and Madlan both do — days on market is real on run 1, and every neighbourhood comparison works from a single snapshot. Only price-cut history needs repeat runs, because no listing carries its past prices. If you have archived scrapes, backfill them and you get that immediately too.

**Can I track several markets?**
Yes — use a separate Tracker ID for each. Keep them separate; merging Tel Aviv sales with Haifa rentals corrupts every median with no way back.

**What if my scraper fails one day?**
Leave **Runs absent before a listing counts as gone** at 2. A single failed scrape then costs one day of latency instead of being read as a wave of sales.

**Can I change the scoring?**
Yes, via **Motivated-seller weights**. Combine it with **Dry run** to test against real history without writing to it.

**Does it work for rentals?**
Yes. Use a separate Tracker ID from your sale tracker.

### Support

Found a bug or need a field that isn't there? Open an issue on the Actor's **Issues** tab — issues are read and answered. Feature requests that come with a concrete use case tend to get built.

The source is open: normalisation, the signal engine and the Hebrew handling are dependency-free and covered by a test suite you can run yourself with `npm test`.

# Actor input Schema

## `sourceDatasetId` (type: `string`):

Pick the dataset produced by your listings scraper run — a Yad2, Madlan or Homeless Actor, or any source with a price and an ID per record. This Actor analyses listings; it does not fetch them, so nothing is charged twice.

## `listings` (type: `array`):

Listing records as JSON, instead of a dataset ID. Handy for testing. Each record needs an identifier (id, listingId or adNumber) and a price (price, priceIls or askingPrice); rooms, squareMeters, floor, city and neighborhood are all used when present.

## `source` (type: `string`):

Keeps listing IDs from different sites separate, so the same number on Yad2 and Madlan is never treated as one property. Use a consistent value per tracker.

## `trackerId` (type: `string`):

The memory this run belongs to. Use one per market you follow — for example tlv-sale or haifa-rent — and always the same value for the same market. Mixing markets into one tracker contaminates every median and cannot be undone afterwards.

## `delistAfterRuns` (type: `integer`):

How many consecutive runs a listing must be missing before it is reported as delisted. 2 is recommended for scheduled runs: it costs one day of latency and stops a single failed scrape being read as a wave of sales.

## `snapshotDate` (type: `string`):

Only for loading old data. If you have archived scrapes, run them through oldest first with the date each was captured (YYYY-MM-DD) and the Actor builds real history in minutes instead of waiting weeks for it. Leave empty for normal daily runs.

## `resetHistory` (type: `boolean`):

Discards everything remembered for this Tracker ID and begins a new baseline. Days on market restarts at zero for every listing. There is no undo.

## `dryRun` (type: `boolean`):

Computes and returns results without recording them. Use it to try different score weights against the same data without disturbing the history you have built up.

## `minMotivatedScore` (type: `integer`):

Only return listings scoring at or above this, from 0 to 100. Leave at 0 to get everything; set around 60 to see just the sellers who look ready to negotiate. You are billed on rows returned, so filtering here lowers your cost.

## `emitAreaReport` (type: `boolean`):

Adds one row per neighbourhood with median price per m², median days on market, share of listings that have cut price, absorption rate and the agent/private split.

## `minAreaSample` (type: `integer`):

Neighbourhoods with fewer active listings than this are still returned, but flagged lowConfidence — their medians are drawn from too small a sample to rely on.

## `scoreWeights` (type: `object`):

Retune how the score is built. staleness rewards time on market, dropDepth rewards how far the price has fallen, dropCount rewards repeated cuts. Defaults are 45 / 40 / 15. Pair this with Dry run while you experiment.

## Actor input object example

```json
{
  "source": "yad2",
  "trackerId": "tlv-sale",
  "delistAfterRuns": 2,
  "resetHistory": false,
  "dryRun": false,
  "minMotivatedScore": 0,
  "emitAreaReport": true,
  "minAreaSample": 3,
  "scoreWeights": {
    "staleness": 45,
    "dropDepth": 40,
    "dropCount": 15
  }
}
```

# Actor output Schema

## `motivatedSellers` (type: `string`):

No description

## `neighbourhoodReport` (type: `string`):

No description

## `leftTheMarket` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "trackerId": "tlv-sale",
    "scoreWeights": {
        "staleness": 45,
        "dropDepth": 40,
        "dropCount": 15
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("competent_gatekeeper/israel-property-market-signals").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 = {
    "trackerId": "tlv-sale",
    "scoreWeights": {
        "staleness": 45,
        "dropDepth": 40,
        "dropCount": 15,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("competent_gatekeeper/israel-property-market-signals").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 '{
  "trackerId": "tlv-sale",
  "scoreWeights": {
    "staleness": 45,
    "dropDepth": 40,
    "dropCount": 15
  }
}' |
apify call competent_gatekeeper/israel-property-market-signals --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,competent_gatekeeper/israel-property-market-signals"
        }
    }
}
```

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/JKs59J9JlhMFaNRRk/builds/ojN2zP9hO5l6fbhyL/openapi.json
