# Contracts Finder, Find a Tender and TED Monitor (`omargnagy/tender-monitor`) Actor

Returns UK and EU public tender notices from Contracts Finder, Find a Tender and TED as JSON: title, buyer, country, value in EUR, deadline, CPV codes, a fit score with reasons and the notice link. Filter by sector, value and deadline; schedule it to get only new notices.

- **URL**: https://apify.com/omargnagy/tender-monitor.md
- **Developed by:** [Omar Nagy](https://apify.com/omargnagy) (community)
- **Categories:** Lead generation, Business, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 tender notices

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Contracts Finder, Find a Tender and TED Monitor

This tender monitor reads UK Contracts Finder, UK Find a Tender and EU TED (Tenders Electronic Daily) in one run and returns the public tender notices that fit your sector, budget and deadline as clean JSON, best match first. Every notice comes with the buyer, country, stated value converted to EUR, deadline, CPV codes, a fit score with the reasons behind it, and the link to the official notice.

Run it once to search the last few days, or put it on a weekly schedule with **Only return notices not delivered before** switched on, and each run hands you only the tenders you have not seen yet.

### What the tender monitor returns

One dataset row per open tender notice:

- **What and who:** `title`, `buyer`, `buyer_country` (ISO code), `source` and the notice `url`
- **Money:** `value` and `currency` as the buyer stated them, `value_eur` converted with ECB reference rates, and a `value_note` when a value was a placeholder (such as 1 EUR) or in a currency with no rate
- **Timing:** `deadline`, `days_to_deadline`, `published` and `contract_months`
- **Classification:** the main `cpv` code, every CPV code in `cpv_all`, and for TED the English `cpv_label`
- **Fit:** a `score` from 0 to 100 and a `why` list in plain words ("CPV 72220000", "stated value 85,000 EUR, under the 100,000 EUR cap", "38 days to the deadline")
- **Monitoring:** `is_new` and `first_seen` when only-new mode is on

Each run also saves a plain-text **digest** of the best notices (the `DIGEST` record), ready to paste into an email or a Slack message.

### Who it is for

- **Bid and sales teams** at agencies, software houses and IT consultancies who want UK and EU public sector opportunities without checking three portals every morning
- **Consultants and analysts** tracking what a sector buys, from whom, and at what value
- **Teams in other sectors** (construction, health, facilities, marketing): change the CPV codes and keywords and the same filter works for your field
- **AI agents and automations** that need tender data as a tool or a scheduled feed

### How to use it

1. **Pick the sources.** Leave the list empty, or as prefilled, for all three.
2. **Set your sector.** The defaults target IT, software and digital services (CPV divisions 72 and 48 plus a keyword list). For another sector, replace the CPV prefixes and TED CPV codes, and switch off the default exclude list if it covers your field.
3. **Set your limits.** Maximum contract value (in any of 11 currencies), maximum contract length and the minimum days left before the deadline.
4. **Run it.** The form starts with a 3-day window for a quick first look.

A notice qualifies when one of its CPV codes starts with one of your prefixes, or its own title matches your keywords. A limit only applies when the notice states that field: a notice that publishes no value, no contract length or no deadline is kept and scored lower, never dropped for the silence. Most public notices publish no value, so this rule is what keeps the results complete.

#### Get only new tenders every week

1. Set **Lookback window** to 8 (a week plus one day of overlap).
2. Switch on **Only return notices not delivered before** and give the store a name for this monitor, for example `it-tenders-uk-eu`.
3. Save the input as a task and add a weekly schedule in Apify (Schedules, for example every Monday at 07:00).
4. Optionally send each run to email, Slack, Google Sheets or a webhook with Apify integrations.

Each run then delivers only notices that no earlier run delivered, so a notice is paid for once.

### Input example

Weekly UK and EU IT monitor, under 100,000 GBP, at least 14 days to the deadline, skipping licence renewals:

```json
{
  "sources": ["contractsfinder", "findatender", "ted"],
  "lookbackDays": 8,
  "maxValue": 100000,
  "maxValueCurrency": "GBP",
  "maxContractMonths": 24,
  "minDeadlineDays": 14,
  "excludeRegex": "licen[cs]e|subscription",
  "onlyNew": true,
  "dedupeStore": "it-tenders-uk-eu"
}
```

UK construction tenders of any value:

```json
{
  "sources": ["contractsfinder", "findatender"],
  "lookbackDays": 8,
  "cpvPrefixes": ["45"],
  "keywordRegex": "\\b(construction|refurbishment|building works?)\\b",
  "applyDefaultExclude": false,
  "maxValue": 0,
  "maxContractMonths": 120
}
```

### Output example

Two real rows from a run on 8 October 2026 (the second description is shortened here):

```json
[
  {
    "source": "ted",
    "id": "693714-2026",
    "title": "Acquisition, déploiement, migration et maintien en conditions opérationnelles d'une suite collaborative, bureautique et de messagerie sécurisée en mode SaaS",
    "buyer": "SDIS 18 Cher",
    "buyer_country": "FR",
    "buyer_countries": ["FR"],
    "value": 85000,
    "currency": "EUR",
    "value_eur": 85000,
    "value_stated_raw": 85000,
    "value_note": null,
    "deadline": "2026-11-16T00:00:00+02:00",
    "published": "2026-10-08T00:00:00+02:00",
    "contract_months": 12,
    "days_to_deadline": 38,
    "sme_flag": null,
    "cpv": "72220000",
    "cpv_label": "Systems and technical consultancy services",
    "cpv_all": ["72220000", "72220000"],
    "score": 90,
    "why": [
      "CPV 72220000",
      "title matches the keywords",
      "stated value 85,000 EUR, under the 100,000 EUR cap",
      "12 month contract",
      "38 days to the deadline"
    ],
    "url": "https://ted.europa.eu/en/notice/-/detail/693714-2026",
    "is_new": null,
    "first_seen": null,
    "run_at": "2026-10-08T19:26:51.481Z",
    "description": "Acquisition, déploiement, migration et maintien en conditions opérationnelles d'une suite collaborative, bureautique et de messagerie sécurisée en mode SaaS"
  },
  {
    "source": "contractsfinder",
    "id": "5f4d9841-b30a-42bb-b122-3f7758606443-916959",
    "title": "GDPR Data Audit",
    "buyer": "Ryde Town Council",
    "buyer_country": "GB",
    "buyer_countries": ["GB"],
    "value": null,
    "currency": null,
    "value_eur": null,
    "value_stated_raw": null,
    "value_note": null,
    "deadline": "2026-10-26T12:00:00Z",
    "published": "2026-10-07T13:13:57+01:00",
    "contract_months": 3,
    "days_to_deadline": 18,
    "sme_flag": true,
    "cpv": "66170000",
    "cpv_label": null,
    "cpv_all": ["66170000", "72222300", "79212000"],
    "score": 65,
    "why": [
      "CPV 72222300",
      "no value published, so the size is unknown",
      "3 month contract",
      "flagged suitable for SMEs",
      "18 days to the deadline"
    ],
    "url": "https://www.contractsfinder.service.gov.uk/Notice/5f4d9841-b30a-42bb-b122-3f7758606443",
    "is_new": null,
    "first_seen": null,
    "run_at": "2026-10-08T19:26:51.481Z",
    "description": "Ryde Town Council invites quotations from suitably qualified organisations to carry out an independent audit of its compliance with data protection law..."
  }
]
```

Besides the dataset, each run saves these records in its key-value store:

| Record | What it holds |
|---|---|
| `DIGEST` | Plain-text digest: per-source counts, then the top notices (set by **Notices shown in the digest**) with buyer, value, deadline, score, reasons and link |
| `OUTPUT` | Delivered and charged counts, and any source that failed or held more notices than were read |
| `RUN_SUMMARY` | The window, per-source counts, the TED query as sent, every rule in force and how many notices each rule dropped |
| `DROPPED` | Up to 500 rejected notices with the rule that dropped each one, for tuning your filter |

### How notices are scored

The score orders the dataset and the digest. It adds up, capped at 100:

| Signal | Points |
|---|---|
| A CPV code in your prefixes | 25 |
| Keywords in the title (in the description only: 7) | 15 |
| Stated value under your cap (10 more under half the cap) | 30 |
| No value published | 5 |
| Stated contract length inside your cap | 10 |
| Flagged suitable for SMEs | 15 |
| Deadline beyond your minimum | 10 |

A notice is dropped when it states a value over your cap, a contract longer than your maximum or a deadline sooner than your minimum, when it matches the exclude list, or (both on by default) when it reads as a framework or purchasing system or asks for a security clearance.

### Pricing

**$0.005 per tender notice delivered, lower on Bronze, Silver and Gold plans.** There is no start fee, and a run that delivers no notices costs nothing.

| Run | Notices | Cost on the Free plan |
|---|---|---|
| The form defaults (3 days, all three sources, IT filter), 8 October 2026 | 221 | $1.105 |
| A week (8 days, all three sources, IT filter), 8 October 2026 | 381 | $1.905 |
| Any run repeated over the same window with only-new mode on | 0 | $0 |

To spend less, narrow the filter: fewer sources, `tedCountries`, a tighter value cap, or your own CPV codes. With only-new mode on a schedule you pay once per notice, not once per run.

**A maximum cost per run is respected.** Set one and the run delivers the best-scoring notices that fit inside it, then stops and says so in the status message ("Stopped at your maximum cost: N of M results delivered."). The number of rows in your dataset always equals the number of notices charged. With only-new mode on, the notices held back stay unseen and arrive in the next run.

### Use it from Claude, Cursor or any MCP client

Add this tender monitor as a tool through Apify's hosted MCP server:

```
https://mcp.apify.com?tools=omargnagy/tender-monitor
```

In Claude Code:

```
claude mcp add --transport http tender-monitor "https://mcp.apify.com?tools=omargnagy/tender-monitor"
```

In Cursor or another client that takes a JSON config:

```json
{
  "mcpServers": {
    "tender-monitor": {
      "url": "https://mcp.apify.com?tools=omargnagy/tender-monitor"
    }
  }
}
```

You connect with your Apify account, and runs are billed like any other run. Then ask in plain words, for example: "Find UK IT tenders under 100,000 GBP published this week that close in more than two weeks, and summarise the five best."

### Use it from code

One HTTP call runs the Actor and returns the rows:

```
POST https://api.apify.com/v2/acts/omargnagy~tender-monitor/run-sync-get-dataset-items
Authorization: Bearer <your Apify API token>
Content-Type: application/json

{ "sources": ["contractsfinder", "findatender"], "lookbackDays": 8 }
```

Python, with the `apify-client` package:

```python
from apify_client import ApifyClient

client = ApifyClient("<your Apify API token>")
run = client.actor("omargnagy/tender-monitor").call(
    run_input={"sources": ["ted"], "tedCountries": ["IE", "NL"], "lookbackDays": 8}
)
for notice in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(notice["score"], notice["title"], notice["deadline"], notice["url"])
```

### Limits

- **Open tender notices only.** Contracts Finder and Find a Tender are read at the tender stage, and TED for contract notices (`cn-standard`, `cn-social`). Award notices, prior information notices and planned procurements are not included.
- **TED notices stay in the buyer's language.** Titles and descriptions are not translated. TED rows carry the English CPV label in `cpv_label`, and keyword matching works best with CPV codes for non-English notices.
- **Most notices publish no value.** In the week to 8 October 2026, 95% of the delivered notices stated none. They are kept and scored lower.
- **Converted values are approximate.** `value_eur` uses ECB reference rates dated 8 October 2026, built into the Actor.
- **Page caps.** Each source is read up to `maxPages` pages of 100 notices (20 at most). TED is read newest first, so a cap drops its oldest notices. A long window on TED can hold more than the cap; the status message, `OUTPUT` and the digest then say how many were read out of how many.
- **Two rules are text checks, not data fields.** Framework agreements and security clearances are detected from English wording, so a German "Rahmenvertrag" or a French "accord-cadre" is not caught. Add such words to `excludeRegex` if you want them dropped. Both checks can be switched off.
- **The SME flag is rare.** Contracts Finder publishes it, Find a Tender almost never does, so it adds score rather than filtering by default.
- **The UK sources are filtered after download.** Every UK tender notice in the window is read, then matched against your CPV codes and keywords.

### FAQ

**Does it only work for IT tenders?**
No. IT and digital services are the default. Set `cpvPrefixes` and `tedCpv` to your sector's CPV division (45 construction, 85 health, 79 business services, 90 cleaning and waste), adjust `keywordRegex`, and switch off **Apply the default exclude list** if it would drop your sector.

**How do I get a weekly email of new tenders?**
Use the weekly setup above, then add an email or Slack integration to the task. The `DIGEST` record is plain text written for that purpose.

**Why do some notices have no value?**
The buyer did not publish one. The notice is kept with `value: null` and the reason in `why`. A stated value under 500 EUR (often 0 or 1) is treated as a placeholder and reported the same way, with the raw number kept in `value_stated_raw`.

**Can I limit TED to certain countries?**
Yes, with `tedCountries`, for example `["IE", "NL", "BE"]`. It does not affect the UK sources.

**Why was a notice I expected not returned?**
Check `RUN_SUMMARY` for the rules in force and `DROPPED` for each rejected notice and the rule that dropped it.

**How fresh is the data?**
Every run reads the live public APIs at run time. TED publishes new notices on working days.

**Can I bid through this Actor?**
No. It finds and filters notices. Each row links to the official notice, where you register and respond on the buyer's own portal.

### Data sources and licence

Notices come from the official public APIs of UK Contracts Finder and UK Find a Tender (Crown copyright, published under the Open Government Licence v3.0) and TED, Tenders Electronic Daily (© European Union, reused under the Commission's open data policy). Both licences ask you to credit the source when you republish the data. The Actor checks each host's robots.txt, sends at most one request per second per source, and identifies itself by its Apify Store URL.

# Actor input Schema

## `sources` (type: `array`):

Which tender sources to read. Leave empty for all three.

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

How many days back to read. 8 fits a weekly schedule and 2 a daily one, each with a day of overlap. 3 makes a quick first run.

## `keywordRegex` (type: `string`):

Optional. Case-insensitive regex over the notice's own title. A notice qualifies when its title matches this or one of its CPV codes starts with a prefix below. Empty uses the built-in IT and digital list (software, SaaS, websites, apps, cloud, data, cyber security, AI and more). An invalid pattern fails the run.

## `cpvPrefixes` (type: `array`):

A notice qualifies when any of its CPV codes starts with one of these. Default: 72 (IT services), 48 (software), 7934 (market research), 7241 and 9231. For another sector use its division, for example 45 for construction or 85 for health.

## `tedCpv` (type: `array`):

CPV codes sent to the TED search. Each covers everything under it, so 72000000 is all IT services. A prefix such as 45 is padded to 45000000. TED only.

## `tedCountries` (type: `array`):

Optional. Two-letter country codes such as DE, FR or IE to limit TED to buyers there. Empty means every country. TED only.

## `maxValue` (type: `integer`):

A notice that states a higher value is dropped. Other currencies are converted with ECB rates. A notice with no value, a placeholder under 500 EUR or a currency with no rate is kept and scored lower. 0 means no cap.

## `maxValueCurrency` (type: `string`):

The currency of the maximum value.

## `maxContractMonths` (type: `integer`):

A notice that states a longer contract is dropped. Notices with no stated length are kept.

## `minDeadlineDays` (type: `integer`):

A notice that closes sooner than this is dropped. Notices with no stated deadline are kept.

## `applyDefaultExclude` (type: `boolean`):

On: drop construction, hardware supply, catering, cleaning, vehicles, staffing and similar by keyword, and notices whose main CPV is 30, 45, 50 (except 503) or 90. Turn it off when you monitor one of those sectors.

## `excludeRegex` (type: `string`):

Optional. Case-insensitive regex over title and description; a match drops the notice. Example: licen\[cs]e|subscription skips software licence purchases. An invalid pattern fails the run.

## `excludeCpvPrefixes` (type: `array`):

Optional. A notice whose main CPV code starts with one of these is dropped, in any language.

## `requireSme` (type: `boolean`):

Off: an SME-suitable flag adds score and never drops a notice. On: keep only flagged notices, which removes most of Find a Tender because it rarely publishes the flag.

## `dropFrameworks` (type: `boolean`):

Drops framework agreements, dynamic purchasing systems and supplier lists. A text check over title and description.

## `dropClearance` (type: `boolean`):

Drops lots that need a security clearance (SC, DV, BPSS, List X and similar). A text check.

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

Turn on for a scheduled monitor: each run returns only notices no earlier run delivered, remembered in the named store below in your account. Off: every run returns every match in the window.

## `dedupeStore` (type: `string`):

Used only when onlyNew is on. A named key-value store in your account: lowercase letters, digits and hyphens. Use a different name for each monitor.

## `resetSeen` (type: `boolean`):

Used only when onlyNew is on. Clears the store first, so every match counts as new. Useful after you change the filter.

## `seenKeepDays` (type: `integer`):

Older entries are pruned so the store stays small.

## `digestTitle` (type: `string`):

Optional. The first line of the plain-text digest saved as the DIGEST record. Defaults to Tender digest.

## `digestFooter` (type: `string`):

Optional. A closing line added to every digest.

## `digestTop` (type: `integer`):

How many of the best-scoring notices the digest lists. Every delivered notice is in the dataset either way.

## `maxPages` (type: `integer`):

Each source is read 100 notices a page, one page a second. TED is read newest first, so a cap drops its oldest notices. A busy 8-day TED window needs 13 to 15 pages. If a source held more than was read, the status message, OUTPUT and digest say so.

## `pageLimit` (type: `integer`):

The three APIs cap a page at 100, so a higher value does nothing and a lower one only costs more requests.

## Actor input object example

```json
{
  "sources": [
    "contractsfinder",
    "findatender",
    "ted"
  ],
  "lookbackDays": 3,
  "cpvPrefixes": [
    "72",
    "48",
    "7934",
    "7241",
    "9231"
  ],
  "tedCpv": [
    "72000000",
    "72200000",
    "72212000",
    "72400000",
    "72500000",
    "72600000",
    "48000000",
    "79340000"
  ],
  "tedCountries": [],
  "maxValue": 100000,
  "maxValueCurrency": "EUR",
  "maxContractMonths": 24,
  "minDeadlineDays": 10,
  "applyDefaultExclude": true,
  "excludeCpvPrefixes": [],
  "requireSme": false,
  "dropFrameworks": true,
  "dropClearance": true,
  "onlyNew": false,
  "dedupeStore": "tender-monitor-seen",
  "resetSeen": false,
  "seenKeepDays": 180,
  "digestTop": 15,
  "maxPages": 15,
  "pageLimit": 100
}
```

# Actor output Schema

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

No description

## `digest` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `runSummary` (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 = {
    "sources": [
        "contractsfinder",
        "findatender",
        "ted"
    ],
    "lookbackDays": 3,
    "cpvPrefixes": [
        "72",
        "48",
        "7934",
        "7241",
        "9231"
    ],
    "tedCpv": [
        "72000000",
        "72200000",
        "72212000",
        "72400000",
        "72500000",
        "72600000",
        "48000000",
        "79340000"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("omargnagy/tender-monitor").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 = {
    "sources": [
        "contractsfinder",
        "findatender",
        "ted",
    ],
    "lookbackDays": 3,
    "cpvPrefixes": [
        "72",
        "48",
        "7934",
        "7241",
        "9231",
    ],
    "tedCpv": [
        "72000000",
        "72200000",
        "72212000",
        "72400000",
        "72500000",
        "72600000",
        "48000000",
        "79340000",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("omargnagy/tender-monitor").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 '{
  "sources": [
    "contractsfinder",
    "findatender",
    "ted"
  ],
  "lookbackDays": 3,
  "cpvPrefixes": [
    "72",
    "48",
    "7934",
    "7241",
    "9231"
  ],
  "tedCpv": [
    "72000000",
    "72200000",
    "72212000",
    "72400000",
    "72500000",
    "72600000",
    "48000000",
    "79340000"
  ]
}' |
apify call omargnagy/tender-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,omargnagy/tender-monitor"
        }
    }
}
```

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/sGjpfYQ1U3Q2sU6Ya/builds/QiV3PLceAf2GAvCEv/openapi.json
