AnswerLine Start free

Use cases

Adverse Media Screening API

Counterparty screening needs repeated checks of the open web. Google News and Search cover negative press, and AI engines answer 'is X trustworthy' questions directly, so their answers belong in the screen too.

Who it's for

Compliance and risk platforms adding an open-web layer, fintechs running KYC/AML checks, and due-diligence providers.

What you get

Try it

curl -X POST "https://api.answerline.dev/v1/monitor/google/news" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"acme holdings fraud lawsuit","country":"US"}'

Sync or queue it with POST /v1/async/task. Full field list on the Google News page and in the API reference.

Engines for this workload

What this data is and is not

Adverse media screening looks for negative public information about a person or organization: fraud allegations, litigation, regulatory action, corruption, financial crime. Much of it appears first in news coverage and search results, and in what AI assistants say when asked about a name.

The API returns that public layer as structured data:

Its limits:

Within those limits it handles collection: consistent, repeatable, logged searches across many subjects, in a form a review queue can use.

Metrics

Metric Definition
Raw hit rate Subjects with at least one result matching name and risk-term rules ÷ subjects screened.
Alert rate Subjects with at least one new, non-dismissed hit sent to review, per 1,000 subjects screened. This sizes your review team.
Review precision Hits confirmed relevant by a reviewer ÷ hits reviewed. Track per query template and risk term to prune templates that generate noise.
Re-alert rate Hits on URLs already dismissed for that subject ÷ hits. Near zero when suppression works.
Screening coverage Subjects whose scheduled screen completed on time ÷ subjects due.
Language coverage Subjects screened in each language relevant to them ÷ subjects with that language requirement.
Review latency Time from hit creation to reviewer decision.

Precision is the metric that improves the pipeline. Every reviewer decision is a label, so feed the decisions back into templates and matching rules.

Architecture

subjects (+ risk tier, names, context, languages)
     │
query builder (templates × languages × engines)
     │
scheduler ── onboarding / periodic rescreen ──> batch (≤500) ──> webhook
                                                                  │
               hits (subject, url, matched terms, evidence) <── matcher
                        │
              suppression (dismissed urls)  ──>  review queue  ──>  decisions (audit log)

Subject model

For each subject store the canonical name, known aliases and former names, transliterations, entity type (person or organization), context terms (employer, city, industry, registration country), relevant languages and markets, and risk tier. Collect only what your lawful basis and policy allow.

Query templates

A template combines a name, optional context and a risk-term group. Keep risk terms in data, per language:

Group English examples
Financial crime fraud, money laundering, embezzlement, Ponzi
Corruption bribery, kickback, corruption
Legal lawsuit, indicted, convicted, charged, settlement
Regulatory fined, sanctioned, license revoked, enforcement action
Other investigation, arrested, scandal

Build queries with Google’s operators:

"Jane Q. Example" (fraud OR "money laundering" OR embezzlement)
"Example Holdings" (lawsuit OR indicted OR settlement)
"Example Holdings" "Rotterdam" (fined OR sanctioned)

Long OR chains mix unrelated risk types in one result list and make precision hard to measure per group, so give each group its own query. Translate names (where they differ) and risk terms for each language, and pair each with country and hl for that market.

For AI engines, ask neutral questions: “What is publicly known about Example Holdings B.V. in Rotterdam?” A leading prompt (“What fraud has Example Holdings committed?”) invites the assistant to produce something.

Schedule

The idempotency key identifies one query for one subject in one screening:

am:{subject_id}:{screen_id}:{template_id}:{engine}:{lang}

A screening interrupted by a crash can be resubmitted in full; tasks already queued come back as RESOURCE_ALREADY_EXISTS. Delivery, signature and retry behavior is in async tasks and webhooks, and the AI visibility tracking guide has a complete receiver. The dashboard’s delivery log keeps delivered and dead deliveries for 7 days.

Storage schema

create table subjects (
  subject_id   text primary key,
  kind         text not null,            -- person | organization
  name         text not null,
  aliases      text[] not null default '{}',
  context      text[] not null default '{}',
  languages    text[] not null default '{en}',
  risk_tier    text not null
);

