Sanctions Source Coverage Review
Pricing
from $21.00 / 1,000 coverage assessments
Sanctions Source Coverage Review
Review coverage in existing sanctions screening datasets. Separate reported potential hits, checked lists and incomplete observations in evidence-linked assessments for human review.
Pricing
from $21.00 / 1,000 coverage assessments
Rating
0.0
(0)
Developer
Tim Zinin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 hours ago
Last modified
Categories
Share
Sanctions Source Coverage Review — turn sanctions-source rows into an honest coverage assessment, never a clearance verdict
Feed this Actor rows from your own zinin/sanctions-screening and zinin/sanctions-update-alert runs, and it returns one coverage assessment per literal screened query or watch name: which lists were actually checked, which were unavailable or reported conflicting coverage, which exact name hits require human review, and where a routine "no change since baseline" heartbeat is honestly kept separate from a real screening result — so your compliance team knows exactly what was checked before anyone relies on silence as an answer.

What you get
You give this Actor a dataset (or an inline list) of sanctions-source rows — the two structurally different shapes produced by zinin/sanctions-screening (recordType: "screening_result", an explicit literal query in input) and zinin/sanctions-update-alert (recordType: "sanctions_update_alert", a scheduled watch heartbeat keyed by baselineKey/watchName). It never screens anyone itself, never calls a model, and never treats an absence of data as a clean result — it reads exactly the rows you supply and reports, per literal identity, a coverage assessment: a factual account of what was actually checked, what evidence exists, and what still needs a human.
Concretely, for each identity observed in your batch, you receive one of two shapes, kept strictly separate and never merged even on a coincidentally identical string:
For a screen: (query) identity — grouped on the exact literal input string from zinin/sanctions-screening rows:
listsChecked/listsComplete/coverageDisagreement— the union of list names actually reported checked, a tri-state completeness verdict (trueonly if every contributing row said so,falseif any did,null/unknown if never reported), and whether repeated checks of the same query reported different list sets.matches/matchCount/omittedMatchCount— deduplicated exact name hits (bounded to 15 for display, exact total and omitted count always reported), each a potential match requiring human review, never a sanctioned/not-sanctioned verdict.matchesEvidenceComplete/matchesUnknownRowCount— whether hit evidence itself is fully known; a row with a missing or non-arraymatchesfield is UNKNOWN evidence, never treated as a fabricated clean no-match.matchCountMismatchObserved—truewhen a source row's own reportedmatchCountdidn't equal its ownmatchesarray length, a genuine upstream data-quality signal.
For a watch: (baseline watch) identity — grouped on the exact literal baselineKey/watchName from zinin/sanctions-update-alert rows:
sourceCoverage/sourceComplete/sourceCoverageConflict— the reconciled completeness/error/lastModified/parsedRecords per distinct list source file, aggregated across every observation before any display capping; a genuine tie with disagreeing content is reported as an unresolved conflict, never an arbitrary scan-order winner.unavailableSourceIds— sourceIds whose reconciled observation reported incomplete, conflicting, or errored — lists that were NOT usably and unambiguously fetched.changeEvents/changeEventCount— deduplicated exact hits where the watch actually reported a genuine detected change (found: true), strictly distinct from routine no-change heartbeat rows, which carry no entity identity to review and never appear here.baselineOnlyObservationCount— how many contributing rows were pure "no change since baseline" heartbeats, retained as source-context evidence only, never joined to any person or company, and never itself implying a negative screening result.
What this Actor deliberately does not do, so a coverage report can never be mistaken for a clearance decision:
- It does not produce a sanctioned/not-sanctioned verdict, a compliance clearance, a fraud score, or any protected-trait inference of any kind.
matches/changeEventsare potential name matches requiring human review against official records — never a confirmed identity. - It does not perform fuzzy identity matching. Grouping is the exact literal
inputstring (for a screen) orbaselineKey/watchName(for a watch), never a similarity join across different queries or people. - It does not treat a watch's routine "no change since baseline" heartbeat (
found: false, every entity field null) as a negative screening result for any person or company — it is watch-level source context only, reported separately inbaselineOnlyObservationCount. - It does not call any network, AI model, or system clock from inside its transform step. Every field in the output traces back to a value on a row you supplied.
- It does not launch
zinin/sanctions-screening,zinin/sanctions-update-alert, or any other upstream Actor on your behalf. It never starts new screening work and never combines multiple dataset IDs automatically on a single run.

