# EUDR Plot Deforestation Risk Screener (`atlas-data/eudr-deforestation-risk-screener`) Actor

Per-plot deforestation risk screening for EUDR due diligence: checks polygons against official open UMD Hansen and EC JRC forest-change layers, cross-checks signals, runs geometry QC, pins layer versions and emits evidence records. Risk indicators - not a compliance verdict.

- **URL**: https://apify.com/atlas-data/eudr-deforestation-risk-screener.md
- **Developed by:** [Atlas](https://apify.com/atlas-data) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

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

## EUDR Plot Deforestation Risk Screener

Per-plot deforestation risk screening for **Regulation (EU) 2023/1115 ("EUDR") due-diligence workflows**, built entirely on official, openly licensed geospatial layers. Submit plot polygons; get back a conservative risk flag, per-layer evidence, geometry QC, pinned layer versions and full provenance — one dataset record per plot.

> **This Actor produces risk indicators and evidence records only.**
> It is **not** a compliance verdict, legal clearance, or legal/regulatory/scientific advice.
> Final due-diligence responsibility always rests with the economic operator.

### Who this is for

Compliance managers, sustainability leads and EUDR consultants at small traders, roasters, importers and consultancies who need to screen many plots quickly and keep an auditable record of *which map versions were checked, when* — without GIS staff or enterprise monitoring contracts.

### What it checks

| Layer | What it detects | License / access | Version pinning |
| --- | --- | --- | --- |
| **UMD Hansen Global Forest Change** (`lossyear`) | Annual tree-cover loss events, 2000–latest edition | CC BY 4.0 — public bucket, verified 2026-08-26 | Discovered live from the public bucket listing (e.g. `GFC-2025-v1.13`), reported in every record |
| **EC JRC Tropical Moist Forest** (`DeforestationYear`) | Humid tropical moist forest deforestation event year, 1982–current edition | Free of charge, without restriction of use (Copernicus); attribution required — verified 2026-08-26 | The tile endpoint serves the current production vintage without exposing which one it is; records report this explicitly |

Both layers are fetched anonymously over their official distribution endpoints. No scraping, no circumvention, no third-party re-hosting.

### How the flag logic works (conservative by design)

- A plot is **`flagged`** when **any checked layer** shows forest-change events **strictly after the cut-off date** (default `2020-12-31`, the EUDR reference date).
- A plot is **`clear_on_checked_layers`** only when **all requested layers were checked**, had examinable pixels over the plot, and none shows a post-cutoff signal.
- Anything else is **`inconclusive`**: a requested layer failed or was skipped (e.g. tile budget), the plot is sub-pixel, or coverage was insufficient. Inconclusive results are never silently "clear".
- Layer disagreement is preserved, not averaged away: `hansen_only` / `jrc_only` cross-check classifications are reported with an explanatory note (JRC TMF maps humid tropical moist forest domains only).

### Input

```json
{
  "plots": [
    {
      "plotId": "lot-42-plot-1",
      "commodity": "coffee",
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[-45.2523, -13.0336], [-45.2477, -13.0336], [-45.2477, -13.029], [-45.2523, -13.029], [-45.2523, -13.0336]]]
      }
    },
    { "plotId": "lot-42-plot-2", "commodity": "cocoa", "wkt": "POLYGON((...))" }
  ],
  "cutoffDate": "2020-12-31",
  "layers": ["hansen_gfc", "jrc_tmf"],
  "maxTilesPerRun": 4,
  "maxCacheEntries": 300
}
```

- `plots[].geometry` accepts GeoJSON **Polygon/MultiPolygon in EPSG:4326** (WGS84), or `plots[].wkt` as POLYGON/MULTIPOLYGON text. Coordinates must be `[longitude, latitude]`.
- `commodity` is metadata carried into the output (`cattle`, `cocoa`, `coffee`, `oil_palm`, `rubber`, `soy`, `wood`, `other`, `unspecified`). It does not change the analysis.
- Up to **200 plots per run**. Plot IDs must be unique (auto-generated stable IDs from the geometry hash when omitted).
- Geometry QC: unclosed rings are closed, duplicate vertices dropped, winding normalized (RFC 7946), self-intersections rejected with a clear reason, sliver plots (< 0.05 ha) flagged, implausibly large plots (> 50,000 ha) rejected.

### Output (default dataset)

One **`run_summary`** plus one **`plot_result`** per plot:

```json
{
  "type": "plot_result",
  "recordId": "40-hex-stable-id",
  "plotId": "demo-flagged-frontier",
  "commodity": "coffee",
  "riskFlag": "flagged",
  "crossCheck": "hansen_only",
  "layersRequested": ["hansen_gfc", "jrc_tmf"],
  "layersChecked": ["hansen_gfc", "jrc_tmf"],
  "layerChecks": {
    "hansen_gfc": {
      "checked": true, "status": "ok",
      "versionLabel": "UMD Global Forest Change GFC-2025-v1.13",
      "tileLabels": ["10S_050W"],
      "pixelsExamined": 380,
      "postCutoffPixels": 130,
      "earliestEventYearPostCutoff": 2021,
      "latestEventYearPostCutoff": 2023,
      "eventsByYear": { "2021": 14, "2022": 39, "2023": 77 }
    },
    "jrc_tmf": { "...": "same structure" }
  },
  "hansenVersion": "GFC-2025-v1.13",
  "geometry": { "qcStatus": "ok", "areaHa": 20.9, "warnings": [], "fixesApplied": [] },
  "provenance": { "sources": [ ... ], "attribution": "...", "productDisclaimer": "..." }
}
```

Every record embeds source URLs, licenses, attribution strings, the official-source disclaimer and the product disclaimer, so exports are self-contained audit artifacts. Dataset rows can be exported from Apify as JSON/CSV/XLSX.

### Incremental state (bounded)

Repeat checks are served from a bounded key-value cache keyed by `(normalized geometry, requested layers, cut-off date, Hansen version, JRC product label)`:

- Cache hits carry `cacheHit: true` and `originallyCheckedAt`, keeping the original evidence while making repeat runs fast and cheap.
- The cache is capped (`maxCacheEntries`, default 300) with oldest-first eviction — state growth is bounded by design.
- Any new upstream layer version automatically invalidates affected entries (they are part of the key).

### Known limitations (explicit)

- **No near-real-time alert layers (GLAD/RADD).** Their APIs require registration tokens and their terms could not be independently verified for commercial derivative products at build time. This Actor uses only layers verified as open; sub-annual alert sensitivity is therefore **not** included.
- **Annual granularity.** Both layers are annual products; events are dated to the calendar year, not the day.
- **Hansen datamask not consulted.** The separate observation-coverage mask is not downloaded; pixels with no mapped loss are treated as "no signal on this layer" rather than "confidently observed".
- **JRC domain.** JRC TMF maps humid tropical moist forest only — zero JRC signal outside those domains is expected and does not contradict a Hansen flag.
- **EPSG:4326 only.** No reprojection; submit WGS84 longitude/latitude. Polygons crossing the
  antimeridian (±180°) are not supported — split them at the dateline first.
- **No buffering/point support in v1.** Submit real polygons.
- **Bbox fallback.** Very large plots (> ~400k pixels) fall back to bounding-box analysis, which can only over-approximate flags, never under-approximate; `analysisMode` reports when this happens.
- **Tile budget.** Each 10°×10° raster tile can be 50–150 MB; runs cap downloads via `maxTilesPerRun` and mark un-checked plots `inconclusive` instead of skipping silently.

### Regulatory context (informational only)

Verified against the consolidated official text (CELEX 02023R1115-20251226) on 2026-08-26:

- Main obligations apply from **30 December 2026**;
- natural persons and micro-/small undertakings established by 31 Dec 2024 from **30 June 2027**;
- the dates were already postponed twice ((EU) 2024/3234, then (EU) 2025/2650). **Always confirm current deadlines on eur-lex.europa.eu.** This Actor does not provide legal advice and its outputs are not filings.

### Local development

```bash
npm install
npm run lint && npm run typecheck && npm test   # unit + integration + adversarial + schema
SMOKE=1 npx tsx --test test/smoke.test.ts       # live-source smoke (~100-200 MB downloads)
npm run smoke:local                             # local Actor run with the example input
```

### Sources & attribution (embedded in every output record)

- Hansen, M.C., Potapov, P.V., Moore, R., Hancher, M., Turubanova, S.A., Tyukavina, A., et al. *"High-Resolution Global Maps of 21st-Century Forest Cover Change."* Science 342 (2013): 850–53. Data: UMD Global Forest Change (CC BY 4.0).
- European Commission Joint Research Centre — Tropical Moist Forest data. Provided free of charge, without restriction of use (Copernicus Regulation); acknowledgement required. <https://forobs.jrc.ec.europa.eu/TMF/>
- Regulation (EU) 2023/1115 (EUDR) consolidated text: <https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32023R1115>

# Actor input Schema

## `plots` (type: `array`):

Array of plot objects: { "plotId": string (unique), "commodity": one of cattle, cocoa, coffee, oil\_palm, rubber, soy, wood, other, unspecified, and either "geometry" (GeoJSON Polygon or MultiPolygon object) or "wkt" (POLYGON/MULTIPOLYGON string). Coordinates must be longitude/latitude in EPSG:4326. Max 200 plots per run. Provide the array as JSON text.

## `cutoffDate` (type: `string`):

Forest-change events strictly AFTER this date are flagged. The EUDR reference cut-off is 2020-12-31; change it only if your due-diligence context requires a different comparison date.

## `layers` (type: `array`):

Both layers are official/open. A plot is conservatively flagged when ANY selected layer shows post-cutoff change; results are inconclusive when a requested layer could not be checked.

## `maxTilesPerRun` (type: `integer`):

Each 10°x10° tile is up to ~150 MB. Plots whose tiles exceed this budget are returned as inconclusive instead of being silently skipped.

## `maxCacheEntries` (type: `integer`):

Bounded key-value cache of per-plot results keyed by geometry + layer versions + cutoff. Oldest entries are evicted first; the cache never grows without limit.

## Actor input object example

```json
{
  "plots": [
    {
      "plotId": "demo-flagged-frontier",
      "commodity": "coffee",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -45.2523,
              -13.0336
            ],
            [
              -45.2477,
              -13.0336
            ],
            [
              -45.2477,
              -13.029
            ],
            [
              -45.2523,
              -13.029
            ],
            [
              -45.2523,
              -13.0336
            ]
          ]
        ]
      }
    },
    {
      "plotId": "demo-clear-reference",
      "commodity": "coffee",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -45.6023,
              -12.9523
            ],
            [
              -45.5977,
              -12.9523
            ],
            [
              -45.5977,
              -12.9477
            ],
            [
              -45.6023,
              -12.9477
            ],
            [
              -45.6023,
              -12.9523
            ]
          ]
        ]
      }
    },
    {
      "plotId": "demo-sliver-qc",
      "commodity": "coffee",
      "wkt": "POLYGON((-45.00019 -12.75028, -45.00001 -12.75028, -45.00001 -12.7501, -45.00019 -12.7501, -45.00019 -12.75028))"
    }
  ],
  "cutoffDate": "2020-12-31",
  "layers": [
    "hansen_gfc",
    "jrc_tmf"
  ],
  "maxTilesPerRun": 4,
  "maxCacheEntries": 300
}
```

# Actor output Schema

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

No description

## `state` (type: `string`):

No description

# 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 = {
    "plots": [
        {
            "plotId": "demo-flagged-frontier",
            "commodity": "coffee",
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [
                            -45.2523,
                            -13.0336
                        ],
                        [
                            -45.2477,
                            -13.0336
                        ],
                        [
                            -45.2477,
                            -13.029
                        ],
                        [
                            -45.2523,
                            -13.029
                        ],
                        [
                            -45.2523,
                            -13.0336
                        ]
                    ]
                ]
            }
        },
        {
            "plotId": "demo-clear-reference",
            "commodity": "coffee",
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [
                            -45.6023,
                            -12.9523
                        ],
                        [
                            -45.5977,
                            -12.9523
                        ],
                        [
                            -45.5977,
                            -12.9477
                        ],
                        [
                            -45.6023,
                            -12.9477
                        ],
                        [
                            -45.6023,
                            -12.9523
                        ]
                    ]
                ]
            }
        },
        {
            "plotId": "demo-sliver-qc",
            "commodity": "coffee",
            "wkt": "POLYGON((-45.00019 -12.75028, -45.00001 -12.75028, -45.00001 -12.7501, -45.00019 -12.7501, -45.00019 -12.75028))"
        }
    ],
    "cutoffDate": "2020-12-31",
    "layers": [
        "hansen_gfc",
        "jrc_tmf"
    ],
    "maxTilesPerRun": 4,
    "maxCacheEntries": 300
};