create table screens (
  screen_id    text primary key,
  subject_id   text not null references subjects,
  reason       text not null,            -- onboarding | periodic | event
  created_at   timestamptz not null default now()
);

create table screen_tasks (
  task_id      uuid primary key,
  screen_id    text not null references screens,
  template_id  text not null,
  engine       text not null,
  lang         text not null,
  query        text not null,
  status       text not null default 'QUEUED',
  finished_at  timestamptz
);

create table hits (
  hit_id       bigserial primary key,
  subject_id   text not null references subjects,
  url_key      text not null,
  url          text not null,
  title        text,
  snippet      text,
  source       text,
  raw_date     text,
  engine       text not null,
  first_task   uuid not null references screen_tasks,
  matched_terms text[] not null,
  name_match   text not null,            -- exact | alias | partial
  created_at   timestamptz not null default now(),
  unique (subject_id, url_key)
);

create table decisions (
  hit_id       bigint references hits,
  reviewer     text not null,
  decision     text not null,            -- relevant | not_subject | not_adverse | duplicate
  note         text,
  decided_at   timestamptz not null default now()
);

unique (subject_id, url_key) makes an article one hit per subject, however many templates and screens return it. A dismissed hit stays in hits with its decision, which makes hits the suppression list. decisions is append-only, so the review history is kept.

Change detection

On rescreen, a hit is new only if (subject_id, url_key) does not exist. Also flag:

End-to-end Python

Build a screening

import datetime as dt

HOOK = {"url": "https://hooks.example.com/adverse-media"}

RISK_TERMS = {
    "en": {"fincrime": ["fraud", '"money laundering"', "embezzlement"],
           "legal": ["lawsuit", "indicted", "convicted", "settlement"],
           "regulatory": ["fined", "sanctioned", '"enforcement action"']},
    "de": {"fincrime": ["Betrug", "Geldwäsche", "Untreue"],
           "legal": ["Klage", "angeklagt", "verurteilt"],
           "regulatory": ["Bußgeld", "Sanktionen"]},
}
MARKET = {"en": ("US", "en"), "de": ("DE", "de")}

def templates(subject):
    names = [subject["name"], *subject["aliases"]]
    for lang in subject["languages"]:
        country, hl = MARKET[lang]
        for n_i, name in enumerate(names):
            for group, terms in RISK_TERMS[lang].items():
                q = f'"{name}" ({" OR ".join(terms)})'
                yield f"{group}-n{n_i}", "GOOGLE_NEWS", lang, {"query": q, "country": country, "hl": hl}
        if subject["risk_tier"] == "high":
            context = f' ({", ".join(subject["context"])})' if subject["context"] else ""
            yield "ai-profile", "CHATGPT", lang, {
                "prompt": f"What is publicly known about {subject['name']}{context}? Cite sources.",
                "country": country}

def screen_tasks(subject, reason):
    screen_id = f"{subject['subject_id']}-{dt.date.today():%Y%m%d}-{reason}"
    tasks = [{"taskType": engine, "payload": payload, "webhook": HOOK,
              "idempotencyKey": f"am:{subject['subject_id']}:{screen_id}:{template_id}:{engine}:{lang}"}
             for template_id, engine, lang, payload in templates(subject)]
    return screen_id, tasks

Insert the screens row, submit the tasks in chunks of 500, and write a screen_tasks row for each accepted task with its template, engine, language and query text. Log refusals other than RESOURCE_ALREADY_EXISTS against the subject.

Match a delivery

apply runs on each verified, non-test delivery:

import hashlib, re, unicodedata
from urllib.parse import urlsplit, urlunsplit

def fold(text):
    return unicodedata.normalize("NFKD", text or "").encode("ascii", "ignore").decode().lower()

def url_key(url):
    p = urlsplit(url)
    return hashlib.sha256(urlunsplit(("", (p.hostname or "").removeprefix("www."), p.path.rstrip("/"), p.query, "")).encode()).hexdigest()

