# Tadawul Disclosure Monitor — Saudi Exchange (`generous_heavens/tadawul-disclosure-monitor-saudi-exchange`) Actor

Saudi Tadawul Disclosure Monitor is an Apify Actor that collects company announcements from the Saudi Exchange (Tadawul) and returns them as structured data.

It supports filtering announcements by company name, ticker, start date, end date, and maximum results.

- **URL**: https://apify.com/generous\_heavens/tadawul-disclosure-monitor-saudi-exchange.md
- **Developed by:** [Mohamed Youssef](https://apify.com/generous_heavens) (community)
- **Categories:** Automation, News, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Tadawul Disclosure Monitor — Saudi Exchange

An [Apify Actor](https://apify.com) that monitors official corporate disclosures published by the
**Saudi Exchange (Tadawul)** — [Issuer & Financial Advisor Announcements](https://www.saudiexchange.sa/wps/portal/saudiexchange/newsandreports/issuer-news/issuer-announcements).

It normalizes results into a clean, consistent schema, remembers what it has already returned so scheduled runs only surface **new** disclosures, and is built with an adapter architecture so more markets could be added later without touching the core logic (this build is intentionally scoped to Saudi Arabia only).

> **Honesty note about reliability.** Tadawul renders its disclosure list client-side with JavaScript. This Actor prefers Tadawul's own structured JSON announcement API when it's reachable, and falls back to a real, headless Chromium browser (via [Crawlee](https://crawlee.dev)'s `PlaywrightCrawler`) to page through the on-page list when more results are needed. It has **not** been run against the live site from inside this development environment (no outbound network access here), so treat the first run on Apify as a validation run — check the log output, and see "If scraping stops finding results" below if Tadawul has changed its markup/API since this was built.

***

### What it does

1. The adapter requests Tadawul's announcement API first, then falls back to a headless-browser pass over the announcement list page if more results are still needed.
2. Every disclosure is normalized into one unified schema (see below).
3. Disclosures are deduplicated against everything this Actor has returned before, using a persistent key-value store — so a daily scheduled run only returns what's new.
4. New (or, if `onlyNew` is off, all matching) disclosures are pushed to the Actor's **Dataset**.

***

### Input

| Field | Type | Description |
|---|---|---|
| `company` | string | Optional company name filter, e.g. `"Saudi Aramco"` |
| `ticker` | string | Optional ticker/symbol filter — Tadawul identifies companies by numeric code, e.g. `"2222"` |
| `Date` | string | Optional lower date bound, `DD-MM-YYYY` |
| `maxResults` | integer | Max disclosures to return (default `100`) |
| `onlyNew` | boolean | If `true` (default), only disclosures not seen in a previous run are returned. If `false`, everything found in the date range is returned, including previously-seen items. |
| `proxyConfiguration` | object | Apify Proxy settings. Recommended if Tadawul starts blocking Apify's default IPs. |

Example — everything:

```json
{
  "maxResults": 100,
  "onlyNew": true
}
```

Example — a single company:

```json
{
  "company": "Saudi Aramco",
  "ticker": "2222",
  "maxResults": 50,
  "onlyNew": false
}
```

A ready-to-use test input is in [`examples/input-tadawul-test.json`](examples/input-tadawul-test.json) — paste it into the Actor's **Input** tab (or the "Start" JSON editor).

#### Validation

- If no `company` or `ticker` is given, the Actor simply collects general disclosures for the date range.

***

### Output (unified schema)

Every row in the Dataset has this shape:

```json
{
  "country": "Saudi Arabia",
  "market": "Tadawul",
  "company_name": "Saudi Aramco",
  "ticker": "2222",
  "disclosure_title": "Saudi Aramco (2222) - Board of Directors' Decisions",
  "disclosure_date": "24-8-2026",
  "disclosure_time": "16:20",
  "disclosure_url": "https://www.saudiexchange.sa/.../announcement/123456",
  "source_data": {}
}
```

Any field the source doesn't provide is `null` — nothing is invented. Anything extra the source exposes that doesn't map cleanly to the unified schema is preserved under `source_data` instead of being discarded.

***

### Deduplication

The Actor keeps a persistent "seen" list in a named Apify Key-Value Store (`SEEN-DISCLOSURES-TADAWUL`), which — unlike the default run-scoped store — survives between runs of the same Actor on Apify. This is what makes `onlyNew` meaningful across scheduled runs.

The unique ID used for deduplication is chosen in this priority order (per disclosure):

1. the official disclosure ID (Tadawul's `anId`)
2. the disclosure's own URL
3. its document URL
4. a SHA-256 hash of its core fields (company, ticker, title, type, date, time), as a last resort

See [`src/utils/deduplication.js`](src/utils/deduplication.js).

***

### Daily monitoring / scheduling

1. In the Apify Console, open this Actor → **Schedules** → **Create new schedule**.
2. Pick a cron expression, e.g. `0 8 * * *` for every day at 8:00 AM (Actor's/container's timezone — set your desired timezone on the schedule itself).
3. Attach the input you want (typically `onlyNew: true`).
4. Each run will then only output disclosures that weren't seen in any previous run.

### Viewing results

- **Dataset tab** on the run: table/JSON/CSV/Excel view of every disclosure pushed in that run (use the **Overview** view for the key columns).
- **Storage → Key-value stores**: `SEEN-DISCLOSURES-TADAWUL` holds the internal "already seen" ledger (not meant for direct consumption, but useful for debugging deduplication).
- Downstream automation (n8n, Telegram, WhatsApp, email, further AI processing) can read the Dataset via the [Apify API](https://docs.apify.com/api/v2) or an Apify integration.

***

### Local development

```bash
npm install
## Windows/macOS/Linux with Chromium already available via `playwright install` if needed
apify run   # or: node src/main.js, after setting an input in storage/key_value_stores/default/INPUT.json
```

On the Apify platform, just create a new Actor from this source (or push with the Apify CLI: `apify push`) — the `Dockerfile` uses Apify's official Playwright/Chrome base image, so no manual browser installation is needed.

***

### Project structure

```
.actor/
  actor.json          Actor metadata + Dataset view definition
  INPUT_SCHEMA.json    Input form shown in the Apify Console
src/
  main.js              Entry point: validation, orchestration, summary logging
  adapters/
    base.js            Abstract MarketAdapter contract
    tadawul.js          Tadawul-specific scraping logic
  utils/
    normalizer.js       Maps the adapter's raw output to the unified schema
    deduplication.js    Persistent "seen disclosures" tracking
    dates.js             Date/time parsing helpers
examples/
  input-tadawul-test.json
Dockerfile
package.json
README.md (this file)
```

#### Adding another market later (e.g. EGX, ADX, DFM, LSE)

1. Create `src/adapters/<market>.js` exporting a class that extends `MarketAdapter` (see `src/adapters/base.js`) and implements `async collect()`, returning an array of `RawDisclosure` objects.
2. Register it and its `{ country, market }` pair in `src/main.js`.

Nothing else needs to change — `normalizeDisclosure()`, deduplication, dataset pushing, and logging are all market-agnostic.

***

### If scraping stops finding results

Tadawul is a JavaScript-rendered site and its exact markup/API can change over time. If a run logs `collected 0 disclosures`:

1. Open the run's log and check for the `WARNING`/`ERROR` lines from the adapter.
2. Try enabling Apify Proxy (residential) in the input — Tadawul may rate-limit or geo-block data-center IPs.
3. If the site's structure changed, the fix is localized to [`src/adapters/tadawul.js`](src/adapters/tadawul.js).

***

### Notes on the Apify Store listing

This Actor does not make guarantees like "100% uptime" or "always works" — like any scraper of a third-party website, it depends on the source site's markup/API staying reasonably stable and being reachable from Apify's infrastructure. Please respect Tadawul's terms of use; this Actor only reads publicly published disclosure pages and applies reasonable rate limiting (see `defaultCrawlerOptions()` in `src/adapters/base.js`).

# Actor input Schema

## `country` (type: `string`):

Which country's exchange to monitor. Leave on "Both" to run every supported market in one go.

## `market` (type: `string`):

Which exchange to monitor. This must match the selected country (Egypt → EGX, Saudi Arabia → Tadawul). Leave on "Both" to run every supported market.

## `company` (type: `string`):

Filter disclosures by company name, e.g. "Telecom Egypt" or "Saudi Aramco". Leave empty to collect disclosures from all companies.

## `ticker` (type: `string`):

Filter disclosures by ticker or symbol code, e.g. "ETEL" (EGX) or "2222" (Tadawul). Leave empty to collect disclosures for all tickers.

## `date` (type: `string`):

Only collect disclosures published on or after this date. Format: DD-MM-YYYY. Leave empty for no lower bound.

## `maxResults` (type: `integer`):

Maximum number of disclosures to return per market in this run.

## `onlyNew` (type: `boolean`):

If ON, the Actor remembers disclosures it has already seen (per market, in the Actor's key-value store) and only returns ones it hasn't seen before — ideal for daily scheduled monitoring. If OFF, it returns everything found in the given date range, including duplicates from earlier runs.

## `proxyConfiguration` (type: `object`):

Optional proxy settings. Using Apify Proxy (residential, if available) can help avoid being blocked by the exchanges' anti-bot protection.

## Actor input object example

```json
{
  "country": "Saudi Arabia",
  "market": "Tadawul",
  "company": "",
  "ticker": "",
  "date": "",
  "maxResults": 100,
  "onlyNew": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("generous_heavens/tadawul-disclosure-monitor-saudi-exchange").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("generous_heavens/tadawul-disclosure-monitor-saudi-exchange").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 '{}' |
apify call generous_heavens/tadawul-disclosure-monitor-saudi-exchange --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,generous_heavens/tadawul-disclosure-monitor-saudi-exchange"
        }
    }
}

```

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/Vs1PoHT1v388ggs7G/builds/ZHG9VKhiQVo2DpKkf/openapi.json
