DEV Community

Fast Data
Fast Data

Posted on

How to screen company names and domains against BaFin warnings in Python, and the false positive you will hit first

If you onboard merchants, pay suppliers or approve affiliates in Germany, one cheap check is: is this firm or website on a current warning from BaFin, the German financial regulator?

BaFin publishes these warnings as news items: unauthorised banking, clone websites, fake job offers, plus fines and other measures. There is no screening API. So I wrapped the public feeds into one, and this post shows how to call it from Python, and what the results look like on real data.

Disclosure: I built the BaFin Warnings actor on Apify. The routing logic below works with any warning source.

What the data looks like

I keep a daily snapshot of BaFin's German RSS feeds. The four snapshots from 8 to 11 November contain 36 notices, published between 9 October and 10 November:

Type Notices
Supervisory measures (fines, own-funds requirements, orders) 22
Consumer warnings (unauthorised firms, clone sites, fake jobs) 14
... of which name at least one website domain 11

The measures are mostly against licensed institutions, so they are a different signal from the warnings. For screening new counterparties, the 14 consumer warnings are the interesting part. They break down into unauthorised business (7), identity theft (4), fake job offers (2) and one general warning.

Two of the 14 had no usable firm name in the feed text alone: one is about a WhatsApp group, the other title quotes the scam sites' advertising slogan. The actor opens each warning page to fill these gaps; my offline archive only has the feed, which is why the numbers here are a lower bound.

The script

Screen mode takes names, domains or full URLs and returns one row per hit, or one matched: false row per name:

import os, requests

ACTOR = "fast_data_asap~bafin-investor-warnings"
URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items"

def screen(queries, min_score=85):
    r = requests.post(URL, params={"token": os.environ["APIFY_TOKEN"]}, timeout=120,
                      json={"mode": "screen", "queries": queries, "minScore": min_score})
    r.raise_for_status()
    return r.json()

def route(hit):
    if not hit.get("matched"):
        return "pass"
    if hit["matchedOn"].startswith("domain:") and hit["matchScore"] == 100:
        return "block"          # the exact website is named in a warning
    if hit["warningType"] == "identity_theft":
        return "review"         # see the next section
    return "review" if hit["matchScore"] < 100 else "block"

for hit in screen(["https://www.meinallianz.com/login", "Landshypotek Bank", "Example Trading Ltd"]):
    print(route(hit), hit["query"], hit.get("matchScore"), hit.get("matchedOn"), hit.get("url"))
Enter fullscreen mode Exit fullscreen mode

Names are normalised before matching: legal forms like GmbH, Ltd or AG are dropped, example(.)com obfuscation is resolved and URLs are reduced to their domain. Exact domain hits score 100.

Real results

I ran the same matching code offline against the 36 archived notices:

Query Score Matched on Warning type
meinallianz.com 100 domain meinallianz.com identity theft
https://www.klever-beratung.de/jobs 100 domain klever-beratung.de fake job offers
Klever Beratung GmbH 100 name Klever Beratung fake job offers
klever-beratung.com (other TLD) 95 name Klever Beratung fake job offers
Landshypotek Bank 97 name landshypotek-bnk identity theft
allianz.de 74 name meinallianz identity theft
Example Trading Ltd 48 (no hit)

The first four rows behave the way you want.

The false positive you will hit first

Look at Landshypotek Bank. Landshypotek is a real Swedish bank. The BaFin warning is about landshypotek-bnk.com, a website that pretends to be that bank. Screening the real bank's name scores 97, above the default threshold of 85.

This is not a matching bug. It is how identity-theft warnings work: the fraudulent site copies a legitimate name on purpose. The better your fuzzy matching, the more reliably the real firm matches the warning about its impostor. 4 of the 14 warnings in my sample are of this type, so you will see it early.

The fix is in the routing, not in the threshold:

  • Exact domain hit: block or escalate. The website itself is named by the regulator.
  • Name hit on an identity_theft warning: send to a human, and check which domain the counterparty actually uses. Here, landshypotek.se scores only 81 against the fake domain, under the threshold.
  • Name hit on other warning types: review. A different TLD (.com vs .de) can be the same operation or an unrelated firm.

Raising minScore to 98 would hide the Landshypotek row, but it would also hide the .com variant, which is exactly the kind of near-miss you want a person to look at.

Coverage, said plainly

  • The feeds cover recent weeks, not a full archive. In my snapshots, the oldest notice was about a month old. If you need history, run list mode daily and store the rows.
  • A missing hit means "not in current warnings", not "licensed". Licences are checked in BaFin's company database, which this does not query.
  • Natural persons are never returned: names of people in warnings are replaced with [name redacted].
  • It is a technical lookup, not legal advice. Every hit links to the BaFin page, which is the authoritative source.

Cost

Use Price
Screen one name or domain $0.01
Daily list of new warnings (20 rows) $0.04

A weekly re-screen of 200 active merchants costs $2.

Try it

BaFin Warnings Scraper on Apify: list mode for monitoring, screen mode for your own names and domains. Also usable from n8n or as an MCP tool for AI agents.

Question: if you run counterparty checks, how do you handle impersonation warnings: by domain only, manual review, or an allow-list of known legitimate firms?

Top comments (0)