# NYC Distressed Property Leads — Violations, Liens, Evictions (`malonestar/distressed-property-signal-stacker`) Actor

NYC motivated-seller leads: stacks open HPD violations, tax lien sale notices, and executed evictions per parcel (BBL), scores distress, and attaches the registered owner or agent contact. Ranked leads for investors, wholesalers, and agents from keyless NYC open data. Pay per result.

- **URL**: https://apify.com/malonestar/distressed-property-signal-stacker.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Real estate, Lead generation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $13.75 / 1,000 results

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

## NYC Distressed Property Leads - Violations, Liens, Evictions

Find **motivated-seller leads in New York City** by stacking three official distress
signals per tax parcel (BBL):

1. **Open HPD housing maintenance code violations** (class A / B / C)
2. **DOF tax lien sale notices** (90/60/30/10-Day Notice and Final Sale cycles)
3. **Marshal-executed evictions**

Every parcel that shows enough distinct signals gets a **weighted distress score**,
the **registered owner, the owning LLC, and the managing agent** from HPD
registrations, and comes out ranked highest-distress first - ready for direct mail,
skip tracing, cold calling, or acquisition underwriting.

### Who it is for

- **Real estate investors and wholesalers** hunting off-market, pre-foreclosure-adjacent deals
- **Agents and brokers** prospecting listings from owners under financial or regulatory pressure
- **Lenders, note buyers and tax-lien investors** screening collateral risk by parcel
- **PropTech and data teams** who want a scored, deduplicated per-BBL feed instead of three raw city dumps

### How the score works

`distress_score = openC*3 + openB*2 + openA*1 + lien(10 * cycle multiplier) + evictions*5`

Lien-sale cycle escalates the weight: 90 Day Notice x1.0, 60 Day x1.2, 30 Day x1.5,
10 Day x1.8, **Final Sale x2.0**. A parcel with 1 open class-C violation, a 30-Day
lien notice and 2 executed evictions scores `3 + 15 + 10 = 28`.

`signals_present` lists which of `violations`, `lien_sale`, `evictions` the parcel
shows; `minSignals` controls how many distinct types are required (default 2).

### Example input

```json
{
  "borough": "BRONX",
  "minSignals": 2,
  "lookbackMonths": 12,
  "maxResults": 100
}
```

All fields are optional - an empty input runs the Bronx with the defaults above.
`borough` accepts a name or a code 1-5; anything else fails the run before any row
is emitted or billed.

### How complete is the contact data? (measured, not claimed)

Measured on the actor's own prefill (Bronx, 12 months, minSignals 2, 100 results)
against the live datasets on **2026-08-01**, n = 100 leads:

| Contact / identity field | Populated |
|---|---|
| `owner_contact` (whole object) | **98.0%** |
| `owner_contact.name` | 98.0% |
| `owner_contact.business_address` | 98.0% |
| `owner_contact.type` | 98.0% |
| `owner_contact.corporation` | 0.0% - see note below |
| `owner_corporation` (owning LLC) | **98.0%** |
| `owner_full_name` (incl. middle initial) | 98.0% |
| `owner_title` | 66.0% |
| `owner_agent_name` | 98.0% |
| `owner_agent_address` | 98.0% |
| `registration_id` | 100.0% |
| `address` / `zip` / `bin` / `latitude` / `longitude` | 100.0% |
| `lien_sale_cycle` and the other `lien_*` fields | 38.0% (only parcels on the lien list) |
| `newest_eviction_date` | 73.0% (only parcels with an eviction) |

Mean HPD contacts on file per lead: **4.4**.

`owner_contact.corporation` reads 0.0% because `owner_contact` is a single
best-ranked contact and the top rank is a natural person (IndividualOwner or
HeadOfficer), who never carries a corporation name. The owning entity is on a
*different* contact row for the same registration. **v1.1 publishes it as
`owner_corporation`** rather than leaving it fetched-and-discarded, and
`owner_contacts_all` carries up to 12 contacts so nothing is lost. The v1.0 field
is unchanged so existing pipelines keep working.

2.0% of leads have no HPD contact at all: those are buildings that never
registered with HPD (small owner-occupied homes, many commercial parcels). They
are **never dropped** - they emit with `owner_contact: null`,
`owner_contact_checked: true` and `owner_contact_found: false`.

### Output fields

#### Parcel identity

