# Sanctions Screening List Change Monitor — CSL / OFAC Delta API (`malonestar/consolidated-screening-list-delta`) Actor

Sanctions screening list change monitor. Diffs the US Trade.gov Consolidated Screening List (OFAC SDN/Non-SDN, BIS Entity/Denied/Unverified/MEU, State DDTC/ISN) against the last run and emits added/removed/modified entries with field-level changes. Keyless. Monitoring tool, not compliance advice.

- **URL**: https://apify.com/malonestar/consolidated-screening-list-delta.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Lead generation, Developer tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.40 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

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

## Sanctions Screening List Change Monitor - CSL / OFAC Delta API

Track **what changed** on the U.S. **Consolidated Screening List (CSL)** - the single feed
that merges **OFAC** (SDN and Non-SDN: SSI, CMIC, NS-MBS, PLC, CAPTA), **BIS** (Entity List,
Denied Persons, Unverified, Military End User) and the **State Department** (DDTC ITAR
Debarred, ISN nonproliferation) restricted-party lists. **12 constituent lists, 25,921
entries** as of 2026-08-01.

Most sanctions actors screen *one name against the list*. This one treats **the list itself
as the object** and emits a **change feed**: on every run it diffs the current CSL against
the previous run and returns only the entries that were **added**, **removed** or
**modified** - down to the field that changed (a new alias, a new address, a programme
change, a delisting, a change to a BIS licence policy). Keyless, official source.

> **Compliance note:** This is a **change-monitoring / research** tool over public U.S.
> government lists. It is **not** a certified sanctions-screening service and its output is
> **not** a compliance, export-licensing or denied-party determination. Always confirm
> against the official source and your compliance programme before acting.

### What was wrong before v1.1, and what it means for you

Three defects were found and fixed on 2026-08-01. All three were reproduced live.

1. **The delta had never actually run.** v1.0 stored its baseline in the actor's *default*
   key-value store, which Apify creates fresh for every run. Two consecutive runs against a
   byte-identical list both logged "No prior snapshot found (first run)" and both reported
   **added=25,825**. A scheduled feed was reporting the entire list as newly designated,
   every single run. v1.1 uses a **named** store, so the baseline survives.
2. **A filter could fabricate delistings.** v1.0 saved the *full* list as the baseline but
   diffed it against the *filtered* current list, so every entry that did not match your
   filter landed in the "in the baseline but not in the current list" branch. Measured with
   `country: "russia"`: **25,027 fabricated `removed` rows**, including AEROCARIBBEAN
   AIRLINES, which is still on the SDN list. v1.1 diffs full-against-full and filters the
   resulting **changes**. This was masked only by defect 1 - fixing the store alone would
   have shipped a catastrophe.
3. **The snapshot key was not unique.** The CSL `_id` is the issuing agency's
   `entity_number`, and **96 ids collide** because the same party is carried on two lists
   (VTB Bank is `17013` on both the SSI list and the SDN list). Those 96 records were
   silently overwritten and could never be diffed. The key is now source-qualified.

**Read `delta_comparable` before treating any row as news.** It is `true` only when a real
baseline existed. The first run after this upgrade establishes a fresh baseline and reports
every entry with `change_type: "added"` and `is_baseline_row: true` - those are not new
designations.

### Who it's for

- **Trade-compliance and export-control teams** who need a daily "what's new / what's gone"
  digest instead of re-diffing a 26k-row file by hand.
- **KYB / AML / onboarding platforms** re-screening only the entities affected by today's
  designations and delistings.
- **RegTech / GRC builders** wiring an alerting pipeline (Slack, email, ticket) off new
  designations, or off a change to an Entity List licence policy.
- **AI agents** doing due diligence that need a clean, chainable "recent sanctions changes"
  tool that can tell a real change from a baseline artefact.

### What it does

1. **Normalizes** every CSL entry into a clean snake\_case record (aliases, addresses,
   programmes, IDs and dates as arrays; a flattened `search_names` for easy matching).
2. **Delta engine:** keyed on `source` + `_id`, it diffs the current list against the stored
   snapshot and sets `change_type` = `added` | `removed` | `modified` | `unchanged`. For
   `modified` it reports `changed_fields` and a field-level `changes` array (before/after,
   plus per-item `added` / `removed` for list fields such as aliases).
3. **Filters:** optional `name` (contains, over name + aliases), `source`, `program` and
   `country`. In delta mode these are applied **after** the diff.

### Example input

```json
{
  "mode": "delta",
  "source": "OFAC",
  "program": "RUSSIA",
  "maxResults": 5000,
  "monitor": true
}
```

