# EU Procurement Award Intelligence (`atlas-data/eu-procurement-award-intelligence`) Actor

Track normalized TED result notices and notice-level buyer, winner, date, identifier, and value sets.

- **URL**: https://apify.com/atlas-data/eu-procurement-award-intelligence.md
- **Developed by:** [Atlas](https://apify.com/atlas-data) (community)
- **Categories:** Business
- **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/platform/actors/running/actors-in-store#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

## EU Procurement Award Intelligence

Turn official EU Tenders Electronic Daily (TED) result notices into a normalized, change-tracked stream of notice-level buyer and winner sets, result-lot identifiers, dates, and value sets.

This Actor is built for bid-intelligence teams, suppliers, procurement consultancies, and market analysts who need to answer questions such as:

- Which organizations are winning contracts from a target buyer or country?
- Which suppliers are active in a CPV category?
- What changed in a recently published award?
- Which result notices and values should be loaded into a CRM, warehouse, competitor dashboard, or analyst queue?

The reason to pay is not access to public TED data. It is the production workflow around that data: safe filtering, bounded exhaustive traversal, deterministic normalization, ambiguity-aware relationship modeling, stable IDs, deduplication, edit detection, replay-safe incremental state, rate limits, retry handling, and concurrency protection.

### Output

Each dataset row is one normalized result notice with output `schemaVersion` **1.0.0**:

- stable `relationshipId` (SHA-256) derived only from the TED publication number, so it survives notice versions and content corrections;
- `contentDigest`, which changes when normalized source content changes and is separate from logical identity;
- TED `noticeId`, `noticeVersion`, canonical notice URL, title, language, form/notice type, and publication date;
- award dates from the official `winner-decision-date` and `contract-conclusion-date` fields;
- buyer names, identifiers, and countries;
- winning supplier names, identifiers, and countries;
- CPV codes and buyer/supplier/performance geography;
- result-lot identifiers with stable `awardId` values derived only from publication number and result-lot ID;
- separate notice-level sets for lot titles, notice/tender lot identifiers, contract/tender identifiers, and contract-to-tender IDs;
- notice-result, lot-result, and tender amount/currency sets without amount-to-currency or value-to-lot joins;
- procedure type and identifier;
- `associationCompleteness` and `associationWarnings`;
- explainable relevance score/signals;
- `changeType`, `changedFields`, `fetchedAt`, and explicit TED attribution.

Logical IDs are deterministic, source-namespaced, and independent of output schema version, notice version, and mutable content. They are not identifiers issued by TED. The original TED notice, lot, contract, tender, organization, and procedure identifiers remain separately available.

### Source-shape safety

The Actor uses only fields listed in the official [TED Search Field List](https://docs.ted.europa.eu/ODS/latest/reuse/field-list.html) and verified against the anonymous live v3 endpoint.

TED Search API results flatten nested eForms structures. Arrays that look related may have unequal cardinalities. A live result notice can expose four lot results and four winner-name occurrences while returning only three unique winner identifiers. Treating all arrays as positional would invent false company/lot relationships.

This Actor therefore:

- emits buyers and winning suppliers as separate notice-level sets;
- never assigns an identifier or country to an individual organization by array position;
- creates `lots` entries only from stable `result-lot-identifier` values, and each entry contains only `lotId` plus its logical `awardId`;
- never uses equal cardinality or source array order to attach titles, contract/tender IDs, dates, amounts, or currencies to a lot or one another;
- preserves those flattened fields as explicit notice-level entity sets;
- always reports `associationCompleteness: "notice-level-only"` and explains the non-association in `associationWarnings`.

This is intentionally conservative. It is safer for competitive intelligence than a plausible-looking but fabricated join.

### Run modes

`full` emits every matching relationship as `snapshot`.

`incremental` emits only `new` and `updated` relationships. It overlaps the last completed publication watermark (three days by default) so later TED edits are detected.

`diff` tracks the same durable state but also emits `unchanged` relationships for reconciliation and warehouse snapshots.

The change digest is keyed by TED notice ID. A changed winner, buyer, value set, award date, lot identifier, procedure, URL, or association warning is reported as an update. `changedFields` identifies the changed top-level normalized fields. The row's stable `relationshipId` remains unchanged while `contentDigest` changes.

### Safe filtering

Supported business filters include:

- notice-title/buyer keywords;
- winner/supplier keywords;
- CPV prefixes;
- buyer, winner, and performance countries;
- inclusive publication and award-date ranges, with one award date required to satisfy both bounds;
- minimum contract value in one exact currency;
- TED result notice types;
- optional inclusion of result notices that expose no winner name;
- preferred output language with English and deterministic fallback.

Keyword groups use OR semantics; different filter groups combine with AND. `minimumValue` and `minimumValueCurrency` must be supplied together. No FX conversion is performed. Because TED values are flattened, the value filter is intentionally strict: only a scope containing exactly one amount and exactly one currency can satisfy it.

User strings are never inserted into TED expert-query syntax. The server-side query contains only strictly validated calendar dates plus the fixed, live-verified `form-type = result` predicate. All variable business filtering occurs against normalized rows inside the Actor.

An optional relevance profile can add or subtract weights across `title`, `buyer`, `winner`, `cpv`, `country`, `place`, and `procedure`. Every contribution is emitted in `relevance.signals`.

### Example input

```json
{
  "mode": "incremental",
  "winnerKeywords": ["atos", "capgemini"],
  "cpvPrefixes": ["72"],
  "buyerCountries": ["DEU", "FRA", "BEL"],
  "winnerCountries": ["FRA", "DEU"],
  "publicationFrom": "2026-01-01",
  "awardDateFrom": "2025-12-01",
  "minimumValue": 250000,
  "minimumValueCurrency": "EUR",
  "noticeTypes": ["can-standard"],
  "language": "eng",
  "maxItems": 500,
  "relevance": {
    "include": [
      { "term": "cloud", "fields": ["title"], "weight": 15 },
      { "term": "72000000", "fields": ["cpv"], "weight": 8 }
    ],
    "exclude": [],
    "minimumScore": 0,
    "requireIncludeMatch": false
  }
}
```

### Traversal, caps, and resume

The Actor uses TED `ITERATION` mode, which the official documentation describes as unbounded traversal with a maximum of 250 notices per page and 10,000 requested fields per page.

`maxItems` counts matched notices, not source rows. Sparse searches continue behind rejected result notices. Cost and forward progress are independently bounded by `maxPages` and `maxScannedItems`. Duplicate-only pages and rows revisited after an expired token do not consume those forward budgets; replay has separate hard page/row ceilings to stop malformed endless loops.

If a cap lands inside a page, durable state records the exact query window, run mode, original page size, inbound opaque token, row offset, and compact source ledger. The next run completes that same window even if the date, page size, or operational caps changed. Expired or invalid iteration tokens replay the same window from its start; the ledger prevents already handled rows from hiding an unprocessed row. Switching an unfinished incremental cursor to `diff` restarts that window without the incremental source ledger so previously handled rows are emitted as `unchanged`. Repeated tokens, replay ceilings, oversized pages, conflicting duplicate notice versions, malformed envelopes, and `timedOut: true` fail closed.

HTTP 429, 5xx, and network errors receive bounded exponential-backoff retries. Requests are sequential and conservatively rate-limited. A failure after partial dataset output never advances durable state.

### Write ordering and recovery

Before contacting TED, the Actor reserves enough space for the maximum new replay-ledger entries permitted by the run. If admission cannot fit the 3.5 MB durable-state budget, source processing and dataset writes do not start.

After admission, the write order is strict:

1. dataset rows;
2. default key-value-store `OUTPUT` summary;
3. durable cursor/digest state last.

A source, dataset, summary, lease, or state failure before step 3 leaves the work replayable. Replays may duplicate already written dataset rows. Consumers should upsert by stable `relationshipId`; use `contentDigest`, `changeType`, and `fetchedAt` to distinguish revisions. Do not use `noticeVersion` or `contentDigest` as the logical upsert key.

The `OUTPUT` record includes scan/match/emission/duplicate counts, replayed page/row counts, change counts, stop reason, status (`succeeded` or `partial`), query window, configuration hash, dataset ID, timestamp, and attribution.

### Isolated bounded state and concurrency

Business state is keyed by a SHA-256 fingerprint of all selection, language, value, and relevance settings. Operational caps do not fork state. Full-snapshot and change-tracking resume cursors are separate.

This Actor uses its own resources:

- named state store: `eu-procurement-award-intelligence-state-v1`;
- state key namespace: `EU_PROCUREMENT_AWARD_STATE_V1`;
- lock queue: `eu-procurement-award-intelligence-lock-v1`.

State commits are immutable uniquely keyed snapshots. A persistent Request Queue entry provides exclusive writer leases, periodic prolongation, ownership checks, and server-side expiry. Snapshot selection compares semantic document revisions before key ordering so a stale writer cannot win merely through a later random key. Release is best-effort after a successful commit.

Each configuration keeps compact digests for 180 days and 5,000 notices by default. Retention is configurable from 7–730 days and 100–12,000 entries. The complete state document is measured against a conservative 3.5 MB budget; old completed configurations and then old digests are pruned. Before a new run is admitted, old ledgers in other saved configurations or resume slots that prevent a safe commit are converted to small recovery cursors: their exact saved windows remain, while their token, offset, and source ledger are cleared so they safely restart from row zero when next selected. The selected active resume is never discarded for admission; if it cannot grow safely, the run fails before reading TED or writing dataset rows. Snapshot listing/cleanup is bounded and tolerant of eventual consistency; any repeatedly unreadable listed candidate that could be newest makes loading fail closed.

Schedule non-overlapping runs where possible. Separate dataset, `OUTPUT`, and state writes are not one cloud transaction, so overlapping runs can still create replayed dataset rows even though stale state commits are rejected.

### Cost expectations

The default memory is **1024 MB**, which consumes 1 compute unit (CU) per hour of runtime. At an example rate of **$0.20/CU**, a 10-minute run costs about **$0.033** in compute and a one-hour run about **$0.20**, before storage operations and data transfer.

These are planning examples, not quoted prices. Actual rates depend on the Apify plan, discounts, pricing model, memory override, storage, transfer, source volume, and run duration. Start with a short publication window and conservative caps, then inspect the run Usage details.

### Limitations

- Coverage and quality are limited to what the official TED Search API exposes. Source omissions and incorrect source data remain omissions/incorrect data.
- Flattened arrays cannot recreate the original eForms buyer–contract–tender–winner graph without documented keys. Output fields are notice-level sets and never use array position as a join.
- Winner details can be absent from result notices; these are excluded by default.
- Organization identifiers may be national IDs, European IDs, UUID-like internal values, or other source strings. The Actor preserves them without relabeling their type.
- Contract currencies are not converted or inflation-adjusted.
- Stable relationship/award IDs do not change with notice versions or mutable normalized content. A corrected result-lot identifier is a different logical lot and therefore receives a different `awardId`.
- This Actor is not legal, procurement, sanctions, or supplier-due-diligence advice.

### Local development and release checks

Requires Node.js 22.

```bash
npm ci
npm run lint
npm run typecheck
npm run build
npm test
npm run smoke:local
npm run smoke:live
npm audit --omit=dev
npm audit
apify validate-schema
```

`npm run smoke:live` boundedly scans up to 500 recent production TED rows, requires one representative result with non-empty buyer and winner names, an award date, and a documented result/tender amount, then validates the normalized row against the dataset schema. The normal fixture/adversarial suite needs no network, cloud storage, credentials, or secrets.

The production Docker image builds TypeScript in an isolated Node 22 stage and installs only runtime dependencies in the final Apify Node 22 image.

### Data source and attribution

Source: [TED — Tenders Electronic Daily](https://ted.europa.eu/) via the anonymous [TED Search API v3](https://api.ted.europa.eu/v3/notices/search).

The API is officially documented as anonymously accessible for published-notice reuse. Preserve the attribution included with every row and review the current [TED legal notice](https://ted.europa.eu/en/legal-notice) for your use case. This Actor does not claim ownership of TED source data.

# Actor input Schema

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

Full emits a snapshot; incremental emits new/updated relationships; diff also emits unchanged relationships.

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

OR match against normalized notice title and buyer names.

## `winnerKeywords` (type: `array`):

OR match against normalized winning supplier names.

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

Two to eight digits; 72 matches the complete IT family.

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

TED/ISO alpha-3 country codes for contracting buyers.

## `winnerCountries` (type: `array`):

TED/ISO alpha-3 country codes for winning suppliers.

## `placeCountries` (type: `array`):

TED/ISO alpha-3 place-of-performance country codes.

## `publicationFrom` (type: `string`):

Inclusive YYYY-MM-DD; defaults to 30 days before publicationTo.

## `publicationTo` (type: `string`):

Inclusive YYYY-MM-DD; defaults to today.

## `awardDateFrom` (type: `string`):

Inclusive lower bound. The same award date must also satisfy awardDateTo when both are set.

## `awardDateTo` (type: `string`):

Inclusive upper bound. The same award date must also satisfy awardDateFrom when both are set.

## `minimumValue` (type: `number`):

Conservatively compared only when one value scope has exactly one amount and one currency; no FX conversion.

## `minimumValueCurrency` (type: `string`):

Exact three-letter currency used with minimumValue; no FX conversion.

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

TED result notice type codes, for example can-standard.

## `includeWithoutWinner` (type: `boolean`):

Keep result notices even when TED exposes no winner name.

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

Falls back to English and then the first available language.

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

Matched result relationships, not raw TED rows.

## `relevance` (type: `object`):

Optional explainable weights across normalized award fields.

## `pageSize` (type: `integer`):

Advanced. Official maximum is 250 notices per page.

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

Stop after this many TED pages containing new rows. Expired-token duplicate-only replay has a separate hard ceiling.

## `maxScannedItems` (type: `integer`):

Bound newly handled source rows independently of matches. Expired-token duplicates do not consume this budget.

## `overlapDays` (type: `integer`):

Recheck recently published result notices to detect edits.

## `stateRetentionDays` (type: `integer`):

Forget completed change digests older than this many days.

## `maxStateEntries` (type: `integer`):

Keep state bounded by retaining the most recently seen notice digests.

## Actor input object example

```json
{
  "mode": "incremental",
  "keywords": [],
  "winnerKeywords": [],
  "cpvPrefixes": [],
  "buyerCountries": [],
  "winnerCountries": [],
  "placeCountries": [],
  "noticeTypes": [],
  "includeWithoutWinner": false,
  "language": "eng",
  "maxItems": 1000,
  "relevance": {
    "include": [],
    "exclude": [],
    "minimumScore": 0,
    "requireIncludeMatch": false
  },
  "pageSize": 250,
  "maxPages": 100,
  "maxScannedItems": 10000,
  "overlapDays": 3,
  "stateRetentionDays": 180,
  "maxStateEntries": 5000
}
```

# Actor output Schema

## `dataset` (type: `string`):

Normalized TED result notices with notice-level buyer, winner, identifier, date, and value sets.

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

Counts, status, stop reason, window, configuration hash, and 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("atlas-data/eu-procurement-award-intelligence").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("atlas-data/eu-procurement-award-intelligence").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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 atlas-data/eu-procurement-award-intelligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=atlas-data/eu-procurement-award-intelligence",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MGs819LRcsrdpLitW/builds/4D2m0168a0Yas7s5c/openapi.json