| Field | Meaning |
| --- | --- |
| `bbl` | 10-digit Borough-Block-Lot parcel id |
| `borough` | Borough name |
| `block` | Tax block, unpadded |
| `lot` | Tax lot, unpadded |
| `address` | Best available street address across the contributing datasets |
| `zip` | ZIP code |
| `bin` | NYC Building Identification Number |
| `latitude` | WGS84 latitude as published by HPD or DOI |
| `longitude` | WGS84 longitude as published by HPD or DOI |
| `community_board` | NYC community board |
| `council_district` | NYC council district |
| `census_tract` | Census tract as published by HPD |

#### Distress signals

| Field | Meaning |
| --- | --- |
| `distress_score` | Weighted score (formula above), sorted descending |
| `signals_present` | Which of violations / lien\_sale / evictions fired |
| `open_violations_a` | Open class-A (non-hazardous) violations in the window |
| `open_violations_b` | Open class-B (hazardous) violations in the window |
| `open_violations_c` | Open class-C (immediately hazardous) violations in the window |
| `open_violations_i` | Open class-I violations. HPD stopped issuing class I in Aug 2014, so this is 0 in any reachable window - it exists so those rows are counted rather than silently discarded |
| `open_violations_total` | A + B + C + I |
| `rent_impairing_violations` | Open violations HPD flags rent-impairing (tenants may deposit rent with the court) |
| `newest_violation_date` | Issue date of the most recent open violation counted |
| `oldest_violation_date` | Issue date of the oldest open violation counted in the window |
| `lien_sale_cycle` | Most severe lien-sale stage, or null when the parcel is not on the list |
| `lien_sale_month` | List month of that most severe notice |
| `lien_notice_count` | How many lien-sale notices the parcel received in the window |
| `lien_water_debt_only` | True when the lien listing is water/sewer debt only |
| `lien_building_class` | NYC building class as published on the lien-sale list |
| `lien_tax_class_code` | NYC tax class code as published on the lien-sale list |
| `evictions_count` | Marshal-executed evictions at the parcel in the window |
| `evictions_residential` | Of those, marked Residential |
| `evictions_commercial` | Of those, marked Commercial |
| `newest_eviction_date` | Execution date of the most recent eviction |

#### Owner and contact

| Field | Meaning |
| --- | --- |
| `owner_contact` | Best single HPD registration contact: `{type, name, corporation, business_address}`, or null |
| `owner_contact_checked` | True when the HPD contact source answered this run. When false, a null contact means "not checked", not "no contact on file" |
| `owner_contact_found` | True/false only when `owner_contact_checked` is true; null otherwise |
| `owner_contact_count` | How many HPD contact rows exist for the parcel |
| `owner_corporation` | Best corporation name across ALL contacts - usually the owning LLC |
| `owner_full_name` | Top-ranked contact name including middle initial |
| `owner_title` | Title as filed with HPD (MEMBER, PRINCIPAL, TREASURER...) |
| `owner_contact_types` | Every HPD contact type present for the registration |
| `owner_agent_name` | Filed Agent or SiteManager - often the reachable party when the owner is an LLC |
| `owner_agent_address` | Business address of that Agent or SiteManager |
| `owner_contacts_all` | Up to 12 contacts, each `{type, name, middle_initial, title, corporation, contact_description, business_address}` |
| `registration_id` | The HPD registration id used for the join |

#### Run provenance and source health

| Field | Meaning |
| --- | --- |
| `violations_status` | ok / unavailable for the HPD violation read |
| `lien_sale_status` | ok / unavailable for the tax-lien-sale read |
| `evictions_status` | ok / unavailable for the marshal-eviction read |
| `contacts_status` | ok / unavailable for the HPD contact join |
| `registrations_status` | ok / unavailable / not\_applicable for the registration-roll fallback |
| `sources_failed` | Dataset URLs that did not answer. Empty on a healthy run |
| `partial_run` | True when any contributing source did not answer |
| `violations_max_date` | Newest record in the HPD violation dataset at run time |
| `lien_sale_max_date` | Newest list month in the lien-sale dataset at run time |
| `evictions_max_date` | Newest execution date in the eviction dataset at run time |
| `violations_cutoff` | Earliest violation issue date counted |
| `lien_sale_cutoff` | Earliest lien-sale list month counted |
| `evictions_cutoff` | Earliest eviction execution date counted |
| `evictions_without_bbl_in_window` | Executed evictions in this borough and window that carry no BBL upstream and cannot be attributed to a parcel |
| `lookback_months` | Window used for this run |
| `data_as_of` | UTC date the run executed |
| `unmapped_borough_rows` | Eviction rows whose borough token this build does not recognise. Below the drift tolerance, so the run continued, but they are excluded from the score rather than mapped to a guess. `0` = checked, nothing unmapped |
| `unmapped_vocabulary_rows` | Same, totalled across all eight closed-vocabulary gates (violation class and status, lien cycle, water-debt flag, eviction borough, contact type, property type, possession type) |
| `vocabulary_drift_note` | Detail of any tolerated unknown value: the value, its row count, and its share of its group. `null` when every token was recognised |
| `source_urls` | NYC Open Data pages that contributed to the row |

