# Sentinel AOI Quality Report (`l3digital/sentinel-aoi-quality`) Actor

Experimental single-scene Sentinel-2 classification and coverage statistics for a small public area.

- **URL**: https://apify.com/l3digital/sentinel-aoi-quality.md
- **Developed by:** [L3Digital](https://apify.com/l3digital) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.05 / complete observed aoi report

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

## Sentinel-2 AOI Quality Report

Get native Sentinel-2 scene-classification counts for one small rectangular area
from one Earth Search Collection 1 L2A scene. The report distinguishes observed
pixels, missing classification and centres outside the scene footprint, so a caller
can assess the actual sampled coverage rather than using whole-tile cloud metadata.

This is an experimental TRYOUT. One hundred offline tests and four private
owner QA runs passed; three geometries from one public scene matched an independent
reference. Customer demand, workflow savings and population reliability remain
unvalidated. This unofficial Actor is not affiliated with Copernicus, ESA,
Element 84 or AWS.

### Use from an AI agent through MCP

Add this URL to a client that supports remote HTTP MCP, then authorize with your
own Apify account using the [Apify MCP setup guide](https://docs.apify.com/integrations/mcp):

```text
https://mcp.apify.com/?tools=l3digital/sentinel-aoi-quality
```

This configuration selects the Actor directly. Availability still depends on
Apify account and Actor eligibility. Call `l3digital/sentinel-aoi-quality`
with the example input below; the pricing and input restrictions on this page apply.

When using `call-actor`, its response contains run status and storage IDs. If the
run is still active, check that run with `get-actor-run`. After success, retrieve
the report with `get-dataset-items` using the returned dataset ID
(`defaultDatasetId` in the run API). These retrieval tools load with the Actor.
Retrieve the existing result instead of starting another run. Inspect the report
status and the coverage or refusal fields described below before using its values.

### Input

The default `{}` runs a deterministic illustrative demo with no upstream requests.
It still writes one Apify dataset result and incurs no useful-report event.

Scene example (identity and AOI frozen before any raster inspection):

```json
{
  "mode": "scene",
  "sceneId": "S2A_T18TWL_20250602T154709_L2A",
  "bbox": [-74.01, 40.7, -74.0, 40.71]
}
```

`sceneId` and `bbox` are required in scene mode. Bbox is `[west,south,east,north]`
in WGS84 degrees, with finite ordered values, latitude from −80 through 84 and
each axis span at most 0.05 degrees. Crossing the antimeridian is unsupported.
West/south edges include centres; east/north edges exclude them. The platform
form uses a flat JSON array editor; runtime validates the conditional/geometric rules.
Only `mode`, `sceneId` and `bbox` are accepted. The Actor does not search or rank
scenes, query dates, accept polygons/uploads/arbitrary URLs, or resample pixels.

### Output and denominators

Each invocation attempts exactly one dataset result. `status` is `complete`,
`demo`, `no_observation`, `below_resolution`, `unsupported`, `source_failure`,
`incomplete` or `invalid_input`. `computationComplete` means the admitted grid
was fully computed, including any missing coverage. `useful` is true only for a
non-demo complete scene with at least one observed pixel. Failure results have
no metrics and make no complete-quality claim. `illustrative` labels the demo.

`metrics.classCounts` has string keys `"0"` through `"11"`, counting only centres
inside the raster footprint. The other integer fields satisfy:

```text
inFootprintCount = sum(classCounts)
observedCount = sum(classCounts[1..11])
noDataCount = classCounts[0]
requestedCount = inFootprintCount + outsideCount
missingCount = outsideCount + noDataCount
requestedCount = observedCount + missingCount
```

A requested sample is a native grid pixel centre inside the caller's rectangle,
including extrapolated positions beyond the raster extent. Outside samples are
counted separately, never filled with SCL0. Coverage is sampled-grid coverage,
not a continuous geographic area ratio. All five coverage fractions
(`inFootprintFraction`, `observedFraction`, `outsideFraction`, `noDataFraction`,
`missingFraction`) divide their matching counts by `requestedCount`.

Quality fractions use `observedCount`: `cloudFraction` counts classes 8+9+10,
`cloudShadowFraction` class 3, and `snowIceFraction` class 11. Class 2 is cast
shadow, not class 3 cloud-shadow. Class 1 defective and class 7 unclassified
stay visible and remain in the observed denominator. A zero denominator yields
JSON `null`. A bbox without sampled centres is `below_resolution`; an admitted
grid with no observed pixels is `no_observation`. Both are uncharged outcomes.

`provenance` carries collection, scene ID, acquisition datetime, processing
baseline, exact Item/asset URLs, application payload `itemBytes`/`rasterBytes`,
native 20 m resolution and raster EPSG. HTTP framing/headers are excluded from
payload accounting. These are SCL classification statistics, not ground-truth
cloud-free percentages or a clear-quality score; no cloud-free guarantee is made.
If the worker is terminated without a receipt, payload byte counts are `null`
because completed transfers cannot be recovered honestly from that process.

### Resource and failure bounds

One fixed Earth Search Item GET (at most 1 MiB) and one supported Collection 1
S3 SCL GET (at most 32 MiB) are allowed; no redirects, retries or alternate source.
Complete compressed data is downloaded to ephemeral local storage before GDAL
opens it; there are no hidden remote GDAL range reads. Missing-length streams
that reach the exact byte ceiling are conservatively refused because an extra
EOF probe would exceed that ceiling. Known HTTP-length streams can use the ceiling.

Only one uint8 band, north-up unrotated 20 m UTM and dimensions at most
6000×6000 are supported. Actual candidate envelope size, including a two-cell
halo, must be at most 262,144 before allocation or read. Geographic edges use
255 densification points; each candidate centre is transformed back to WGS84
for inclusion. This finite numerical bound has denser independent synthetic
controls; it is not a formal mathematical projection guarantee.

Each request has a 30-second deadline. Acquisition and native computation run
in one subprocess with a 120-second total deadline; timeout kills and waits for
that process, then removes all temporary files. Source failures/refusals produce
explicit uncharged results. Initial hosted settings are 512 MiB and 180 seconds;
The four private runs peaked below 114 million bytes; worst-case hosted memory
and cost remain unmeasured. Compressed bytes do not
bound decoded blocks; source dimension, candidate and GDAL cache bounds also apply.

### Useful-report pricing

The initial event is `report-produced` at **$0.05** per persisted
complete useful scene report. This code does not configure platform pricing.
It checks the effective event price in a PPE run, persists the single dataset
result first, then requests at most one event and checks accepted count equals one.
Demo, refusal, source failure, incomplete, zero observation and below resolution
never emit this event. Unpriced runs persist results without an event. Storage
failure prevents charging; charge failure fails the run after retaining its result.
The persisted payload does not assert billing success. The public offer charges
only this event, with no separate platform-usage charge to the customer.

### Source attribution

Reports contain modified Copernicus Sentinel data. The source year is the year
of `provenance.datetime`; the supplied example contains modified Copernicus
Sentinel data 2025. Preserve the notice “Contains modified Copernicus Sentinel
data \[Year]”, substituting that acquisition year, when redistributing results.
Source classification is supplied without a quality or fitness warranty; see
the [Copernicus Sentinel data notice](https://sentinels.copernicus.eu/documents/247904/690755/Sentinel_Data_Legal_Notice).

### Development and independent comparison

Python 3.13 and the committed uv lock are required. The pinned Rasterio wheel
supports glibc Linux; no Alpine/musl compatibility is claimed. Run the four gates
from the repository root through rexec as recorded in [VALIDATION.md](VALIDATION.md).
Deployment/build/source acquisition and hosting are owned by the parent job.

The offline oracle accepts a local raster, the frozen receipt and one JSON result:

```bash
rexec -- uv run --directory actors/sentinel-aoi-quality --locked python scripts/validate_public_sample.py \
  --receipt ../../research/evidence/2026-09-29-sentinel-public-sample.json \
  --raster /path/to/local-scl.tif --report /path/to/actor-result.json \
  --output /path/to/comparison.json
```

Inputs must exist on the worker; use repository-relative private evidence paths
and explicit `--pull` paths when retrieving artifacts. The oracle makes no requests,
imports no production metric/window/model helpers, enumerates the full native
source blockwise, and independently adds same-grid exterior centres using 4097
edge samples plus eight cells of slack. It compares integer counts exactly,
fractions within absolute 1e-9, nulls exactly, and checks receipt/source identity.
Every compared metric key must exist, including explicit null fractions. The
`--report` input has a `--result` compatibility alias; `--output` writes comparison
JSON only after successful comparison, while stdout also displays that JSON.
Public outcomes are not part of local synthetic acceptance.

Source documentation: [Earth Search](https://earth-search.aws.element84.com/v1),
[Sentinel COG registry](https://registry.opendata.aws/sentinel-2-l2a-cogs/),
[Rasterio warp API](https://rasterio.readthedocs.io/en/stable/api/rasterio.warp.html).

# Actor input Schema

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

Demo is deterministic, illustrative and uncharged, with no upstream requests.

## `sceneId` (type: `string`):

Required in scene mode. Only sentinel-2-c1-l2a and its supported public SCL asset are used.

## `bbox` (type: `array`):

Required in scene mode. Finite ordered WGS84 bounds; latitude -80..84; axis spans <=0.05 degrees. No antimeridian crossing. West/south inclusive, east/north exclusive.

## Actor input object example

```json
{
  "mode": "demo"
}
```

# Actor output Schema

## `reports` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("l3digital/sentinel-aoi-quality").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("l3digital/sentinel-aoi-quality").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 '{}' |
apify call l3digital/sentinel-aoi-quality --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,l3digital/sentinel-aoi-quality"
        }
    }
}
```

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/xuHbKg38Tnen0KP7l/builds/Qv0HKmzhv3q3hkLoa/openapi.json
