# EU Insider Trading Scanner (AFM Directors' Dealings) (`goobertje/eu-insider-trading-scanner`) Actor

Read managers' transactions (MAR Art. 19) from the Dutch AFM register, convert values to EUR, and score open-market buys with a documented 0-100 heuristic and cluster detection. Covers AFM-supervised issuers only. Not investment advice.

- **URL**: https://apify.com/goobertje/eu-insider-trading-scanner.md
- **Developed by:** [goober](https://apify.com/goobertje) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 transaction scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## EU Directors' Dealings API: insider buys and sells from the AFM register, scored 0-100

Get recent insider trades (managers' transactions under EU MAR Article 19) as clean JSON: issuer, ISIN, person and role, buy or sell, price, quantity, value in EUR and a link to the official record. It is for investors, analysts and developers who want a directors' dealings feed without reading registers by hand. Open-market buys also get a documented conviction score with cluster detection.

> **Coverage: Netherlands only (AFM register).** Germany, France and Belgium are not covered, see the table below. The register includes foreign companies listed in Amsterdam, but it is not all EU insider trades.
>
> **Not investment advice.** The score is a heuristic computed from public filings. It is not a recommendation to buy or sell anything and says nothing about future returns.

### Why use this Actor

- Official source: the public AFM register of MAR Article 19 notifications, no scraping of third-party sites.
- Every row is normalised (buy / sell / other) and flagged as open-market or not, with the reason.
- Values converted to EUR with ECB reference rates of the transaction date.
- Conviction score 0-100 for open-market buys, with the points per component in the output.
- Plain HTTP, no proxies, no browser. One request per second to the AFM, so it is polite and cheap.

### Quick start

Click **Start** with the prefilled input:

```json
{ "lookbackDays": 14, "maxItems": 5, "transactionTypes": ["buy"] }
```

After about 2 minutes you get up to 5 open-market buys from the last 14 days, each with value in EUR, role, conviction score and `sourceUrl`.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `lookbackDays` | 30 (prefill 14) | Only transactions dated within this many days before today |
| `maxItems` | 100 (prefill 5) | Hard cap on output rows; each row is billed |
| `openMarketOnly` | true | Skip grants, share plans, exercises, swaps, dividends, gifts, transfers, bonds |
| `transactionTypes` | buy, sell (prefill buy) | Any of `buy`, `sell`, `other` |
| `minConvictionScore` | 0 | Keep only open-market buys with at least this score |
| `minValueEur` | 0 | Minimum trade value in EUR |
| `clusterWindowDays` | 14 | Window (+/- days) for cluster detection |
| `issuerContains`, `isins` | empty | Optional issuer/ISIN filters; blank entries are ignored |
| `maxNotifications` | 1500 | Safety cap on pages read |
| `requestDelaySecs` | 1 | Delay between requests (minimum 1) |

### What you get

Real item from a live run (input: `lookbackDays` 30, `maxItems` 5, buys only, `minValueEur` 5000). More samples are in `sample_run/`.

```json
{
  "id": "202609245F7409F8-223C-41D0-8368-4CA34634D263-0",
  "country": "NL",
  "regulator": "AFM",
  "issuer": "Shell Plc",
  "issuerLei": "21380068P1DRHMJ8KU70",
  "isin": "GB00BP6MXD84",
  "person": "Sawan W.",
  "personRole": "Chief Executive Officer",
  "roleCategory": "ceo",
  "transactionType": "buy",
  "registerTransactionType": "Koop",
  "registerTransactionCategory": "Verwerving",
  "instrument": "Gewoon aandeel",
  "instrumentClass": "equity",
  "isOpenMarket": true,
  "nonMarketReason": null,
  "price": 42.21,
  "currency": "EUR",
  "quantity": 2211.85,
  "valueEur": 93362.19,
  "fxRateEur": 1.0,
  "fxRateDate": null,
  "transactionDate": "2026-09-24",
  "notificationDate": null,
  "venue": "EURONEXT - EURONEXT AMSTERDAM",
  "partOfOptionProgram": false,
  "convictionScore": 79,
  "convictionBreakdown": { "role": 25, "size": 14, "relativeSize": 10, "cluster": 30, "venuePenalty": 0 },
  "clusterInsiderCount": 8,
  "clusterWindowDays": 14,
  "sourceUrl": "https://www.afm.nl/en/sector/registers/meldingenregisters/transacties-leidinggevenden-mar19-/details?id=202609245F7409F8-223C-41D0-8368-4CA34634D263",
  "scrapedAt": "2026-10-18T22:20:13Z"
}
```

Field notes:

- `registerTransactionType`, `registerTransactionCategory` and `instrument` are the register's original (Dutch) words; `transactionType`, `instrumentClass` and `roleCategory` are the normalised versions.
- `notificationDate` is always `null`: the AFM register publishes only the transaction date. `scrapedAt` tells you when the row was read.
- `valueEur` is `price x quantity` converted at the ECB reference rate (`fxRateEur` = currency units per EUR, `fxRateDate` = rate date). It is `null` for bonds (prices are quoted in percent of nominal), for currencies the ECB does not publish, and when price or quantity is missing. Pence quotes (GBX) are converted to pounds.
- Unknown values stay `null`; nothing is guessed.

### Open-market definition

A row is `isOpenMarket: true` only if all of these hold: the register's transaction type is a plain purchase (`Koop`) or sale (`Verkoop`); the instrument is plain equity (shares, share classes, depositary receipts); it is not flagged as part of a stock-option programme; and price and quantity are positive. Everything else (`Dividend`, `Schenking/gift`, `Uitoefening` of options, `Omwisseling` of instruments, `Overdracht`, `Uitlenen`, conditional awards, restricted/performance share rights, bonds, purchases made by an AIF manager) is `isOpenMarket: false` with the reason in `nonMarketReason`. The register's `Koop` label is used as reported; some small `Koop` rows are in practice automatic plan or dividend-reinvestment purchases (see limits).

### Conviction score (0-100, open-market buys only)

| Component | Max | Rule |
| --- | --- | --- |
| Role | 25 | CEO or CFO 25, chair 20, other executive 15, board member 12, other 8, unknown 5, closely associated person 6 (role read from the free-text position) |
| Size | 25 | Log scale of the trade value in EUR: EUR 5,000 gives 0, EUR 1,000,000 or more gives 25 |
| Size relative to the person's prior buys | 20 | Value vs the median of that person's earlier open-market buys in the data window: 3x or more 20, 1.5x 15, 0.75x 10, lower 5; no earlier buy seen gives a neutral 10 |
| Cluster | 30 | Other insiders of the same issuer with an open-market buy within +/- `clusterWindowDays`: 1 other 15, 2 others 24, 3 or more 30. Only buys worth at least EUR 5,000 count, on both sides |
| OTC penalty | -10 | Trade reported with venue `OTC` (may be a negotiated or private deal) |

The total is clamped to 0-100, and the per-component points are in `convictionBreakdown`. "Prior buys" and clusters only see data inside the walked window (lookback plus the cluster window), not the whole history of a person.

### Pricing

Pay per event: one `transaction-scraped` event per transaction row written to the dataset, **USD 10.00 per 1,000 transactions** (USD 0.01 each).

- Not charged: rows removed by your filters, non-market rows when `openMarketOnly` is on, empty rows, and rows that could not be read.
- `maxItems` caps the rows and therefore the cost of a run. The run also stops when your maximum cost per run is reached.
- Example: `maxItems` 100 with enough matching trades costs at most USD 1.00.

Runtime is dominated by the polite 1 request per second: roughly 100 notifications take about 2 minutes, and one notification holds about 1.5 transaction rows on average.

### Use with AI agents (MCP, Claude, Cursor, n8n, Make)

AI agents can call this Actor through the Apify MCP server. Add it to your MCP client config (sign in with OAuth in the browser on first use):

```json
{ "mcpServers": { "apify": { "url": "https://mcp.apify.com?tools=goobertje/eu-insider-trading-scanner" } } }
```

Example prompts and the input each one maps to:

1. "Show me open-market insider buys at AFM-registered companies from the last 7 days."
   `{ "lookbackDays": 7, "maxItems": 50, "transactionTypes": ["buy"] }`
2. "Which insider buys over 50,000 euro have a conviction score of at least 60 this month?"
   `{ "lookbackDays": 30, "transactionTypes": ["buy"], "minValueEur": 50000, "minConvictionScore": 60, "maxItems": 50 }`
3. "Any insider trades at Unilever or Shell in the last 30 days, buys and sells?"
   `{ "lookbackDays": 30, "issuerContains": ["unilever", "shell"], "transactionTypes": ["buy", "sell"], "maxItems": 100 }`

In n8n, Make or Zapier use the Apify node or module, pick this Actor and paste the same JSON.

### Use cases

- Screen for clusters of open-market buys by several insiders of one issuer.
- Daily or weekly directors' dealings feed for a watchlist (`issuerContains`, `isins`).
- Compare insider buying and selling by role or issuer over time for research.
- Add a "recent insider activity" field to a stock screener or newsletter.

### FAQ

**Where does the data come from and is it allowed?** From the public AFM register of managers' transactions (MAR Article 19) and ECB reference rates. The AFM allows reuse with the AFM named as source. Details under Legal.

**How fresh is it?** As fresh as the register, usually within days of the trade. Late or corrected notifications can change later.

**How much does a typical run cost?** The prefilled run (5 rows) costs at most USD 0.05. 1,000 rows cost USD 10.

**What if the AFM blocks me or changes its pages?** On repeated HTTP 403/429 the run stops, logs an error and delivers the rows already read; the status message starts with PARTIAL. If the page layout changes, rows may be missing and the status message says so.

**Can I schedule it or get only new items?** You can schedule it in Apify. There is no built-in "only new" mode; use the stable `id` field to skip rows you already stored.

**What is not included?** Other countries, the notification or publication date (the register does not give it), intraday timestamps, and any investment recommendation.

### Coverage

| Country | Source | Status |
| --- | --- | --- |
| Netherlands | AFM register "Managers' transactions MAR 19" | **Supported, tested live** |
| Germany (BaFin) | portal.mvp.bafin.de | Not built: its robots.txt is `Disallow: /` for all agents |
| France (AMF / BDIF) | bdif.amf-france.org | Not built: the open API only returns document metadata, the transaction details are inside PDFs (no PDF parsing here) |
| Belgium (FSMA) | fsma.be | Not built: no machine-readable PDMR export was verified, robots.txt asks for a 30 s crawl delay |

The AFM register lists notifications for issuers supervised by the AFM. Many large foreign issuers (for example Shell, Coca-Cola Europacific, Unilever, STMicroelectronics) also appear there because they are listed in Amsterdam.

### Limits and honest caveats

- Only the Netherlands (AFM) is covered, see the coverage table.
- Data freshness is that of the AFM register (usually within days of the trade). Late or corrected notifications can change after you scraped them.
- Fractional quantities, tiny values and many insiders buying the same amount on the same day often mean automatic plan or dividend-reinvestment purchases, not discretionary conviction. The EUR 5,000 floor for cluster counting reduces but does not remove this. Check the source filing.
- Register values are entered by the notifying companies and can contain mistakes (for example inconsistent prices inside one notification). They are passed through as published.
- The run reads one page per notification, so very large lookbacks (hundreds of days) take a long time; use `maxNotifications` and `lookbackDays` to bound it.
- If the AFM site answers with HTTP 403/429 repeatedly, the run stops, logs an error and delivers (and bills) the rows already read; the status message then starts with PARTIAL. Any page that cannot be read mid-walk (not found, redirected off-site, unparsable) also ends the walk with a PARTIAL status message and a log line. It never tries to bypass blocking.
- People are matched by name only, per issuer (the register has no person ID). Two different people with the same name at one issuer are treated as one, and a person spelled differently across filings is treated as two. Closely associated persons are scored but never count as an independent insider in a cluster. Duplicate or corrected notifications of the same trade (same issuer, person, ISIN, date, price, quantity, type) are output once.
- The register is ordered by publication, not transaction date, so the walk stops only after 10 consecutive notifications older than the lookback plus cluster window plus a 14-day margin. The lookback date uses the Europe/Amsterdam calendar day.
- Rows with no ISIN, no instrument, and neither price nor quantity are dropped and never billed.

### Legal

Data comes from the public register that the AFM publishes under MAR Article 19 and from the ECB euro foreign exchange reference rates. The AFM website states that its information may be copied or distributed only with explicit acknowledgement of the AFM as the source: **source: Autoriteit Financiele Markten (AFM), afm.nl**. Every item carries the `sourceUrl` of the official record. The Actor applies the AFM robots.txt rules checked on 2026-10-19 (Disallow: /sitecore only), identifies itself in the User-Agent, waits at least one second between requests and does not use proxies, browsers or any evasion. You are responsible for using the data in line with applicable law (including data protection law for the personal names in the register) and the AFM terms.

This Actor is not affiliated with, endorsed by or sponsored by the AFM, the ECB or any issuer. **Nothing here is investment advice.**

Found a problem or want a field added? Open an issue on the Actor page.

# Actor input Schema

## `lookbackDays` (type: `integer`):

Only transactions with a transaction date within this many days before today are output. The run walks the register from newest to oldest and stops at this date (it reads a further Cluster window of context for cluster detection). Range 1 to 365. Example: 14.

## `maxItems` (type: `integer`):

Maximum number of transactions (dataset items, each one billed) a run outputs; the newest matching ones are kept. Range 1 to 10000. Example: 100.

## `openMarketOnly` (type: `boolean`):

If enabled, only plain-equity buys and sells are output. Option grants, share-plan awards, option exercises, share swaps, dividends, gifts, transfers, bond trades and rows without a positive price or quantity are skipped and not billed. Disable to get every row, flagged by isOpenMarket. Example: true.

## `transactionTypes` (type: `array`):

Which normalised transaction types to output: buy, sell, other (everything that is neither, such as grants, gifts and dividends). Example: \["buy"] for buys only.

## `minConvictionScore` (type: `integer`):

Only output open-market buys whose 0-100 conviction score is at least this value (sells and other rows have no score and are dropped when this is above 0). Range 0 to 100. Example: 50.

## `minValueEur` (type: `number`):

Only output transactions worth at least this many euros (price x quantity, converted with the ECB reference rate of the transaction date). Rows whose value cannot be computed are dropped when this is above 0. Example: 25000.

## `clusterWindowDays` (type: `integer`):

Cluster detection: other insiders of the same issuer who also made an open-market buy within plus or minus this many days raise the conviction score. Range 1 to 90. Example: 14.

## `issuerContains` (type: `array`):

Optional. Only output transactions whose issuer name contains one of these texts (case-insensitive). Example: \["unilever", "asml"].

## `isins` (type: `array`):

Optional. Only output transactions in these ISINs (case-insensitive). Rows without an ISIN are skipped when this is set. Example: \["NL0010273215"].

## `maxNotifications` (type: `integer`):

Safety cap on the number of AFM notification pages fetched (one request each, at least 1 second apart; a notification holds one or more transaction rows). Range 1 to 10000. Example: 1500.

## `requestDelaySecs` (type: `number`):

Pause between requests to the AFM website to stay polite. Minimum 1, maximum 30. Example: 1.0.

## Actor input object example

```json
{
  "lookbackDays": 14,
  "maxItems": 5,
  "openMarketOnly": true,
  "transactionTypes": [
    "buy"
  ],
  "minConvictionScore": 0,
  "minValueEur": 0,
  "clusterWindowDays": 14,
  "issuerContains": [],
  "isins": [],
  "maxNotifications": 1500,
  "requestDelaySecs": 1
}
```

# Actor output Schema

## `transactions` (type: `string`):

Normalised directors dealings with value in EUR, open-market classification and conviction score.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "lookbackDays": 14,
    "maxItems": 5,
    "transactionTypes": [
        "buy"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("goobertje/eu-insider-trading-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "lookbackDays": 14,
    "maxItems": 5,
    "transactionTypes": ["buy"],
}

# Run the Actor and wait for it to finish
run = client.actor("goobertje/eu-insider-trading-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "lookbackDays": 14,
  "maxItems": 5,
  "transactionTypes": [
    "buy"
  ]
}' |
apify call goobertje/eu-insider-trading-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,goobertje/eu-insider-trading-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TyXHU9enyjJeCAQ8u/builds/WyKSwiuqdKjXVZZ3e/openapi.json