### Freshness: dataset-anchored lookback

The lien-sale list is published in cycles and lags the other sources (currently
through the **2025-06 cycle**). Every lookback window is therefore anchored to
**that dataset's own most-recent record date, not today**, and each anchor is
published on the row (`violations_max_date`, `lien_sale_max_date`,
`evictions_max_date`). The run log prints them too.

### What v1.1 fixed (2026-08-01)

All five sources were re-probed live and the actor was rebuilt around what they
actually return. Nothing was removed or renamed.

- **The default run was reading 100,000 of 165,733 open Bronx violations.** v1.0
  paged at a hard 10 x 10,000 ceiling with no row-count reconciliation. Because
  the read is ordered by `violationid` ascending, the missing 39.7% was the
  *newest* data - every open violation issued after 2026-05-04 was invisible.
  Effect on the shipped prefill: **25 of the 100 published leads did not belong in
  the top 100**, 25 genuine top-100 leads were never seen, and all 75 survivors
  had an understated score (mean understatement 150 points). Worst case, 2338
  Morris Avenue: published 66 open class-B violations against a true **244**, and
  a score of 266 against a true **880**. Every read now reconciles against the
  `count(*)` the service itself reports and **fails the run** if it cannot.
- **`lien_water_debt_only` was inverted on the freshest rows.** The 2025 lien
  cycle emits the short tokens `Y` and `N` alongside `YES`/`NO`; v1.0 compared
  against the literal string `YES`, so all 802 `Y` rows nationwide (176 in the
  Bronx window alone) were published as "not water-debt-only" - the opposite of
  the truth, and it makes a weak signal look like a strong one.
- **A contacts outage was published as "no contact on file".** The whole
  enrichment sat inside one catch that emitted `owner_contact: null` for every
  parcel. On a lead product that is a confident wrong answer. Rows now carry
  `contacts_status` and `owner_contact_checked`.
- **The owning LLC was fetched on every row and thrown away**, along with the
  managing agent, the title and the middle initial. `owner_corporation` now
  reads 98.0% populated on the prefill where it read 0.0%.
- **Class-I violations were silently discarded** by a bare `else continue`.
- **`residential_commercial_ind` was fetched and dropped** in the mapper.
- **Contact paging could starve a batch.** One HPD registration carries 8,688
  contact rows (registration 911741), so a 100-id batch under a flat
  `$limit=10000` with no `$order` truncated non-deterministically.
- **A bad borough raised a bare module-level throw**; it now fails loudly with an
  actionable message and bills nothing.

### Honest notes (read before you buy)

- **NYC-only.** The five sources are NYC Open Data (Socrata).
- **A distress score needs all three signal sources.** A score built on two of
  them is not a smaller answer, it is a wrong one, and it is indistinguishable on
  the row from a parcel that genuinely has no lien and no eviction. So if any
  signal source fails, the run **fails and bills nothing** rather than publishing
  a partial score.
- **`evictions_count` is a floor.** Roughly 5% of executed evictions carry no BBL
  in the city dataset and cannot be attributed to a parcel. The exact count for
  your run is published on every row as `evictions_without_bbl_in_window`.
- **Contact join path:** `registrationid` carried on the HPD violation rows, with
  a fallback through HPD Registrations (`tesw-yqqr`) by borough/block/lot for
  parcels flagged only by liens or evictions. Buildings that never registered
  with HPD emit with a null contact and are never dropped.
- **Lien-sale data vintage:** the DOF list updates only when the city runs a
  lien-sale cycle; months-old `lien_sale_month` values between cycles are the
  source, not a bug. The run fails if that list is ever rolled back earlier than
  the 2025-06 cycle this build was verified against.