Run it once to seed the baseline, then schedule it daily - each later run returns only that
day's added / removed / modified entries. Set `mode` to `full` for a complete current
snapshot. Set `monitor: false` for a one-off read that must not touch the stored baseline.

### Output fields (48)

**Change and trust**

| Field | Meaning |
|---|---|
| `change_type` | `added`, `removed`, `modified` or `unchanged` (full-snapshot mode). |
| `changed_fields` | Names of the tracked fields that changed. `modified` rows only. |
| `changes` | Field-level before/after, with per-item `added` / `removed` for list fields. |
| `delta_comparable` | **Read this first.** `true` only when a real baseline existed and this row is a genuine change. |
| `is_baseline_row` | `true` when the row exists only because the run had no baseline. Not a new designation. |
| `delta_baseline_status` | `ok`, `no_baseline_first_run`, or `not_applicable_full_mode`. |
| `baseline_entry_count` | Size of the baseline this delta was computed against. |
| `baseline_retrieved_at` | When the baseline was captured. |

**Per-source outcome contract** (on every row)

| Field | Meaning |
|---|---|
| `csl_fetch_status` | `ok` or `unavailable`. A run where the list did not load emits nothing at all. |
| `csl_endpoint` | The keyless CSV endpoint that answered. |
| `csl_endpoints_failed` | Endpoints tried first and why each failed. `null` when the first worked. |
| `csl_entry_count` | Entries in the full unfiltered list on this run. |
| `csl_retrieved_at` | ISO 8601 timestamp of the download. |
| `source_lists_expected` | How many constituent government lists the CSL should contain (12). |
| `source_lists_present` | How many were actually present. |
| `source_lists_missing` | Any constituent list entirely absent. `null` on a healthy run - a missing list means parties on it would screen clean. |

**The entry**

| Field | Meaning |
|---|---|
| `id` | The issuing agency's entity number. Not unique across the feed - the change key is `source` + `id`. |
| `entity_number` | Agency entity number as published (OFAC rows only). |
| `source` | The exact constituent list, e.g. `Entity List (EL) - Bureau of Industry and Security`. |
| `source_list_group` | Coarse agency group: `OFAC (Treasury)`, `BIS (Commerce)`, `DDTC/ISN (State)`. Nothing may fall through to `Other`; that is asserted live. |
| `type` | `Entity`, `Individual`, `Vessel`, `Aircraft`, or `null` (BIS and State rows do not publish it). |
| `name`, `title` | Primary name and title. |
| `alt_names` | Published aliases. |
| `search_names` | Flattened lower-cased name plus aliases, for matching. |
| `programs` | Sanctions or control programme tags, e.g. `CMIC-EO13959`. |
| `addresses`, `nationalities`, `citizenships`, `places_of_birth`, `dates_of_birth`, `ids` | Published identifiers, as arrays. |
| `standard_order` | BIS Denied Persons standard order text, the operative terms of the denial (1,594 entries). |
| `license_requirement` | BIS Entity List licence requirement: which exports need a licence (3,490 entries). |
| `license_policy` | The licence review policy applied, e.g. `Presumption of denial` (3,490 entries). |
| `call_sign`, `vessel_type`, `vessel_flag`, `vessel_owner`, `gross_tonnage`, `gross_registered_tonnage` | The maritime block for designated vessels (1,524 vessels in the feed). |
| `federal_register_notice`, `start_date`, `end_date` | Designation paperwork and dates. |
| `remarks` | Free-text remarks as published. |
| `source_list_url`, `source_information_url` | Links back to the authoritative list. |
| `source_system` | Constant: `US Trade.gov Consolidated Screening List`. |

The nine export-control and maritime columns above were parsed out of the CSV on every run
since v1.0 and then discarded. `license_requirement` and `license_policy` are the operative
terms of an Entity List designation, and a change to either is now a `modified` event.

### Live drift assertions

Every run asserts the following **before any billable row is produced**, and fails the run
when an assertion breaks:

- the exact 29-column CSV header, catching both missing columns and **unexpected new ones**
  (v1.0 required only 3 of the 29, so a rename anywhere else silently nulled that field)
- total row count inside a measured band (15,000 to 60,000; live 25,921)
- the **closed 12-list source vocabulary**: an unknown constituent list fails the run,
  because it would otherwise be grouped as `Other` and missed by every source filter
- per-list count bands, and any constituent list **entirely absent** fails the run, which is
  the dangerous direction since parties on a missing list would screen clean
- zero records may fall through to `source_list_group: "Other"`
- freshness: the newest `start_date` must be within 400 days
- **delta sanity**: more than 25 percent of the baseline reported removed, or more than 50
  percent reported added, fails the run. A five-figure swing is a key-format or data problem,
  not news