def name_match(text, subject):
    t = fold(text)
    if re.search(rf"\b{re.escape(fold(subject['name']))}\b", t):
        return "exact"
    if any(re.search(rf"\b{re.escape(fold(a))}\b", t) for a in subject["aliases"]):
        return "alias"
    tokens = fold(subject["name"]).split()
    return "partial" if len(tokens) > 1 and all(tok in t for tok in tokens) else None

def matched_terms(text, lang):
    t = fold(text)
    return sorted({term.strip('"') for group in RISK_TERMS[lang].values() for term in group
                   if fold(term.strip('"')) in t})

def apply(db, d):
    task = d["task"]
    with db.transaction():
        row = db.execute(
            """update screen_tasks st set status = %s, finished_at = now()
               from screens s where st.task_id = %s and st.status not in ('COMPLETED','FAILED')
                 and s.screen_id = st.screen_id
               returning s.subject_id, st.engine, st.lang""", (task["status"], task["id"])).fetchone()
        if not row or task["status"] != "COMPLETED":
            return
        subject_id, engine, lang = row
        s = db.execute("select name, aliases from subjects where subject_id = %s", (subject_id,)).fetchone()
        subject = {"name": s[0], "aliases": s[1]}
        r = d["response"]["result"]
        if engine == "GOOGLE_NEWS":
            items = [(i["link"], i.get("title"), i.get("snippet"), i.get("source"), i.get("date"))
                     for i in r.get("newsResults", [])]
        else:  # AI answer: the answer is one lead; its sources are the evidence to review
            answer = r.get("text", "")
            items = [(src["url"], src.get("label"), answer[:500], "AI answer source", None)
                     for src in r.get("sources", [])] if matched_terms(answer, lang) else []
        for url, title, snippet, source, raw_date in items:
            text = f"{title or ''} {snippet or ''}"
            nm = name_match(text, subject) if engine == "GOOGLE_NEWS" else "exact"
            terms = matched_terms(text, lang)
            if not nm or not terms:
                continue
            db.execute(
                """insert into hits (subject_id, url_key, url, title, snippet, source, raw_date, engine,
                                     first_task, matched_terms, name_match)
                   values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) on conflict (subject_id, url_key) do nothing""",
                (subject_id, url_key(url), url, title, snippet, source, raw_date, engine, task["id"], terms, nm))

The matcher is conservative: a hit needs a name match and a risk term in the title or snippet. The snippet is Google’s excerpt, not the article, so an article whose snippet lacks the risk term is missed. The query already required the risk terms, which is why matching on title plus snippet is enough without fetching pages.

For AI answers, a hit is created per cited source only when the answer text contains a risk term, and the reviewer reads the source, not the answer.

Review queue and metrics

-- Open review queue: newest hits without a decision, exact matches first
select h.hit_id, h.subject_id, h.name_match, h.matched_terms, h.source, h.title, h.url, h.created_at
from hits h
where not exists (select 1 from decisions d where d.hit_id = h.hit_id)
order by (h.name_match = 'exact') desc, h.created_at;

-- Review precision per template and engine over 90 days
select st.template_id, h.engine,
       count(*)                                                 as reviewed,
       avg((d.decision = 'relevant')::int)                      as precision
from hits h
join screen_tasks st on st.task_id = h.first_task
join lateral (select decision from decisions d where d.hit_id = h.hit_id order by decided_at desc limit 1) d on true
where h.created_at > now() - interval '90 days'
group by st.template_id, h.engine
order by precision;

-- Alert rate per 1,000 subjects screened this month
select 1000.0 * count(distinct h.subject_id) / nullif(count(distinct s.subject_id), 0) as alerts_per_1000
from screens s
left join screen_tasks st on st.screen_id = s.screen_id
left join hits h on h.first_task = st.task_id
where s.created_at >= date_trunc('month', now());

If your process must show what was seen and when, keep each task’s full response in immutable storage with the task id and completion time.

