# Global Tender Monitor — UK & EU Public Procurement (`dottti/global-tender-monitor`) Actor

Monitor UK and EU public tenders in one feed. Returns only notices published since your last run, with buyer, contract value, deadline and CPV codes. Official sources, no API key.

- **URL**: https://apify.com/dottti/global-tender-monitor.md
- **Developed by:** [Dottti](https://apify.com/dottti) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 tender delivereds

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

## Global Tender Monitor — UK & EU Public Procurement

Watch UK and EU public tenders in one feed and get **only the notices published since your last run**. Buyer, contract value, deadline, CPV codes and a direct link, normalised into one schema across both sources.

Official government APIs. No API key, no scraping, no proxy needed.

### Sources

| Source | Endpoint | What it gives |
| --- | --- | --- |
| **UK Find a Tender** | OCDS release packages | Contract value and currency, buyer, status, deadline |
| **TED (European Union)** | Expert search API | 322,000+ notices across all member states |

### Output

Both sources produce the same fields, so a CSV export stays rectangular and you can sort UK and EU notices together.

```json
{
  "source": "uk",
  "sourceName": "UK Find a Tender",
  "noticeId": "ocds-h6vhtk-05a1b2",
  "title": "Assertive Outreach Rough Sleepers",
  "buyer": "Caerphilly County Borough Council",
  "country": "GB",
  "valueAmount": 2400000,
  "valueAmountGross": 2880000,
  "currency": "GBP",
  "status": "active",
  "publishedDate": "2026-09-14T13:45:47.000Z",
  "deadline": "2026-10-16T11:00:00.000Z",
  "cpvCodes": ["85300000"],
  "url": "https://www.find-tender.service.gov.uk/Notice/..."
}
```

### Three things handled properly

**TED is multilingual and not always in English.** Every TED text field arrives as a language map — `{"eng": "...", "fra": "...", "hun": "..."}` — and some notices carry no English at all. A preferred language is chosen, English is the fallback, and whatever the notice actually has is the last resort. Returning `null` because English was missing would silently drop real tenders.

**IDs are namespaced by source.** UK and EU both use plain numeric identifiers, so tracking them in a shared list would let one source's notice hide behind the other's. State is keyed `source:id`, with a test that asserts two notices numbered `100` both survive.

**The first run shows you the current notices.** It delivers up to `maxItemsPerSource` per source and remembers them. Every later run returns only what is new since the run before. `RUN_SUMMARY` says exactly what was delivered.

### Input

| Field | What it does |
| --- | --- |
| `sources` | `uk`, `eu`, or both. |
| `mode` | `new` returns only unseen notices. `all` returns everything each run. |
| `keywords` | Keep only notices whose title, description or buyer matches. |
| `tedQuery` | TED's own expert-search syntax. It rejects a bare wildcard. |
| `language` | Three-letter code for picking from TED's language maps. |
| `stateKey` | One key per watchlist so separate schedules do not interfere. |

#### TED query examples

```
notice-title ~ (software)
classification-cpv IN (72000000)
buyer-country = NLD
```

#### Example: daily watch for IT contracts

```json
{
  "sources": ["uk", "eu"],
  "mode": "new",
  "keywords": ["software", "IT services", "cloud"],
  "tedQuery": "classification-cpv IN (72000000)",
  "stateKey": "it-contracts"
}
```

Schedule that daily. The first run returns the current notices; every run after returns only newly published ones.

### Notes and limits

- Contract values are published by the UK source. TED does not include a value in this response shape, so `valueAmount` is `null` for EU rows rather than estimated.
- A rejected TED query returns its error message directly instead of being retried, since a syntax error will not fix itself.
- If one source fails, the other still delivers and `RUN_SUMMARY` records the reason.
- Deleting the named state store resets the baseline, and the next run records a fresh one.

### Development

```bash
npm install
npm test
node src/main.js
```

# Actor input Schema

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

Which procurement portals to read. Both are official government sources requiring no API key.

## `mode` (type: `string`):

new: deliver only notices not seen in a previous run. all: deliver everything fetched each time.

## `keywords` (type: `array`):

Keep only notices whose title, description or buyer contains one of these. Applied to both sources after fetching. Leave empty to keep everything.

## `tedQuery` (type: `string`):

TED requires its own expert-search syntax and rejects a bare wildcard. Examples: notice-title ~ (software) | classification-cpv IN (72000000) | buyer-country = NLD

## `language` (type: `string`):

Three-letter code used to pick a language from TED's multilingual fields. Falls back to English, then to whatever the notice provides.

## `stateKey` (type: `string`):

Names the saved list of already-seen notices. Use one key per watchlist so separate schedules do not interfere.

## `maxItemsPerSource` (type: `integer`):

Cap per source per run, and therefore on cost. The first run delivers up to this many current notices per source.

## `requestDelayMs` (type: `integer`):

Both APIs are generous, but paging politely avoids throttling.

## `maxRetries` (type: `integer`):

Retries with exponential backoff on 429 and 5xx. A 4xx is surfaced immediately, since it means the query was rejected.

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

Not required. Both sources answer from datacenter IPs.

## Actor input object example

```json
{
  "sources": [
    "uk",
    "eu"
  ],
  "mode": "new",
  "tedQuery": "notice-title ~ (software)",
  "language": "eng",
  "stateKey": "default",
  "maxItemsPerSource": 50,
  "requestDelayMs": 400,
  "maxRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `tenders` (type: `string`):

Source and source name, notice ID, title, description, buyer, country, contract value and currency where published, status, publication date, submission deadline, CPV codes and a direct link to the notice.

## `runSummary` (type: `string`):

Per-source outcome: notices fetched, how many survived the keyword filter, how many were delivered, and the reason any source failed.

# 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("dottti/global-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("dottti/global-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 '{}' |
apify call dottti/global-tender-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dottti/global-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/Cd9HOfdK7VlQkaYTi/builds/whn9sfUFHfDJh1s5U/openapi.json
