# EPA AirToxScreen Cancer Risk Screener - Census Tract Air Toxics (`malonestar/epa-airtoxscreen-cancer-risk-screener`) Actor

EPA AirToxScreen 2019 census-tract cancer-risk API: total risk per million, top contributing pollutants, point/on-road/non-road/non-point source attribution, non-cancer hazard index, and a live national percentile. For Phase I due diligence and environmental-justice screening.

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

## Pricing

from $4.40 / 1,000 screened sites

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

## EPA AirToxScreen Cancer Risk Screener — Census Tract Air Toxics

**EPA AirToxScreen 2019 census-tract air-toxics cancer-risk screener.** Give it a list of coordinates and it runs a server-side point-in-polygon lookup against EPA's national **AirToxScreen 2019** tract-level modeled cancer-risk layer and returns, for each point: the **total estimated lifetime cancer risk per million people**, the **top contributing pollutants** ranked from EPA's own per-pollutant risk fields, a **source-group breakdown** (stationary point sources, on-road and non-road mobile, non-point, fire, biogenics, secondary formation, background), the **non-cancer hazard index** by target organ, and a live **national percentile** computed against all 73,445 risk-rated census tracts.

One clean, billable row per asset — including for a point outside any 2010 census tract (`out_of_coverage`) and for a tract EPA genuinely publishes no risk total for (`matched_no_risk_data`), both of which are valid, useful, non-fabricated answers.

**This is a screening tool, not a personal health-risk or regulatory determination.** AirToxScreen models *ambient* air-toxics concentrations at the census-tract level from EPA's National Emissions Inventory — it does not measure any individual's actual exposure. Consult EPA's own AirToxScreen documentation and a qualified environmental professional before using this for a health, legal or real-estate decision.

***

### Two vintages, and both matter

AirToxScreen risk is **EPA's 2019 modeling cycle** (`assessment_vintage: 2019`), reported against **2010 census tract boundaries** (`tract_geography_vintage: 2010`, an 11-digit GEOID). Both are published on **every row**, matched or not, because they are properties of the underlying EPA service, not of any one lookup. If you join `tract_geoid` against a 2020-vintage tract layer (ACS 2020+, this portfolio's own `fema-nri-county-risk-profile` tract mode, etc.), the boundaries do not line up 1:1 — some 2010 tracts were split, merged or renumbered in 2020. Re-derive the join key from a crosswalk rather than assuming GEOID equality across vintages. **The 2020 AirToxScreen cycle does not exist as a service** — EPA's `NATA20AC_CRP_CRSG` layer answers an ArcGIS "Invalid URL" error (live-verified 2026-08-11) — so 2019 is the only year this actor, or any keyless AirToxScreen consumer, can serve today.

### Who it's for

- **Phase I / Phase II environmental due-diligence consultants** — add an authoritative EPA air-toxics cancer-risk data point to a site assessment, with pollutant- and source-level attribution, not just a single number.
- **Environmental-justice (EJ) screening teams** — a national percentile lets you flag disproportionately high-risk tracts without hosting or joining EPA's own EJScreen infrastructure.
- **Industrial and facility siting teams** — see whether a candidate site sits in an elevated-risk tract and *why* (traffic vs. industry vs. background) before committing.
- **Real-estate and relocation research** — screen a shortlist of addresses for a nationally comparable, EPA-published risk figure.
- **GIS & data teams / AI agents** — enrich any coordinate list with EPA AirToxScreen risk without hosting the 318-field national layer yourself.

### Example input

```json
{
  "assets": [
    { "lat": 29.7355, "lon": -95.2400, "label": "Houston Ship Channel / Manchester, TX" },
    { "lat": 34.0339, "lon": -118.2076, "label": "Boyle Heights, Los Angeles, CA" },
    { "lat": 39.7392, "lon": -104.9903, "label": "Denver, CO" },
    { "lat": 39.3000, "lon": -101.0000, "label": "Rural Thomas County, KS" }
  ],
  "topPollutantsCount": 5
}
```

| Site | `match_status` | `total_cancer_risk_per_million` | `percentile_context.percentile_upper_bound` | Top pollutant |
|---|---|---|---|---|
| Houston Ship Channel, TX | `matched` | 40 | 99.1 | Formaldehyde (17.41) |
| Boyle Heights, Los Angeles, CA | `matched` | 40 | 99.1 | Formaldehyde |
| Denver, CO | `matched` | 30 | ~90 | — |
| Rural Thomas County, KS | `matched` | 10 | ~30 | — |

