# Realty AI Enricher (`tri_angle/realty-ai-enricher`) Actor

Downstream post-processor for the Real Estate Aggregator. Reads a completed listings dataset, extracts structured amenity data (parking, pet policy, laundry, A/C, and more) from each listing's free-text description with an LLM, and re-emits the original items with an added `amenities` field.

- **URL**: https://apify.com/tri\_angle/realty-ai-enricher.md
- **Developed by:** [Tri⟁angle](https://apify.com/tri_angle) (Apify)
- **Categories:** AI, Real estate
- **Stats:** 3 total users, 0 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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 AI Enricher turns messy, free-text real-estate listings into **clean, structured amenity data** you can filter and analyze. It's a run-and-exit post-processor: point it at a dataset you already scraped, and it re-emits every listing with an added `extractedAmenities` object — parking, pet policy, laundry, air conditioning, and more.

This is the **amenities-context** variant. Unlike the description-only build, it feeds each listing's *already-tagged* `amenities` into the model **alongside** the free-text description, so the extraction is grounded in the structured facts the source already provides and refined by what the prose adds.

### What does Real Estate AI Enricher do?

It reads a completed dataset produced by the [Real Estate Aggregator](https://apify.com) (or any dataset with the same shape), sends each listing's **description** and **native `amenities`** to a fast, low-cost LLM through the Apify OpenRouter proxy, and writes back a normalized `extractedAmenities` object. Every original field is passed through untouched — the Actor only **adds** a field, it never rewrites your data.

Because it runs on the Apify platform, you get API access, scheduling, integrations, automatic retries, and monitoring out of the box. It shares no code with the scraper that produced the data — it only depends on that data's output shape.

### Why use Real Estate AI Enricher?

- **Make listings filterable.** Free-text descriptions can't be queried; a structured `extractedAmenities` object can ("dishwasher = true", "parking = garage").
- **Higher accuracy than text-only extraction.** It combines the source's structured amenity tags with the description, so it confirms and fills gaps rather than guessing from prose alone.
- **Enrich listings with weak descriptions.** A listing with structured amenities but a thin or missing description still gets enriched — the structured tags alone are enough to extract from.
- **Cheap and fast.** Defaults to `anthropic/claude-haiku-4.5` with tunable batch concurrency.
- **Non-destructive.** Your original `amenities` field and every other field survive; the result lands under a distinct `extractedAmenities` key.

### How to use Real Estate AI Enricher

1. Run the Real Estate Aggregator (or any compatible scraper) and copy the resulting **dataset ID** from its run's Storage tab.
2. Open this Actor, paste the ID into **Dataset ID**.
3. (Optional) Adjust the model, concurrency, and the "skip listings without a usable description" toggle.
4. Click **Start**. The Actor reads the whole dataset, enriches each listing in batches, and streams the results to its own output dataset.
5. Download the enriched data, or wire the output into your database or spreadsheet via the API or an integration.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `datasetId` *(required)* | string | – | ID of the upstream dataset to enrich. |
| `model` | string | `anthropic/claude-haiku-4.5` | OpenRouter model slug used for extraction. Override with a stronger model for tricky listings. |
| `concurrency` | integer | `10` | Listings extracted in parallel per batch (1–50). Higher is faster but more rate-limit prone. |
| `skipIfNoDescription` | boolean | `true` | When on, descriptions shorter than 20 chars are left out of the prompt. Native amenities are always used when present, regardless of this flag. |

**Expected input shape.** This Actor is built for **Real Estate Aggregator**-shaped items. It reads each listing's `description` and, when present, its structured `amenities` field; every other field is passed through untouched. You can point it at any dataset with a compatible shape, but items with neither a usable description nor an `amenities` field come back with `extractedAmenities: null`. If a run reports that no listing carried a native `amenities` field, it logs a warning and falls back to description-only extraction — a sign the input dataset may not be aggregator output.

Example input:

```json
{
    "datasetId": "aBcDeF1234567890",
    "model": "anthropic/claude-haiku-4.5",
    "concurrency": 10,
    "skipIfNoDescription": true
}
````

### Output

Each item is your original listing plus an `extractedAmenities` object (or `null` when there was nothing usable to extract). Your source's native `amenities` field is preserved untouched.

```json
{
    "id": "listing-42",
    "description": "Sun-drenched 1BR with in-unit washer/dryer, dishwasher, stainless appliances, hardwood floors, a gas fireplace, and a shared roof deck with city views. Cats welcome. Garage parking available.",
    "amenities": { "Building": ["Elevator", "Gym"], "Unit": ["Dishwasher"] },
    "extractedAmenities": {
        "parking": "garage",
        "petPolicy": "cats_only",
        "balcony": null,
        "garden": null,
        "gym": true,
        "pool": null,
        "inUnitLaundry": true,
        "sharedLaundry": null,
        "dishwasher": true,
        "airConditioning": null,
        "furnished": null,
        "elevator": true,
        "concierge": null,
        "storage": null,
        "fireplace": true,
        "patio": null,
        "walkInCloset": null,
        "stainlessSteelAppliances": true,
        "ceilingFan": null,
        "highSpeedInternet": null,
        "controlledAccess": null,
        "highCeilings": null,
        "countertops": null,
        "flooring": ["hardwood"],
        "view": ["city"]
    }
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

#### Data table

| Field | Type | Values |
| --- | --- | --- |
| `parking` | string / null | `garage`, `street`, `lot`, `carport`, `covered`, `driveway`, `none` |
| `petPolicy` | string / null | `allowed`, `cats_only`, `dogs_only`, `not_allowed` |
| `countertops` | string / null | `granite`, `quartz`, `marble`, `laminate`, `other` |
| `flooring` | array / null | any of `hardwood`, `carpet`, `tile`, `vinyl`, `laminate`, `concrete` |
| `view` | array / null | any of `water`, `city`, `mountain`, `park`, `golf`, `other` |
| `balcony`, `garden`, `patio`, `pool` | boolean / null | present or not |
| `gym`, `storage`, `fireplace` | boolean / null | present or not |
| `inUnitLaundry`, `sharedLaundry`, `dishwasher` | boolean / null | present or not |
| `stainlessSteelAppliances`, `walkInCloset` | boolean / null | present or not |
| `airConditioning`, `ceilingFan` | boolean / null | present or not |
| `furnished`, `elevator`, `concierge` | boolean / null | present or not |
| `highSpeedInternet`, `controlledAccess`, `highCeilings` | boolean / null | present or not |

A field is `null` when it can't be determined from either the description or the native amenities — the Actor never invents data. Array fields (`flooring`, `view`) can list several values.

### Pricing / Cost estimation

The Actor charges an `actor-start` event plus one `listing-enriched` event per successfully enriched listing (skipped and failed listings are not charged). On top of that, LLM token usage is billed to your Apify account at the OpenRouter proxy's rates. The default model is chosen to keep per-listing cost low; switch to a stronger model only when accuracy on ambiguous listings matters. Listings with no usable description **and** no native amenities are skipped with no LLM call, so you're not charged for data that has nothing to extract.

### Tips and advanced options

- **Tune `concurrency`** to trade speed against rate limits — start at 10 and raise it if you're not seeing throttling.
- **Keep `skipIfNoDescription` on** for the cheapest runs; the amenities signal still enriches listings whose description is thin.
- **Use a stronger model** only for datasets with long, ambiguous prose — the default handles structured, tag-backed listings well.

### FAQ, disclaimers, and support

**Does it modify my original data?** No. It only *adds* `extractedAmenities`; every other field, including the native `amenities`, passes through unchanged.

**What if a listing has no description?** If it also has no native amenities, it's skipped with `extractedAmenities: null`. If it has amenities, it's still enriched from those.

**Why did I get `null` for a listing?** Either it was skipped (no usable signal) or the extraction failed/returned an unusable response — the Actor logs a warning with the item index and moves on, so one bad listing never aborts the run.

**Legality.** Use this Actor only on data you are permitted to process. You are responsible for complying with the terms of the source you scraped and with applicable law.

Found a bug or want a field added? Open an issue on the Actor's **Issues** tab — feedback and custom-field requests are welcome.

# Actor input Schema

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

ID of a dataset produced by the Real Estate Aggregator (or any dataset whose items follow its output schema). Each item's `description` and, when present, its structured `amenities` field are used as extraction inputs; all other fields pass through untouched. Copy it from the aggregator run's Storage tab.

## `model` (type: `string`):

OpenRouter model slug used for amenity extraction. The default is a fast, low-cost model that is sufficient for this structured classification task. Override with a stronger model for higher accuracy on ambiguous descriptions.

## `concurrency` (type: `integer`):

How many listings to extract in parallel per batch. Higher is faster but more likely to hit rate limits.

## `skipIfNoDescription` (type: `boolean`):

When enabled (default), listings whose description is missing or shorter than 20 characters get `extractedAmenities: null` with no LLM call (nothing reliable to extract). Disable to also send short descriptions to the model.

## Actor input object example

```json
{
  "model": "anthropic/claude-haiku-4.5",
  "concurrency": 10,
  "skipIfNoDescription": 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 = {
    "model": "anthropic/claude-haiku-4.5"
};

// Run the Actor and wait for it to finish
const run = await client.actor("tri_angle/realty-ai-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 = { "model": "anthropic/claude-haiku-4.5" }

# Run the Actor and wait for it to finish
run = client.actor("tri_angle/realty-ai-enricher").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "model": "anthropic/claude-haiku-4.5"
}' |
apify call tri_angle/realty-ai-enricher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=tri_angle/realty-ai-enricher",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Realty AI Enricher",
        "description": "Downstream post-processor for the Real Estate Aggregator. Reads a completed listings dataset, extracts structured amenity data (parking, pet policy, laundry, A/C, and more) from each listing's free-text description with an LLM, and re-emits the original items with an added `amenities` field.",
        "version": "0.0",
        "x-build-id": "WKZolfQ6PKx8cEMwB"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/tri_angle~realty-ai-enricher/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-tri_angle-realty-ai-enricher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/tri_angle~realty-ai-enricher/runs": {
            "post": {
                "operationId": "runs-sync-tri_angle-realty-ai-enricher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/tri_angle~realty-ai-enricher/run-sync": {
            "post": {
                "operationId": "run-sync-tri_angle-realty-ai-enricher",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "datasetId"
                ],
                "properties": {
                    "datasetId": {
                        "title": "Dataset ID",
                        "minLength": 1,
                        "type": "string",
                        "description": "ID of a dataset produced by the Real Estate Aggregator (or any dataset whose items follow its output schema). Each item's `description` and, when present, its structured `amenities` field are used as extraction inputs; all other fields pass through untouched. Copy it from the aggregator run's Storage tab."
                    },
                    "model": {
                        "title": "Model",
                        "type": "string",
                        "description": "OpenRouter model slug used for amenity extraction. The default is a fast, low-cost model that is sufficient for this structured classification task. Override with a stronger model for higher accuracy on ambiguous descriptions.",
                        "default": "anthropic/claude-haiku-4.5"
                    },
                    "concurrency": {
                        "title": "Concurrency",
                        "minimum": 1,
                        "maximum": 50,
                        "type": "integer",
                        "description": "How many listings to extract in parallel per batch. Higher is faster but more likely to hit rate limits.",
                        "default": 10
                    },
                    "skipIfNoDescription": {
                        "title": "Skip listings without a usable description",
                        "type": "boolean",
                        "description": "When enabled (default), listings whose description is missing or shorter than 20 characters get `extractedAmenities: null` with no LLM call (nothing reliable to extract). Disable to also send short descriptions to the model.",
                        "default": true
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
