# Sanctions Screening API (`zinin/sanctions-screening`) Actor

Screen up to 100 names against fresh official OFAC SDN primary and alias files plus the EU consolidated sanctions list. Get ranked potential-match evidence, exact source coverage, confidence, gaps, failure truth, billing metadata, and a manual review action. Not sanctions clearance or legal advice.

- **URL**: https://apify.com/zinin/sanctions-screening.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Developer tools, Automation, Agents
- **Features:** Limited permissions
- **Stats:** 11 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $4.25 / 1,000 delivered sanctions screening rows

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

## Sanctions Screening — OFAC & EU List Checker

Check a name against the two sanctions lists that actually matter before you onboard a client, counterparty or vendor — the **US Treasury OFAC SDN list** (including its published aliases) and the **EU consolidated financial sanctions list** — read live from their own public exports. No API key, no login, no third-party screening vendor.

![Official-List Name Screening Evidence: buyer input to evidence-backed action](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/5e30e56a0a41067879934b997348a37550d32697/commercial115/sanctions-screening/readme-hero.webp)

### Who uses it

Anyone onboarding clients, running KYC/KYB, or vetting a new counterparty needs a sanctions check. Commercial screening tools (Refinitiv World-Check, Dow Jones, ComplyAdvantage) run hundreds to thousands of dollars a month and are built for compliance teams, not a one-off check or a lightweight internal tool. The underlying data — OFAC's SDN list and the EU's consolidated list — is public and free; this Actor reads it directly.

**This is a preliminary screening tool, not a compliance determination.** Every row carries that disclaimer. Sanctions screening in any serious compliance workflow always ends with human review of the hits — this Actor is built to make that review list, not replace it.

### What you get

- **Two official lists, screened together**: OFAC SDN (US Treasury) and the EU consolidated financial sanctions list, both fetched fresh at the start of every run — never a stale cache.
- **OFAC's published aliases are screened too**, not just primary names. OFAC's own guidance is explicit that checking primary names alone is an incomplete screen — a sanctioned party is routinely listed, and searched for, under an a.k.a./f.k.a./n.k.a. spelling. This Actor loads OFAC's own alias export alongside the primary list.
- **Honest partial-coverage flag.** If one of the two source lists fails to load for a run, every row carries `listsComplete: false` so you know exactly what wasn't checked — rather than a clean-looking row that quietly skipped half the screen.
- **Ranked candidates, not one silent guess.** `matches` returns every candidate above your score threshold (up to 25, ranked), because on a common name picking one match and hiding the rest is worse than showing you the list.
- **Tunable sensitivity.** `minScore` defaults soft, on purpose — a missed hit is worse than a false one — and it's yours to tighten.
- Runs on Apify: schedule it, monitor it, call it from the API or the MCP server, export to JSON/CSV/Excel, or push straight into your own pipeline.

### Sources and rights

- **OFAC SDN list + aliases** (US Treasury) — `sanctionslistservice.ofac.treas.gov`, the same SDN.CSV and ALT.CSV flat-file exports OFAC itself publishes for download.
- **EU consolidated list** (financial sanctions) — `webgate.ec.europa.eu`, the EU's own public CSV export, names and aliases both.

Both are fetched fresh on every run — no scraping, no unofficial mirrors.

### Matching

Names are normalized (uppercase, diacritics stripped, punctuation removed) and compared by token overlap: a match score is the fraction of the *query's* words found in a candidate entity's name (matched against its primary name or any of its aliases, whichever hits). A single distinctive word matched inside a longer name still counts as a strong hit — e.g. checking "Wagner" against an entry like *"PRIVATE MILITARY COMPANY 'WAGNER'"* scores 1.0. This is deliberate: **a missed hit is worse than a false one**, so the default threshold (`minScore: 0.5`) is soft, and it's tunable per your own risk tolerance.

Common/ambiguous names will produce more hits now that aliases are in scope, not fewer — that's expected of any sanctions screen and is why `matches` returns everything above the threshold (up to 25, ranked by score) for you to review, rather than silently picking one.

![Official-List Name Screening Evidence: evidence-to-action workflow](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/5e30e56a0a41067879934b997348a37550d32697/commercial115/sanctions-screening/readme-workflow.webp)

