# EPA Toxic Releases — What a ZIP Code's Industry Emits (`alauddinburki/epa-toxic-releases`) Actor

Look up US facilities by ZIP code and report what each one released: chemical, CAS number, year, total pounds and the split across air, water, land and underground. Source: the EPA Toxic Release Inventory.

- **URL**: https://apify.com/alauddinburki/epa-toxic-releases.md
- **Developed by:** [Alauddin burki](https://apify.com/alauddinburki) (community)
- **Categories:** Other, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.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.

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

## EPA Toxic Releases — What a ZIP Code's Industry Emits

Give it a US ZIP code and get back **what the facilities there released**: chemical, CAS number, reporting year,
total pounds, and the split across **air, water, land and underground**.

Source: the **EPA Toxic Release Inventory** through Envirofacts — public, keyless, official.

### Why this needs an Actor rather than a query

The data is four tables that **cannot be joined through the API**:

```
tri_facility            (name, address, ZIP)
   └── tri_reporting_form     one chemical, ONE YEAR, per doc_ctrl_num
          ├── tri_release_qty      one row PER MEDIUM (AIR FUG, AIR STACK, WATER, LAND TREA …)
          └── tri_chem_info        the chemical name and CAS number
```

The Actor walks the chain for you: ZIP → facilities → forms → the chosen year → quantities → chemical names.

### Two traps that make naive numbers wrong

**1. `total_release` is often `null`, and null is not zero.** The API also sets `release_na` when a facility reports
"not applicable". Summing null as zero understates a facility — and printing "0 lb" when the truth is "not reported"
is worse than printing nothing. So an unreported chemical keeps `totalPounds: null` and carries a `note` saying so.

**2. A facility has decades of forms.** Without picking one year deliberately you would add 1987 to 2023. The Actor
uses the year you ask for, or the newest the facility filed, and never mixes years in one row.

### What you get

| Field | Example |
|---|---|
| `facilityName` / `facilityId` | GOLDEN BELT MANUFACTURING CO · 27701GLDNB807EA |
| `chemical` / `casNumber` | Toluene · 108-88-3 |
| `year` | 1996 |
| `totalPounds` | 2,895 |
| `airPounds` / `waterPounds` / `landPounds` / `undergroundPounds` | 2,000 · — · 895 · — |
| `city` / `state` / `zip` | RALEIGH · NC · 27701 |

Two views: **Releases** (who released what) and **By medium** (air vs water vs land).

### Use cases

- **Site due diligence** — what has been released near a property or a school.
- **ESG and supply-chain screening** — a supplier's reported releases by chemical.
- **Local journalism** — the numbers behind a neighbourhood story, row by row.

### Limitations — read these

- **Coverage is US facilities that must report to TRI.** Not every business does.
- **Reporting is annual and often years behind.** The example above is 1996 because that is the newest report that
  facility filed; a ZIP with no recent filer returns old years, and the `year` column says which.
- **Quantities are self-reported estimates** by the facility, not measurements.
- **`null` means "not reported"**, never "zero" — filter on `totalPounds !== null` when you need numbers.
- **Several API calls per facility**, so `maxFacilities` bounds the run.

### Why this is different

The TRI tables are public and free, so the usual answer is "write a script". The script is where the
work is: four tables that share no single join key, one row per medium that has to be summed without
turning "not reported" into zero, and decades of forms per facility that have to be narrowed to one
year before anything adds up. This returns that finished join as flat rows — facility, chemical, CAS
number, year and the medium split — with `null` kept honest and a `note` explaining every null. It is
**standalone**: no key, no ETL, no warehouse, no per-lookup form.

### FAQ

**Do I need an EPA API key?** No. Envirofacts is public.

**Why is `totalPounds` null instead of 0?** The facility reported that release as not applicable or
left it blank. Treating that as zero would understate the plant, so it stays `null` with a note.

**Which year am I looking at?** The year you ask for, or the newest year that facility filed. The
`year` field on every row says which.

**Why is the newest year old?** TRI reporting is annual and published with a lag; a plant that closed
in 2005 has no later reports.

**Does a missing facility mean nothing was released?** No. It means the facility does not report to
TRI, or the ZIP has no TRI filer.

### Pricing

Pay per result: **$0.0005 per release row**. Each stored dataset row is billed, including any diagnostic row returned instead of results.

An Actor-start event is also charged at **$0.0001 per GB of allocated memory (minimum one event)**, even if no rows are returned. At 1 GB or less, 1,000 result rows cost $0.5001 before any applicable Store discount.

### Demand — stated honestly

**Zero competing Actors in the Store** for this data. That is a fact about supply, not proof of demand: the
independent signals are modest (30–70 GitHub repositories around TRI data, low Hacker News volume). This is a
compliance/ESG niche, not a mass market — the trade-off that comes with a zero-competition lane.

### Verified

**28 unit tests** (buckets, the null-is-not-zero rule, ZIP validation, year selection, row shape) + **9 live checks**
against the real Envirofacts service (facility → form → release → chemical).

***

Built for the second account (`alauddinburki`), Actor 1000, per `plan/OPERATIONS.md`.

# Actor input Schema

## `zipCodes` (type: `array`):

Five-digit US ZIP codes.

## `year` (type: `integer`):

Leave empty for the newest report each facility filed.

## `maxFacilities` (type: `integer`):

Per run, across all ZIPs. Each facility costs several API calls, so this bounds the run time.

## `maxRows` (type: `integer`):

Hard limit. Strictly enforced.

## `timeoutSecs` (type: `integer`):

Per request.

## `budgetSecs` (type: `integer`):

Stop and report partial results before the run is killed.

## Actor input object example

```json
{
  "zipCodes": [
    "27701"
  ],
  "maxFacilities": 5,
  "maxRows": 500,
  "timeoutSecs": 30,
  "budgetSecs": 240
}
```

# Actor output Schema

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

One row per facility, chemical and year, with the 19 fields listed under Views.

# 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 = {
    "zipCodes": [
        "27701"
    ],
    "maxFacilities": 5,
    "maxRows": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("alauddinburki/epa-toxic-releases").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 = {
    "zipCodes": ["27701"],
    "maxFacilities": 5,
    "maxRows": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("alauddinburki/epa-toxic-releases").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 '{
  "zipCodes": [
    "27701"
  ],
  "maxFacilities": 5,
  "maxRows": 500
}' |
apify call alauddinburki/epa-toxic-releases --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alauddinburki/epa-toxic-releases"
        }
    }
}
```

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/immaHXkm2HVHrZOwL/builds/wMtROmKYtdHlDtA35/openapi.json
