# EU Tenders Scraper & Opportunity Monitor (`herazur/eu-tenders-ted-scraper-monitor`) Actor

Find and monitor EU public tenders from official TED data. Score opportunities for company fit and track deadline changes, amendments, contract awards, and other important updates.

- **URL**: https://apify.com/herazur/eu-tenders-ted-scraper-monitor.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Categories:** Business, Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $14.00 / 1,000 actionable tender updates

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

## EU Tenders Scraper & Opportunity Monitor

Status: **Phase 0 GO; production Actor implemented and locally verifiable.**

This Apify Actor turns public TED procurement notices into deterministic company-fit signals,
notice-version correlation, material change detection, and actionable monitoring events. It is not
a generic tender scraper: the default dataset contains events that describe what requires action.

The benchmark uses the anonymous official TED API v3 and does not use an LLM, paid upstream API,
browser, customer credentials, or proxy.

### Run the Actor

Requirements: Node.js 20 or newer. No credentials, cookies, proxy, or API key are used.

```bash
npm install
npm run build
npm run lint
npm start
```

On Apify, configure a schedule using the same `monitorId` and `stateStoreName`. The named key-value
store carries notice, procedure, pending-update, and deduplication state across runs. Use a distinct
monitor ID for every company profile. Set `resetState` only when intentionally creating a new
monitoring baseline.

At least one positive fit signal is required: target CPV, country, NUTS region, buyer term, or
capability keyword. Signals add fit points rather than acting as hard filters; for example, a CPV
parent match adds 20 points and a target buyer country adds 15, so `minimumFitScore` 35 requires
both while 20 accepts any matching CPV.

The Store's example input (IT services CPV `72*`, buyer countries DEU and FRA, a 7-day lookback,
100 scanned notices, at most 10 rows, minimum fit score 20) is a deliberately small demo that
finishes in well under a minute. For production monitoring, raise `maxScanNotices` and
`maxResults` to cover your full profile. Optional `expertQuery` text uses the official TED expert-query syntax and is
combined with the Actor's publication lookback window.

`maxScanNotices` is an acquisition safety cap; `maxResults` limits customer-visible rows. The Actor
groups positive signals into country, CPV, NUTS, buyer, and capability query shards, scans them in
round-robin order, unions them by stable notice-version identity, then ranks all scanned matches.
It never describes a capped scan as complete. Check `searchTruncated`, `coverageComplete`,
`totalMatchesObserved`, and per-shard metrics in `OUTPUT`. Unreturned ranked updates remain pending
for later runs.

Each dataset row represents one logical update and contains `eventTypes[]` plus
`materialChanges[]`. Headline event types include:

- `NEW_MATCHING_TENDER`, `NEW_HIGH_PRIORITY_TENDER`, `FIT_SCORE_INCREASED`
- `URGENCY_INCREASED`, `DEADLINE_EXTENDED`, `DEADLINE_SHORTENED`
- `MATERIAL_CHANGE_DETECTED`, `PROCEDURE_LIFECYCLE_UPDATE`
- `PROCEDURE_CANCELLED`, `AWARD_PUBLISHED`, `NO_LONGER_ACTIONABLE`

`fitScore` measures company/tender compatibility and never changes merely because time passed.
`urgencyScore` changes in deterministic deadline bands; `priorityScore` combines the two. Value
ranges require an explicit matching three-letter currency. Eight-digit CPVs are exact; only CPVs
ending in `*` are prefix matches.

Every update includes stable identities, lifecycle classification, source URL, previous scores,
and the current normalized notice. Stored open tenders are reevaluated every run even when they are
outside `lookbackDays`, so deadline aging can produce urgency or expiry events without a new TED
publication.

Overlapping runs for the same `stateStoreName + monitorId` are rejected using a leased monitor lock,
verified ownership, and a state revision guard. This is the safest practical key-value-store lock,
not a database transaction: a platform crash after dataset write but before state commit can still
produce an at-least-once duplicate on retry. Do not intentionally schedule overlapping runs. State
is bounded by `stateRetentionDays` and `maxStateNotices`; any open-state or pending-update pruning is
reported explicitly in `OUTPUT`.

### Pricing

This Actor uses Apify Pay Per Event pricing:

- **Actionable tender update: $0.02 per dataset row** on the Apify Free plan, with lower prices on
  paid Apify plans (down to $0.014). One row is one consolidated new or changed tender.
- **Actor start: $0.01 per GB of run memory** (one event per GB, minimum one), charged once per run.

Runs that find nothing new deliver no rows and pay only the start fee. `maxResults` and your run's
maximum total charge cap the number of paid rows; anything not delivered stays pending for the next
run. TED data is fetched from the free official TED API, so there are no upstream costs.

When Apify reports `PAY_PER_EVENT` pricing, each successfully delivered consolidated row is charged
exactly once as `actionable-tender-update`. Rows containing multiple `eventTypes[]` or
`materialChanges[]` still represent one event. The Actor preflights the remaining event budget,
stops before an unpaid row, and retains undelivered rows in pending state for a later run. Under any
other pricing model, rows are delivered without a custom PPE charge. The synthetic
`apify-actor-start` event remains platform-managed, and `apify-default-dataset-item` must not be
configured because it would double-charge the default dataset write.

