# Motivated Seller Score — Listing Motivation Rank (`noahadler/listing-motivation-enricher`) Actor

Motivated seller score for property listings you already have. Same rows, plus a 0-100 score from keywords and days on market. Listing motivation rank from 200 rows. Paste JSON or a dataset ID. No portal scrape and no phones.

- **URL**: https://apify.com/noahadler/listing-motivation-enricher.md
- **Developed by:** [Noah Adler](https://apify.com/noahadler) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 motivated listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Motivated Seller Score — Listing Motivation Rank

Motivated seller score for property listings you already have. Paste JSON, or pass an Apify `datasetId`. Each output row is the same listing plus a **motivated seller score** from 0 to 100. From 200 rows, the run also adds a listing motivation rank.

This Actor does not scrape Zillow, Idealista, Fotocasa, SeLoger, or any other portal. It does not return phones, emails, or owner names. With `redactContactsInText` left on (the default), phones and emails inside the description are replaced with `[redacted]`.

### What you get

| Field | When |
|-------|------|
| `motivationScore` | Always. Integer 0–100. |
| `motivationFactors` | Always. Short reasons. |
| `keywordHits` | Always. Empty when nothing matched. |
| `daysOnMarket` | From `publishedAt`, `firstPublicationDate`, `daysOnPortal`, or another listed date. `null` if there is no date. |
| `scoreVersion` | Always `"0.2"`. |
| `rarityApplied` | `false` under 200 rows. `true` from 200 rows up. |
| `motivationPercentile`, `relativeTier` | `null` under 200 rows. From 200 rows, a rank **inside this run**. |
| `specialCase` | `true` when a zero-point case phrase matched (`subasta`, `asta`, `nuda propiedad`, …). |
| `detectedLanguage` | Hint only (`es`, `fr`, `en`, `de`, `it`). Every dictionary still runs. |
| `priceDropped`, `priceCutPct` | Only when **this row** brings `previousPrice`, `priceHistory`, or `priceDropped: true`. |
| `previousPriceObserved`, `priceCutPctObserved`, `relisted`, `firstSeenAt`, `daysObserved` | Only when `previousDatasetId` was set. |

`relativeTier` is `"top"` at or above the 95th percentile of **this run**, `"high"` from the 80th up to that line, and `"normal"` below the 80th. It is not an absolute motivated-seller threshold.

### How the score is calculated

Deterministic. No LLM.

- Base weight 25: `urge`, `herencia`, `estate sale`, `as-is`, `must sell`, `foreclosure`, `venta urgente`, `liquidación`, `Nachlass`, `Zwangsversteigerung`, and the other distress phrases in the dictionary.
- Base weight 12: `por traslado`, `por divorcio`.
- Base weight 8: `a reformar`, `para reformar`, `à saisir`.
- Base weight 5: `oportunidad`, `negociable`, `coup de coeur`, `price reduced`. A weak hit alone is marketing language, not distress.
- Weight 0, `specialCase` only: `subasta`, `procedente de banco`, `procedente de entidad`, `nuda propiedad`, `asta`.
- A negation within three tokens (`no`, `sin`, `nicht`, `non`, `pas de`, `no hay`) cancels that phrase.
- Days on market ≥ 60: +15. ≥ 120: +25 more (both apply). Cap 100.
- Price cut on **this row**: +20, only from `previousPrice`, `oldPrice`, `priceBefore`, `originalPrice`, `priceHistory`, or `priceDropped: true`.
- Relist versus `previousDatasetId`: +15.

**From 200 rows up, rarity reweights `motivationScore` even when you pass no new inputs.** Each phrase weight is multiplied by `clamp(log(1/df) / log(100), 0.1, 1.5)`, where `df` is the share of rows in this run that contain the phrase. A phrase everyone uses (high `df`) falls near the floor, about 0 to 1 point instead of 5. Under 200 rows, rarity is off and `oportunidad` stays at 5.

On a normal Madrid sample, strong distress phrases are rare. Below 200 rows the score mostly moves with **days on market** and weak words such as `oportunidad`. This is not a probate detector.

No description (some Leboncoin list feeds): keyword hits stay empty unless the title itself contains a phrase. Days on market still score.

### Works with datasets from

Pass `datasetId` from a dataset you already stored. Examples, none of which include phone numbers:

- `fotocasa-spain-property-scraper`
- `seloger-scraper`
- `leboncoin-immobilier` (often title and date only, no description)
- a Zillow or Idealista export you already downloaded and saved as an Apify dataset

You can also paste `listings` JSON. Contact fields are dropped either way.

### Weekly schedule

Run once a week. Point `datasetId` at this week's dataset and `previousDatasetId` at last week's dataset:

```json
{
  "datasetId": "DATASET_ID_THIS_WEEK",
  "previousDatasetId": "DATASET_ID_LAST_WEEK",
  "maxItems": 5000
}
```

A fingerprint that matches exactly one older row with a different `listingId` is `relisted`. The same `listingId` with a newer `publishedAt` is `relisted` as well. Day points then use `max(daysObserved, daysOnMarket)`.

`SUMMARY.notInThisFile` counts previous rows that are absent from this input. It does not mean those listings sold. The count changes if filters, area, or `maxItems` change.

If `previousDatasetId` cannot be read, the run returns an error row. It does not pretend the diff was empty.

### Input

- `listings` or `datasetId` (one of them).
- `maxItems`: default 100, max 5000.
- `asOfDate`: ISO date for days on market. Default is the run time in UTC.
- `redactContactsInText`: default `true`.
- `customKeywords`: `[{ "phrase": "vendido por divorcio", "weight": 12 }]`. Weight must be an integer from 1 to 25. Anything else stops the run.
- `previousDatasetId`: optional.

### Pricing

Pay per enriched listing: **$0.005**. No residential proxy.

### Limitations

- Not a skip-trace product and not a portal scraper.
- `priceDropped` means the previous price was already on the row. A drop seen only by comparing `previousDatasetId` is `priceCutPctObserved`, and it does not change `priceDropped`.
- `relativeTier` compares rows inside the run you just scored.

# Actor input Schema

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

Array of listing objects. Useful fields: description/title, publishedAt or firstPublicationDate or daysOnPortal, price, previousPrice or priceHistory, listingId, listingUrl. Phones/emails are ignored and never returned.

## `datasetId` (type: `string`):

Optional. Load listings from an existing Apify dataset (e.g. output of fotocasa-spain-property-scraper). Used instead of paste when set.

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

Cap on rows to enrich. Default 100, max 5000.

## `asOfDate` (type: `string`):

ISO date used to compute daysOnMarket from publishedAt. Default = run time (UTC).

## `redactContactsInText` (type: `boolean`):

Replace phone numbers and emails inside description and other text with \[redacted]. Default true. Contact fields are always removed.

## `customKeywords` (type: `array`):

Optional phrases added to the dictionary. Each item is {phrase, weight}. Weight must be an integer from 1 to 25. Out of range stops the run.

## `previousDatasetId` (type: `string`):

Optional Apify dataset from an earlier run. Used to flag relists and observed price changes. If it cannot be read, the run fails instead of pretending nothing changed.

## Actor input object example

```json
{
  "listings": [
    {
      "listingId": "demo-1",
      "listingUrl": "https://example.com/listing/1",
      "title": "Piso 3 hab Madrid",
      "description": "Piso en herencia, urge vender, negociable, a reformar.",
      "price": 189000,
      "currency": "EUR",
      "publishedAt": "2026-05-01T10:00:00Z"
    },
    {
      "listingId": "demo-2",
      "listingUrl": "https://example.com/listing/2",
      "title": "Flat near metro",
      "description": "Charming flat near metro, recently renovated, move-in ready.",
      "price": 320000,
      "currency": "EUR",
      "publishedAt": "2026-09-20T10:00:00Z"
    },
    {
      "listingId": "demo-3",
      "listingUrl": "https://example.com/listing/3",
      "title": "Estate sale home",
      "description": "Beautiful home sold as-is, estate sale, cash only preferred.",
      "price": 275000,
      "currency": "USD",
      "previousPrice": 310000,
      "publishedAt": "2026-01-15T10:00:00Z"
    }
  ],
  "maxItems": 100,
  "asOfDate": "2026-10-04",
  "redactContactsInText": true
}
```

# Actor output Schema

## `listings` (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 = {
    "listings": [
        {
            "listingId": "demo-1",
            "listingUrl": "https://example.com/listing/1",
            "title": "Piso 3 hab Madrid",
            "description": "Piso en herencia, urge vender, negociable, a reformar.",
            "price": 189000,
            "currency": "EUR",
            "publishedAt": "2026-05-01T10:00:00Z"
        },
        {
            "listingId": "demo-2",
            "listingUrl": "https://example.com/listing/2",
            "title": "Flat near metro",
            "description": "Charming flat near metro, recently renovated, move-in ready.",
            "price": 320000,
            "currency": "EUR",
            "publishedAt": "2026-09-20T10:00:00Z"
        },
        {
            "listingId": "demo-3",
            "listingUrl": "https://example.com/listing/3",
            "title": "Estate sale home",
            "description": "Beautiful home sold as-is, estate sale, cash only preferred.",
            "price": 275000,
            "currency": "USD",
            "previousPrice": 310000,
            "publishedAt": "2026-01-15T10:00:00Z"
        }
    ],
    "asOfDate": "2026-10-04"
};

