# Phase I ESA & Environmental Due Diligence: EPA Database Search (`malonestar/epa-contaminated-site-screener`) Actor

Environmental due diligence by address: an environmental database report over EPA Superfund/NPL, RCRA CORRACTS/TSD/generators, TRI, UST, LUST, Brownfields, NPDES, AIR, TSCA and RMP, scored at ASTM E1527-21 search distances, plus on-site Superfund and AUL boundary checks. No API key.

- **URL**: https://apify.com/malonestar/epa-contaminated-site-screener.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Agents, Developer tools, Real estate
- **Stats:** 4 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.50 / 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/actors/running/actors-in-store.md#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

## Phase I ESA & Environmental Due Diligence - EPA Database Search by Address

Run an **environmental database report** on any US address or coordinate: ten EPA regulatory databases screened by distance, scored against the **ASTM E1527-21** standard search distances, returned as structured rows. This is the multi-database **regulatory records review** at the core of a Phase I Environmental Site Assessment, and the cheapest way to put **environmental due diligence** on a property or a whole portfolio before you pay for the full report. Built for Phase I ESA consultants, CRE buyers and lenders, environmental attorneys and underwriters. No API key.

### Environmental database report by address

Give it an address (or lat/lon) and a radius; get one row per nearby regulated site, each tagged with its `program`, its distance, its EPA status, and a direct link to the EPA source record.

| `program` | EPA database | Source |
|---|---|---|
| `SUPERFUND` | NPL / Superfund (SEMS) + FRS SEMS points | EPA Envirofacts `ENVIROFACTS_SITE` + EPA FRS `FRS_INTERESTS_SEMS` / `_SEMS_NPL` |
| `RCRA` | Hazardous-waste handlers, **classified into CORRACTS / TSD / generator** | EPA ECHO `echo_rest_services` + `rcra_rest_services` |
| `TRI` | Toxics Release Inventory reporters | EPA ECHO |
| `UST` | Underground storage tank facilities | EPA UST Finder (2018-2021 vintage) |
| `LUST` | Leaking underground storage tank releases | EPA UST Finder |
| `BROWNFIELD` | Brownfields / ACRES | EPA FRS ACRES |
| `NPDES` *(opt-in)* | Clean Water Act discharge permits (ICIS-NPDES) | EPA ECHO |
| `AIR` *(opt-in)* | Clean Air Act permitted stationary sources | EPA ECHO |
| `TSCA` *(opt-in)* | TSCA-regulated facilities incl. PCB handlers | EPA ECHO |
| `RMP` *(opt-in)* | Risk Management Plan chemical-accident facilities | EPA ECHO |

Addresses are geocoded with the free US Census geocoder. Leave `programs` empty to screen the original six; name the opt-in four to include them (they come from the *same* EPA ECHO response, so they cost no extra upstream call).

### ASTM E1527-21 standard environmental record sources and search distances

Set `astmMode: true` and each asset gets one extra `astm_summary` row scoring the databases against the ASTM E1527-21 Sec. 8.2.1 search-distance table — delivered as **flat, CSV-safe columns** (`astm_npl_flag`, `astm_rcra_corracts_nearest_mi`, ...) plus a nested object.

| ASTM line item | Required radius | Column prefix |
|---|---|---|
| NPL (Superfund, Final) | 1.0 mi | `astm_npl_*` |
| Delisted NPL / SEMS-CERCLIS (non-NPL) | 0.5 mi | `astm_cerclis_*` |
| **RCRA CORRACTS** (corrective action) | 1.0 mi | `astm_rcra_corracts_*` |
| **RCRA TSD** (treatment/storage/disposal) | 0.5 mi | `astm_rcra_tsd_*` |
| **RCRA generators** (LQG/SQG/VSQG) | 0.25 mi | `astm_rcra_generators_*` |
| LUST | 0.5 mi | `astm_lust_*` |
| UST (registered) | 0.25 mi | `astm_ust_*` |
| Brownfields | 0.5 mi | `astm_brownfield_*` |

`TRI` is not an ASTM standard record source and never gets an entry. `radiusMiles` accepts fractional miles (0.1–50) so you can screen at 0.25 or 0.5 mi directly.

### RCRA CORRACTS, TSD and LQG/SQG generator search

Earlier versions applied the widest 1.0-mile CORRACTS radius to **every** RCRA facility, because `echo_rest_services` does not expose the distinction. `rcra_rest_services` does — via `RCRAUniverse`, `Tsdf` and `CleanupActionFlag` — so each RCRA hit now carries `rcra_astm_class` (`CORRACTS` / `TSD` / `GENERATOR` / `OTHER`), `rcra_corracts`, `rcra_universe` and `rcra_tsd_types`, and is scored at its correct ASTM radius.

