Adverse media — exactly how it works, and what it does not do.
Adverse-media screening looks for negative news about your subject: financial crime,
regulatory action, cyber offences, organised crime. This page documents the whole pipeline — where
the articles come from, how a subject is bound to an article, what evidence you get back, what the
Unavailable status means, and the limits we have measured and not hidden.
Unavailable
— we did not check — instead of returning a clean pass.
Unavailable; put the same subject under monitoring and the coverage accrues behind them. Judge the feature against the job it actually does: ongoing monitoring of a known book of subjects. If your workflow is onboard a stranger and decide in this request, adverse media will return Unavailable essentially every time, and you should not price it into that decision.
What a row means — read this before you read a row. A row says one thing: this article mentions this name, and this article carries this theme. GDELT links names to themes across a whole document, so the name and the theme need never appear near one another — the person may be quoted as a commentator, or named in an unrelated paragraph. That binding is document-level co-occurrence. A row is not an allegation, and it is not a finding about the person.
Read the theme sceptically too. 54.5% of the
records that clear the gate are admitted by a catch-all rule and stamped
FinancialCrime, so that theme on its own tells you very little. Two rows carrying
FinancialCrime can have arrived by different routes: one because GDELT tagged the
article with a specific financial-crime theme, the other because it tagged nothing more precise and
the catch-all admitted it. The label does not record which. Do not total themes into a
per-person verdict — the API withholds the keys such a total would need: no name
identifier, no article identifier, no count, no score.
1 · Where the data comes from
We use GDELT — the Global Database of Events, Language and Tone — a free, open news monitoring project that indexes worldwide news coverage. We use three of its surfaces:
| Surface | What it gives us | How we use it |
|---|---|---|
GDELT GKG 2.0data.gdeltproject.org |
The entities GDELT itself extracted from each article — including the person entities named in it — plus themes and a document tone score. | Evidence. Tells us whether GDELT's own extractor identified your subject as a person named in that article. |
GDELT bulk 15-minute slicesdata.gdeltproject.org |
Every GKG record GDELT published in a 15-minute window — roughly 3 MB per slice, downloaded whole. | Forward coverage. Tails the live news firehose and matches each slice against the subjects we already know, so new coverage lands without asking the rate-limited search API for anything. |
GKG is what makes the feature work at all. DOC matches an article's full text but only hands back the headline — and real adverse coverage very often does not name the subject in the headline. A genuine hit like “Goldman Sachs settles shareholder lawsuit over 1MDB scandal” names no person at all. Reading GDELT's extracted person entities recovers exactly those articles.
adverse-media-expansion.md §2.2 rejected OpenSanctions' bulk data on exactly that ground (CC BY-NC 4.0, no exemption for commercial users). GDELT's terms are the opposite, and they are worth quoting rather than paraphrasing — see below. The obligation that comes back with that freedom is attribution, which is why a GDELT citation travels with the evidence into the API response, the PDF report and the regulator pack rather than sitting in a footnote. One boundary we hold ourselves to: the licence covers GDELT's own metadata and extracted entities, not the underlying article text. So we store URL, publisher, date, the bound person entity, theme and tone — and never article prose.
Using GDELT
“The GDELT Project is an open platform for research and analysis of global society and thus all datasets released by the GDELT Project are available for unlimited and unrestricted use for any academic, commercial, or governmental use of any kind without fee.”
Redistributing GDELT
“You may redistribute, rehost, republish, and mirror any of the GDELT datasets in any form. However, any use or redistribution of the data must include a citation to the GDELT Project and a link to this website (https://www.gdeltproject.org/).”
Quoted verbatim from gdeltproject.org/about.html, fetched 2026-07-20 and recorded in our own feasibility study rather than paraphrased from memory.
2 · Our own index — a screen never calls the network
Adverse media is not fetched live during your request. A background GKG-file tailer plus a one-time historical backfill populate our own index; the screening request reads that index and nothing else. The screening-time provider has no HTTP client at all — it is structurally incapable of making a network call.
-
Background sweep
Two background workers build our own index. The slice tailer downloads every new 15-minute GKG file and scans it for subjects; the historical backfill indexes past GKG files once, to extend coverage further back. Neither runs inside your request.
-
Bind the article to a person
An article is only attributed to your subject if GDELT's own extractor named a person whose tokens match the subject as an ordered, adjacent span within a single extracted person entity. Never a bag of tokens pooled across the article.
-
Score and store
What survives the relevance filter is stored as evidence: headline, publisher, date, category, matched person entity, and GDELT's tone (captured for display, never for a decision).
-
Your screen reads the cache
POST /v1/screenings/checkperforms one indexed read against the evidence cache. The screening-time provider is constructed without an HTTP client at all — it is structurally incapable of making a network call.
GdeltAdverseMediaProvider.cs:112). A name the system has no coverage for is not fetched during the screen — it is only covered once the slice tailer or a historical backfill reaches it, and the screen returns Unavailable immediately. That is precisely the onboarding moment, which is the worst possible time for a control to be silent, so we would rather you designed around it than discovered it: an applicant's very first adverse-media screen is expected to be Unavailable, and it is not a clean pass.
AdverseMedia:LiveFallbackEnabled is obsolete and has no effect. Since 2026-09-11 a screen answers only from our own GDELT corpus index, which the historical backfill and the 15-minute slice-tail fill: no screen makes a network call and no screen reads a prefetch cache. The key is still bound so an old config file keeps loading, and the service logs a boot warning when it is set to anything but its default. A screen with no hits reports Ok only when the index window reaches back to the retention start; over a shorter window it reports Unavailable and states the window it did check.
data.gdeltproject.org, GkgOptions.cs:20), no key, no rate limit — measured across multiple sampling runs without a single 429 (GkgSliceClient.cs:22-31). That is 96 batches a day, each a measured mean of ~2.4 MB compressed carrying ~587 articles, ~77% of which name at least one person. Two workers build our index from this feed: a tailer takes every new batch as it publishes, checks each article against the names already in the monitoring book, keeps what binds, and discards the rest — nothing from a slice is stored beyond the matched rows (GkgSliceClient.cs:28-31, GkgRecord.cs:33-38); and a historical backfill works back through the same free, unmetered archive — verified reachable years back over the same free HTTP (a June 2019 slice fetched clean: HTTP 200, 7.9 MB, no key, no throttle) — to extend our index's coverage further back than the tailer alone would reach. State the consequence plainly: we are not short of data. We receive all of it and filter down to names we already track. Our index's coverage is exactly what the tailer and the backfill have actually reached, never an assumed depth — see the coverage statement for the current floor.
20260720124500.gkg.csv.zip is the window beginning 2026-07-20 12:45 UTC — change the digits and you have another slice, which is exactly what GkgSliceClient.cs:72 does when it builds {BaseUrl}/{sliceStamp}.gkg.csv.zip against http://data.gdeltproject.org/gdeltv2. There is no index to crawl and nothing to authenticate against: a tiny pointer file, lastupdate.txt, always names the three newest files with their sizes and MD5s. GDELT publishes three files per window — .export. (events), .mentions. and .gkg. — and we take only .gkg., the one carrying extracted entities. It is a static file server, not an API (GkgSliceClient.cs:20): no key, no quota, and no query — you cannot ask it for a name, only for a time window.
Measured on 2026-07-20, the slice above was 4.7 MB zipped, 15 MB of CSV, 1,074 articles — tab-separated, one article per row. Slices vary in size; this one runs roughly double the ~2.4 MB / ~587-article mean quoted earlier, which is the normal spread of a news day rather than a discrepancy.
A single real row from that slice, trimmed to the fields we actually read:
URL https://www.intelligencer.ca/news/controversial-conference-presentation-sparks-protest…
SOURCE intelligencer.ca
THEMES PROTEST;TAX_FNCACT;EDUCATION;SOC_POINTSOFINTEREST_UNIVERSITY;EPU_POLICY…
PERSONS person-a; person-b; person-c; person-d; person-e; person-f (6 extracted — REDACTED)
ORGS hebrew university;instagram;goodes hall at queen university…
TONE -3.2590051457976,2.57289879931389,5.83190394511149,… (7 values; the first is overall tone)
The PERSONS field is redacted and is the only altered field. The real row names six identifiable living people who have done nothing wrong; publishing them in our own documentation would sit badly beside the Article 10 position further down this page. The URL, source, themes, organisations and tone values are verbatim. Trailing … marks a field trimmed for width, never edited.
3 · Unavailable is not a pass — read this before you integrate
adverseMediaStatus. An empty media result with status
Unavailable means the check did not happen. Your integration
MUST NOT treat it as a clean pass. A control that silently reports “no adverse media”
when it never looked is 100% false negatives while still looking like coverage — that is the single worst
failure mode this feature can have, so we surface it explicitly instead of hiding it.
adverseMediaStatus | Meaning | Is an empty result a clean pass? |
|---|---|---|
Skipped |
You opted out for this screen ("adverseMedia": false), or the feature is disabled. |
No — we did not look. |
Ok |
Evidence was read successfully. | Yes — an empty result is trustworthy. |
Stale |
Evidence was served from an older cached lookup. It may be out of date. Defined in the contract; not currently produced. | Qualified — treat with the age in mind. |
Unavailable |
No evidence exists yet for this subject in our index. | No. Adverse media was not checked. |
The status is persisted with the screening and rendered in the PDF report and the regulator pack, so an auditor can see, months later, whether a given screen actually checked the news.
4 · How a subject is bound to an article
Naive name matching on news text is how an adverse-media feature earns an analyst's contempt and gets switched off. Two controls stand between a name and a hit.
Ordered, adjacent name binding
A name must appear as an ordered, adjacent span — not a bag of tokens. A token-set match would let “Adam Smith and John Maynard Keynes” resolve to the subject “John Smith”. It cannot, here.
The rarity gate
A common name can never clear the bar on the name alone. For a high-collision surname, an extracted person entity is not accepted as identification, and we fall back to stronger headline evidence. The surname list is generated from our own corpus of 490,072 names — every surname shared by 25 or more distinct individuals.
The reason the second control exists is worth stating plainly: a better entity signal tells you an article is about a person of that name. It never tells you which one. Name ambiguity sets the bar; evidence has to clear it.
The rarity gate is also a fairness control, not only a precision lever. Common names concentrate in particular ethnic and national groups, so a matcher that floods on common names effectively discriminates by surname.
5 · What you get back with a hit
An adverse-media match is not a bare hyperlink. Every hit carries the evidence an analyst needs to triage it without clicking out, and an auditor needs to resolve it after the link has rotted.
| Field | What it is |
|---|---|
headline | The article headline. |
publisher | The publishing outlet's domain, for source credibility. |
publishedAt | When the article was published/indexed. A 2009 allegation and a 2026 conviction are not the same signal. |
externalId | The article URL. |
adverseMediaCategory | The FATF-style category (see below). |
matchedPerson | The person entity that bound the subject to the article. This is the “why” behind a hit whose headline names nobody. Null when the subject was identified from the headline alone. |
exculpatoryOutcome | An outcome term in the headline suggesting the subject was cleared rather than accused (“acquitted”, “charges dropped”). See below. |
articleTone | GDELT's own published tone score. Display-only. See §7. |
We never store article bodies. Adverse media on a natural person is criminal-conviction and offence data, so retention is kept to the metadata above. GDELT hands us every person it extracted from an article — typically several — and we store only the one that binds your data subject. The other names belong to people who never asked to be screened.
Exculpatory headlines are down-ranked, never suppressed
An article saying “Smith cleared of money laundering” puts the subject and a crime word in the same headline — which naive scoring reads as maximum adverse, for an article that exonerates him. Such a match is now down-ranked and flagged with the outcome term. It is not hidden: auto-suppressing it would trade a visible false positive for an invisible false negative, and an acquittal is still information an analyst wants to see.
6 · Categories
A hit is categorised from the financial-crime term that matched, so a tenant can set a different decision per category. The categories are:
| Category | Classified from |
|---|---|
| Financial crime | money laundering, fraud, corruption, bribery, embezzlement, Ponzi, scam |
| Cybercrime | cyber, hacking, ransomware, data breach |
| Regulatory | regulator, sanction, fine, censure, enforcement |
| High risk | terror, trafficking, organised crime, smuggling, racketeering |
| Unknown | matched adverse coverage that fits none of the above rules |
Classification runs over two inputs: the headline keywords, and — where the headline carries no crime word at all — the GDELT GKG themes attached to the article, run through the same table. Themes are language-independent; the keyword list is English.
The mapping is a keyword table, on purpose. It is auditable, and it can be shown to a regulator and contested by a data subject. A black-box classifier can be neither.
7 · Why we do not use sentiment analysis
The obvious idea is to score how negative an article is and flag the negative ones. We measured it, and it does not work — so tone is captured for display and is barred from every decision path (a build-time architecture test fails if tone ever reaches the scoring engine).
Negative ≠ relevant
An article can be scathing about a politician's dress sense — very negative, zero AML relevance. On a live sample of 1,440 articles, the most negative items were a prison riot, a depression story and a celebrity death. Zero predicate offences.
Relevant ≠ negative
A money-laundering indictment is often reported in clinical, neutral prose. A tone gate at −5 was measured to discard 44% of genuinely AML-relevant articles — an embezzlement suspension scores only −3.21.
Adversity is established by what the article is about — it matched a financial-crime term in its full text and the subject is named in it — not by how cross the journalist sounds. “The headline contains 'acquitted'” survives a regulator. “Tone: −1.8” does not.
8 · What it actually catches — measured
Against our adverse-media golden benchmark, replayed through the real filter, binder and emitter, at the shipped relevance threshold (0.7):
38.9% recall
14 of 36 benchmark positives — subjects with known, dated adverse media — are caught. Reading the PERSON entities GDELT already extracted, instead of the headline alone, lifted this from 16.7% (6 of 36), with no change to the threshold.
100.0% precision on common names
15 of 15 clean common-name subjects (the false-positive stress set) produce no hit. This is the number that decides whether an analyst keeps the feature switched on — and it is unchanged by the recall work above: the recall was bought with better evidence, not a lower bar.
2026-07-14T21:35:39Z. These figures are regenerated by the test suite and injected here by a
generator — they are never hand-written.
Unavailable — we had never fetched evidence for them. Adverse media actually ran for 4 of them, roughly 1 in 500.
The 15-minute slice tailer (shipped after that measurement) exists precisely to fix this, and it is enabled in our hosted deployment. But its effect on coverage is unmeasured, and we will not quote a projection as though it were a result. A benchmark run confirmed the mechanism works — the evidence store went from 0 to 1,530 lookups, the tailer's subject book from 0 to 1,317, at about 2 hits per slice — but that is proof of function, not of coverage. A cold one-shot batch cannot measure the improvement, because the batch that measures coverage is the same batch that enrols the subjects: every name is being met for the first time as it is counted. A real figure has to accrue over days of live tailing, and none is recorded. Treat the coverage figure as 0.2% measured (2026-07-15), improvement unmeasured.
9 · The limits — what this does not do
An honest limit is worth more to you than an inflated claim.
| Limit | Detail |
|---|---|
| Coverage of arbitrary names is low | The measured figure is above and it is the single most important number on this page. A name nobody has screened before will very likely return Unavailable on its first screen. That is the honest failure mode, but it is a coverage gap, not a feature. |
| Matching is exact, not fuzzy | Sanctions and PEP matching is fuzzy, phonetic and nickname-aware. Adverse-media binding is not. It compares normalized tokens with ordinal equality, so a transliteration, a diminutive or a misspelling in the news will not bind. This is a deliberate precision choice — fuzzy matching against an open news corpus floods — but it means we miss real coverage. |
| A common surname cannot self-identify | On a high-collision surname — measured from our own 490,072-name corpus as a surname shared by 25 or more distinct individuals — a name match alone is never accepted as identification, and provenance is suppressed so no report claims a resolution it did not earn. This is a fairness control as much as a precision one: a matcher that floods on common names discriminates by surname. |
| Source-coverage gaps we cannot tune away | Some subjects are missed because the source never names them. Our own benchmark still misses Roger Ng, a convicted 1MDB banker: GDELT returns on-topic articles, but its extractor names Jho Low and Tim Leissner and never him. No threshold change fixes that. |
| English retrieval terms | The negative-keyword retrieval terms and the headline keyword classifier are English. GKG themes are language-independent and partly compensate, but non-English coverage is under-represented. |
| No article bodies, no story clustering | We hold headlines and metadata, never article prose — it is unlicensed and we will not pretend otherwise. We also do not yet collapse one story syndicated across many outlets into a single event, so a widely-carried story can appear as several hits. |
| Discovery, not adjudication | We can tell you an article is about a person with your subject's name. We do not confirm it is your subject. A commercial vendor sells human-curated, entity-resolved dossiers. This is not that, and we will not imply it is. |
10 · Using it from the API
Adverse media runs by default. Send "adverseMedia": false to opt out of it for a screen
(the status then comes back as Skipped).
# Screen with adverse media (the default)
curl -X POST https://wl.proovid.com/v1/screenings/check \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fullName": "Jho Low", "adverseMedia": true }'
The response carries adverseMediaStatus alongside the matched entities. Check it
before you treat an empty media result as clean:
{
"decision": "Review",
"adverseMediaStatus": "Ok",
"matchedEntities": [
{
"sourceList": "ADVERSE_MEDIA",
"rejectionTag": "adverse_media",
"adverseMediaCategory": "FinancialCrime",
"headline": "...",
"publisher": "...",
"publishedAt": "...",
"matchedPerson": "..."
}
]
}
adverse_media and resolves to Review — a human dispositions it. The automatic-Fail branches key on sanctions and criminal matches only. A hit can still reach Fail, but only because a tenant configured it to — a per-category warning-type override, or an escalate-high-risk-band policy. It never happens on the strength of the news hit alone.
Bringing your own adverse-media endpoint is also supported — see Data providers.
Browsing the index — GET /v1/adverse-media/mentions
One row per mention — this article, which mentions this name, carrying this theme —
newest capture first. It reads the article corpus, not your screening history: the index carries no
tenant column, and this query never joins tenant-scoped screening output. The caller must hold the
Screening.Read policy.
curl -G https://wl.proovid.com/v1/adverse-media/mentions \
-H "X-Api-Key: $API_KEY" \
--data-urlencode "name=Jho Low" \
--data-urlencode "from=2024-01-01T00:00:00Z" \
--data-urlencode "pageSize=50"
| Query parameter | What it does |
|---|---|
name | Mentioned name. Trigram-matched against the normalized name held in the index, so spelling variants match. |
from | Inclusive lower bound on capture time. |
to | Exclusive upper bound on capture time. |
cursor | Opaque continuation token, copied from the previous page’s nextCursor. A token this API did not issue returns 400. |
pageSize | Default 50, maximum 100. A larger request is clamped to 100 and served, not rejected. |
The page carries its rows, the cursor that continues it, the size actually served, and the statement of what a row means. That last field travels on the payload so a client cannot render rows without also being handed the caveat:
{
"rows": [
{
"capturedAt": "2026-02-14T09:15:00Z",
"url": "https://...",
"title": "...",
"domain": "...",
"mentionedName": "...",
"matchedTheme": "FinancialCrime"
}
],
"nextCursor": "MTc3...",
"pageSize": 50,
"disclosure": "Each row means: this article mentions this name and carries this theme. GDELT binds names to themes at document level, so a row is co-occurrence within one article - not an allegation against the person named, and not a person-level finding."
}
nextCursor until it
comes back null; that is the end of the result set. No row count is returned, for two
reasons: a keyset page does not need one, and a count of mentions is exactly the per-person tally the
caveat above says this data cannot support. OFFSET paging is
not offered either — on a table heading for roughly 1.3 million rows, page 40 would cost forty
times page 1, while a cursor seek costs the same at any depth.
| Status | When |
|---|---|
200 | One keyset page of mentions. An empty rows array with a null cursor means the filters matched nothing. |
400 | The continuation cursor could not be read. |
403 | The caller does not hold Screening.Read. |
Operator configuration — the knobs that matter
These are the settings that change what this feature is, rather than tuning it at the margin.
Everything below is read from configuration at startup; there is no AdverseMedia section in
appsettings.json, so every value comes either from the C# option defaults or from your
deployment's environment.
| Knob | Default | What moving it does |
|---|---|---|
AdverseMedia:Prefetch:* |
— |
Retired 2026-09-15, no effect. The DOC-based per-subject prefetch is removed; a set value now only logs a boot warning. |
AdverseMedia:LiveFallbackEnabled |
false |
Retired, no effect. A screen reads only the corpus index. Any non-default value logs a boot warning. |
AdverseMedia:LookbackYears |
— |
Retired 2026-09-15, no effect. Coverage depth is now whatever the GKG slice tail + historical backfill have indexed, from 2026-05-28. A set value now only logs a boot warning. |
AdverseMedia:SliceTail:Enabled |
false in code |
The 15-minute forward tailer. Not rate-limited, but forward-only on its own — back-history comes from the separate historical backfill. It ships OFF: turning it on is a deliberate, separately-approved action, never a side effect of a deploy. On our own hosted deployment this key is ON, alongside the historical backfill, giving index coverage from 2026-05-28. A self-hosted install that leaves the key unset collects nothing until it sets it. |
Per-tenant BYO provider |
unset |
Point a tenant at their own adverse-media endpoint/credentials, independent of our own index. |
Seeding the index — what you will see while it fills
A fresh deployment starts with an empty GKG index and fills it by replaying GDELT’s 15-minute slices backwards through the retention window. The figures below are measured on our own node.
| Measure | Value |
|---|---|
| Backfill rate | ~6,194 slices per hour. |
| A full five-year seed | 175,296 slices — 1,826 days at 96 slices a day. At the rate above that is roughly 28 hours, not an afternoon. |
| Ingest passes | 48–68 minutes each, during which the backfill does not advance. |
ingest claims the slot it holds it for 48–68 minutes, and for that whole window the
backfill’s progress counter does not move at all — same phase, same done-of-total, no
errors. That is the scheduler doing what it is designed to do. Before reporting a stalled seed, open
Background jobs and check which job is currently running: if it is
ingest, the backfill is queued, not broken.
Data protection — what you are responsible for
Attribution
Adverse-media results are derived from The GDELT Project (gdeltproject.org). Headlines, publishers and links are the property of their respective publishers. This citation is a condition of GDELT's terms, not a courtesy, so it travels with the data: it is rendered alongside adverse-media results in the API response, the PDF report and the regulator pack. Screening assists your compliance programme; it is not legal advice, and an adverse-media hit is an indication to review, not a finding of wrongdoing.