- **positive canaries**: each agency group must be reachable by a source filter and return
  only rows from that group, and an entry must be findable by its own exact name
- **negative controls**: nonsense `name`, `country`, `program` and `source` filters must each
  match **nothing**, which is what catches a filter that has started matching everything

Every measured value is logged on each run (`[drift]` and `[canary]`) so bands can be
tightened on evidence.

### Use as an MCP tool

Callable by AI agents (Claude, Cursor, etc.) via **mcp.apify.com**. An agent can pull recent
sanctions-list changes, filter by programme or country, and check `delta_comparable` and
`source_lists_missing` to know whether the answer is trustworthy before acting on it.

### Data source

US International Trade Administration, **Consolidated Screening List**, keyless CSV:
`https://data.trade.gov/downloadable_consolidated_screening_list/v1/consolidated.csv`
with `api.trade.gov/static/consolidated_screening_list/consolidated.csv` as a fallback. That
host's wildcard certificate expired on 2026-07-28 and Node surfaces the failure only as the
opaque "fetch failed", so the healthy-certificate host is tried first. No API key required.

### Pricing

Pay-per-result: you are billed per change (or snapshot) record returned. A run with no
changes costs only the fractional actor start, which is a cheap way to monitor daily. A run
that fails its drift assertions emits nothing and bills nothing beyond that.

### FAQ

**Is this an OFAC sanctions screening API?** It monitors the official OFAC, BIS and State
Consolidated Screening List for *changes*. It is a monitoring and research tool, not a
certified screening or compliance determination. Verify against the official source.

**How do I get only new designations?** Use `mode: "delta"` (the default) on a schedule. The
first run seeds the baseline; every run after returns only real changes. Filter to
`delta_comparable = true`.

**Can I watch one company or person?** Yes, set `name` to a substring; it matches the primary
name and every alias. In delta mode the filter is applied to the changes, so it can never
fabricate a delisting.

**Which lists are covered?** All 12: OFAC SDN, SSI, CMIC, NS-MBS, PLC and CAPTA; BIS Entity
List, Denied Persons, Unverified and Military End User; State DDTC (ITAR Debarred) and ISN.

**Do I need an API key?** No. The source is a keyless public government feed.

### Related actors

- **Medicaid Exclusion Screener** - the healthcare-exclusion side of a vendor or clinician check.
- **KYB Company Verifier** - cross-registry business verification for the entity you just matched.

# Actor input Schema

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

'delta' returns only entries ADDED, REMOVED or MODIFIED since this actor's previous run (the change feed, computed against a named cross-run key-value store). 'full' returns the complete current list as a snapshot. On the FIRST delta run there is no baseline, so every current entry is reported with change\_type 'added' and is\_baseline\_row=true - read delta\_comparable before treating any row as a new designation.

## `name` (type: `string`):

Case-insensitive substring match against the primary name AND all aliases (alt\_names). Use to watch a specific person, company or vessel. Empty = all entries. In delta mode the filter is applied to the CHANGES, after the full-list diff, so filtering can never fabricate 'removed' rows.

## `source` (type: `string`):

Keep only entries whose source list or agency group matches this text. Matches the full list name or the coarse group, e.g. 'OFAC', 'Treasury', 'Entity List', 'BIS', 'Denied Persons', 'State', 'ITAR'. Empty = all lists.

## `program` (type: `string`):

Keep only entries tagged with a sanctions/control program containing this text, e.g. 'IRAN', 'RUSSIA', 'CMIC', 'SDGT', 'CAATSA'. Empty = all programs.

## `country` (type: `string`):

Keep only entries whose nationality, citizenship, place of birth, or address mentions this text, e.g. 'China', 'CN', 'Russia', 'Iran'. Empty = all countries.

## `maxResults` (type: `integer`):

Maximum number of change (or snapshot) records to emit after filtering. One record = one billable result.

## `monitor` (type: `boolean`):

When true, save the current full list to the actor's key-value store so the NEXT run can compute a delta against it. Keep true for scheduled change-monitoring. Set false for a one-off read that must not overwrite the stored baseline.

## Actor input object example

```json
{
  "mode": "full",
  "maxResults": 100,
  "monitor": true
}
```

# Actor output Schema

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

Added/removed/modified Consolidated Screening List entries (or a full snapshot), in the default dataset.

# 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 = {
    "mode": "full",
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/consolidated-screening-list-delta").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 = {
    "mode": "full",
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/consolidated-screening-list-delta").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "full",
  "maxResults": 100
}' |
apify call malonestar/consolidated-screening-list-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/consolidated-screening-list-delta",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/VfffglYdlDrwoellY/builds/ulk4PWVBWeGCyqWYK/openapi.json