All four values above are live-measured (2026-08-11), not illustrative.

***

### Input options

| Field | Type | Default | Meaning |
|---|---|---|---|
| `assets` | array | **required** | `[{ lat, lon, label }]` in WGS84 decimal degrees. One billable row per asset. |
| `includeAllPollutants` | boolean | `false` | When `true`, also emit `cancer_risk_by_pollutant_full` (all 72 named cancer-risk pollutants) and `ambient_concentrations_ug_m3` (all 177 EPA-modeled ambient concentrations) on every matched row, instead of just the top-ranked ones. |
| `topPollutantsCount` | integer | `10` | How many top cancer-risk pollutants to rank per tract (max 72, the full named list). |
| `maxAssets` | integer | `200` | Safety cap, max 2000. |

### Output fields — one row per asset

#### Identity & vintage

| Field | Type | Description |
|---|---|---|
| `asset_label` | string | Your label, or `asset_N` if omitted. |
| `asset_lat` / `asset_lon` | number | Screened coordinate (WGS84). |
| `assessment_vintage` | integer | Always `2019` — the AirToxScreen modeling cycle. |
| `tract_geography_vintage` | integer | Always `2010` — the census tract boundary vintage the risk is reported against. |
| `match_status` | string | `matched` · `matched_no_risk_data` · `out_of_coverage` · `invalid_input` · `source_unavailable`. See below. |
| `lookup_error` | string | Detail for `invalid_input` / `source_unavailable`; `null` otherwise. |
| `tract_geoid` | string | 11-digit 2010 census tract FIPS. |
| `state` / `county` / `stcofips` / `epa_region` | string | Matched tract identity. `epa_region` is `null` on `matched_no_risk_data` rows — EPA leaves it null there too. |
| `population_2010` | integer | 2010 census population of the tract. |
| `area_sq_miles` | number | Tract area, converted from EPA's published square-meter field. |
| `checked_at` | string | UTC timestamp of the run. |
| `source_url` | string | The EPA AirToxScreen FeatureServer layer used. |
| `disclaimer` | string | Informational-use disclaimer. |

#### `match_status` — what each value means and does not mean

| Value | Meaning | Risk fields |
|---|---|---|
| `matched` | A 2010 tract was found and EPA published a real cancer-risk total for it. | Populated. |
| `matched_no_risk_data` | A 2010 tract was found, but EPA publishes **no** risk total for it — measured 2026-08-11: **266 of 73,711 tracts (0.36%)**, almost all unpopulated land or open water (`POP2010: 0` on the fixture tract). **Never reported as a risk of 0** — 0 is a real, different, achievable value on this layer (national minimum, measured 5) and must not be confused with "not modeled". | `null`. |
| `out_of_coverage` | The point does not fall inside any 2010 census tract in this layer (e.g. open ocean, outside the 50 states + DC + territories the layer covers). A valid, useful, non-error answer. | `null`. |
| `invalid_input` | The supplied `lat`/`lon` was unusable. | `null`. |
| `source_unavailable` | The FeatureServer did not answer after retries, or matched an anomalous >1 tracts (structurally impossible for non-overlapping census tracts — reported rather than silently guessed). | `null`. |

#### Headline risk

| Field | Type | Description |
|---|---|---|
| `total_cancer_risk_per_million` | number | EPA's published `Total_Cancer_Risk` — estimated lifetime cancer cases per million people from modeled ambient air toxics. The headline number. |
| `total_cancer_risk_per_million_legacy_field` | number | EPA's separate `CR_Total_Risk` field. Live-verified identical to the field above on all 73,711 tracts (2026-08-11); a live drift check fails the run if EPA ever lets the two diverge. |
| `percentile_context` | object | `{tracts_evaluated, tracts_ranked_lower, tracts_ranked_lower_or_tied, percentile_lower_bound, percentile_upper_bound, note}` — where this tract ranks nationally, computed live from a once-per-run histogram of all 73,445 risk-rated tracts. **Two bounds, not one number**: EPA's published total is a rounded integer with only 18 distinct values nationally, so many tracts tie exactly at the same published risk — a single percentile would fabricate precision the data does not support. `null` unless `match_status` is `matched`. |
| `top_pollutants` | array | Top `topPollutantsCount` named cancer-risk pollutants, each `{pollutant, cancer_risk_per_million, percent_of_published_total}`. `percent_of_published_total` divides by `total_cancer_risk_per_million` (what you actually see), not an internally recomputed sum — see `source_groups.source_group_sum_per_million` for why those two totals are not identical. |
| `risk_change_flag` / `risk_change_flag_recognized` | string / boolean | EPA's raw `Risk_Change` code, passed through verbatim (observed live: `E` on 0.12% of tracts, `N` on the rest; EPA's own documentation of the exact meaning was not located, so it is not decoded further). `recognized: false` flags a code outside those two. |
| `popup_note` | string | EPA's own free-text note for the tract, when published (rare). |