### How to run

1. Open **Try for free** and review the live pricing panel for your Apify tier.
2. Paste names into **Names**, one per row — people or companies.
3. Press **Start** and read the results from the dataset — UI, API or webhook.

### Pricing

Pay-per-event has two named units: the automatic `apify-actor-start` event and `result-found` for one delivered screening-result row. At the current FREE tier each is `$0.005`; BRONZE through DIAMOND apply the live volume discounts shown in the Apify pricing panel. Apify determines automatic start units from the run configuration, including allocated memory. The reviewed 512 MB default is expected to record one start unit; the real 2,048 MB legacy run below recorded two. Treat the pricing panel and each run ledger as authoritative.

With complete official-list coverage, a two-name FREE-tier run that delivers two useful rows is therefore `$0.005 + 2 × $0.005 = $0.015`. A normalized duplicate is not screened or result-billed again. An incomplete no-match, source-failure advisory, pricing diagnostic, or budget advisory does not intentionally emit `result-found`. Always treat the live pricing panel and the run's `chargedEventCounts` as the settlement authority; examples here explain the current contract rather than guaranteeing future prices.

If one of the two source lists fails to load for a run, every row that run is flagged `listsComplete: false`. In that state a **match is still charged** as usual, but a **"no match" verdict is not charged** — because that's the case where we ourselves are telling you not to treat the answer as clean. You only pay for an answer this Actor stands behind. If both lists fail to load entirely, every name in the run returns `found: false` with the reason, and none of it is charged.

### Input contract

| Field | Type | Description |
|-------|------|-------------|
| `names` | array of strings | Person or company names to screen against the OFAC SDN (+aliases) and EU consolidated sanctions lists, one per row. Up to 100. |
| `minScore` | number | Minimum score (0–1) for a candidate to be reported. Default `0.5`. Lower = more (weaker) hits reported. |
| `maxConcurrency` | integer | Parallelism (1–20, default 5). |

```json
{
  "names": ["Yevgeniy Prigozhin", "Acme Trading LLC"]
}
```

### Output contract

One dataset row per name. This is a real row from a real run, `matches` trimmed to the top few of 25:

```json
{
  "input": "Yevgeniy Prigozhin",
  "found": true,
  "match": true,
  "matchCount": 25,
  "matches": [
    { "listName": "OFAC SDN", "entityName": "PRIGOZHIN, Yevgeniy Viktorovich", "entityType": "individual", "program": "UKRAINE-EO13661, CYBER2, ELECTION-EO13848, RUSSIA-EO14024", "matchedName": "PRIGOZHIN, Yevgeniy Viktorovich", "matchedNameType": "primary", "score": 1 },
    { "listName": "EU Consolidated", "entityName": "Yevgeniy Viktorovich PRIGOZHIN", "entityType": "individual", "program": "UKR", "matchedName": "Yevgeniy Viktorovich PRIGOZHIN", "matchedNameType": "primary", "score": 1 },
    { "listName": "OFAC SDN", "entityName": "BALYTSKYI, Yevhen Vitaliiovych", "entityType": "individual", "program": "RUSSIA-EO14024", "matchedName": "BALITSKIY, Yevgeniy", "matchedNameType": "aka", "score": 0.5 },
    "…"
  ],
  "highestScore": 1,
  "matchedName": "PRIGOZHIN, Yevgeniy Viktorovich",
  "matchedNameType": "primary",
  "listsChecked": ["OFAC SDN", "EU Consolidated"],
  "listsComplete": true,
  "disclaimer": "Screening signal only — NOT a legal/compliance determination; verify against official sources.",
  "summary": "Yevgeniy Prigozhin — 25 potential matches (highest score 1). Top hit: PRIGOZHIN, Yevgeniy Viktorovich [OFAC SDN], program UKRAINE-EO13661, CYBER2, ELECTION-EO13848, RUSSIA-EO14024.",
  "checkedAt": "2026-07-26T15:09:32.370Z"
}
```