- **Tenant privacy:** eviction records are used only as a per-parcel count.
  Respondent names, court index numbers, docket numbers and marshal names are
  never even requested. This is a landlord-side product.
- Violation counts cover **open violations issued within the lookback window**;
  raise `lookbackMonths` to widen.
- This is a screening tool, not a determination. Verify anything you act on
  against the city record.

### Use as an MCP tool

This actor works as a tool for AI agents (Claude, Cursor and others) via
[mcp.apify.com](https://mcp.apify.com) - ask your agent for "the 50 most
distressed parcels in Brooklyn with owner contacts" and it can call this actor
directly. Every output field carries a description in the dataset schema, so an
agent can chain it without guessing.

### FAQ

**How do I find distressed properties in NYC from free data sources?**
This actor does it for you: it merges the three public distress feeds per parcel
and scores them, so you skip the joins, BBL normalization and cycle parsing.

**Is this a foreclosure or pre-foreclosure list?**
No - it is upstream of foreclosure. Code violations, tax-lien-sale notices and
evictions are the pressure signals that often precede a distressed sale.

**How do I get the property owner's contact from a BBL?**
Each lead carries the best HPD registration contact, the owning corporation, the
managing agent and up to 12 contacts in total, with business mailing addresses.

**Can I run all five boroughs?**
Yes - run the actor once per borough. Per-borough runs keep results focused and
your per-result spend predictable.

**How is this priced?**
Pay-per-result: you are charged only for the ranked parcel leads actually
emitted (cap with `maxResults`). Runs that fail a pre-flight check emit nothing
and bill nothing.

### Related actors - the NYC investor shelf

All key on the same borough-block-lot (BBL), so they stack into one workflow:

| Actor | What it adds |
|---|---|
| [NYC ACRIS Deed Transfer Intel](https://apify.com/malonestar/acris-deed-transfer-intel) | Who actually bought and sold - deed price, LLC-buyer, cash-sale and out-of-state-buyer flags |
| [NYC Landlord Registry Lead List](https://apify.com/malonestar/nyc-landlord-registry-lead-list) | The owner's whole portfolio - filter to landlords holding 5-50 buildings, with business contact |
| [Parcel Owner Lookup](https://apify.com/malonestar/parcel-owner-lookup) | Address to owner of record, for one-off checks outside NYC too |

Typical play: score distress here, check ACRIS for recent transfer activity on
the same BBL, then pull the owner's full portfolio and mailing contact from the
HPD registry.

# Actor input Schema

## `borough` (type: `string`):

NYC borough to scan. Accepts a name (MANHATTAN, BRONX, BROOKLYN, QUEENS, STATEN ISLAND) or a borough code 1-5. One borough per run keeps runs fast and cheap.

## `minSignals` (type: `integer`):

Only emit parcels showing at least this many DIFFERENT distress signal types (open violations, tax-lien-sale notice, executed evictions). 2 = solid motivated-seller leads; 3 = only the most distressed parcels; 1 = everything with any signal.

## `lookbackMonths` (type: `integer`):

How many months of history to count, anchored to each dataset's own most-recent record date (not today) so a lagging source like the lien-sale list still yields its latest cycle. Applies to violation issue dates, lien-sale list months, and eviction execution dates.

## `maxResults` (type: `integer`):

Maximum number of ranked parcel leads to emit (highest distress score first). You are billed per result, so this caps your spend.

## Actor input object example

```json
{
  "borough": "BRONX",
  "minSignals": 2,
  "lookbackMonths": 12,
  "maxResults": 100
}
```

# Actor output Schema

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

The default dataset of ranked distressed-parcel leads.

# 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 = {
    "borough": "BRONX",
    "minSignals": 2,
    "lookbackMonths": 12,
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/distressed-property-signal-stacker").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 = {
    "borough": "BRONX",
    "minSignals": 2,
    "lookbackMonths": 12,
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/distressed-property-signal-stacker").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 '{
  "borough": "BRONX",
  "minSignals": 2,
  "lookbackMonths": 12,
  "maxResults": 100
}' |
apify call malonestar/distressed-property-signal-stacker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/distressed-property-signal-stacker",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dX0b4eGwR0wYWRhIr/builds/5w2x0ZV05xON7nVsR/openapi.json