#### Source-group attribution

| Field | Type | Description |
|---|---|---|
| `source_groups.point_stationary_per_million` | number | Major stationary point sources. |
| `source_groups.onroad_mobile_per_million` (+ `onroad_mobile_detail`) | number / object | On-road vehicles; detail splits by light/heavy duty, gas/diesel, on/off network, refueling, hoteling (10 subcategories). |
| `source_groups.nonroad_mobile_per_million` (+ `nonroad_mobile_detail`) | number / object | Non-road mobile sources; detail covers construction, agriculture, lawn/garden, marine (CMV), rail, aviation, and more (13 subcategories). |
| `source_groups.nonpoint_per_million` (+ `nonpoint_detail`) | number / object | Diffuse stationary sources; detail covers industrial, commercial cooking, oil & gas, solvents/coatings, residential wood combustion, waste disposal, and more (10 subcategories). |
| `source_groups.fire_per_million` / `biogenics_per_million` / `secondary_formation_per_million` / `background_per_million` | number | Wildfire/prescribed burn, natural biogenic emissions, secondarily-formed pollutants, and national background, respectively. |
| `source_groups.source_group_sum_per_million` | number | **Informational, unrounded** sum of the 8 components above. Live-verified (Harris County TX 48201233702): this sum is `41.4569`, while EPA's own published `total_cancer_risk_per_million` is `40` — EPA's AirToxScreen tool rounds internally before publishing the total, and this actor reports both numbers rather than "correcting" one to match the other. |

#### Non-cancer hazard index

| Field | Type | Description |
|---|---|---|
| `noncancer_hazard_index` | object | EPA's 14 target-organ chronic hazard index values (`respiratory`, `neurological`, `liver`, `developmental`, `reproductive`, `kidney`, `ocular`, `endocrine`, `hematological`, `immunological`, `skeletal`, `spleen`, `thyroid`, `whole_body`). A value above 1 indicates modeled exposure at or above a level of concern for that organ system. Already fetched alongside cancer risk on every query — emitted, not discarded. |

#### Full pollutant vectors (only when `includeAllPollutants: true`)

| Field | Type | Description |
|---|---|---|
| `cancer_risk_by_pollutant_full` | object | All 72 named EPA cancer-risk pollutants (per million), keyed by pollutant name. Superset of `top_pollutants`. |
| `ambient_concentrations_ug_m3` | object | All 177 EPA-modeled ambient air-toxics concentrations (µg/m³), keyed by field name with the `AC_` prefix stripped. |

***

### Live drift protection