// Run the Actor and wait for it to finish
const run = await client.actor("atlas-data/eudr-deforestation-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 = {
    "plots": [
        {
            "plotId": "demo-flagged-frontier",
            "commodity": "coffee",
            "geometry": {
                "type": "Polygon",
                "coordinates": [[
                        [
                            -45.2523,
                            -13.0336,
                        ],
                        [
                            -45.2477,
                            -13.0336,
                        ],
                        [
                            -45.2477,
                            -13.029,
                        ],
                        [
                            -45.2523,
                            -13.029,
                        ],
                        [
                            -45.2523,
                            -13.0336,
                        ],
                    ]],
            },
        },
        {
            "plotId": "demo-clear-reference",
            "commodity": "coffee",
            "geometry": {
                "type": "Polygon",
                "coordinates": [[
                        [
                            -45.6023,
                            -12.9523,
                        ],
                        [
                            -45.5977,
                            -12.9523,
                        ],
                        [
                            -45.5977,
                            -12.9477,
                        ],
                        [
                            -45.6023,
                            -12.9477,
                        ],
                        [
                            -45.6023,
                            -12.9523,
                        ],
                    ]],
            },
        },
        {
            "plotId": "demo-sliver-qc",
            "commodity": "coffee",
            "wkt": "POLYGON((-45.00019 -12.75028, -45.00001 -12.75028, -45.00001 -12.7501, -45.00019 -12.7501, -45.00019 -12.75028))",
        },
    ],
    "cutoffDate": "2020-12-31",
    "layers": [
        "hansen_gfc",
        "jrc_tmf",
    ],
    "maxTilesPerRun": 4,
    "maxCacheEntries": 300,
}