// Run the Actor and wait for it to finish
const run = await client.actor("noahadler/listing-motivation-enricher").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 = {
    "listings": [
        {
            "listingId": "demo-1",
            "listingUrl": "https://example.com/listing/1",
            "title": "Piso 3 hab Madrid",
            "description": "Piso en herencia, urge vender, negociable, a reformar.",
            "price": 189000,
            "currency": "EUR",
            "publishedAt": "2026-05-01T10:00:00Z",
        },
        {
            "listingId": "demo-2",
            "listingUrl": "https://example.com/listing/2",
            "title": "Flat near metro",
            "description": "Charming flat near metro, recently renovated, move-in ready.",
            "price": 320000,
            "currency": "EUR",
            "publishedAt": "2026-09-20T10:00:00Z",
        },
        {
            "listingId": "demo-3",
            "listingUrl": "https://example.com/listing/3",
            "title": "Estate sale home",
            "description": "Beautiful home sold as-is, estate sale, cash only preferred.",
            "price": 275000,
            "currency": "USD",
            "previousPrice": 310000,
            "publishedAt": "2026-01-15T10:00:00Z",
        },
    ],
    "asOfDate": "2026-10-04",
}

# Run the Actor and wait for it to finish
run = client.actor("noahadler/listing-motivation-enricher").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 '{
  "listings": [
    {
      "listingId": "demo-1",
      "listingUrl": "https://example.com/listing/1",
      "title": "Piso 3 hab Madrid",
      "description": "Piso en herencia, urge vender, negociable, a reformar.",
      "price": 189000,
      "currency": "EUR",
      "publishedAt": "2026-05-01T10:00:00Z"
    },
    {
      "listingId": "demo-2",
      "listingUrl": "https://example.com/listing/2",
      "title": "Flat near metro",
      "description": "Charming flat near metro, recently renovated, move-in ready.",
      "price": 320000,
      "currency": "EUR",
      "publishedAt": "2026-09-20T10:00:00Z"
    },
    {
      "listingId": "demo-3",
      "listingUrl": "https://example.com/listing/3",
      "title": "Estate sale home",
      "description": "Beautiful home sold as-is, estate sale, cash only preferred.",
      "price": 275000,
      "currency": "USD",
      "previousPrice": 310000,
      "publishedAt": "2026-01-15T10:00:00Z"
    }
  ],
  "asOfDate": "2026-10-04"
}' |
apify call noahadler/listing-motivation-enricher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,noahadler/listing-motivation-enricher"
        }
    }
}
```

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/5hI39DjaYUbJQjtkv/builds/E1LtCfvjmcDG4gyFz/openapi.json