Every run checks the EPA FeatureServer **before producing a single row**: layer identity and name, the field set this actor depends on (an unknown `outFields` name 400s on this specific service rather than silently returning zero features — verified live and checked anyway, against the layer's own metadata), a tract-count band (measured 73,711), agreement between EPA's two published "total" fields, a positive canary (a known, stable, elevated-risk Houston tract), a negative control (mid-Pacific Ocean must match nothing), the integrity of the once-per-run national percentile histogram, and a projection/coordinate-magnitude check. If any check fails, the run fails and **nothing is billed** — no confident wrong answer is ever produced from a degraded upstream.

### Use as an MCP tool

This actor is callable directly by AI agents (Claude, Cursor, etc.) via Apify's MCP endpoint at `mcp.apify.com`, using your Apify API token. Example client config for Claude Desktop / Claude Code:

```json
{
  "mcpServers": {
    "apify-airtoxscreen": {
      "command": "npx",
      "args": ["-y", "@apify/actors-mcp-server", "--actors", "malonestar/epa-airtoxscreen-cancer-risk-screener"],
      "env": { "APIFY_TOKEN": "<your Apify API token>" }
    }
  }
}
```

Worked prompt: *"Screen 40.7128,-74.0060 and 29.7355,-95.2400 for EPA AirToxScreen cancer risk and tell me the top contributing pollutant at each."* Billing when called as a tool is identical to a normal run — one Result event per asset row — and a run that cannot answer fails without billing, same as any other caller.

### FAQ

**Is this the same as EPA's EJScreen tool?** No. EJScreen bundles many environmental and demographic indicators; this actor is EPA's underlying **AirToxScreen** cancer-risk layer specifically, with full pollutant- and source-level detail EJScreen's own UI does not expose per-tract.

**Why is `total_cancer_risk_per_million` sometimes null even though the tract matched?** EPA genuinely does not publish a risk total for about 0.36% of census tracts — almost always unpopulated land or open water. `match_status` reports this as `matched_no_risk_data`, distinct from a real low score.

**Can I get a single precise percentile?** No, deliberately. EPA's published total is a rounded integer with only 18 distinct values nationally, so many tracts share the exact same published risk. `percentile_context` reports a lower and upper bound instead of a single number that would fabricate precision the data does not have.

**Does this work outside the United States?** No — AirToxScreen covers the 50 states, DC and US territories on 2010 census tract geography. Points elsewhere return `out_of_coverage`.

### Pricing

$8 per 1,000 Result events (one event per screened asset row), with the standard graduated volume discounts.

# Actor input Schema

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

List of points to screen against EPA AirToxScreen 2019 census-tract air-toxics cancer risk. Each item needs numeric lat/lon and an optional label.

## `includeAllPollutants` (type: `boolean`):

When true, also emit ambient\_concentrations\_ug\_m3 (all 177 EPA-modeled ambient air-toxics concentrations) and cancer\_risk\_by\_pollutant\_full (all 72 named cancer-risk pollutants, not just the top ranked ones) on every matched row. Off by default to keep the row curated rather than a 318-field dump.

## `topPollutantsCount` (type: `integer`):

How many top-contributing cancer-risk pollutants to rank and return per matched tract (by CR\_\* value, descending). Maximum 72 (the full named-pollutant list).

## `maxAssets` (type: `integer`):

Safety cap on how many assets one run will screen, regardless of how many are supplied in "assets".

## Actor input object example

```json
{
  "assets": [
    {
      "lat": 29.7355,
      "lon": -95.24,
      "label": "Houston Ship Channel / Manchester, TX"
    },
    {
      "lat": 34.0339,
      "lon": -118.2076,
      "label": "Boyle Heights, Los Angeles, CA"
    },
    {
      "lat": 39.7392,
      "lon": -104.9903,
      "label": "Denver, CO"
    },
    {
      "lat": 39.3,
      "lon": -101,
      "label": "Rural Thomas County, KS"
    }
  ],
  "includeAllPollutants": false,
  "topPollutantsCount": 10,
  "maxAssets": 200
}
```

# Actor output Schema

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

The default dataset.

# 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 = {
    "assets": [
        {
            "lat": 29.7355,
            "lon": -95.24,
            "label": "Houston Ship Channel / Manchester, TX"
        },
        {
            "lat": 34.0339,
            "lon": -118.2076,
            "label": "Boyle Heights, Los Angeles, CA"
        },
        {
            "lat": 39.7392,
            "lon": -104.9903,
            "label": "Denver, CO"
        },
        {
            "lat": 39.3,
            "lon": -101,
            "label": "Rural Thomas County, KS"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/epa-airtoxscreen-cancer-risk-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 = { "assets": [
        {
            "lat": 29.7355,
            "lon": -95.24,
            "label": "Houston Ship Channel / Manchester, TX",
        },
        {
            "lat": 34.0339,
            "lon": -118.2076,
            "label": "Boyle Heights, Los Angeles, CA",
        },
        {
            "lat": 39.7392,
            "lon": -104.9903,
            "label": "Denver, CO",
        },
        {
            "lat": 39.3,
            "lon": -101,
            "label": "Rural Thomas County, KS",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("malonestar/epa-airtoxscreen-cancer-risk-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 '{
  "assets": [
    {
      "lat": 29.7355,
      "lon": -95.24,
      "label": "Houston Ship Channel / Manchester, TX"
    },
    {
      "lat": 34.0339,
      "lon": -118.2076,
      "label": "Boyle Heights, Los Angeles, CA"
    },
    {
      "lat": 39.7392,
      "lon": -104.9903,
      "label": "Denver, CO"
    },
    {
      "lat": 39.3,
      "lon": -101,
      "label": "Rural Thomas County, KS"
    }
  ]
}' |
apify call malonestar/epa-airtoxscreen-cancer-risk-screener --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,malonestar/epa-airtoxscreen-cancer-risk-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/sTB86xC5WPGa06jK3/builds/lI90I4W9E1I6gZ2FB/openapi.json
