# EU TED Tender Monitor (`luoyanpeng/ted-tender-monitor`) Actor

Monitor new EU public tenders from TED (Tenders Electronic Daily) via the official API. Get clean, deduplicated notices with buyer, CPV, value, deadline and links. Optional AI English summaries. Pay only per notice.

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

## Pricing

from $3.00 / 1,000 notices

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 TED Tender Monitor (TED Search API v3)

Monitors EU public procurement notices from **TED (Tenders Electronic Daily)** via the official anonymous
Search API v3 (`POST https://api.ted.europa.eu/v3/notices/search`). HTTP only, no browser, no proxy, 1 request/second max.
Part of a tender pack (TED / SAM.gov / UK FTS & Contracts Finder) sharing one output schema (`packages/shared`).

### Data licence & attribution

- Source: © European Union, https://ted.europa.eu. Per the TED legal notice, "procurement notices published in the
  Supplement to the Official Journal of the European Union can be freely reused, for commercial or non-commercial
  purposes" (Commission Decision 2011/833/EU on the reuse of Commission documents). Editorial content: CC BY 4.0; metadata CC0.
- Every record has `licenseAttribution`. When you republish: credit TED/EU, link `sourceUrl`, and indicate that
  `summaryEn`/`keyRequirementsEn` (and `titleEn` when `titleEnSource: "generated"`) are AI-generated derivatives, not official text.
- Do not use the TED/EU logos. Only the notices published in the OJ S are authentic.

### Input (main)

`cpvCodes`, `keywords` (full text), `query` (TED expert query, ANDed), `noticeTypes`, `buyerCountries`,
`publishedFrom`/`publishedTo` (YYYY-MM-DD), `maxItems`, `dedupe` + `dedupeScope` (skip notices already output by earlier
runs — stored in named KV `tender-ted-seen`), `enableSummaries`, `summaryBudgetUsd` (default 0.20, hard max 2.00).

### Output

`noticeId, source, noticeType, titleOriginal, titleEn, buyer, cpv, naics, psc, estimatedValue{amount,currency},
awardValue{amount,currency}, awardees, deadlineAt, documentUrls, sourceUrl, licenseAttribution, summaryEn, keyRequirementsEn,
summaryStatus, model, promptVersion, retrievedAt` (+ `estimatedValueBasis`, `deadlineDateRaw`, `publicationDate`).

- Money fields are always `{amount, currency}` (ISO 4217) or `null`. An amount without a stated currency is reported as `null`. Values are never converted between currencies.
- `estimatedValue`: the procedure estimate (BT-27 procedure); else the sum of lot estimates if all lots share a single currency (`estimatedValueBasis`). It is **always null on award/result notices** (can-\*, veat, pmc…).
- `awardValue`: `{amount, currency}` from the notice total value (`total-value` + `total-value-cur`), only for result notices (can-\*, veat, …).
- `titleEn` / `titleEnSource: "official"`: the English title supplied by TED. **It may not actually be translated** (TED builds it as "Country – CPV label – original-language title").
- `deadlineAt`: earliest lot deadline, ISO 8601 with the offset stated in the notice; **null if date or time is not stated** (never invented).
- `summaryStatus`: ok | cached | disabled | skipped_no_key | skipped_budget | failed.

### Default input

With no filters at all (e.g. the Store default input `{}`), the Actor returns notices **published in the last 24 h**. TED `publication-date` is day-level (UTC), so the query is `publication-date >= yesterday AND <= today` (UTC). TED publishes Monday–Friday, so a weekend or holiday window can return 0 notices. That is a successful run (`runStatus: ok`), not an error. The window used is reported in RUN_SUMMARY `defaultWindow`.

### Source and licence