| Field | Description |
|-------|-------------|
| `input` | The name you passed |
| `found` | Whether screening completed (`false` only if both lists failed to load) |
| `match` | Whether any candidate scored at or above `minScore` |
| `matchCount` | How many candidates cleared the threshold (capped at 25 in `matches`) |
| `matches` | Ranked list of candidates: `listName`, `entityName`, `entityType`, `program`, `score`, plus `matchedName`/`matchedNameType` when the hit came through an alias rather than the entity's primary name |
| `highestScore` | Score of the top match (0 if none) |
| `matchedName` / `matchedNameType` | The top hit's own name and whether it was the entity's primary name or an alias (`aka`/`fka`/`nka`) |
| `listsChecked` | Which source lists actually loaded and were screened this run |
| `listsComplete` | `false` when one of the two lists failed to load — a "no match" row in that state is not a clean bill of health, and is not billed either |
| `disclaimer` | Always present — this is a signal, not a determination |
| `summary` | Human-readable one-liner, notes an alias match by name when that's what fired |

#### Related tools

Related tools for adjacent workflows in compliance and public procurement.

| Actor | What it does |
|---|---|
| [Sanctions Update Alert — Watchlist Change Monitor](https://apify.com/zinin/sanctions-update-alert) | Pair it in the compliance and public procurement workflow: Watch a list of names against OFAC SDN and EU consolidated sanctions lists and get ONLY the changes since... |
| [Counterparty Risk Rollup — Sanctions, Courts, Registry, Hiring](https://apify.com/zinin/counterparty-risk-rollup) | Pair it in the compliance and public procurement workflow: One call, one row per counterparty: sanctions screening (OFAC + EU), legal-entity registry (GLEIF),... |
| [Litigation Check — Company Litigation History Screener](https://apify.com/zinin/litigation-check) | Pair it in the compliance and public procurement workflow: Screen a counterparty for litigation history across US federal PACER dockets, UK case law and Poland's... |
| [Company Registry Enricher](https://apify.com/zinin/company-registry-enricher) | Pair it in the compliance and public procurement workflow: Turn a company name, LEI or UK company number into an official registry card: legal name, status,... |
| [Gov Tender Radar](https://apify.com/zinin/gov-tender-radar) | Pair it in the compliance and public procurement workflow: Search EU public-sector tenders (TED Europa) by keyword or CPV code, optionally filtered by buyer country |

### FAQ

**Does it need an API key or login?** No — both source lists are public, keyless exports.

**How fresh is the data?** Both lists are downloaded fresh at the start of every run, straight from OFAC and the EU — not cached.

**Is this legally sufficient for KYC/AML?** No single automated tool is. This is a preliminary screening tool, not a compliance determination — use it to build your review list, not as your only control, and always verify a hit against the official OFAC/EU sources before acting on it.

**Does it catch aliases (a.k.a. names)?** Yes. OFAC's own alias export (a.k.a./f.k.a./n.k.a.) and the EU list's alias rows are both loaded and screened alongside primary names — a hit through an alias is marked as such via `matchedNameType`. Free-text alias mentions buried in OFAC's unstructured remarks field are not indexed separately.

**What if a name is very common?** Expect more hits now that aliases are in scope, not fewer — that's the nature of sanctions screening, not a bug. Review the ranked `matches` list; lower `minScore` catches more, higher `minScore` narrows to only near-exact hits.

**What does `listsComplete: false` mean for what I'm charged?** If a source list failed to load, a "no match" answer for that run is not charged — an unverifiable clean result is free. A match, or any result from a run where both lists loaded, is charged as usual.

**Can I call it from an AI agent?** Yes — it's a standard Apify Actor, callable via the Apify API or the Apify MCP server.

**What this is NOT.** Not a legal or compliance determination, and not a substitute for your own KYC/AML program — it is an inexpensive way to build the review list a human then checks against the official sources.

Found a wrong result, or need a source list we don't cover? Open an issue on this Actor's page.

***

Built by [zinin](https://apify.com/zinin). Questions? Telegram [@timzinin](https://t.me/timzinin).

### What this Actor is — and is not

Sanctions Screening is an official-list name-screening evidence collector and review router. It
downloads the supported OFAC and EU list files at run time, checks each unique submitted name
against primary and alias names, returns ranked candidates, records exactly which list components
were available, and tells an operator what to review next. The useful product is a repeatable,
source-linked review queue—not an automated “clear” or “blocked” decision.

It is not an identity-verification service, beneficial-ownership graph, PEP database, adverse-media
service, transaction-monitoring system, legal opinion, export-control classifier, KYC program or
sanctions-clearance authority. A name can belong to many people or entities; a listed party can use
another spelling; a program can have licenses or exceptions; and legal effect depends on facts and
current law outside these files. Every decision row therefore keeps `safeToAutomate: false`.

### Evidence and boundaries

The run requests OFAC's primary SDN file, alias file and address file plus the EU consolidated
financial-sanctions list. Primary and alias names participate in matching. Bounded addresses are
context only and do not increase the match score. The Actor does not use an unofficial mirror to
hide an official-source outage.

Coverage is explicit:

- `listsChecked` names list families that produced usable rows;
- `listsComplete` is true only when the expected match-bearing components loaded;
- `sourceEvidence` identifies the official list family and observation time;
- `partial` is true when list coverage is incomplete;
- `dataGaps` explains what the screen cannot establish;
- a no-match on incomplete coverage is free and is never presented as clearance.

The EU public download token in the source URL is the list publisher's public access token, not a
buyer credential. The Actor does not request a login, store a customer secret, submit a form, read
a private page or enrich a name with personal data from another provider.

### How to interpret a row

Start with `found`. When false, screening did not produce a usable observation and `failureType`,
`retryable` and `failureDiagnostics` explain what happened. When true, inspect `listsComplete`
before interpreting `match`.

`match: true` means at least one official-list name cleared the configured fuzzy threshold. It does
not mean the submitted subject and listed party are the same person or legal entity. Review
`matchedName`, `matchedNameType`, the ranked `matches`, programs and available addresses against
first-party identity documents and the current official record.

`match: false` means no loaded name record cleared the threshold in this bounded run. It does not
mean the subject is legally cleared, is not owned or controlled by a listed party, or cannot be
affected by another program. A complete no-match can support a documented preliminary control; an
incomplete no-match should be repeated after source recovery.

### Field dictionary

The original screening fields remain backward compatible. Commercial115 adds a stable decision
contract to every delivered row:

| Field | Buyer meaning |
|---|---|
| `recordType` | `screening_result` for a usable screen or `advisory` for failure/budget truth |
| `schemaVersion` | Version of the additive decision contract |
| `entityId` | Stable normalized identity for the submitted name inside this Actor |
| `observedAt` | When this run produced the observation |
| `confidenceScore` / `confidenceBand` | Confidence in the evidence path, never confidence in legal clearance |
| `confidenceReasons` | Why the available source path supports the assigned evidence score |
| `confidenceRisks` | Conditions that can make the evidence incomplete or ambiguous |
| `sourceEvidence` | Official list-family references retained for review |
| `freshness` | Observation-time freshness contract; consumers must recalculate age later |
| `change` | Explicitly unavailable because this is not a snapshot-diff product |
| `dataGaps` | Identity, ownership and legal facts not established by this row |
| `negativeSignals` | Machine-readable reasons for heightened review |
| `recommendedAction` | Bounded human next step, never an enforcement command |
| `actionPriority` / `actionReason` | Review urgency and the reason for it |
| `interpretationBoundary` | Durable statement of what the row cannot prove |
| `safeToAutomate` | Always false |
| `failureType` / `retryable` / `partial` | Failure and coverage truth |
| `billing` | Settlement-neutral delivery intent; current-run KVS `OUTPUT` proves paid, free, anomalous or unknown settlement |

Confidence measures source availability and match-path clarity. A high-confidence potential match
can still be the wrong person. Conversely, a lower-confidence result may simply reflect incomplete
list coverage rather than greater or smaller legal risk.

### Decision routing

1. Normalize the intended subject using reliable customer or vendor records.
2. Confirm `listsComplete`; repeat the screen if an expected source component was unavailable.
3. Review the top candidate and every material alternative above your threshold.
4. Compare date of birth, registration number, address, nationality, ownership and other identifiers
   in current primary records. This Actor does not perform that identity join.
5. Read the current program entry, amendments, general licenses, specific licenses and applicable
   jurisdictional guidance.
6. Escalate a plausible match to qualified compliance or legal reviewers.
7. Record the human disposition, evidence and review date in your own case system.
8. Re-screen according to your policy because lists and customer facts change.

Do not connect `match` directly to account closure, payment blocking, employment action or adverse
customer treatment. This Actor deliberately returns a review instruction, not a final decision.

### Example integration

Start a run with explicit input and an Apify bearer token:

```bash
curl -sS -X POST 'https://api.apify.com/v2/acts/zinin~sanctions-screening/runs?waitForFinish=180' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"names":["Example Person","Example Trading LLC"],"minScore":0.7,"maxConcurrency":2}'
```

Read Dataset rows from the returned `defaultDatasetId` and the reconciled envelope from KVS key
`OUTPUT`. The envelope reports requested, unique, duplicate, attempted, delivered, paid, free,
withheld and source-failure counts. `COMPLETE` means no known source failure or withheld row;
`PARTIAL` exposes one of those conditions; `FAILED` carries `fatalError` and may set
`replaySafe: false` when linked delivery cannot be established.

For an Apify MCP client exposing `call-actor`, use:

```json
{
  "name": "call-actor",
  "arguments": {
    "actor": "zinin/sanctions-screening",
    "input": {
      "names": ["Example Person", "Example Trading LLC"],
      "minScore": 0.7,
      "maxConcurrency": 2
    }
  }
}
```

### Billing and budget behavior

The run-start event is charged by Apify according to allocated memory. A `result-found` event is
linked to a delivered usable screening row. Source failures, pricing diagnostics, incomplete
no-match rows, normalized duplicates and budget-withheld advisories are not result-billed.

The billing critical section serializes “budget still available → deliver row with event → update
receipt.” A worker cannot independently pass the spend check while another worker is charging. If
the SDK throws during linked delivery or returns an ambiguous charged state, the Actor marks the
run fatal and `replaySafe: false` instead of reporting success. KVS `OUTPUT` lets an operator compare
Dataset delivery to settled event counts before downstream use.

### Important limitations

- Token-overlap matching is explainable but not phonetic, transliteration-complete or identity aware.
- Short and common names can produce many false positives.
- Misspellings, non-Latin scripts and aliases absent from official files can produce false negatives.
- Addresses are bounded context and can be old, incomplete or shared.
- The Actor does not evaluate 50-percent ownership rules or indirect control.
- It does not cover every national, UN, export-control, law-enforcement or internal watchlist.
- It does not establish whether a program applies to a specific transaction, product or jurisdiction.
- Official publication timing, availability and corrections remain upstream dependencies.
- `minScore` changes recall and precision; it does not change the legal standard.

### Operational acceptance checklist

- Verify the intended subject and permitted processing purpose.
- Keep the exact input, threshold and observation time with the case.
- Require complete expected source coverage for a clean preliminary screen.
- Treat every potential match as an identity-review candidate, not a confirmed hit.
- Treat every no-match as bounded evidence, not sanctions clearance.
- Review current official records and applicable licenses.
- Keep human approval before any adverse action.
- Reconcile Dataset, `OUTPUT` and pay-per-event counts.
- Do not retry a fatal run when `replaySafe` is false without inspecting delivery state.
- Schedule re-screening according to your own risk policy.

### Release evidence contract

Production publication is accepted only when the immutable source matches the reviewed local
candidate, one bounded no-retry canary proves a potential or complete screen plus a free error,
duplicate and budget-withheld outcome, Dataset rows reconcile with KVS `OUTPUT`, and settled
pay-per-event counts cover measured Apify usage by the campaign floor. The public Store is then
checked anonymously for title, Input, pricing, boundaries and exactly two 1600×900 selling visuals.

### Real run-bound evidence

The projections below come from retained Apify run, Dataset and KVS records. They are included to show what the existing production path proved and what the rejected candidate did not prove. They are not fabricated examples and they do not substitute for the required future no-retry canary of the current source.

#### Successful production potential-match screen

Run `HGGEq1xUfOEAWFsDL` used production build `xY7T3qFzAXtkJhwhU`, completed `SUCCEEDED`, and wrote one row to Dataset `Yo0Oy8qZHINQBsnE1`. The submitted name was `Rosneft`. The row recorded three potential candidates across both official list families, with the top normalized score equal to `1`. The platform ledger recorded `result-found: 1`. That production build predated the current KVS `OUTPUT` contract, so its default KVS `Lhsm5niWPhPf1BQwt` returned no `OUTPUT` record; the missing receipt is disclosed rather than reconstructed.

```json
{
  "evidenceAccepted": true,
  "runId": "HGGEq1xUfOEAWFsDL",
  "buildId": "xY7T3qFzAXtkJhwhU",
  "status": "SUCCEEDED",
  "memoryMbytes": 2048,
  "datasetId": "Yo0Oy8qZHINQBsnE1",
  "keyValueStoreId": "Lhsm5niWPhPf1BQwt",
  "chargedEventCounts": {
    "apify-actor-start": 2,
    "result-found": 1
  },
  "usageTotalUsd": 0.0011474827882498504,
  "datasetRow": {
    "input": "Rosneft",
    "found": true,
    "match": true,
    "matchCount": 3,
    "highestScore": 1,
    "matchedName": "ROSNEFT AERO",
    "matchedNameType": "alias",
    "listsChecked": ["OFAC SDN", "EU Consolidated"],
    "listsComplete": true,
    "summary": "Rosneft — 3 potential matches (highest score 1). Top hit: RN AERO [EU Consolidated] (matched on alias ROSNEFT AERO, alias), program UKR."
  },
  "outputHttpStatus": 404
}
```

This proves that the legacy production source could load both list families and deliver a billed potential-match row. It does not prove that `ROSNEFT AERO` and the submitted subject are the same legal entity, and the row must not be treated as a final sanctions determination. It also does not prove the enhanced run receipt, because that build did not write one.

#### Second accepted production screen and measured economics

Run `BSCIRB2pkgD5PKIjE` used the same production build `xY7T3qFzAXtkJhwhU` on
9 August 2026. It completed `SUCCEEDED`, wrote one row to Dataset
`ewxEs7bpsKK5NvcKT`, and used KVS `vpnLECjW2YQg2yfeX`. Authenticated GET readback
confirmed the exact input `Rosneft`, `minScore:0.6`, `maxConcurrency:3`, complete
OFAC/EU coverage, three candidates, top score `1`, and an alias top match
`ROSNEFT AERO`. The event ledger recorded start2/result1. Platform runtime usage was
`$0.0008573099164267383`; the retained gate calculated `16.622×` gross
charge-to-platform-runtime-usage coverage. That is measured execution economics,
not profit, customer demand, or a legal conclusion.

```json
{
  "evidenceAccepted": true,
  "runId": "BSCIRB2pkgD5PKIjE",
  "buildId": "xY7T3qFzAXtkJhwhU",
  "status": "SUCCEEDED",
  "startedAt": "2026-08-09T05:19:58.226Z",
  "finishedAt": "2026-08-09T05:20:05.384Z",
  "datasetId": "ewxEs7bpsKK5NvcKT",
  "keyValueStoreId": "vpnLECjW2YQg2yfeX",
  "input": {
    "names": ["Rosneft"],
    "minScore": 0.6,
    "maxConcurrency": 3
  },
  "chargedEventCounts": {
    "apify-actor-start": 2,
    "result-found": 1
  },
  "usageTotalUsd": 0.0008573099164267383,
  "coverageMultiple": 16.622,
  "dataset": {
    "rows": 1,
    "input": "Rosneft",
    "found": true,
    "match": true,
    "matchCount": 3,
    "highestScore": 1,
    "matchedName": "ROSNEFT AERO",
    "matchedNameType": "alias",
    "listsChecked": ["OFAC SDN", "EU Consolidated"],
    "listsComplete": true,
    "checkedAt": "2026-08-09T05:20:04.676Z"
  },
  "outputHttpStatus": 404
}
```

This second accepted run independently proves the same legacy product unit and
official-list coverage. The missing legacy OUTPUT is again stated explicitly; the
new enhanced receipt still requires the one future exact-build canary.

#### Rejected 512 MB candidate and measured remediation

Run `eRZReWfgZqb6Q3uTn` used candidate build `qhPazqB0wlur0doz9` with 512 MB. It loaded 39,382 OFAC names, then exited `137` before completing the EU list or screening. Dataset `cxTutKVAtQXafPFH8` stayed empty, KVS `tYYHj6Eyauzh2bsqZ` returned no `OUTPUT`, and the event ledger recorded the start event but no `result-found`. There was no retry.

```json
{
  "evidenceAccepted": false,
  "runId": "eRZReWfgZqb6Q3uTn",
  "buildId": "qhPazqB0wlur0doz9",
  "buildNumber": "0.2.1",
  "status": "FAILED",
  "exitCode": 137,
  "memoryMbytes": 512,
  "timeoutSecs": 180,
  "maxTotalChargeUsd": 0.017,
  "datasetId": "cxTutKVAtQXafPFH8",
  "keyValueStoreId": "tYYHj6Eyauzh2bsqZ",
  "datasetRows": 0,
  "outputHttpStatus": 404,
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 0
  },
  "usageTotalUsd": 0.0002619125844008393,
  "noRetry": true
}
```

The current implementation addresses the measured cause rather than hiding it with a larger default. CSV parsing is lazy, official files are downloaded and processed sequentially, tokens are attached in place, and implausibly small official-list outputs fail coverage checks. A local profile on fresh official files measured these inputs: SDN 5,632,887 bytes and 19,199 named rows; ALT 1,063,098 bytes and 20,183 aliases; ADD 1,676,170 bytes; EU 25,166,172 bytes and 31,053 named rows. The profile process peaked at 175,620,096 RSS bytes, below the reviewed 512 MB default. This is strong local evidence for the memory fix, but only a future exact-build cloud canary can prove the hosted runtime and settlement contract.

### Happy, partial, and failure output

#### Happy complete row

A complete screen has `found:true`, `listsComplete:true`, a bounded ranked `matches` array, evidence confidence and `safeToAutomate:false`. The Dataset row records billing intent only. KVS `OUTPUT` must bind the current run and Dataset, show `resultEvent.before → after` with exact delta `1` for each confirmed paid row, and reconcile requested, unique, duplicate, attempted, delivered, paid and free counts.

#### Partial official coverage

If an official component is unavailable or implausibly small, the run exposes it in `source.listErrors`, rows carry `listsComplete:false`, and `partial:true`. A potential match may remain useful review evidence; an incomplete no-match is delivered free because the missing source could contain the match. It is never described as clearance.

```json
{
  "found": true,
  "match": false,
  "listsChecked": ["OFAC SDN"],
  "listsComplete": false,
  "partial": true,
  "recommendedAction": "REPEAT_AFTER_OFFICIAL_SOURCE_RECOVERY",
  "billing": {
    "billable": null,
    "billingEligible": false,
    "billingIntent": "free_dataset_write",
    "eventName": null,
    "settlementSource": "current_run_kvs_output"
  }
}
```

#### Failed or ambiguous delivery

An unreadable pre-push named counter fails before Dataset delivery. A thrown linked push sets `unknownDeliveryCount`, `fatalError`, and `replaySafe:false`. A returned push followed by an unreadable named counter is a known Dataset delivery with `unknownSettlementCount:1`; a contradictory aggregate receipt produces `anomalousChargeCount:1`. All post-push uncertainty is terminal and must not be blindly retried. Reconcile the original run, Dataset, KVS and platform event ledger.

### Integration recipes

#### JavaScript client

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('zinin/sanctions-screening').call({
  names: ['Example Person', 'Example Trading LLC'],
  minScore: 0.7,
  maxConcurrency: 2,
});
const [{ items }, outputRecord] = await Promise.all([
  client.dataset(run.defaultDatasetId).listItems({ clean: true }),
  client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT'),
]);
if (!outputRecord?.value) throw new Error('Missing current-run OUTPUT');
if (outputRecord.value.runId !== run.id) throw new Error('Receipt belongs to another run');
if (outputRecord.value.status === 'FAILED'
    || outputRecord.value.unknownDeliveryCount > 0
    || outputRecord.value.unknownSettlementCount > 0
    || outputRecord.value.anomalousChargeCount > 0) {
  throw new Error('Manual reconciliation required before any rerun');
}
console.log({ rows: items.length, paid: outputRecord.value.paidRowCount });
```

#### Python client

```python
from apify_client import ApifyClient
import os

client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('zinin/sanctions-screening').call(run_input={
    'names': ['Example Person', 'Example Trading LLC'],
    'minScore': 0.7,
    'maxConcurrency': 2,
})
rows = list(client.dataset(run['defaultDatasetId']).iterate_items())
output = client.key_value_store(run['defaultKeyValueStoreId']).get_record('OUTPUT')
if not output or output['value']['runId'] != run['id']:
    raise RuntimeError('Missing or foreign current-run receipt')
if (output['value']['status'] == 'FAILED'
        or output['value']['unknownDeliveryCount'] > 0
        or output['value']['unknownSettlementCount'] > 0
        or output['value']['anomalousChargeCount'] > 0):
    raise RuntimeError('Inspect delivery settlement before rerun')
print({'rows': len(rows), 'paid': output['value']['paidRowCount']})
```

#### Webhook consumer

Use an Actor run terminal webhook only as a notification. Read the run object, then fetch `OUTPUT` from that run's `defaultKeyValueStoreId`. Reject a missing or foreign `runId`. Compare Dataset item count with `deliveredRowCount`, platform `chargedEventCounts['result-found']` with `paidRowCount`, and inspect unknown/anomalous counters before moving the rows into a case-management queue.

### Commercial playbooks

- **Onboarding:** screen exact counterparty names, retain official-list evidence, and route every
  plausible match to identity and legal review before any account or payment action.
- **Periodic re-screening:** schedule independent observations under the buyer's policy and compare
  retained rows downstream; this Actor does not silently invent a cross-run clearance state.
- **Case enrichment:** attach the bounded row and source links to an existing private case, preserving
  access, correction, retention, and reviewer controls rather than publishing the result.

### Operating guide

- Keep `names` at 100 or fewer and use 1–200 characters per value.
- Normalize and deduplicate subjects in your source system, but retain the buyer-entered string for case evidence.
- Use a higher `minScore` only as part of a documented screening policy; do not tune it merely to eliminate inconvenient candidates.
- Keep `maxConcurrency` modest when downstream Dataset or webhook consumers have rate constraints. Matching shares an in-memory read-only index, while paid delivery is serialized.
- Archive the run ID, source observation time, threshold, official list families, matched name, program and human disposition.
- Apply retention and access controls because names and case dispositions may be personal data.
- Re-screen according to policy and official-list update cadence; this Actor is a point-in-time observation, not a durable clearance.
- Never infer beneficial ownership, control, identity or legal effect from a fuzzy name alone.
- Never retry an unknown or anomalous settlement without reading the original platform ledger.

# Actor input Schema

## `names` (type: `array`):

Person or company names to screen against the OFAC SDN and EU consolidated sanctions lists, one per row.

## `minScore` (type: `number`):

Only report candidate matches scoring at or above this (0 = report everything, 1 = only a full token match). Kept soft on purpose — a missed hit is worse than a false one. At the default 0.5, a two-word name matching on one word still surfaces as a low-confidence hit.

## `maxConcurrency` (type: `integer`):

How many names to screen in parallel.

## Actor input object example

```json
{
  "names": [
    "Yevgeniy Prigozhin",
    "Acme Trading LLC"
  ],
  "minScore": 0.5,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

API URL for the default dataset items produced by this run.

## `output` (type: `string`):

API URL for the decision and billing reconciliation receipt stored at KVS key OUTPUT.

# 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 = {
    "names": [
        "Yevgeniy Prigozhin",
        "Acme Trading LLC"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/sanctions-screening").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 = { "names": [
        "Yevgeniy Prigozhin",
        "Acme Trading LLC",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("zinin/sanctions-screening").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 '{
  "names": [
    "Yevgeniy Prigozhin",
    "Acme Trading LLC"
  ]
}' |
apify call zinin/sanctions-screening --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/sanctions-screening"
        }
    }
}
```

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/RYwuWHt3affl21mgr/builds/1CYj480dTP94xOf8p/openapi.json