`rcra_astm_class` is a single, mutually exclusive class resolved in precedence order **CORRACTS → TSD → GENERATOR → OTHER**, so a facility that is both a corrective-action site and a TSD is reported once, as `CORRACTS`. Its raw `rcra_tsd_types` still shows the TSD designation — `BRODERICK WOOD PRODUCTS CO` carries `Land Disposal (L)` while classing as `CORRACTS`. This is why `astm_rcra_tsd_count` can be lower than the raw count of TSD-designated facilities in range: each facility is scored at one ASTM line item, not several.

Measured live at 39.8037, -104.9986 (Denver) within 1 mile: EPA lists **96–97 RCRA facilities, of which exactly 2 carry a corrective action (CORRACTS)** — `BRODERICK WOOD PRODUCTS CO` and `KOPPERS INC (DENVER CO)` — and 3 carry a TSD designation. Scoring all of them at the 1.0 mi CORRACTS radius, as earlier versions did, over-flags by roughly **48x**. (EPA's `RCRRows` count at this point drifts by a facility or two between refreshes; it read 96 on 2026-07-26.)

### Recognized Environmental Condition (REC) screening — is the property ON a site?

With `includeBoundaries: true` (default) each asset also gets a true **point-in-polygon** test, not a distance-to-centroid guess:

- `on_superfund_site`, `superfund_site_name`, `superfund_epa_id`, `superfund_epa_url`, `superfund_site_description` — is the property inside a published EPA NPL Superfund site boundary? (2,114 polygons nationally.)
- `institutional_control_flag`, `institutional_control_site_name`, `institutional_control_description`, `institutional_control_area_acres` — is it inside a published EPA Superfund **institutional control** boundary?

A boundary match also sets `superfund_npl_status` (a readable label such as `Final NPL`). Verified live 2026-07-26: `39.686641, -105.003564` returns `DENVER RADIUM SITE`; `39.793679, -104.982364` returns `ASARCO, INC. (GLOBE PLANT)` plus the institutional control `OU4 - Environmental Covenant.`

### Institutional controls and activity and use limitations (AULs)

The IC layer answers the ASTM E1527-21 AUL record-source question where EPA has published a boundary. **Coverage is thin and stated plainly: EPA publishes ~165 IC boundary polygons nationally.** A `false` result means "not inside a published federal Superfund IC", **not** "no AUL exists on this property". A `null` means the layer was not consulted or failed — never read null as a clean result.

### Business Environmental Risk — enforcement, penalties and compliance history

Every ECHO-derived hit now carries the facility's regulatory track record, which is what ASTM E1527-21 Business Environmental Risk is about: `compliance_status`, `significant_noncompliance`, `inspection_count`, `last_inspection_date`, `formal_action_count`, `total_penalties_usd`, `last_penalty_date`, plus `sic_codes` / `naics_codes` (an industry pre-screen: 7216 dry cleaner, 5541 gasoline station, 2491 wood preserving) and cross-program IDs (`npdes_ids`, `air_ids`, `rcra_ids`, `sems_ids`, `tri_ids`, `tsca_ids`, `rmp_ids`).

Every hit also carries **`dfr_url`** — a direct link to the facility's EPA Detailed Facility Report, the citable source for a report appendix.

### What this costs against a commercial environmental database report

The **environmental database report** (often called an "EDR report" after the company that pioneered it) is how this records search is normally bought. Published 2026 list pricing from one national consultancy, A3 Environmental Consultants, running ERIS data:

| Commercial product | Price | Turnaround |
|---|---|---|
| EDR report / environmental screen - database search plus a P.G.'s written interpretation | **$250** | 48-72 hrs |
| Complete database package, no topographic maps | **$375** | 5 business days |
| Complete database package with topographic maps | **$415** | 5 business days |
| Record Search with Risk Assessment - adds a signed risk opinion | $850 | 5 business days |
| Full Phase I ESA - adds a site visit, ASTM E1527-21, AAI | $2,200-$4,000 | 2-3 weeks |

Source: <https://a3e.com/what-does-an-edr-report-cost/>, list prices as published, page last updated 2026-05-29. The other vendors in this market are EDR/LightBox, ERIS, Envirosite and GeoSearch; their pricing is quoted per order rather than published, so only the figures above are cited here.

**This Actor is $10 per 1,000 result rows, which is $0.01 per record returned.** Measured on its own five published example runs (4 properties each, 1-mile radius, `astmMode: true`, `maxHitsPerProgram: 8`): 148, 177, 202, 209 and 231 rows per run, so **37 to 58 records per property, or $0.37 to $0.58 per property** at the list price. Widening `radiusMiles` or raising `maxHitsPerProgram` returns more records and costs proportionally more. Against the $250-$415 band that is roughly **430x to 1,100x cheaper per property**, in seconds rather than days, as JSON/CSV/Excel rather than a PDF, and callable from an API or an AI agent.

#### What you are NOT buying here, and it is a real list

The price gap is scope, not magic. A commercial environmental database report carries several things this Actor cannot produce from keyless federal APIs. Assume you still need them:

- **Historical aerial photography** and **topographic map** series - the $415 tier exists precisely because topo costs extra.
- **Sanborn fire insurance maps.**
- **City directory research.** The vendors buy or transcribe these, partly from public-library collections. There is no free API for them.
- **State and tribal database coverage.** This Actor is federal-only. Pair it with the [State UST, LUST & Spill Registry Screener](https://apify.com/malonestar/state-tank-spill-registry-screener), which adds a national EPA UST Finder baseline plus deeper NY, CA, TX and FL registries - still not all 50 states.
- **A professional interpretation.** Even the $250 screen includes a Professional Geologist's written read of what the hits mean. This Actor returns records, distances and flags; the judgement is yours.
- **A signed, liability-backed deliverable.** An RSRA or a Phase I ESA is signed by an Environmental Professional and, for the Phase I, backed by that firm's errors-and-omissions insurance. Nothing produced here is signed by anyone and nothing here carries E\&O cover.
- **AAI / ASTM E1527-21 compliance.** `astmMode` scores federal records against the standard's search-distance table. Scoring against the table is not the same as satisfying the standard, and it does not establish the landowner liability protections under 40 CFR Part 312.

**The honest positioning:** this is the federal regulatory-records layer of an environmental database report, unbundled, priced per record and delivered on demand. Use it to triage a portfolio, to re-screen on a schedule at a price that makes re-screening thinkable, and to decide which properties deserve a $250 report and which deserve a $2,200 Phase I. It replaces neither.

### Environmental due diligence API for commercial real estate

Screen a whole portfolio in one call: pass as many items as you like in `assets`; each nearby site is one row. Every row carries the per-asset rollup, so you can sort a hundred properties by `phase1_esa_flag` and `astm_refined_verdict` before anyone opens a file.

### Two modes

**`assets` mode (default)** — screen your properties. Each asset item is either `{ "address": "...", "label": "..." }` or `{ "lat": <n>, "lon": <n>, "label": "..." }`. **`state` is now optional with lat/lon** — Superfund is screened spatially against the EPA FRS SEMS point layers, which need no state; supplying `state` additionally pulls that state's full Envirofacts SEMS roster for wider non-NPL coverage.

```json
{
  "mode": "assets",
  "assets": [
    { "lat": 39.8037, "lon": -104.9986, "state": "CO", "label": "Denver warehouse" },
    { "address": "5980 Lipan St, Denver, CO 80221", "label": "Denver warehouse (by address)" }
  ],
  "radiusMiles": 1,
  "programs": ["SUPERFUND", "RCRA", "TRI", "UST", "LUST", "BROWNFIELD", "NPDES", "AIR", "TSCA", "RMP"],
  "maxHitsPerProgram": 50,
  "astmMode": true,
  "includeBoundaries": true
}
```

**`inventory` mode** — list the raw EPA SEMS/Superfund roster for the states you name.

```json
{ "mode": "inventory", "states": ["CO", "NJ"], "onlyNpl": true }
```

### Key output fields (assets mode)

- `result_type` — `listing` (a hit), `clear` (screened, nothing within radius), or `error`.
- `program`, `list` — the EPA program and its readable database name.
- `site_name`, `site_address`, `site_city`, `site_state`, `site_zip`, `site_county`; `site_id`, `registry_id`.
- `distance_miles` — great-circle distance from the asset to the site.
- `coordinate_source` / `positional_accuracy_m` / `coordinate_method` — **how the site was located** (state-supplied survey point vs. address geocode). This qualifies every distance in the row.
- `status`, `status_detail`, `on_npl`, `phase1_flag`.
- UST/LUST: `ust_open_tanks`, `ust_closed_tanks`, `ust_temporarily_out_of_service`, `lust_substance`, `lust_reported_date`, `lust_case_open`.
- UST tank counts, unrounded: `ust_open_tanks_reported`, `ust_closed_tanks_reported`, `ust_tos_tanks_reported` and `ust_tank_counts_unreported`. EPA publishes **no** tank count for a slice of UST Finder — measured 2026-08-01, `Closed_USTs` is null on 15,221 of 742,855 facilities and `TOS_USTs` on 15,235. The three original fields report `0` there and always have, so they are unchanged; these four say plainly whether that `0` is a measurement or an absence. On this actor's own five published example runs, 23 of 243 UST rows (9.5%) carried a count EPA never published.
- Receptors: `population_within_1500ft`, `private_wells_within_1500ft`, `in_source_water_protection_area`, `in_wellhead_protection_area`, `in_100yr_floodplain`, `land_use`.
- `asset_*`, `radius_miles`, `checked_at`, `error`, `disclaimer`, `source_url`.

#### Did the databases actually answer? (`data_complete`)

Every row carries a data-completeness ledger, because a screening result that quietly means "we could not reach EPA" is worse than no result at all:

- `programs_requested`, `programs_screened`, `programs_failed`, `programs_failed_detail`
- `data_complete` — true only when **every** requested database answered
- `truncated_programs` — programs whose upstream result hit a hard record cap, **or** whose retrieved record count did not match the upstream's own exact count
- `sems_roster_complete`, `sems_roster_expected`, `sems_roster_retrieved` — the Superfund/SEMS state roster is now reconciled against EPA Envirofacts' own `.../COUNT/JSON` endpoint, queried with exactly the filter used to fetch it. If paging stopped early the row says so instead of ranking NPL proximity over a partial roster.

A `clear` result is only ever emitted when at least one requested database actually answered. If **all** of them failed for an asset, the row is `result_type: "error"` and says so — it never claims the property is clean. If every database fails for every asset, the run **fails** and nothing is billed. As of v1.3.4 that sentence is literally true: rows are held back until at least one asset has been screened by at least one live database, so on a total outage the run fails having pushed nothing at all. Before v1.3.4 one row per asset had already been written when the failure fired.

#### Live upstream drift assertions

Every EPA read is checked at runtime against what that service returned when this actor was baselined, because the expensive failure is not an outage — it is a source that quietly changes shape, keeps answering `200`, and lets the screen return a confident wrong answer. Thirty assertions run on a full screen:

- **Column contracts.** Every column the actor reads is checked on the payload that arrived. A column it needs to *identify or locate a site* going missing (`FacLat`, `Latitude`, `primary_latitude_decimal_val`, `RCRAFlag`, …) is treated as fatal: the run stops before anything is pushed, because listings would otherwise be silently missed. A column it only reads for *enrichment* going missing is a warning and the matching field goes null.
- **Record-count canaries.** Each layer's national record count must sit inside a band measured live on the baseline date — UST Finder 742,855 facilities, LUST 531,310, FRS ACRES 44,235, FRS SEMS 14,970 (+1,837 NPL), Superfund boundaries 2,114, institutional-control boundaries 165, Envirofacts SEMS 55,588 sites. Bands are roughly a third of the measured value, so ordinary growth or a data-cycle refresh cannot trip them; only a layer being replaced by a subset can.
- **Retrieval completeness.** Every paged ArcGIS read is reconciled against `returnCountOnly`, and every SEMS state roster against efservice `COUNT/JSON`. Both counts are issued with exactly the filter used to fetch, never through a chained join.
- **Parser completeness.** EPA ECHO states its row count before sending the CSV, so a row dropped in parsing is caught for free.
- **Coordinate coverage.** A latitude column that is present but entirely empty is invisible to a column check and would make every asset read `clear`; a roster of 500+ records with zero geocoded records fails the assertion.

The outcome is on every row: `drift_status` (`ok` / `warning`), `drift_checks_run`, `drift_checks_failed`, `drift_warnings`, `programs_degraded`, `drift_baseline_date`. A degraded source reports `null` for its derived answers, never `false` — including the Superfund and institutional-control boundary flags, which are skipped entirely rather than answered from a layer that failed its canary.

#### NPL/Superfund proximity rollup (per asset)

`nearest_npl_site_name`, `nearest_npl_distance_miles`, `nearest_npl_status`, `npl_sites_within_radius`, `phase1_esa_flag`, `phase1_summary`. Computed against the union of the state SEMS roster and the fully-geocoded FRS SEMS points, and not bounded by `radiusMiles` — a Phase I report conventionally states the nearest NPL site even when it falls outside the search radius. The NPL life-cycle now includes EPA's pre-proposal status, used for sites already under remedial enforcement; it surfaces as a readable label in `nearest_npl_status` (and, on a Superfund boundary match, `superfund_npl_status`) rather than as a raw status code.

#### Was that "clear" actually verified? (v1.3.4)

Three companion fields state, per row, whether a negative was checked or merely
not checked. `null` means NOT CHECKED; `false` means checked and negative.

- `phase1_screen_performed` - true when the asset was actually screened
  (coordinates resolved AND at least one requested database answered). False on
  an unresolvable address and on an asset where every requested database failed.
- `phase1_esa_flag_verified` - true whenever a database actually returned a
  listing (a positive is self-verifying). `false` only when every requested
  program answered and none matched. `null` when the negative was never
  verified.
- `npl_sites_within_radius_verified` - the same count as
  `npl_sites_within_radius`, but only when the Superfund/SEMS roster actually
  answered for this asset; `null` when SUPERFUND was not requested, failed, or
  the asset was never screened.

The original `phase1_esa_flag` and `npl_sites_within_radius` are unchanged and
always will be: they have been live since v1.2 and pipelines are keyed to them.
If a false clear would carry liability for you, filter on the `_verified`
columns instead, which are blank rather than `false` / `0` when the check was
never run.

### Fields that are often empty, and why

Some columns are legitimately null on most rows because EPA does not carry the data, not because the actor failed to read it. Rather than hide them, here is the measured fill rate for each (2026-07-26) and an input that does populate it.

| Field | Why it is usually null | Measured fill | An input that populates it |
|---|---|---|---|
| `ust_last_inspection_date` | EPA barely populates `Date_of_Last_Inspection` in UST Finder | **1,445 of 742,855** (0.19%) nationally; 17 of 11,330 in CO | `{"lat":37.15785,"lon":-107.605833}` (BIA Warehouse #40, CO) |
| `lust_substance` | State-reported; most states file no substance | **~19% nationally**; MI 17,398, KS 6,058, CA 9, TX 0, OH 0, CO 0 | `{"lat":42.44684,"lon":-83.10325}` (Detroit, MI) |
| `rcra_open_violation_since` | Only exists for a facility with an *open* violation | 0 of 97 within 1 mi of Denver; 26 of 3,238 within 10 mi of Houston | `{"lat":29.66984,"lon":-95.23289}` |
| `rcra_land_release_chemicals` | Only exists where EPA has a land-release record | 0 of 97 at Denver; 7 of 3,238 at Houston | `{"lat":29.71247,"lon":-95.22496}` |
| `institutional_control_*` | EPA publishes very few IC boundary polygons | **~165 polygons nationally** | `{"lat":39.793679,"lon":-104.982364}` (ASARCO Globe Plant) |
| `truncated_programs` | Null is the healthy value — nothing was truncated | fills only at large radii in dense metros | `radiusMiles: 25` in a major metro |
| `error` | Null on every successful row | fills on `error` rows only | an asset with neither address nor coordinates |
| `drift_checks_failed`, `drift_warnings`, `programs_degraded` | Null is the healthy value — every live upstream assertion passed | 0 of 1,457 rows on the 2026-08-01 audit across the prefill and all five example runs | fills only when an EPA service changes shape or a layer collapses |
| `name`, `epa_id`, `address`, `city`, `state`, `zip`, `county`, `npl_status_code`, `npl_status_name`, `non_npl_status_name`, `is_superfund`, `federal_facility`, `archived`, `archived_date`, `lat`, `lon` | Inventory-mode columns; structurally absent from an assets-mode row | 50 of 50 rows in inventory mode | `{"mode":"inventory","states":["CO"],"onlyNpl":false,"onlyWithCoords":true,"maxResults":50}` |

A null in these columns is a fact about EPA's records, not a gap in the screen. Where a field means something stronger — `on_superfund_site`, `institutional_control_flag`, `data_complete` — null and false are deliberately different: **false means checked-and-negative, null means not checked.**

Field availability also depends on which programs a row came from: `dfr_url`, `sic_codes`, `compliance_status`, `total_penalties_usd` and the `rcra_*` block exist only on EPA ECHO-derived rows (RCRA/TRI/NPDES/AIR/TSCA/RMP); `coordinate_source`, receptor counts and the `ust_*`/`lust_*` blocks only on UST/LUST rows; `sems_coord_source` only on Superfund rows.

### Caveats worth knowing

- **This is a screening aid, not a Phase I ESA.** It does not replace a licensed Environmental Professional's records search, and it does not by itself satisfy ASTM E1527-21 or the AAI rule. Use it to triage, then order the real report on anything flagged.
- **Federal databases only.** State and tribal registries, historical aerials, fire-insurance maps and city directories are not included. Pair it with the companion [State UST, LUST & Spill Registry Screener](https://apify.com/malonestar/state-tank-spill-registry-screener) for state-level coverage.
- **SEMS/Superfund geocoding varies sharply by state.** Measured 2026-07: 3% of Texas SEMS records carry coordinates, 16% California, 48% Colorado, 60% New Jersey, 77% New York. NPL-track records, however, are ~96–100% geocoded everywhere, and the FRS SEMS point layers (100% geocoded, ~14,967 non-NPL + 1,837 NPL points) are unioned in to close much of the remaining gap. A `clear` result still means "no *listed, geocoded* record within radius", not a guarantee of no history.
- `distance_miles` is measured to a database point, not a property boundary — check `coordinate_source` before relying on a tight distance. The `on_superfund_site` polygon test is exact.
- Institutional-control coverage is thin (~165 national polygons). See the AUL section above.
- **An EPA service that changes shape now stops the run rather than answering wrongly.** If a source drops a column the screen depends on to identify or locate a site, the run fails with a message naming the column, before any row is emitted and before anything is billed. That is deliberate: on a Phase I screen, a red run you can retry is far cheaper than a `clear` row that missed a listing. Outages behave as they always did — the affected program is marked failed on the row, and the run only fails if every database fails for every asset.

### Use as an MCP tool

This Actor is callable directly by any MCP-compatible AI agent through Apify's hosted
MCP server. There is no server to run and no integration code to write - the tool
schema an agent sees is generated from this Actor's own input and dataset schemas.

**Endpoint**

```
https://mcp.apify.com?tools=malonestar/epa-contaminated-site-screener
```

**Claude Desktop, Claude Code or Cursor** - add to `claude_desktop_config.json`,
`.mcp.json` or `.cursor/mcp.json` respectively:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=malonestar/epa-contaminated-site-screener",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

Get a token at https://console.apify.com/settings/integrations. Claude Desktop can
also authenticate interactively via OAuth against `https://mcp.apify.com` with no
`headers` block. Full reference: https://docs.apify.com/platform/integrations/mcp

**Try asking your agent**

> Screen 1200 Broadway, Denver CO for environmental risk under ASTM E1527-21 and tell me which findings are within the standard's search distance.

**Chains well with** - expose these alongside it by comma-separating the `tools`
parameter, and the agent can carry results from one into the next:

- `malonestar/parcel-owner-lookup`
- `malonestar/site-due-diligence-bundle`
- `malonestar/fws-wetlands-proximity-screener`

```
https://mcp.apify.com?tools=malonestar/epa-contaminated-site-screener,malonestar/parcel-owner-lookup,malonestar/site-due-diligence-bundle,malonestar/fws-wetlands-proximity-screener
```

Billing is unchanged when called as an MCP tool: this Actor is Pay-Per-Event and an
agent pays the same per-result price a human does. A run that cannot answer fails
without billing rather than returning an unverified negative.

### Use in n8n

This Actor is published as an n8n community node, so it can be dropped into a
workflow without writing an HTTP request by hand.

- npm: https://www.npmjs.com/package/n8n-nodes-epa-contaminated-site-screener
- source: https://github.com/malonestar/n8n-nodes-epa-contaminated-site-screener

Install from **Settings > Community Nodes > Install** in any self-hosted n8n
instance, enter `n8n-nodes-epa-contaminated-site-screener`, then add your Apify API key as an
**Apify API** credential. The node calls this Actor through the Apify API, so there
is no extra infrastructure to run and billing is identical to a normal run.

#### Was this useful?

If a run answered your question, a short review on this Store page helps other buyers find the Actor; every published review is read. If a run gave a wrong or confusing answer, open an Issue on this page with the run ID and it will be treated as a defect, not feedback.

### FAQ

**Which databases does it search?** Superfund/NPL, RCRA (split into CORRACTS/TSD/generators), TRI, UST, LUST and Brownfields by default; NPDES, AIR, TSCA and RMP on request.

**Does this replace a Phase I ESA?** No. It is a pre-screen; order a licensed Phase I ESA for anything flagged.

**Does this satisfy an ASTM E1527-21 records review or AAI?** No. `astmMode` approximates the standard's search-distance table against this actor's EPA databases. It is not a Phase I ESA and does not satisfy ASTM E1527-21 or 40 CFR Part 312.

**What are the ASTM E1527-21 search distances?** See the table above — 1.0 mi for NPL and RCRA CORRACTS, 0.5 mi for SEMS-CERCLIS / RCRA TSD / LUST / Brownfields, 0.25 mi for registered UST and RCRA generators.

**Can I screen a whole portfolio at once?** Yes — pass as many items as you want in `assets`; each site hit is one row.

**How does this compare to a commercial environmental database report?** On price, this Actor's five published example runs cost $0.37-$0.58 per property against a published $250-$415 for a commercial database report. On scope it is narrower: federal databases only, no historical aerials, no Sanborn maps, no city directories, no Professional Geologist's interpretation and no signed, E\&O-backed deliverable. See the cost comparison section above for the full list of what is and is not included.

**How is billing calculated?** One dataset row per site hit (assets mode, including the optional `astm_summary` row) or per SEMS site (inventory mode). Boundary and AUL checks add fields, not rows, and are not billed. Runs that cannot screen anything fail without billing.

# Actor input Schema

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

"assets" (default) runs a multi-database Phase I ESA-style regulatory-records screen on your addresses/coordinates — one billable row per nearby EPA-listed site (across Superfund, RCRA, TRI, UST, LUST and Brownfields). "inventory" instead dumps the raw list of EPA SEMS/Superfund sites for the states you pick — one billable row per site.

## `assets` (type: `array`):

Locations to screen against EPA contaminated-site databases. Each item is EITHER {"address": "...", "label": "..."} (geocoded via the free Census geocoder) OR {"lat": <number>, "lon": <number>, "label": "...", "state": "<2-letter, OPTIONAL>"}. The "state" hint is no longer required with lat/lon — Superfund is now also screened spatially against the EPA FRS SEMS point layers, which need no state. Supplying "state" additionally pulls that state's full Envirofacts SEMS roster for wider non-NPL coverage. One dataset row (one billable check) is produced per site hit found within the radius; assets with no hits return a single "clear" row. If EVERY asset fails input validation the run FAILS and nothing is billed.

## `radiusMiles` (type: `number`):

Distance from each asset within which EPA-listed sites are counted and reported across all selected programs. Accepts fractional miles (0.1-50) so you can screen at the ASTM E1527-21 standard search distances directly: 1.0 mi (NPL / RCRA CORRACTS), 0.5 mi (SEMS-CERCLIS, RCRA TSD, LUST, Brownfields), 0.25 mi (registered UST, RCRA generators). Default 1 mile covers the widest ASTM distance.

## `programs` (type: `array`):

Which EPA program databases to include. The six defaults: SUPERFUND (NPL/SEMS), RCRA (hazardous-waste handlers, now classified into CORRACTS / TSD / generator), TRI (Toxics Release Inventory), UST (underground storage tanks), LUST (leaking USTs), BROWNFIELD (ACRES/FRS). Four additional opt-in programs come from the SAME EPA ECHO response at no extra upstream call: NPDES (Clean Water Act discharge permits), AIR (Clean Air Act permitted sources), TSCA (incl. PCB handlers), RMP (Risk Management Plan chemical-accident facilities). Leave EMPTY to screen the original six only — that keeps row counts and cost identical to previous versions. Ignored in inventory mode.

## `maxHitsPerProgram` (type: `integer`):

Assets mode only: cap on the number of nearest site hits reported per program per asset (keeps output and billing bounded near dense industrial areas). The nearest sites are kept. Default 50. Range 1-1000.

## `states` (type: `array`):

2-letter US state codes (e.g. \["CO", "NJ"]) whose EPA SEMS/Superfund site records to list. Required when Mode = inventory. Ignored in assets mode (state is derived automatically per-asset).

## `onlyNpl` (type: `boolean`):

Inventory mode only: when true, keep only sites currently on (or part of) the National Priorities List — the actual Superfund program sites. When false, include all SEMS site statuses. Default false.

## `onlyWithCoords` (type: `boolean`):

Inventory mode only: when true (default), drop SEMS records with no latitude/longitude. Coordinate coverage varies sharply by state — measured 2026-07: 3% of Texas SEMS records carry coordinates, 16% California, 48% Colorado, 60% New Jersey, 77% New York, while NPL-track records are ~96-100% geocoded everywhere. Set false to see the full raw roster including un-mappable rows.

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

Safety cap on total dataset rows produced across the run: max site hits emitted (assets mode) or max SEMS site rows (inventory mode).

## `astmMode` (type: `boolean`):

Assets mode only. Adds ONE extra "astm\_summary" row after each asset's normal rows, scoring this actor's databases against the ASTM E1527-21 Sec. 8.2.1 standard search distances. The refined table splits RCRA into its three real ASTM line items — CORRACTS 1.0 mi, TSD 0.5 mi, LQG/SQG/VSQG generators 0.25 mi — resolved from EPA ECHO, alongside NPL 1.0 mi, SEMS-CERCLIS 0.5 mi, LUST 0.5 mi, UST 0.25 mi and Brownfields 0.5 mi (TRI has no ASTM search distance and is excluded). Results come as flat CSV-safe columns (astm\_npl\_flag, astm\_rcra\_corracts\_flag, ...) plus a nested object, with an astm\_refined\_verdict. Automatically widens the underlying fetch to 1 mile; your normal per-hit rows still respect radiusMiles unchanged. Screening aid only — not a substitute for an ASTM E1527-21 Phase I ESA. Default false.

## `includeBoundaries` (type: `boolean`):

Assets mode only. Runs two extra point-in-polygon queries per asset to answer "is this property ON a Superfund site?" (on\_superfund\_site, superfund\_site\_name, superfund\_epa\_url) and "is it inside a published EPA Superfund institutional-control / activity-and-use-limitation boundary?" (institutional\_control\_flag, institutional\_control\_description). These are true boundary intersections, not distance-to-centroid. Note EPA publishes ~2,114 NPL site polygons but only ~165 IC polygons nationally, so a false IC result means "not inside a published federal Superfund IC", NOT "no AUL exists". Adds no dataset rows and no billing. Default true.

## `strictDataCompleteness` (type: `boolean`):

Assets mode only. Reserved for callers that must not accept partial coverage. Regardless of this setting, every row already carries programs\_screened / programs\_failed / data\_complete, and an asset whose databases ALL failed is reported as result\_type "error" — never as a "clear" result. If every database fails for every asset the run FAILS so nothing is billed. Default false.

## Actor input object example

```json
{
  "mode": "assets",
  "assets": [
    {
      "lat": 39.8037,
      "lon": -104.9986,
      "state": "CO",
      "label": "Denver industrial parcel (lat/lon input)"
    },
    {
      "address": "5980 Lipan St, Denver, CO 80221",
      "label": "Denver industrial parcel (address input)"
    }
  ],
  "radiusMiles": 1,
  "programs": [
    "SUPERFUND",
    "RCRA",
    "TRI",
    "UST",
    "LUST",
    "BROWNFIELD",
    "NPDES",
    "AIR",
    "TSCA",
    "RMP"
  ],
  "maxHitsPerProgram": 50,
  "onlyNpl": false,
  "onlyWithCoords": true,
  "maxResults": 1000,
  "astmMode": true,
  "includeBoundaries": true,
  "strictDataCompleteness": false
}
```

# Actor output Schema

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

The default dataset with one row per site hit or SEMS site.

# 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": "assets",
    "assets": [
        {
            "lat": 39.8037,
            "lon": -104.9986,
            "state": "CO",
            "label": "Denver industrial parcel (lat/lon input)"
        },
        {
            "address": "5980 Lipan St, Denver, CO 80221",
            "label": "Denver industrial parcel (address input)"
        }
    ],
    "radiusMiles": 1,
    "programs": [
        "SUPERFUND",
        "RCRA",
        "TRI",
        "UST",
        "LUST",
        "BROWNFIELD",
        "NPDES",
        "AIR",
        "TSCA",
        "RMP"
    ],
    "maxHitsPerProgram": 50,
    "onlyWithCoords": true,
    "astmMode": true,
    "includeBoundaries": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/epa-contaminated-site-screener").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": "assets",
    "assets": [
        {
            "lat": 39.8037,
            "lon": -104.9986,
            "state": "CO",
            "label": "Denver industrial parcel (lat/lon input)",
        },
        {
            "address": "5980 Lipan St, Denver, CO 80221",
            "label": "Denver industrial parcel (address input)",
        },
    ],
    "radiusMiles": 1,
    "programs": [
        "SUPERFUND",
        "RCRA",
        "TRI",
        "UST",
        "LUST",
        "BROWNFIELD",
        "NPDES",
        "AIR",
        "TSCA",
        "RMP",
    ],
    "maxHitsPerProgram": 50,
    "onlyWithCoords": True,
    "astmMode": True,
    "includeBoundaries": True,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/epa-contaminated-site-screener").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": "assets",
  "assets": [
    {
      "lat": 39.8037,
      "lon": -104.9986,
      "state": "CO",
      "label": "Denver industrial parcel (lat/lon input)"
    },
    {
      "address": "5980 Lipan St, Denver, CO 80221",
      "label": "Denver industrial parcel (address input)"
    }
  ],
  "radiusMiles": 1,
  "programs": [
    "SUPERFUND",
    "RCRA",
    "TRI",
    "UST",
    "LUST",
    "BROWNFIELD",
    "NPDES",
    "AIR",
    "TSCA",
    "RMP"
  ],
  "maxHitsPerProgram": 50,
  "onlyWithCoords": true,
  "astmMode": true,
  "includeBoundaries": true
}' |
apify call malonestar/epa-contaminated-site-screener --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,malonestar/epa-contaminated-site-screener"
        }
    }
}

```

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/kf11nFUbahh8G39h5/builds/ZEerAXgucl7qPUMOA/openapi.json