- **Source:** TED (Tenders Electronic Daily), operated by the Publications Office of the European Union — https://ted.europa.eu (TED Search API v3).
- **Licence:** TED site content is © European Union and licensed under **CC BY 4.0** (https://creativecommons.org/licenses/by/4.0/). Reuse follows Commission Decision 2011/833/EU on the reuse of Commission documents.
- **TED logo:** not used anywhere in this Actor, its output or its Store page. There is no claim of EU endorsement.
- Every record's `licenseAttribution` states the source, the CC BY 4.0 licence and Decision 2011/833/EU. Keep it if you republish. AI summaries are derived content, not official text.

### AI summaries

DeepSeek (`deepseek-flash`, non-thinking mode, OpenAI-compatible, JSON mode), key from env `DEEPSEEK_API_KEY` (absent → `skipped_no_key`).
Prompt allows only facts from the source and requires "not stated" for missing items. Cache: named KV
`tender-ted-summary-cache` keyed `noticeId+promptVersion`. Budget: per-run budget + global daily cap $1, cost computed from response `usage` tokens at official peak/off-peak rates (named KV `tender-ted-llm-budget`; the daily cap applies per Actor);
over budget → `skipped_budget`, run continues.

### Pricing (pay-per-event)

| Event | Price (USD) | When |
|---|---|---|
| `actor-start` | $0.005 | once per run |
| `notice` | $0.003 ($3 / 1,000) | per record written to the dataset |
| `notice-summary` | $0.01 | per newly generated AI summary (optional) |

No charge for failed records, duplicates, or for cached, skipped or failed summaries. Config: `.actor/pay_per_event.json`; local simulation: `scripts/ppe-local.sh`.

### Privacy

Contact persons, emails and phone numbers are **not requested** from the API, and a scrubber removes email/phone
patterns from every text field before output and before sending text to the LLM. Notices may still name natural
persons as buyers/awardees (sole traders); handle under GDPR if you redistribute.

### Run locally

```bash
cd /workspace/apify/tender && npm install
cd actors/ted && apify run -p            # input: storage/key_value_stores/default/INPUT.json
npx tsx src/check.ts                     # numeric/field checker vs raw (needs includeRaw: true)
./scripts/ppe-local.sh                   # simulate pay-per-event charges (local charging_log dataset)
```

Dev: `mockSummaries: true` uses a fake LLM (local only) to test cache/budget logic without spending.

# Actor input Schema

## `query` (type: `string`):

Raw TED expert-search query, ANDed with other filters

## `cpvCodes` (type: `array`):

e.g. 72000000 (matches TED classification-cpv)

## `keywords` (type: `string`):

TED full-text (FT ~) search

## `noticeTypes` (type: `array`):

e.g. cn-standard, can-standard, pin-only

## `buyerCountries` (type: `array`):

e.g. DEU, FRA

## `publishedFrom` (type: `string`):

Published from (YYYY-MM-DD) If no filter at all is given, the Actor defaults to the last 24 h (publication-date >= yesterday UTC).

## `publishedTo` (type: `string`):

Published to (YYYY-MM-DD)

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

Max new notices

## `dedupe` (type: `boolean`):

Skip notices output in earlier runs

## `dedupeScope` (type: `string`):

Separate seen-lists per monitor

## `enableSummaries` (type: `boolean`):

AI English summaries (DeepSeek)

## `summaryBudgetUsd` (type: `number`):

Max LLM spend per run (USD)

## `includeRaw` (type: `boolean`):

Store raw API records in KV store

## `mockSummaries` (type: `boolean`):

(dev) mock LLM, local only

## Actor input object example

```json
{
  "query": "buyer-country=DEU",
  "maxItems": 20,
  "dedupe": true,
  "dedupeScope": "default",
  "enableSummaries": false,
  "summaryBudgetUsd": 0.2,
  "includeRaw": false,
  "mockSummaries": false
}
```

# Actor output Schema

## `notices` (type: `string`):

Normalized public procurement notices with source and licence attribution.

# 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 = {
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("luoyanpeng/ted-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 = { "maxItems": 20 }

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

```

## MCP server setup

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