### Verify

```bash
npm run build
npm test
npm run lint
npm run format:check
npx apify-cli validate-schema
```

The real-data benchmark writes `PHASE0_REPORT.md`, `benchmark-output/phase0-report.json`,
`benchmark-output/normalized-sample.json`, and `benchmark-output/amendment-pairs.json`. Live results
will vary as TED data and service behavior change.

### Phase 0 result

The passing run tested 60/60 matrix queries, 4,679 unique notice versions, 100 correlated version
pairs, page sizes 25/100/250, ten consecutive pages, resume/repeat behavior, ITERATION continuation,
and concurrency 1/3/5/10. See `PHASE0_REPORT.md` for measured retries, costs, field availability, and
limitations. Scores remain transparent deterministic heuristics; no LLM, paid upstream API,
credentials, browser, proxy, or HTML scraping is used.

# Actor input Schema

## `monitorId` (type: `string`):

Stable ID used to isolate recurring state. Use a distinct monitor ID for every company profile and reuse it on scheduled runs.

## `stateStoreName` (type: `string`):

Named key-value store retained across Actor runs.

## `expertQuery` (type: `string`):

Optional official TED expert-search expression combined with the publication window.

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

How far back to search by TED publication date on every run.

## `maxScanNotices` (type: `integer`):

Acquisition safety cap across all query shards. A capped run explicitly reports incomplete coverage.

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

Maximum customer-visible update rows returned per run. Overflow remains pending for the next run.

## `targetCpvCodes` (type: `array`):

Exact 8-digit CPV codes or parent prefixes ending in *, for example 72* for IT services.

## `targetCountries` (type: `array`):

Three-letter TED country codes such as DEU or FRA.

## `targetNutsRegions` (type: `array`):

NUTS codes or prefixes for target places of performance.

## `buyerKeywords` (type: `array`):

Case-insensitive terms matched against buyer names.

## `procedureTypes` (type: `array`):

Exact TED procedure-type values such as open or restricted.

## `capabilityKeywords` (type: `array`):

Case-insensitive terms matched in buyer, title, description, and criteria text.

## `excludedKeywords` (type: `array`):

Any matching term makes the notice ineligible.

## `minimumContractValue` (type: `integer`):

Optional inclusive lower value bound in the notice's stated currency.

## `maximumContractValue` (type: `integer`):

Optional inclusive upper value bound in the notice's stated currency.

## `contractCurrency` (type: `string`):

Optional three-letter currency required before applying value-range points.

## `minimumFitScore` (type: `integer`):

Minimum deterministic score for an eligible match.

## `highPriorityScore` (type: `integer`):

Score threshold for NEW_HIGH_PRIORITY_TENDER.

## `deadlineNearDays` (type: `integer`):

Remaining-day threshold for DEADLINE_NEAR scoring.

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

How long closed, expired, and emitted update state is retained.

## `maxStateNotices` (type: `integer`):

Safety bound for non-actionable history; open actionable notices are never dropped solely to meet this bound.

## `resetState` (type: `boolean`):

Ignore existing monitor state on this run.

## Actor input object example

```json
{
  "monitorId": "it-services-de-fr",
  "stateStoreName": "eu-tender-radar-state",
  "lookbackDays": 7,
  "maxScanNotices": 100,
  "maxResults": 10,
  "targetCpvCodes": [
    "72*"
  ],
  "targetCountries": [
    "DEU",
    "FRA"
  ],
  "targetNutsRegions": [],
  "buyerKeywords": [],
  "procedureTypes": [],
  "capabilityKeywords": [],
  "excludedKeywords": [],
  "minimumFitScore": 20,
  "highPriorityScore": 50,
  "deadlineNearDays": 21,
  "stateRetentionDays": 365,
  "maxStateNotices": 20000,
  "resetState": false
}
```

# Actor output Schema

## `events` (type: `string`):

No description

## `summary` (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 = {
    "monitorId": "it-services-de-fr",
    "lookbackDays": 7,
    "maxScanNotices": 100,
    "maxResults": 10,
    "targetCpvCodes": [
        "72*"
    ],
    "targetCountries": [
        "DEU",
        "FRA"
    ],
    "minimumFitScore": 20,
    "highPriorityScore": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("herazur/eu-tenders-ted-scraper-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 = {
    "monitorId": "it-services-de-fr",
    "lookbackDays": 7,
    "maxScanNotices": 100,
    "maxResults": 10,
    "targetCpvCodes": ["72*"],
    "targetCountries": [
        "DEU",
        "FRA",
    ],
    "minimumFitScore": 20,
    "highPriorityScore": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("herazur/eu-tenders-ted-scraper-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 '{
  "monitorId": "it-services-de-fr",
  "lookbackDays": 7,
  "maxScanNotices": 100,
  "maxResults": 10,
  "targetCpvCodes": [
    "72*"
  ],
  "targetCountries": [
    "DEU",
    "FRA"
  ],
  "minimumFitScore": 20,
  "highPriorityScore": 50
}' |
apify call herazur/eu-tenders-ted-scraper-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/eu-tenders-ted-scraper-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/QOpj8DMXm619hANOj/builds/bsxVDXidbDAFwp0Sl/openapi.json