Cost planning

Price per request at the Starter tier: Google News $0.0002 (extra pages free); ChatGPT $0.0006; Gemini $0.0006. Failed tasks are not charged, and higher tiers, reached by 30-day spend, cost less. See balance and pricing.

The template builder above produces, per language and per name or alias, 3 risk groups = 3 Google News tasks at $0.0002 = $0.0006, plus one $0.0006 ChatGPT question per language for high-risk subjects.

Worked example: a fintech screens its business customers.

Workload Subjects Tasks per subject Cost per subject Cost
Onboarding, standard risk, 1 language, name only 400 per month 3 $0.0006 $0.24
Onboarding, high risk, 2 languages, name + 1 alias, with AI question 40 per month 14 2 × 2 × $0.0006 + 2 × $0.0006 = $0.0036 $0.144
Monthly rescreen, standard risk, reduced to 2 news queries 5,000 2 $0.0004 $2.00
Monthly rescreen, high risk, full set 300 14 $0.0036 $1.08
Monthly total $3.46

Moving standard-risk rescreens to quarterly cuts that row to about $0.67 a month on average. A new account’s 100 free requests run 33 standard onboarding screens of 3 tasks each for evaluation. Tiers are on pricing.

Pitfalls

Common names. Name plus risk term on a common name returns results about other people. Require context terms for persons with common names, rank exact matches first, and measure precision per subject type.

Transliteration and diacritics. “Müller”, “Muller” and “Mueller” are the same name. Store variants as aliases and fold diacritics before matching.

Treating AI answers as findings. Assistants can merge people with similar names or state unsupported claims. Send only their cited sources to review, and let reviewers decide from the source.

Leading prompts. Asking an assistant what crimes a subject committed invites fabrication. Ask neutral questions.

Relative dates. date is display text such as “3 days ago”. Keep it raw, record capture time, and let reviewers open the article for the publication date.

Silent coverage gaps. A screen is complete only when every task is COMPLETED or FAILED. Tasks that could not start within 4 hours fail with QUEUE_WAIT_EXCEEDED; batch items refused with INSUFFICIENT_CREDITS were never queued. Report screening coverage and resubmit gaps before marking a screen done.

Over-retention. Search results about individuals are personal data in many jurisdictions. Agree retention and access rules with your legal team before storing hits and responses.

Which engines fit

Engine Screening use Limits to remember
Google News Primary adverse news layer per market and language Snapshot of current results, not an archive
Google Search Pages outside news coverage, such as regulator notices, court-related pages and forums, when Google ranks them Broader and noisier; needs strict matching
ChatGPT What an assistant tells someone who asks about the subject, with sources as leads Answers can be wrong; review sources, not the answer
Gemini A second assistant view with sources Same caveat

Brand crisis AI monitoring covers watching AI answers during an incident, and the Google News engine page lists every request field.

Questions

Is this a complete adverse media or sanctions screening solution?

No. The API returns what public Google News results and AI assistant answers showed for a query at the time of the request. It does not check sanctions or PEP lists, verify identities or establish facts, and using it does not by itself satisfy any regulatory requirement. It is one input to a screening process your compliance team designs and reviews.

How do I reduce false positives for common names?

Add context terms that identify the subject, such as company, city, industry or date of birth year where lawful, require the full name as a quoted phrase, and route every hit to human review with the snippet and link. Record dismissed URLs per subject so they do not alert again.

Can AI answers be used as evidence?

Treat them as leads, not evidence. An assistant's answer can be wrong or conflate people with similar names. Follow its sources to the underlying articles and review those.

How often should subjects be rescreened?

That is a policy decision for your risk framework. Technically, each rescreen is a set of tasks; a common pattern is a full screen at onboarding and periodic rescreens whose frequency depends on the subject's risk tier.

What does screening one subject cost?

It depends on the query templates. A Google News task costs $0.0002, so a screen with one news query for each of three risk groups is $0.0006; adding a ChatGPT question adds $0.0006, a Gemini question $0.0006.

More use cases