# Cian Real Estate Monitor (`isolovyev/cian-price-history-monitor`) Actor

Monitor Moscow secondary apartments and commercial property for sale or rent with retained price history, suitability checks, and explicit collection coverage.

- **URL**: https://apify.com/isolovyev/cian-price-history-monitor.md
- **Developed by:** [Ivan Solovyev](https://apify.com/isolovyev) (community)
- **Categories:** Real estate, Automation
- **Stats:** 2 total users, 1 monthly users, 93.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 real estate observations

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

## 🏢 Cian Real Estate Monitor

<table width="100%" style="table-layout:fixed">
  <tr>
    <td style="padding:24px 28px; background:#EEF2FF; border:1px solid #C7D2FE; border-top:4px solid #4F46E5; border-radius:12px">
      <span style="font-size:23px; font-weight:800; color:#1E1B4B">Track Moscow residential and commercial property over time</span><br>
      <span style="font-size:15px; color:#4B5563">Comparable prices, retained history, suitability checks, and explicit collection coverage.</span>
    </td>
  </tr>
</table>

<table width="100%" style="table-layout:fixed">
  <tr>
    <td width="33.33%" style="padding:14px; background:#FFFFFF; border:1px solid #E0E7FF; border-radius:10px 0 0 10px">
      <span style="font-size:15px; font-weight:800; color:#4338CA">Price history</span><br>
      <span style="font-size:12px; color:#57534E">Every successful observation is retained.</span>
    </td>
    <td width="33.33%" style="padding:14px; background:#FFFFFF; border:1px solid #E0E7FF; border-left:none">
      <span style="font-size:15px; font-weight:800; color:#4338CA">Commercial screening</span><br>
      <span style="font-size:12px; color:#57534E">Area, budget, floor, and preferred-use checks stay explainable.</span>
    </td>
    <td width="33.33%" style="padding:14px; background:#FFFFFF; border:1px solid #E0E7FF; border-left:none; border-radius:0 10px 10px 0">
      <span style="font-size:15px; font-weight:800; color:#4338CA">Safe monitoring</span><br>
      <span style="font-size:12px; color:#57534E">A missing result is never labelled as sold or delisted.</span>
    </td>
  </tr>
</table>

### Overview

Cian Real Estate Monitor collects Moscow secondary apartments for sale and commercial property for sale or rent. It accepts a validated Cian search URL or structured filters, records comparable observations across runs, and keeps each watch isolated by `stateKey`.

For commercial rentals, `price` is normalized to the total monthly rent and `priceUnit` is `MONTHLY_RENT_RUB`. For sales, `price` is the total sale price and `priceUnit` is `SALE_TOTAL_RUB`. Source price fields remain available for audit.

The Dataset includes current property data, previous price, change classification, bounded history, and optional suitability checks. Contacts, phone numbers, owner data, and address-based identity matching are outside the dataset.

### Supported markets

- Moscow secondary apartments: sale.
- Moscow commercial property: sale and rent.
- Maximum 5 result pages and 100 unique listings per run.
- Listing identity uses the Cian listing identifier, never the address alone.

### How to use

1. Choose `search` for a current snapshot or `monitor` for a scheduled watch.
2. Provide either `searchUrl` or structured property, deal, price, area, and room filters.
3. Keep the Residential proxy country set to RU.
4. Reuse one stable `stateKey` only for the same search contract.
5. Schedule repeated monitor runs to build history.

### Pricing

- `$0.005` per Actor start.
- `$0.0025` per observation written to the default Dataset.
- Platform usage, including the default Residential RU proxy, is included.

In monitor mode, set `emitUnchanged` to `false` to write and pay only for new, updated, or reappeared observations. Successful observations still extend the retained history even when unchanged rows are not emitted.

Commercial rent example:

```json
{
  "mode": "monitor",
  "assetType": "commercial",
  "dealType": "rent",
  "minArea": 150,
  "maxArea": 400,
  "maxPrice": 800000,
  "preferredSpecialties": ["gymHall", "fitnessCentre"],
  "groundFloorOnly": true,
  "maxPages": 1,
  "maxItems": 20,
  "stateKey": "moscow-commercial-gym-watch",
  "historyLimit": 20,
  "emitUnchanged": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "RU"
  }
}
```

### Input

- `mode`: `search` or `monitor`.
- `searchUrl`: optional HTTPS Cian URL for a supported Moscow market. Do not combine it with structured search filters.
- `assetType`: `secondaryFlat` or `commercial` when `searchUrl` is empty.
- `dealType`: `sale` or `rent`. Apartment rent is not supported.
- `rooms`: optional room counts from 1 to 6 for secondary apartments.
- `minPrice`, `maxPrice`: total sale price or total monthly rent bounds in RUB.
- `minArea`, `maxArea`: total area bounds in square meters and suitability criteria.
- `preferredSpecialties`: optional Cian specialty codes or names, such as `gymHall` and `fitnessCentre`.
- `groundFloorOnly`: evaluates known floor data against a ground-floor requirement.
- `maxPages`: 1 to 5 result pages.
- `maxItems`: 1 to 100 unique listings.
- `stateKey`: optional observation identity. An automatic key is built from the normalized search and evaluation contract when empty.
- `historyLimit`: 2 to 100 retained observations per listing.
- `emitUnchanged`: includes `UNCHANGED` records in the Dataset. History is updated either way.
- `proxyConfiguration`: Apify proxy settings. The default uses Residential with country RU.

The Actor rejects unsupported cities, new builds, unsupported deal and property combinations, non-Cian hosts, and plain HTTP. It does not silently rewrite an unsupported URL into another market.

### Output

```json
{
  "platform": "cian",
  "id": "123456789",
  "title": "Помещение свободного назначения, 220 м²",
  "assetType": "commercial",
  "dealType": "rent",
  "price": 550000,
  "priceUnit": "MONTHLY_RENT_RUB",
  "sourcePrice": 2500,
  "sourcePriceType": "squareMeter",
  "paymentPeriod": "monthly",
  "monthlyRent": 550000,
  "annualRentPerSqm": 30000,
  "totalArea": 220,
  "minArea": 150,
  "pricePerSqm": 2500,
  "address": "Москва, ул. Примерная, 10",
  "url": "https://www.cian.ru/rent/commercial/123456789/",
  "commercialType": "freeAppointment",
  "specialtyTypes": ["gymHall"],
  "specialtyNames": ["Спортивный зал"],
  "suitabilityScore": 100,
  "suitabilityStatus": "MATCH",
  "suitabilityChecks": [
    {"criterion": "minArea", "status": "MATCH", "expected": 150, "actual": 220}
  ],
  "observedAt": "2026-10-03T10:00:00+00:00",
  "runId": "example-run",
  "firstSeenAt": "2026-10-02T10:00:00+00:00",
  "lastSeenAt": "2026-10-03T10:00:00+00:00",
  "previousPrice": 600000,
  "priceChange": -50000,
  "priceChangePct": -8.33,
  "changeType": "UPDATED",
  "changedFields": ["price", "monthlyRent", "pricePerSqm"],
  "historyCount": 2
}
```

`changeType` values:

- `NEW`: first observation under this state key.
- `UPDATED`: one or more tracked fields changed.
- `UNCHANGED`: the listing was observed again with the same tracked fields.
- `REAPPEARED`: the listing returned to the monitored result set after an earlier complete comparable run did not contain it.

The Actor does not emit removed, expired, delisted, or sold events. A listing missing from a limited run is not classified as a market event.

### Suitability checks

Suitability evaluates only data available in the listing. Every criterion is reported as `MATCH`, `MISMATCH`, or `UNKNOWN`; unknown values do not lower the percentage score. The result is a screening aid, not an engineering, legal, licensing, accessibility, electrical-capacity, ventilation, or noise assessment.

Commercial search pages can include promoted or grouped offers whose areas exceed requested bounds. The Dataset retains the source values and shows the result through suitability checks rather than hiding it.

### Coverage and run summary

The default key-value store record `RUN_SUMMARY` contains:

- `pagesFetched`, `offersObserved`, `totalOffers`, and `itemsEmitted`;
- `limitReached`, `scanComplete`, and `coverageScope`;
- counts for every `changeType`;
- run status and duration.

`scanComplete` is true only when unique collected listings cover the reported result total without an item or page limit. `FULL_RESULT_SET`, `LIMITED_BY_MAX_ITEMS`, `LIMITED_BY_MAX_PAGES`, `PARTIAL_ERROR`, and `UNPROVEN` make the coverage decision explicit.

Observation state is stored in the named key-value store `cian-price-history-state`. Record names are hashed from `stateKey`, and a short TTL lock protects the state update from overlapping runs of the same watch.

### Local development

Python 3.12 is required.

```bash
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m pytest -q
```

### RU / На русском

Актор собирает вторичные квартиры на продажу и коммерческие помещения на продажу или в аренду в Москве. Для аренды поле `price` означает полную месячную ставку, для продажи полную цену объекта. История сохраняет успешные замеры и отделяет новое, изменившееся, неизменившееся и повторно появившееся в выборке объявление.

Для коммерческих объектов можно проверять площадь, бюджет, этаж и назначение. Неизвестные значения не ухудшают оценку. Такая оценка помогает отобрать кандидатов, но не заменяет техническую, юридическую и эксплуатационную проверку помещения.

Полнота запуска отражена в `RUN_SUMMARY`. Ограниченная выдача не создаёт вывод о снятии объявления с продажи или аренды.

# Actor input Schema

## `mode` (type: `string`):

Search returns current observations. Monitor compares each observation with saved history.

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

Optional HTTPS Cian URL for Moscow secondary apartments for sale or commercial property for sale or rent. Leave search filters empty when using this field.

## `assetType` (type: `string`):

Property market used when searchUrl is empty.

## `dealType` (type: `string`):

Sale is supported for both property types. Rent is supported for commercial property.

## `rooms` (type: `array`):

Optional room counts for secondary apartments when searchUrl is empty.

## `minPrice` (type: `integer`):

Optional minimum total sale price or total monthly rent when searchUrl is empty.

## `maxPrice` (type: `integer`):

Optional maximum total sale price or total monthly rent when searchUrl is empty.

## `minArea` (type: `number`):

Optional minimum total area when searchUrl is empty. Also used as a suitability criterion.

## `maxArea` (type: `number`):

Optional maximum total area when searchUrl is empty. Also used as a suitability criterion.

## `preferredSpecialties` (type: `array`):

Optional Cian specialty codes or names used to evaluate commercial listings, for example gymHall or fitnessCentre.

## `groundFloorOnly` (type: `boolean`):

Mark listings on other known floors as unsuitable. Unknown floor data remains unknown.

## `maxPages` (type: `integer`):

Maximum number of result pages to collect.

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

Maximum number of unique listings to keep in one run.

## `stateKey` (type: `string`):

Optional stable name that isolates one observation history. An automatic key is created from the normalized search when empty.

## `historyLimit` (type: `integer`):

Number of recent successful observations retained for each listing.

## `emitUnchanged` (type: `boolean`):

Include unchanged listings in the Dataset while still recording every successful observation in history.

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

Use Apify Residential proxy with country RU for this source.

## Actor input object example

```json
{
  "mode": "search",
  "searchUrl": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&offer_type=flat&region=1&object_type%5B0%5D=1",
  "groundFloorOnly": false,
  "maxPages": 1,
  "maxItems": 20,
  "historyLimit": 20,
  "emitUnchanged": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "RU"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset records for the current run.

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

Coverage, counters, and completion status for the 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 = {
    "mode": "search",
    "searchUrl": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&offer_type=flat&region=1&object_type%5B0%5D=1",
    "maxPages": 1,
    "maxItems": 20,
    "historyLimit": 20,
    "emitUnchanged": true,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "RU"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("isolovyev/cian-price-history-monitor").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 = {
    "mode": "search",
    "searchUrl": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&offer_type=flat&region=1&object_type%5B0%5D=1",
    "maxPages": 1,
    "maxItems": 20,
    "historyLimit": 20,
    "emitUnchanged": True,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "RU",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("isolovyev/cian-price-history-monitor").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 '{
  "mode": "search",
  "searchUrl": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&offer_type=flat&region=1&object_type%5B0%5D=1",
  "maxPages": 1,
  "maxItems": 20,
  "historyLimit": 20,
  "emitUnchanged": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "RU"
  }
}' |
apify call isolovyev/cian-price-history-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,isolovyev/cian-price-history-monitor"
        }
    }
}
```

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/PO9hLJMsCYeWI00kD/builds/XNduwatkE7Stk52BT/openapi.json