Who uses it
- Compliance and KYC analysts who already run
zinin/sanctions-screeningorzinin/sanctions-update-alertand need an honest coverage report before a review file closes — one that distinguishes "checked, zero matches" from "coverage unknown," rather than treating silence as clearance. - Trade-compliance and export-control teams tracking counterparty or vendor names against OFAC SDN, EU Consolidated and similar lists, who need
matcheswith full evidence (list, program, matched-name type, score) routed to a human, never an automated block/allow decision. - Ongoing-monitoring operations teams running scheduled
zinin/sanctions-update-alertwatches, who need to know when a watch's underlying list sources actually failed to fetch (unavailableSourceIds) — a silent source outage is exactly the failure mode that makes a "no new hits" watch dangerously misleading if unreported. - Data engineers building an internal screening dashboard who need a dependable, pure post-processing step between two scrapers' raw output and a BI/case-management table — one that never silently collapses "unknown" into "clean" and always states exactly how many hits or list observations were capped.
- Search-term note for compliance-ops teams: if your own vocabulary uses terms like "sanctions screening coverage report", "OFAC SDN hit review queue", "sanctions watch source availability", or "compliance evidence aggregation", this Actor's identity and completeness rules apply directly to that workflow.
How to run
- Open the Apify Console and start a new run of Sanctions Source Coverage Review.
- Choose a source, in order of precedence (an explicit dataset always wins over a run reference, which always wins over the inline example — a dataset/run source never merges with inline rows):
- Source Dataset (
datasetId) — point at an existing Dataset from azinin/sanctions-screeningorzinin/sanctions-update-alertrun (or both, combined by you into one dataset — this Actor does not auto-concatenate multiple dataset IDs). - Source run ID (
runId) — supply a run ID instead if you have one but not its Dataset ID directly; its default Dataset is resolved automatically, using your own run's granted permissions. - Integration payload (
payload.resource) — set automatically by Apify's Integrations "Actor/task run" template when you chain this Actor after an upstream screener; carries{{resource.defaultDatasetId}}/{{resource.id}}/{{resource.actId}}so no manual mapping is needed. - Source records (inline) — paste or edit rows directly, or leave the prefilled real public sample in place for a first test run: four real rows drawn from actual
zinin/sanctions-screeningandzinin/sanctions-update-alertruns (one real screening query with three genuine matches, plus three real watch heartbeats).
- Source Dataset (
- Optionally set Max source rows (
maxInputRows, default 100, up to 1000) and Max results (maxResults, default 2, up to 1000). The platform's own Max total charge USD run option is a separate, independent stop: whichever limit is hit first ends delivery. - Run it. Delivered rows land in the run's default Dataset, one per identity,
screen:results beforewatch:results, then exact identity key ascending — never a fuzzy relevance ranking. Each row carriessourceRowIndexesback into your original batch so you can trace any figure to its source rows. - Read
partial,matchesEvidenceComplete/sourceComplete, and the summary'sresultsCapped/sourcePartialflags before treating a run as a complete coverage picture — see Evidence and boundaries for exactly when those flags are set and why.
For a repeatable pipeline (e.g. a daily/weekly compliance-watch refresh), save this configuration as an Apify Task and chain it after your zinin/sanctions-screening/zinin/sanctions-update-alert runs via Apify's Integrations, or trigger it from your own system via the API — see Integration recipes below for both paths.
Pricing
This Actor bills on the Apify Pay-Per-Event model under a single primary event, result-found — described to buyers under the pricing noun "coverage assessment." There is no start fee and no per-row charge: only a delivered, useful coverage assessment is billed. Free diagnostics (invalid rows, missing identity, source failures, budget stops, empty-batch outcomes) never request the paid event and are never charged.
| Tier | Price per coverage assessment |
|---|---|
| Free | $0.0300 |
| Bronze | $0.0270 |
| Silver | $0.0240 |
| Gold | $0.0210 |
| Platinum | $0.0210 |
| Diamond | $0.0210 |
Bronze, Silver and Gold apply Apify's standard 10%/20%/30% volume discounts off the Free-tier price; Platinum and Diamond are held at the Gold rate. There is no owner-run revenue claim and no guarantee of any particular run volume in this pricing table — it states only the price actually configured on this Actor's result-found event, verified against the live Actor record at publication time.
A useful coverage assessment is a real aggregate result — a reconciled, evidence-linked report with at least one usable observation — never a raw source row passed through unprocessed. A partial: true assessment (for example, one delivered while maxInputRows cut off part of your batch, or one where list coverage is unresolved) is still a useful aggregate and is still billed; only genuinely empty, invalid, or unclassifiable-row outcomes are free. As the real final-prefill canary shows, three of the four real prefilled rows are free diagnostics (the watch heartbeats carry no baselineKey/watchName in the saved public sample and cannot be grouped) while the one genuine screening query is the paid result — see Evidence and boundaries for the exact mechanics.
Input contract
This Actor's input schema is deliberately small: a source selector plus two bounds. There are no product-specific configuration fields — every screening/watch behavior is driven entirely by the row shape.
{"items": [{"recordType": "screening_result","schemaVersion": "1.0.0","found": true,"checkedAt": "2026-09-22T05:34:24.086Z","input": "Rosneft","matchCount": 3,"matches": [{"listName": "OFAC SDN","entityName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","entityType": "entity","program": "UKRAINE-EO13662, RUSSIA-EO14024","matchedName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","matchedNameType": "primary","score": 1}],"listsChecked": ["OFAC SDN", "EU Consolidated"],"listsComplete": true}],"maxInputRows": 100,"maxResults": 2}
This example is a trimmed projection of the Actor's own real prefill (the full prefill carries four real rows — one zinin/sanctions-screening screening_result row for the query "Rosneft" with three genuine OFAC SDN / EU Consolidated matches, plus three real zinin/sanctions-update-alert heartbeat rows, each with its own __source provenance block). Every list name, entity name, and program string in the prefill is a real value observed from a real upstream run on 2026-09-22, not synthetic fixture data.
A real, honest gap in this exact saved prefill: the three watch heartbeat rows in the public sample carry baselineKey/watchName values that were not captured in the public projection (both are null in the saved snapshot) — their own private baseline identifiers were not part of the public-safe fields this Actor's own upstream evidence exposed. This Actor never invents a key for a row that doesn't have one: each of those three rows is reported as a free missing_identity diagnostic rather than silently grouped under a guessed watch name or treated as a clean negative query for any entity. Inspect your production rows for an actual baselineKey or watchName; without one these rows remain free diagnostics. The saved source evidence does not prove every upstream configuration supplies those identifiers.
| Field | Type | Notes |
|---|---|---|
items | array, optional | Inline source rows. Ignored if datasetId or a resolvable runId/payload.resource is present. Bounded to 1000 rows / 4 MiB total / 64 KiB per row. |
datasetId | string, optional | Reads an existing Dataset with your run's own granted permissions (resourcePicker, READ). Overrides inline items. For Integrations, map {{resource.defaultDatasetId}} here. |
runId | string, optional | Reads a run's default Dataset when you have a run ID but not its Dataset ID directly. No elevated access — the run's own default Dataset is resolved and read exactly as datasetId would be. |
payload | object, optional | A supplied Integrations/webhook envelope with resource.id, resource.defaultDatasetId, and resource.actId. The default does not fetch a source or create an envelope. Private Dataset access requires the destination run’s explicit READ grant. |
maxInputRows | integer, default 100, max 1000 | Hard cap on source rows read for this run, independent of maxResults. |
maxResults | integer, default 2, max 1000 | Hard cap on paid coverage assessments delivered this run. Free diagnostics are never counted against this cap. |
Recognized row shapes
| Field | Which record type carries it | Role in the assessment |
|---|---|---|
input | screening_result | Identity — the exact literal screened query string, ≤500 chars |
baselineKey / watchName | sanctions_update_alert | Identity — the exact literal watch label, ≤500 chars; baselineKey preferred when both are present |
listsChecked / listsComplete | screening_result | Coverage — union of checked lists and a per-row completeness flag |
matches / matchCount | screening_result | Hit evidence — a genuine Array (even empty) is known evidence; missing/non-array is unknown evidence |
sourceEvidence[].{sourceId,complete,error,lastModified,parsedRecords} | sanctions_update_alert | Per-list-source coverage evidence, reconciled across repeated observations |
found / entityName / listName / matchedName / entityType / program / changeType | sanctions_update_alert | A genuine detected change (found: true with identifiable fields) becomes a changeEvents entry; found: false with empty fields is the normal heartbeat |
checkedAt | both | Source capture timestamp, used only for the freshest-observation tie-break on sourceCoverage |
error / failureType / errorCode / partial / sourceComplete | both | A truthy error/failureType/errorCode excludes the row entirely as source_row_failed; partial/sourceComplete feed the aggregate's own honesty signals |
Other business fields are not automatically promoted into evidence. Control fields such as error, failed, found, partial and source status affect validation; source observation timestamps are distinct from processing time. Use the field dictionary for the specific recognized contract.
Output examples
Every row and diagnostic below is copied verbatim from real private cloud canary runs of this Actor's value-add-candidate build, never hand-written. Field order and values match the real Dataset item exactly, one example per listed evidence scenario.
Happy path — the full four-row prefill
Real run against the complete real prefill with default maxInputRows/maxResults. The run's summary reported "rowsProcessed": 4, "resultsCount": 1, "diagnosticsCount": 3" — three free missing_identity diagnostics (the watch heartbeats, explained above) plus the one genuine paid screening result:
{"entityId": "screen:Rosneft","found": true,"status": "matches_require_review","action": "review_matches_against_official_records","confidence": "medium","sourceRowCount": 1,"sourceRowIndexesOmittedCount": 0,"identityNamespace": "screen","query": "Rosneft","listsChecked": ["EU Consolidated","OFAC SDN"],"listsCheckedOmittedCount": 0,"listsComplete": true,"coverageDisagreement": false,"matchCountMismatchObserved": false,"matchesEvidenceComplete": true,"matchesUnknownRowCount": 0,"excludedUnidentifiableHitCount": 0,"matches": [{"listName": "EU Consolidated","entityName": "RN AERO","matchedName": "ROSNEFT AERO","entityType": "entity","program": "UKR","matchedNameType": "alias","score": 1,"sourceRowIndex": 0},{"listName": "OFAC SDN","entityName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","matchedName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","entityType": "entity","program": "UKRAINE-EO13662, RUSSIA-EO14024","matchedNameType": "primary","score": 1,"sourceRowIndex": 0},{"listName": "OFAC SDN","entityName": "ROSNEFT TRADING S.A.","matchedName": "ROSNEFT TRADING S.A.","entityType": "entity","program": "UKRAINE-EO13662, VENEZUELA-EO13850","matchedNameType": "primary","score": 1,"sourceRowIndex": 0}],"matchCount": 3,"omittedMatchCount": 0,"observationWindow": {"earliest": "2026-09-22T05:34:24.086Z","latest": "2026-09-22T05:34:24.086Z","validCount": 1},"evidence": [{"field": "matches","value": "EU Consolidated: RN AERO","rule": "observed_match","sourceRowIndex": 0},{"field": "matches","value": "OFAC SDN: OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","rule": "observed_match","sourceRowIndex": 0},{"field": "matches","value": "OFAC SDN: ROSNEFT TRADING S.A.","rule": "observed_match","sourceRowIndex": 0},{"field": "listsChecked","value": "EU Consolidated, OFAC SDN","rule": "listsCheckedUnion","sourceRowIndex": 0}],"businessPartial": false,"sourceRowIndexes": [0],"partial": false,"sourceCoverage": {"complete": true,"reason": null,"droppedOversized": 0},"product": "sanctions-source-coverage-review","schemaVersion": "1.0","sourceDatasetId": null,"sourceRunId": null,"suppliedSourceRunId": null,"sourceRunIdVerified": null,"processedAt": "2026-09-23T08:10:52.190Z"}
status: "matches_require_review" with confidence: "medium" reflects three genuine, fully-evidenced name matches (matchesEvidenceComplete: true, listsComplete: true) against two real lists — every match is a potential name collision requiring a compliance analyst's review against official records, never a confirmed sanctioned-entity finding by this Actor. The three missing_identity diagnostics for the watch rows in this same run were free and are shown in full in Input contract above.
Partial path — a real maxInputRows: 1 cutoff, with a genuinely delivered result
Real run against the same four-row batch with maxInputRows: 1 (so only the batch's first row — the Rosneft screening row — was actually read) and maxResults: 2. The run wrote a free partial_source diagnostic ("reason": "max_input_rows_reached") plus the one coverage assessment it could reconcile from the single row it read:
{"entityId": "screen:Rosneft","found": true,"status": "matches_require_review","action": "review_matches_against_official_records","confidence": "medium","sourceRowCount": 1,"sourceRowIndexesOmittedCount": 0,"identityNamespace": "screen","query": "Rosneft","listsChecked": ["EU Consolidated","OFAC SDN"],"listsCheckedOmittedCount": 0,"listsComplete": true,"coverageDisagreement": false,"matchCountMismatchObserved": false,"matchesEvidenceComplete": true,"matchesUnknownRowCount": 0,"excludedUnidentifiableHitCount": 0,"matches": [{"listName": "EU Consolidated","entityName": "RN AERO","matchedName": "ROSNEFT AERO","entityType": "entity","program": "UKR","matchedNameType": "alias","score": 1,"sourceRowIndex": 0},{"listName": "OFAC SDN","entityName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","matchedName": "OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","entityType": "entity","program": "UKRAINE-EO13662, RUSSIA-EO14024","matchedNameType": "primary","score": 1,"sourceRowIndex": 0},{"listName": "OFAC SDN","entityName": "ROSNEFT TRADING S.A.","matchedName": "ROSNEFT TRADING S.A.","entityType": "entity","program": "UKRAINE-EO13662, VENEZUELA-EO13850","matchedNameType": "primary","score": 1,"sourceRowIndex": 0}],"matchCount": 3,"omittedMatchCount": 0,"observationWindow": {"earliest": "2026-09-22T05:34:24.086Z","latest": "2026-09-22T05:34:24.086Z","validCount": 1},"evidence": [{"field": "matches","value": "EU Consolidated: RN AERO","rule": "observed_match","sourceRowIndex": 0},{"field": "matches","value": "OFAC SDN: OPEN JOINT-STOCK COMPANY ROSNEFT OIL COMPANY","rule": "observed_match","sourceRowIndex": 0},{"field": "matches","value": "OFAC SDN: ROSNEFT TRADING S.A.","rule": "observed_match","sourceRowIndex": 0},{"field": "listsChecked","value": "EU Consolidated, OFAC SDN","rule": "listsCheckedUnion","sourceRowIndex": 0}],"businessPartial": false,"sourceRowIndexes": [0],"partial": true,"sourceCoverage": {"complete": false,"reason": "max_input_rows_reached","droppedOversized": 0},"product": "sanctions-source-coverage-review","schemaVersion": "1.0","sourceDatasetId": null,"sourceRunId": null,"suppliedSourceRunId": null,"sourceRunIdVerified": null,"processedAt": "2026-09-23T08:12:08.922Z"}
This row's business content is identical to the happy-path row above — the one row this narrower run actually read was the same first row — but partial is now true and sourceCoverage names the exact reason. This states explicitly that the batch's remaining three real rows (the watch heartbeats) were never read this run — not dropped or degraded, simply outside the row window configured for this run.
Failure path — the platform's own charge limit stops delivery before any row is read
Real run with the platform's Max total charge USD run option set to $0.005 — below the configured $0.03 Free-tier price of a single coverage assessment:
{"product": "sanctions-source-coverage-review","schemaVersion": "1.0","status": "budget_stopped","reason": "Run charge limit cannot cover any result; no source was read.","found": false,"processedAt": "2026-09-23T08:11:50.265Z"}
This diagnostic is free — the real run's terminal record reported "paid": 0, "free": 1, "budgetStopped": true. The Actor checks whether the run's own charge limit can cover even a single result before reading any source rows, so a misconfigured (too-low) Max total charge USD fails safely and instantly rather than partially reading your batch and then stalling mid-run.
Empty-source path — a batch with nothing usable in it
Real run with items: [] (an explicitly empty inline batch):
{"product": "sanctions-source-coverage-review","schemaVersion": "1.0","status": "no_results","found": false,"reason": "no_usable_rows_in_batch","sourceCoverage": {"complete": true,"reason": null,"droppedOversized": 0},"processedAt": "2026-09-23T08:11:40.649Z"}
Also free. sourceCoverage.complete: true here means exactly what it says: the Actor read the entire (empty) batch you supplied — the emptiness is a fact about your input, not a read failure on the Actor's part.
Field dictionary
| Field | Meaning |
|---|---|
entityId | screen:<query> or watch:<watchName> — the stable identity string for this delivered row. |
found | true for a useful result eligible for the result-found event; every free diagnostic row has found: false. |
status | matches_require_review, no_matches_lists_complete, no_matches_lists_incomplete_or_unknown (screen); watch_hits_detected, no_new_hits_lists_complete, no_new_hits_lists_incomplete_or_unknown (watch); otherwise a diagnostic status. |
action | A routing hint (review_matches_against_official_records, no_action_lists_complete, review_list_coverage_before_relying_on_absence, etc.) — never an automated compliance decision. |
confidence | medium only when the relevant completeness signal is fully true; low otherwise — see Decision routing. |
identityNamespace | screen or watch — the two namespaces are never merged, even on identical literal strings. |
query / watchName | The exact literal string this result was grouped by, present only for its own namespace. |
listsChecked / listsCheckedOmittedCount | Union of list names actually reported checked (screen only), capped to 30 with an honest omitted count. |
listsComplete | Tri-state: true only if every contributing row said so, false if any did, null when never reported — never rounded up to "clear." |
coverageDisagreement | true when repeated checks of the same query reported different sets of checked lists. |
matchCountMismatchObserved | true when a row's own reported matchCount didn't equal its own matches array length. |
matchesEvidenceComplete / matchesUnknownRowCount | Whether every contributing row supplied a genuine matches array; a missing/non-array field is unknown evidence, never a fabricated clean result. |
excludedUnidentifiableHitCount | Count of matches entries dropped for lacking both listName and entityName; nonzero always sets partial: true. |
matches / matchCount / omittedMatchCount | Deduplicated hits (cap 15 display) with exact total and omitted count, deduped on the full raw value. |
sourceCoverage / sourceCoverageOmittedCount / sourceCoverageConflict | Per-list-source reconciled coverage (watch only), capped to 30; a genuine tied-freshest disagreement is reported as an unresolved conflict, not a scan-order winner. |
unavailableSourceIds / unavailableSourceIdsOmittedCount | sourceIds that were incomplete, conflicting, or errored — not usably fetched, capped to 30. |
excludedInvalidSourceEvidenceCount | Count of sourceEvidence entries dropped for lacking a sourceId. |
sourceComplete | true only when every row said so AND no conflict occurred; false if any row said so; null when never reported or unresolved. |
changeEvents / changeEventsOmittedCount / changeEventCount | Deduplicated genuine detected changes (watch only, found: true rows), capped to 15 display; changeEventCount is always the full uncapped total. |
baselineOnlyObservationCount | Count of pure no-change heartbeat rows — source context only, never joined to any entity. |
observationWindow | { earliest, latest, validCount } raw checkedAt strings across every contributing row. |
evidence | Bounded list (cap 20) of { field, value, rule, sourceRowIndex } sample matches/coverage entries. |
sourceRowIndexes / sourceRowIndexesOmittedCount | The first 50 original batch offsets contributing to this identity, plus exact count beyond that cap. |
partial | true when any completeness signal is not fully known, any list capped, a conflict occurred, or the row window wasn't fully read. |
sourceCoverage (run-level) | { complete, reason, droppedOversized } — whether the reader established the end of the supplied source, including a bounded probe at a row limit. |
product / schemaVersion | Fixed identifiers ("sanctions-source-coverage-review" / "1.0") for schema pinning. |
processedAt | When this Actor processed the row — never a screening or watch event date. |
Diagnostics (never billed) share a smaller, honest shape: { product, schemaVersion, status, found: false, error/reason, sourceRowIndexes? }, with status one of invalid_row, source_row_failed, unclassified_record_type, missing_identity, no_results, budget_stopped, partial_source, or the run-level max_results_reached summary row.
Evidence and boundaries
What "found" actually means here. A delivered coverage assessment requires found: true with a nonempty entityId/status/action, populated evidence, and valid sourceRowIndexes — never fabricated from a batch that contained nothing usable for a given identity.
Why a watch's routine heartbeat is never a negative result. A sanctions_update_alert row with found: false and every entity field null is the normal "no change since baseline" state, not a failure and not a clean-screening claim for any specific person or company. These rows are counted in baselineOnlyObservationCount only — they never contribute to changeEvents, and this product never asserts that the underlying watched entity is "clear" based on their absence of content.
Diagnostics carry no result event. The transform records rejections, but delivery is bounded to200 diagnostics and64KiB per row; the summary reports truncation/oversized omissions. After storage failures a summary itself may be unavailable. The rejection statuses are: invalid_row (not an object), source_row_failed (a truthy error/failureType/errorCode, checked even for non-string payloads like { message: "..." }), unclassified_record_type (a recordType other than the two known adapters'), or missing_identity (no usable input for a screening row, or no usable baselineKey/watchName for a watch row — exactly what happens to this Actor's own three prefilled watch rows, as explained in Input contract).
Why maxInputRows/maxResults caps and platform charge limits interact the way they do. This Actor reads at most maxInputRows source rows and delivers at most maxResults paid coverage assessments, independent of each other and both independent of the platform's own Max total charge USD run option — whichever bound is hit first ends delivery. Before reading any source row, the Actor verifies the run's charge limit can cover at least one assessment at the confirmed live price; if it cannot, the run stops immediately with the free budget_stopped diagnostic shown above.
Delivery is a dataset write, then an explicit charge — never the reverse, and never guaranteed exactly-once. Every delivered coverage assessment is written to your run's Dataset before the corresponding result-found charge event is requested, and the two steps are not transactional. On any ambiguity, this Actor fails closed rather than guessing — it never automatically retries a possibly-already-charged write. A run that fails partway through may still contain already-delivered, already-charged rows; that is expected for a partial run, not a bug. A same-run resurrection is refused by the shared billing ledger — a within-run guard only, never cross-run deduplication.
Re-running is new processing, not deduplication. Running this Actor again against the exact same input performs genuinely new processing and can bill again for the same queries/watches. Dedupe on your own side using identityNamespace + query/watchName if you need to avoid double-billing across scheduled refreshes.
Source resolution and permissions. Dataset references from datasetId/payload.resource.defaultDatasetId take precedence over run-only references and inline items. Conflicting same-kind IDs are rejected. Accessible run metadata is checked: a proven run/Dataset mismatch is rejected, while inaccessible metadata with a readable explicit Dataset permits processing with unverified run provenance. The Dataset resourcePicker READ grant enables private access; an envelope alone grants none. Source reads use bounded GET requests to https://api.apify.com. SDK output storage and billing perform writes. Tokens belong in Authorization headers, never in URLs.
Bounded everything, always. 1000 source rows / 4 MiB total input / 64 KiB per source row, per run. matches/changeEvents capped at 15 entries, sourceCoverage/unavailableSourceIds at 30, listsChecked at 30, evidence at 20, sourceRowIndexes at 50 — every cap ships with an exact omitted count and sets partial: true when anything was actually omitted. Deduplication identity for matches/changeEvents, and the freshest-observation tie-break for sourceCoverage, are always computed on the full raw value across your entire contributing row set, never a display-bounded copy.
What this Actor cannot verify. It cannot confirm whether a named entity is a real, currently-sanctioned party, whether a matched name is the same real-world person/company as your screened query, or whether a list source that fetched successfully is itself current and complete at the source. It reports only what your rows actually said, never a live list lookup or a compliance-clearance opinion.
Decision routing
For a screen: identity, confidence and action are set by one deterministic rule after the full batch for that query has been aggregated:
- If
matchesis nonempty →status: "matches_require_review",confidence: "medium",action: "review_matches_against_official_records". Real hits always route to human review regardless of list completeness. - Else, if
listsComplete === trueANDmatchesEvidenceComplete === true→status: "no_matches_lists_complete",confidence: "medium",action: "no_action_lists_complete". This reports only a no-hit observation with source-reported coverage; it is not independent verification or clearance. - Else →
status: "no_matches_lists_incomplete_or_unknown",confidence: "low",action: "review_list_coverage_before_relying_on_absence". An absence of matches with incomplete or unknown coverage is never treated as equivalent to a confirmed clean result.
For a watch: identity, the same shape applies to source-list availability instead of hit lists:
- If
changeEventsis nonempty →status: "watch_hits_detected",confidence: "medium",action: "review_watch_hits_against_official_records". - Else, if
sourceComplete === true(no conflict, every row agreed) →status: "no_new_hits_lists_complete",confidence: "medium",action: "no_action_lists_complete". - Else →
status: "no_new_hits_lists_incomplete_or_unknown",confidence: "low",action: "review_list_availability_before_relying_on_no_change". A watch whose underlying list sources were unavailable or conflicting is never treated as equivalent to a genuinely clean watch.
Route accordingly: a confidence: "low" result on either namespace means the absence of a hit cannot be trusted at face value — check unavailableSourceIds (watch) or the raw listsChecked/listsComplete signal (screen) before relying on a no-hit observation; this Actor never grants clearance. partial: true is an orthogonal signal from confidence — it flags that displayed lists were capped or a conflict occurred, not that the assessment itself is wrong.
Commercial playbooks
Case-file evidence attachment. Chain zinin/sanctions-screening → this Actor for every new counterparty screen, and attach the resulting coverage assessment (not the raw screener output) to the KYC case file. matchesEvidenceComplete/listsComplete give a reviewer an immediate, honest signal of whether "no matches" actually means something before they sign off.
Scheduled watch health check. Run this Actor daily/weekly against your zinin/sanctions-update-alert watch dataset. unavailableSourceIds and sourceCoverageConflict surface silent list-source outages that a "no new hits" summary alone would never reveal — a genuinely dangerous blind spot for ongoing monitoring if it goes unnoticed.
Cross-list coverage audit. Feed a batch of repeated screens for the same high-priority counterparty across weeks through this Actor. coverageDisagreement flags when your source actually checked a different set of lists between runs — a real configuration or outage signal worth investigating before trusting week-over-week "clean" results.
Internal compliance-ops validation gate. Insert this Actor immediately after your screener/watch pipeline, before rows reach a shared case-management system. A confidence: "low" result blocks that query/watch from being marked "cleared" until a human resolves the coverage gap — a cheap, deterministic gate against silently treating unknown coverage as a clean result.
Integration recipes
Chained via Apify Integrations (recommended for scheduled pipelines). In the Apify Console, open your zinin/sanctions-screening or zinin/sanctions-update-alert Task, add an "Actor/task run" integration pointed at this Actor, and map the upstream run's {{resource.defaultDatasetId}} into this Actor's Source Dataset field and supply any webhook envelope explicitly. The supplied references are reconciled. Accessible run metadata is checked against the Dataset; a proven mismatch is rejected. If run metadata is inaccessible but the explicit Dataset is readable, processing may continue with unverified run provenance (sourceRunIdVerified: false).
Direct API call. Start a run via POST https://api.apify.com/v2/acts/zinin~sanctions-source-coverage-review/runs?maxTotalChargeUsd=0.50 with your Authorization header, passing a JSON body such as:
{"datasetId": "<upstream dataset id>","maxInputRows": 200,"maxResults": 10}
Set maxTotalChargeUsd on the run options to a positive, bounded value covering at least one coverage assessment at your account's tier. Poll GET /v2/actor-runs/<runId> for status: "SUCCEEDED", then read GET /v2/datasets/<defaultDatasetId>/items for the delivered rows.
Webhook-notified pipeline (template, not a live registration). Attach a webhook for ACTOR.RUN.SUCCEEDED, pointed at your own HTTPS endpoint, with doNotRetry: true and Authorization as a header your endpoint checks — never a credential embedded in the URL:
{"eventTypes": ["ACTOR.RUN.SUCCEEDED"],"requestUrl": "https://your-service.example.com/webhooks/sanctions-coverage-review","headersTemplate": "{\"Authorization\": \"Bearer <YOUR_ENDPOINT_SECRET>\"}","doNotRetry": true}
The webhook payload itself never grants elevated access to the run's Dataset — treat it as a "go read the Dataset now" signal, and fetch rows via the Dataset API using your own stored token.
MCP / agent tool use. Pass the same datasetId/runId/items + maxInputRows/maxResults shape as any other caller. Surface confidence, matchesEvidenceComplete/sourceComplete, and matches/changeEvents directly to the end user rather than only a status string — those fields distinguish "source reports no observed hits" from "needs a human to check coverage first."
Operating guide
- Start with the real prefill. Run it once against the prefilled four-row sample to confirm the delivered shape and routing rules match your expectations — it costs the price of one coverage assessment (or less, at your account's tier) and uses genuinely real, previously-observed public data. Expect three free
missing_identitydiagnostics for the watch rows in this specific saved sample, explained in Input contract. - Use real
baselineKey/watchNamevalues from your ownzinin/sanctions-update-alertruns. The public prefill's watch rows cannot group (their identifiers were not captured in the public-safe projection); only rows that actually contain usable identifiers can formwatch:results. Inspect the source output before assuming those fields exist. - Set
maxInputRowsto your batch's real combined size. The default of 100 exists for a safe first test; under-setting it silently caps coverage (sourceCoverage.reason: "max_input_rows_reached",partial: true) without failing the run. - Set
maxResultsto your real number of distinct queries/watches. A real daily batch with many distinct screens or watches needsmaxResultsraised, or you will seemax_results_reachedand only get a partial report back. - Always set the platform's own Max total charge USD for any run touching a batch you have not sized before — a cap below one result stops before source reads; a cap that covers only part of the batch can deliver that part and then stop. Inspect the budget and delivery counters.
- Never treat a
low-confidence, no-hit result as clearance. That combination specifically means coverage or evidence was incomplete/unknown — checkunavailableSourceIdsorlistsChecked/listsCompletebefore signing off on any absence of a match. - Combine both feeds before this run, not after. This Actor's per-identity aggregation only sees rows supplied in one run's batch — merge screening and watch datasets (or list both inline) before submitting if you want them processed together; running each through separate runs is fine since the two namespaces never merge regardless.
FAQ
Does this Actor screen anyone itself, or check any list directly?
No. It is a pure post-processing coverage-reconciliation step over rows you already have — from zinin/sanctions-screening, zinin/sanctions-update-alert, or a hand-built list following the recognized row shapes. It never launches an upstream Actor and never fetches anything from a sanctions list source directly.
Why did my watch rows come back as missing_identity instead of a watch: result?
Because they lacked a usable baselineKey or watchName — exactly what happens with this Actor's own public prefill sample, explained in Input contract. A real zinin/sanctions-update-alert production run will carry these identifiers.
Is a matches_require_review result a confirmed sanctions hit?
No. It is a potential name match — a real string collision your source reported between the screened query and an entity on a list — that a human must review against official records before any conclusion is drawn. This Actor performs no identity verification, no fuzzy-vs-real-person judgment, and issues no verdict.
Why is listsComplete sometimes null instead of true or false?
Because no contributing row ever reported the field at all. A missing completeness signal is unknown, never rounded up to "complete" and never down to "incomplete" — this Actor never guesses at a value your source never supplied.
What does sourceCoverageConflict: true mean, and why doesn't the Actor just pick a winner?
It means two or more observations for the same list source tied on the freshest checkedAt timestamp but genuinely disagreed on content (completeness, error, or parsed-record count). Picking an arbitrary winner based on scan order would silently hide a real disagreement in your own source data — this Actor reports it as unresolved instead, with sourceComplete also set to null for that identity.
Can I get more than 15 matches or 30 source-coverage entries per identity?
Not in a single delivered row — those caps keep each row's payload bounded and predictable. omittedMatchCount and sourceCoverageOmittedCount always tell you the exact count beyond the cap; partition that query/watch’s source rows into smaller batches and inspect the omitted counts on each. A single-query filter alone does not remove per-query caps, and overlapping partitions must be deduplicated before combining counts.
Will running this Actor again on the same dataset cost me twice?
Yes, if the same queries/watches still qualify as useful assessments. There is no cross-run deduplication, only a same-run resurrection guard. Build your own dedupe key (identityNamespace + query/watchName) for a recurring schedule against overlapping data.
Sources and rights
This Actor processes only sanctions-source rows you supply — your own zinin/sanctions-screening runs, zinin/sanctions-update-alert runs, or any hand-built inline list following the recognized row shapes above. It holds no screening rights or licensing relationship of its own with OFAC, the EU Consolidated list, or any other sanctions-list publisher; you remain responsible for the terms under which your source data was originally obtained.
Related public Store Actors referenced or usable alongside this one, verified against this project's own live upstream inventory at the time this page was written:
zinin/sanctions-screening— the primary upstream source for this Actor'sscreening_resultrows.zinin/sanctions-update-alert— the primary upstream source for this Actor'ssanctions_update_alertrows.zinin/company-registry-enricher— a related public tool for verifying a screened company's real corporate-registry identity before treating a name match as confirmed; not itself compatible source input for this Actor.zinin/new-company-detector— a related public tool for building an initial counterparty watch list of newly registered companies to feed into a screening pipeline; not itself compatible source input for this Actor.zinin/litigation-check— a related public tool for pairing sanctions coverage with public litigation-mention evidence as part of a broader counterparty due-diligence file; not itself compatible source input for this Actor.
Public examples in this Actor's prefill and in this README are curated, provenance-tagged projections of actual public source runs captured on 2026-09-22, never synthetic fixtures — every value traces back to a real upstream Actor, run, and Dataset recorded in the row's own __source block, not a fresh scrape performed on every run of this Actor. The saved public projections exclude dates of birth, home addresses, nationality, and personal contact details. User-supplied records remain the caller’s responsibility.