# Supply Chain Risk Monitor — Earthquakes & Wildfires (`mouadapi/supply-chain-risk-monitor`) Actor

Returns a 0–100 earthquake and wildfire risk score per supplier site or port from USGS and NASA FIRMS data, with reasons and alerts when a level rises; failed and unchanged rows are never charged. Our own score from the listed facts; not advice. Not affiliated with or endorsed by the USGS or NASA.

- **URL**: https://apify.com/mouadapi/supply-chain-risk-monitor.md
- **Developed by:** [COMPASS DEV](https://apify.com/mouadapi) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 site checkeds

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

Returns a 0–100 natural-hazard risk score for each supplier site, plant, warehouse or port you give, from recent earthquakes nearby (USGS) and satellite fire detections nearby (NASA FIRMS), with the reasons in plain words; failed and unchanged rows are never charged.

Give it your sites by their coordinates; get one row per site with:

- a **risk score** (0 to 100) and a **level** (low, moderate, high, severe), split into its earthquake part and its fire part;
- the **reasons** in plain words ("M5.2 earthquake 166.6 km away on 2026-09-28 …; 3 fire detections …, the nearest 6.3 km away");
- the earthquake facts: how many, the largest magnitude, the nearest, and the one that weighed most (its USGS ID, time,
  magnitude, distance, place, PAGER alert and link);
- the fire facts: how many detections, how many of high confidence, how many within 25 km, the nearest, their total fire
  radiative power and the latest scan.

Built for supply-chain, procurement and logistics teams, insurers and AI agents that screen many sites for natural hazards
in one call. *Not affiliated with or endorsed by the USGS or NASA.*

**Our own score from the listed facts; not advice.** It is a screening signal from public data, not an engineering,
insurance or safety assessment; it covers earthquakes and fires only (no trade, political, port or weather risk).

### What it does

- **One row per site,** from two official sources read in the same run: the USGS earthquake catalog and NASA FIRMS fire
  detections (VIIRS on NOAA-20 and NOAA-21, optionally MODIS).
- **A published score** (below), computed only from the sources' own facts: magnitude, distance, the USGS PAGER alert, and
  the distance and number of fire detections.
- **Supplier watch with alerts:** give a watch list name (`watchListName`) and run it daily:
  - the first run returns every site (`baseline`);
  - later runs return only the sites whose score or level changed, with the previous values, and `levelRose: true` when the
    level went up (your alert);
  - a run with nothing new returns exactly one free `no_data` row that says so.
- **Your own NASA FIRMS key** for fires (`firmsApiKey`, free, sent to you by email from
  [FIRMS](https://firms.modaps.eosdis.nasa.gov/api/map_key/)); used only in requests to FIRMS and never logged, stored or
  returned. Without a key the score covers earthquakes only (0 to 60), and each row says so (`scoreCovers`).
- **You are never charged for failed results:** failed rows are free, a row whose fire part failed is free, and unchanged
  rows are free in watch mode. One charge per checked site.

### How the score works

The score is our own formula over the sources' facts, the same for every site:

**Earthquake part, 0 to 60.** Each earthquake within the radius and the window gets magnitude points times a distance
factor; the part is the highest of these.

| Magnitude | Points | | Distance | Factor |
|---|---|---|---|---|
| under 4 | 0 | | up to 25 km | 1.0 |
| 4.0 to 4.9 | 10 | | up to 50 km | 0.8 |
| 5.0 to 5.9 | 25 | | up to 100 km | 0.6 |
| 6.0 to 6.9 | 45 | | up to 200 km | 0.4 |
| 7.0 and up | 60 | | farther | 0.25 |

Then: at least 30 when an earthquake in the radius has a USGS PAGER alert of yellow, 45 for orange, 60 for red; plus 5 for
a swarm (3 or more earthquakes of M4 or more); at most 60.

**Fire part, 0 to 40** (with a FIRMS key). Points for the nearest fire detection (up to 5 km: 30; 10 km: 22; 25 km: 15;
50 km: 8; farther in the radius: 4), plus points for the detections within 25 km (1 or more: 2; 10 or more: 6; 50 or more:
10\); at most 40.

**Level:** 0–19 low, 20–39 moderate, 40–59 high, 60–100 severe.

### Quick start

```json
{ "locations": ["Tokyo office: 35.68,139.69", "Santiago plant: -33.45,-70.66", "Los Angeles warehouse: 34.05,-118.24"], "firmsApiKey": "YOUR_FIRMS_MAP_KEY" }
```

A daily supplier watch with a 50 km radius:

```json
{
  "locations": ["Shenzhen plant: 22.54,114.06", "Port of Rotterdam: 51.95,4.14", { "name": "Hilo depot", "latitude": 19.72, "longitude": -155.08, "radiusKm": 150 }],
  "radiusKm": 50,
  "firmsApiKey": "YOUR_FIRMS_MAP_KEY",
  "watchListName": "suppliers"
}
```

### Input

| Field | What it does |
|---|---|
| `locations` | Main input, up to 200 sites: `"latitude,longitude"` with an optional name first and an optional radius after (`"Shenzhen plant: 22.54,114.06"`, `"Port of Rotterdam: 51.95,4.14,50"`), or objects `{ "name", "latitude", "longitude", "radiusKm" }`. Coordinates only: no address lookup. |
| `radiusKm` | How far around each site to look, unless the site gives its own; default `100` (1 to 500 km). |
| `earthquakeDays` | Earthquakes of the last N UTC days, today included; default `30` (1 to 365). |
| `minMagnitude` | Smallest earthquake magnitude counted; default `4` (2.5 to 10). |
| `fireDays` | Fire detections of the last N UTC days, today included; default `3` (1 to 10). |
| `minFireConfidence` | Smallest FIRMS confidence class counted: `low`, `nominal` or `high`; default `nominal`. |
| `fireSources` | FIRMS products; default `VIIRS_NOAA20_NRT,VIIRS_NOAA21_NRT` (add `MODIS_NRT` for MODIS on Terra and Aqua). |
| `firmsApiKey` | Your own free NASA FIRMS MAP_KEY. Without it the score covers earthquakes only. Never logged, stored or returned. |
| `mode` | Empty: watch when a watch list name is given, otherwise export. An explicit `watch` or `export` wins. |
| `watchListName` | Your site watch list (kept in your own storage between runs). A name turns on watch mode; watch mode without a name uses the list "default". Changing the windows or filters starts the list again. |
| `includeUnchanged` | Watch mode: also return unchanged sites (free); default `false`. |

Field names from other tools: `sites` (→ `locations`); `stateName` (→ `watchListName`); `apiKey`, `mapKey` (→ `firmsApiKey`).

### Output

One row per site. A site with both parts (lists shortened):

```json
{
  "status": "ok",
  "input": "Port Hardy depot: 50.72,-127.49,300",
  "locationName": "Port Hardy depot",
  "latitude": 50.72,
  "longitude": -127.49,
  "radiusKm": 300,
  "riskScore": 34,
  "riskLevel": "moderate",
  "earthquakePoints": 10,
  "firePoints": 24,
  "scoreCovers": "earthquakes and fires",
  "reasons": "M5.2 earthquake 166.6 km away on 2026-09-28; 2 earthquakes of M4 or more within 300 km in 30 days (USGS); 3 fire detections within 300 km in 3 days, the nearest 6.3 km away (NASA FIRMS)",
  "earthquakeCount": 2,
  "maxMagnitude": 5.4,
  "strongestEarthquakeId": "us6000ty43",
  "strongestEarthquakeUrl": "https://earthquake.usgs.gov/earthquakes/eventpage/us6000ty43",
  "fireCount": 3,
  "firesWithin25Km": 2,
  "nearestFireKm": 6.3,
  "fireStatus": "ok",
  "url": "https://www.openstreetmap.org/?mlat=50.72&mlon=-127.49#map=10/50.72/-127.49"
}
```

A watch run, a site whose level rose:

```json
{ "status": "ok", "input": "Tokyo office: 35.68,139.69", "riskScore": 36, "riskLevel": "moderate", "changeType": "changed", "changedFields": "riskLevel, riskScore", "previousValues": "{\"riskLevel\":\"low\",\"riskScore\":0}", "levelRose": true }
```

### Output fields

| Field | Type | Description |
|---|---|---|
| `status` | string (or null) | ok = the site was checked (charged in export mode, and in watch mode when it is a baseline, new or changed site; free when its fire part failed, or when unchanged and includeUnchanged is on); no_data = a site given again (the same coordinates and radius: checked and charged once), or nothing new since the last run (free); failed = an invalid location, or the USGS could not be read for this site (free) |
| `error` | string (or null) | Why a row is no_data or failed; null on ok rows |
| `attempts` | integer (or null) | Requests made for the USGS answer (retries included) |
| `input` | string (or null) | The location entry that produced this row, as given ("Name: latitude,longitude\[,radiusKm]" or an object), or the watch list for the nothing-new row |
| `locationName` | string (or null) | The name given before the coordinates, or null |
| `latitude` | number (or null) | The site's latitude (degrees, WGS 84) |
| `longitude` | number (or null) | The site's longitude (degrees, WGS 84) |
| `radiusKm` | number (or null) | How far around the site the check looked (km) |
| `mode` | string (or null) | watch or export |
| `watchList` | string (or null) | Watch list name (watch mode); null in export mode |
| `riskScore` | integer (or null) | The site hazard score, 0 to 100: the earthquake part (0 to 60) plus the fire part (0 to 40); the formula is in the README |
| `riskLevel` | string (or null) | low (0-19), moderate (20-39), high (40-59) or severe (60-100) |
| `earthquakePoints` | integer (or null) | The earthquake part of the score, 0 to 60 |
| `firePoints` | integer (or null) | The fire part of the score, 0 to 40; null when fires were not checked |
| `scoreCovers` | string (or null) | "earthquakes and fires", or "earthquakes only" (no NASA FIRMS key, or FIRMS failed) |
| `reasons` | string (or null) | The score in plain words: the earthquake that weighed most, and the fire detections |
| `earthquakeCount` | integer (or null) | Earthquakes of the minimum magnitude or more within the radius in the earthquake window |
| `maxMagnitude` | number (or null) | The largest of those magnitudes |
| `nearestEarthquakeKm` | number (or null) | The nearest of those earthquakes (km, 0.1) |
| `strongestEarthquakeId` | string (or null) | The USGS event ID of the earthquake that weighed most in the score |
| `strongestEarthquakeTime` | string (or null) | Its origin time (ISO 8601, UTC) |
| `strongestEarthquakeMagnitude` | number (or null) | Its magnitude |
| `strongestEarthquakeKm` | number (or null) | Its distance from the site (km, 0.1) |
| `strongestEarthquakePlace` | string (or null) | The USGS place text; null when the people filter left it out (it looked like a person's name) |
| `strongestEarthquakeAlert` | string (or null) | Its USGS PAGER alert level (green, yellow, orange, red), or null |
| `strongestEarthquakeUrl` | string (or null) | The earthquake's USGS page |
| `fireCount` | integer (or null) | NASA FIRMS fire detections (at the minimum confidence or more) within the radius in the fire window; null when fires were not checked |
| `highConfidenceFireCount` | integer (or null) | Of those, the high-confidence ones |
| `firesWithin25Km` | integer (or null) | Of those, the ones within 25 km of the site |
| `nearestFireKm` | number (or null) | The nearest fire detection (km, 0.1) |
| `totalFrpMw` | number (or null) | The fire radiative power of those detections, summed (MW) |
| `latestFireTime` | string (or null) | The newest of those detections (the satellite scan, ISO 8601, UTC) |
| `earthquakeStatus` | string (or null) | "ok", or "failed: ..." with the reason |
| `fireStatus` | string (or null) | "ok", "not checked: no NASA FIRMS key (...)", or "failed: ...; this row is free" |
| `earthquakeWindow` | string (or null) | The earthquake window (UTC days, today included) |
| `fireWindow` | string (or null) | The fire window (UTC days, today included); null without a FIRMS key |
| `changeType` | string (or null) | Watch mode: baseline (first run of the list), new (a site the list did not have), changed (its score or level changed) or unchanged (only with includeUnchanged); null in export mode, and when the fire part failed |
| `changedFields` | string (or null) | Watch mode, changed rows: riskLevel and/or riskScore |
| `previousValues` | string (or null) | Watch mode, changed rows: the changed fields' previous values, as JSON |
| `levelRose` | boolean (or null) | Watch mode: true when the risk level is higher than at the last run (an alert) |
| `url` | string (or null) | The site on OpenStreetMap (a link only; nothing is read from it) |
| `source` | string (or null) | Both sources and the citations they ask for, with NASA's advice on local-scale use of fire data |
| `license` | string (or null) | Both sources' licences or terms |
| `scrapedAt` | string (or null) | When the row was made (ISO 8601) |

### Pricing

Pay per event, one event per checked site (`site-check`), whatever the number of earthquakes and fires found:

| Plan | Price per site | Per 1,000 sites |
|---|---|---|
| Free (no discount) | $0.0015 | $1.50 |
| Bronze | $0.0013 | $1.30 |
| Silver | $0.00115 | $1.15 |
| Gold (and Platinum, Diamond) | $0.0010 | $1.00 |

- Charged: ok rows in export mode; baseline, new and changed rows in watch mode. A score of 0 is an answer and is charged.
- Never charged: failed rows (an invalid location, the USGS not readable), rows whose fire part failed, no_data rows (a
  site given twice, the nothing-new row) and unchanged rows. You are never charged for failed results.
- Pay per event only, with no usage fees, so the actor is eligible for x402 agent payments.

### Use it from AI agents

One clear main input, `locations`; every row has `status`, `error`, `riskScore`, `riskLevel`, `reasons` and `scrapedAt`.
Call it through the Apify API, the Apify MCP server (`mouadapi/supply-chain-risk-monitor`) or x402 agentic payments.
Copy-paste call (your Apify token in place of `YOUR_APIFY_TOKEN`):

```bash
curl -X POST "https://api.apify.com/v2/acts/mouadapi~supply-chain-risk-monitor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" -H "Content-Type: application/json" -d '{"locations": ["Shenzhen plant: 22.54,114.06", "Port of Rotterdam: 51.95,4.14"], "radiusKm": 100, "earthquakeDays": 30}'
```

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('mouadapi/supply-chain-risk-monitor').call({ locations: ['22.54,114.06', '51.95,4.14,50'], firmsApiKey: 'YOUR_FIRMS_MAP_KEY', watchListName: 'suppliers' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

### Limits

- **Earthquakes and fires only.** No trade-flow, political, port-congestion, tariff, flood, storm or weather data: no
  source for them passed our rules for public, reusable data.
- **Fire detections are satellite pixels.** One fire gives many detections, and a gas flare, a volcano or a hot factory roof
  can be detected too. NASA advises that, due to their spatial resolution, these data are not suited to tactical
  decision-making or to informing about conditions at a local scale: confirm with local authorities.
- **FIRMS near-real-time products only,** up to 10 days; each satellite passes about twice a day, and clouds hide fires.
- **The USGS catalog** is complete worldwide for larger earthquakes (about M4.5 and up) and in more detail in the United
  States; smaller earthquakes elsewhere may be missing.
- **Coordinates only:** no address, port-code or place-name lookup. At most 200 sites per run, 500 km radius.
- **Suomi NPP** (VIIRS_SNPP) is not offered: NASA ends its data products on 1 November 2026.

### Known issues

- The FIRMS key: a key FIRMS refuses, or one whose limit is used up (5,000 requests per 10 minutes), stops FIRMS for the
  rest of the run: those rows keep their earthquake part, say why in `fireStatus`, and are free.
- A source that cannot be read (a rate limit that three pauses don't clear, an outage) stops for the rest of the run;
  another IP or proxy is never tried. Without the USGS a site has no score: its row is `failed` (free).
- The place text of an earthquake is left out when it has the shape of a person's name; the coordinates stay.
- The daily self-test of this actor checks the earthquake path; the fire path is checked with a key in every release test.

### Sources and licences

- USGS earthquake catalog (FDSN event web service): U.S. Public Domain
  ([USGS copyrights and credits](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits)).
- NASA FIRMS: "NASA promotes full and open sharing of data"; please cite it as "NASA FIRMS"
  ([FIRMS FAQ](https://www.earthdata.nasa.gov/data/tools/firms/faq)). We acknowledge the use of data and/or imagery from
  NASA's Land, Atmosphere Near real-time Capability for Earth observations (LANCE) (https://earthdata.nasa.gov/lance), part
  of NASA's Earth Science Data and Information System (ESDIS). The data are provided "as is"
  ([LANCE disclaimer](https://www.earthdata.nasa.gov/data/projects/lance#ed-lance-disclaimer)).
- The map link is an OpenStreetMap link to the site; nothing is read from it.

Each row's `source` and `license` name both sources.

# Actor input Schema

## `locations` (type: `array`):

Your sites, up to 200: "latitude,longitude" with an optional name first and an optional radius in km after ("Shenzhen plant: 22.54,114.06", "Port of Rotterdam: 51.95,4.14,50"), or objects { "name", "latitude", "longitude", "radiusKm" }. Coordinates only: no address or place-name lookup.

## `radiusKm` (type: `number`):

How far around each site to look, unless the site gives its own radius (1 to 500 km).

## `earthquakeDays` (type: `integer`):

Earthquakes of the last N UTC days, today included (1 to 365).

## `minMagnitude` (type: `number`):

Smallest earthquake magnitude counted (2.5 to 10). Earthquakes under M4 add no points to the score; they still count in earthquakeCount when you set a lower minimum.

## `fireDays` (type: `integer`):

NASA FIRMS fire detections of the last N UTC days, today included (1 to 10).

## `minFireConfidence` (type: `string`):

Smallest NASA FIRMS confidence class counted: low (every detection, more false alarms), nominal or high.

## `fireSources` (type: `array`):

The NASA FIRMS near-real-time products to read: VIIRS on NOAA-20 and NOAA-21 (375 m pixels) and MODIS on Terra and Aqua (1 km pixels).

## `firmsApiKey` (type: `string`):

Your own free NASA FIRMS MAP_KEY, sent to you by email when you ask for it at https://firms.modaps.eosdis.nasa.gov/api/map_key/. Without it the score covers earthquakes only (0 to 60) and each row says so. Used only in requests to NASA FIRMS; never logged, stored or returned.

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

Empty: watch when a watch list name is given, otherwise export. "watch" returns only sites whose score or level changed since the last run of the watch list; "export" returns every site. An explicit mode wins.

## `watchListName` (type: `string`):

Name of your site watch list (kept in your own storage between runs). Giving a name turns on watch mode. Watch mode without a name uses the list "default". Changing the windows or filters starts the list again (every site a baseline row).

## `includeUnchanged` (type: `boolean`):

Watch mode: also return sites whose score did not change since the last run (free).

## Actor input object example

```json
{
  "locations": [
    "Shenzhen plant: 22.54,114.06",
    "Port of Rotterdam: 51.95,4.14,50"
  ],
  "radiusKm": 100,
  "earthquakeDays": 30,
  "minMagnitude": 4,
  "fireDays": 3,
  "minFireConfidence": "nominal",
  "fireSources": [
    "VIIRS_NOAA20_NRT",
    "VIIRS_NOAA21_NRT"
  ],
  "includeUnchanged": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset with one row per site, or one free row when a watch run finds nothing new

## `runReport` (type: `string`):

Summary of the run (counts, charged and free rows, stop reason)

# 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 = {
    "locations": [
        "Tokyo office: 35.68,139.69",
        "Santiago plant: -33.45,-70.66",
        "Los Angeles warehouse: 34.05,-118.24"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mouadapi/supply-chain-risk-monitor").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 = { "locations": [
        "Tokyo office: 35.68,139.69",
        "Santiago plant: -33.45,-70.66",
        "Los Angeles warehouse: 34.05,-118.24",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mouadapi/supply-chain-risk-monitor").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 '{
  "locations": [
    "Tokyo office: 35.68,139.69",
    "Santiago plant: -33.45,-70.66",
    "Los Angeles warehouse: 34.05,-118.24"
  ]
}' |
apify call mouadapi/supply-chain-risk-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mouadapi/supply-chain-risk-monitor"
        }
    }
}
```

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/7LLyMOi7qIK0y4KgL/builds/D4SnE1Ltj3E45NtMy/openapi.json
