EPA Superfund NPL Status Monitor
Pricing
$10.00 / 1,000 npl status changeds
EPA Superfund NPL Status Monitor
Tracks official EPA Superfund/NPL site status by state/county and alerts on real changes: new EPA Superfund site records, or NPL status shifts (Proposed, Final NPL, Deleted, NFRAP). For CRE investors, developers, banks, title companies and ESG due-diligence teams. Free when nothing changes.
Pricing
$10.00 / 1,000 npl status changeds
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Durable, informational monitor of the official U.S. EPA Superfund/NPL site registry — the free,
public REST feed EPA Envirofacts publishes at
https://data.epa.gov/dmapservice/sems.envirofacts_site/fk_ref_state_code/equals/<STATE>. No API key,
no account, no proxy, no browser.
Most Superfund/NPL tools are one-shot lookups by address. This actor is different: it is a recurring watch — "tell me if the status of any of the Superfund sites I'm tracking changes" — not a one-time screen. Built for CRE investors, developers, banks/title companies and ESG/environmental due-diligence teams who need to know when a site near their portfolio moves from Proposed for NPL to Currently on the Final NPL, gets Deleted from the Final NPL, or otherwise changes status.
This is a mirror-with-diff of a public government registry, not an environmental-risk opinion. It is not legal advice, not an estimate of contamination or property value impact, and not a substitute for a Phase I Environmental Site Assessment (ASTM E1527) before a real-estate transaction. It tells you, reliably and cheaply, when EPA's own site records for the states/counties you name gain a new entry or change NPL/non-NPL status.
Why
EPA already publishes every Superfund/NPL site record for free. What it doesn't give you is a durable, per-organization "what changed since I last looked" — you either poll the whole state list yourself and diff it client-side, or you don't watch at all. This actor keeps that diff for you: a list of watches (state + optional county), a durable memory of every matching site's status between runs, and a bill only for sites that are genuinely new to the watch or have a genuinely changed status.
How it works
- Each
watchfetches everysems.envirofacts_siterecord for itsstate(required, one two-letter USPS state/territory code) via the source's own path-segment filter — one GET per watch per run, no pagination (the source has none; the largest state observed, Pennsylvania, returns ~3,500 records / ~2.9 MB in one response). An optionalcountyfurther narrows the result on the client side (case-insensitive substring match against the source'scounty_namefield) — the source'ssems.envirofacts_sitetable does not support a server-side county filter (confirmed live:.../fk_ref_county_name/equals/...returns HTTP 500 "column does not exist"). - The first run for a watch establishes a baseline: every matching site's
site_idand current status fingerprint (NPL status code, non-NPL status code, non-NPL status date, archived flag, archived date) are stored, nothing is billed or delivered. - Every later run compares the fresh set against the stored index:
- a
site_idnever seen before under this watch is new; - a
site_idseen before whose status fingerprint differs from what was stored is status_changed (the row carries both the new and the previous NPL/non-NPL status for context); - a site whose name/address/coordinates changed but whose status fingerprint did not is not treated as a billable delta (administrative record corrections, not a status change).
- a
- A hard per-watch cap (
maxResultsPerWatch) protects you from a single run billing an unbounded backlog — anything over the cap is picked up cleanly on the next run, nothing is lost or double-billed.
Input
{"monitorId": "my-superfund-watch","watches": [{ "watchId": "connecticut-sites", "state": "CT", "county": "Hartford" }],"maxResultsPerWatch": 200,"notifyOn": "new_alerts","webhookUrl": "https://example.com/webhook"}
watches— 1-25 objects, each{watchId, state, county?}.stateis one two-letter USPS state/territory code (e.g.CA,TX,PR,DC,VI,GU,AS,MP).countyis optional (case-insensitive substring match againstcounty_name; omit for every county in the state). AwatchIdis bound to itsstate/countyon first use — reusing the samewatchIdwith a differentstate/countylater fails the run honestly (watch_config_mismatch) instead of silently hiding records under a stale index.maxResultsPerWatch— 1-2000, default 200.
Output
One row per new/changed site: watchId, status (new/status_changed), siteId, epaId, name,
state, county, nplStatusCode/nplStatusName, nonNplStatusCode/nonNplStatusName, and (for
status_changed) previousNplStatusCode/previousNplStatusName,
previousNonNplStatusCode/previousNonNplStatusName. A run that finds nothing new still writes an
honest run_summary row to the default dataset (never silently empty).
Billing
Pay-per-event, one named event: npl-status-changed — covers both a site newly appearing under a
watch and an already-known site's NPL/non-NPL status changing. Deduplicated by site_id + a hash of
its current status fields (the same site can be billed again later if its status genuinely changes
again, but never twice for the same status content). The baseline run establishes history without
charging. Failed/blocked runs (source fetch failed, run timed out) are never charged.
Delivery guarantee: at-most-once (not exactly-once)
The right to write a row and to charge for it is granted by a single atomic primitive — one
addRequest(uniqueKey) into a dedicated, named claim-journal Request Queue
(<prefix>-<monitorId>-claims). Exactly one run ever wins that key. Claim requests are never deleted
and never handled: the queue is a permanent journal of irreversible attempts, not a work list.
What this buys you, stated honestly:
- You will never be charged twice for the same event. That is the guarantee.
- It is not exactly-once. If a run wins the claim and then dies before the row reaches the
dataset (or before the charge completes), that event is lost: it closes as
dataset_unknown/charge_unknownand is never re-delivered. We deliberately prefer losing a delivery over double-charging you. - Boundary of the guarantee: it holds for as long as the named claim-journal queue exists. Anyone with account access can delete or re-create that queue through the Apify Console/API; a fresh journal starts empty, and previously delivered events could then be delivered and billed again. That is an inherent limit of any durable storage, not a defect of the protocol.
- Migration boundary: the guarantee applies from the build that introduced the claim gate onward.
Older builds of this actor must not keep running against the same
monitorId— they predate the journal and would not see the claims it holds. coverage.claimJournalSizereports the journal's size each run (best-effort;nullif the queue's metadata could not be read, and the value lags by a few seconds because Apify'stotalRequestCountis eventually consistent). Use it to watch growth, not to make decisions.
Honest limits
- The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset.
Each row is written to the durable dataset first, then mirrored to the run's default dataset before
billing proceeds for that row. If the durable write succeeds but the default-dataset mirror write
fails (e.g. transient Apify storage error), the item is marked
dataset_unknown, billing for it is permanently blocked (fail-closed — we never charge for a row we can't confirm was delivered), and the run is not retried into re-creating that exact row. The durable dataset can therefore end up with a small number of orphan rows that were never mirrored and never billed. The default dataset is the canonical log of rows successfully written to this run's output (see itsrun_summaryrow) — but a default-dataset row does not by itself prove the row was billed: the row is written beforeActor.charge()runs, so if charging then fails or comes backcharge_unknown, the row is present but not confirmably paid.run_summary.eventsBilledand Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset. - We mirror EPA's registry as published; we do not offer an opinion on contamination severity, cleanup cost, or property value impact, and we are not a substitute for a Phase I ESA — this is an informational monitor of official NPL status records, nothing more.
- The source gives no way to verify a response isn't truncated. Unlike some other government feeds
in this product line (FEMA, CISA KEV),
sems.envirofacts_sitereturns a bare JSON array with no declared total count and noContent-Lengthheader (chunked transfer). We do not invent an integrity check the source can't support. The one real failure mode observed live (13.09.2026): the server occasionally closes the response stream before the JSON is complete, which surfaces as a JSON parse error on an otherwise-200 response — we treat that as transient and retry once. See ROADMAP.md for the full write-up. - A site's name/address/coordinates can change without its NPL status changing (administrative record corrections). Only a change in the status fingerprint (NPL code, non-NPL code, non-NPL status date, archived flag/date) is billed; other field changes update our internal cache silently.
- The source has no server-side county filter for this table —
countyis a client-side, best-effort substring match, not an authoritative EPA filter. - If the feed is temporarily unavailable or its shape changes, the affected watch reports that honestly instead of silently returning zero results.
Author: OmniCoder (https://t.me/OmniCoder)