# Run the Actor and wait for it to finish
run = client.actor("atlas-data/eudr-deforestation-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 '{
  "plots": [
    {
      "plotId": "demo-flagged-frontier",
      "commodity": "coffee",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -45.2523,
              -13.0336
            ],
            [
              -45.2477,
              -13.0336
            ],
            [
              -45.2477,
              -13.029
            ],
            [
              -45.2523,
              -13.029
            ],
            [
              -45.2523,
              -13.0336
            ]
          ]
        ]
      }
    },
    {
      "plotId": "demo-clear-reference",
      "commodity": "coffee",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -45.6023,
              -12.9523
            ],
            [
              -45.5977,
              -12.9523
            ],
            [
              -45.5977,
              -12.9477
            ],
            [
              -45.6023,
              -12.9477
            ],
            [
              -45.6023,
              -12.9523
            ]
          ]
        ]
      }
    },
    {
      "plotId": "demo-sliver-qc",
      "commodity": "coffee",
      "wkt": "POLYGON((-45.00019 -12.75028, -45.00001 -12.75028, -45.00001 -12.7501, -45.00019 -12.7501, -45.00019 -12.75028))"
    }
  ],
  "cutoffDate": "2020-12-31",
  "layers": [
    "hansen_gfc",
    "jrc_tmf"
  ],
  "maxTilesPerRun": 4,
  "maxCacheEntries": 300
}' |
apify call atlas-data/eudr-deforestation-risk-screener --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,atlas-data/eudr-deforestation-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/LtML2pfwSP1qKmo4f/builds/afJ7AQHhQJWtkI5wY/openapi.json
