Tender Opportunity Intelligence
Pricing
Pay per usage
Tender Opportunity Intelligence
Normalize, deduplicate, rank, and track EU procurement opportunities from TED.
Turn the EU's Tenders Electronic Daily (TED) feed into a ranked, normalized opportunity stream your sales, bid, and market-intelligence workflows can use directly.
This Actor is more than an API relay. It traverses TED's unbounded ITERATION mode, normalizes multilingual and multi-lot fields, applies safe business filters, explains every relevance score, deduplicates notices, detects edits, and keeps each search profile's cursor and digest history isolated.
Who it is for
Use it if you run EU public-sector pipeline discovery for a bid team, sales operation, specialist supplier, procurement consultancy, or market-intelligence product. It is especially useful when people currently search TED by hand, maintain brittle exports, or receive broad alerts that still require hours of triage.
The reason to pay is the workflow around the public data: bounded exhaustive traversal, stable normalized records, auditable ranking, change detection, and replay-safe incremental state. Those are the parts a team would otherwise have to build, operate, and monitor around the TED API before the feed is dependable enough for a CRM, warehouse, alerting flow, or analyst queue.
What you get
Each dataset row uses output schemaVersion 1.0.0 and includes:
- stable TED notice ID, version, canonical URL, preferred-language title, and buyer details;
- buyer countries and place-of-performance countries as distinct fields, raw performance places, normalized CPV codes, and canonical calendar dates;
- every procedure/lot estimate as an amount-currency pair; a convenience largest value appears only when all estimates are safely comparable;
- notice type, procedure type/identifier, contract nature, and legal basis;
- a numeric relevance score plus the exact positive and negative signals that produced it;
changeType(snapshot,new,updated, orunchanged) and top-levelchangedFields;fetchedAtand explicit TED attribution.
Run modes
full emits every match with changeType: "snapshot".
incremental emits only new and updated matches. It overlaps the previous completed publication watermark (three days by default) to catch edits to recently published notices.
diff performs the same durable tracking but also emits unchanged matches, which is useful for reconciliation and warehouse snapshots.
State is keyed by a SHA-256 fingerprint of all selection and relevance settings. Changing a country, keyword, date bound, value currency, language, or scoring profile cannot accidentally consume another configuration's cursor. Operational caps such as maxItems do not fork business state. Full-snapshot and change-tracking resumes are separate, so full never consumes or advances an incremental cursor.
Example input
{"mode": "incremental","keywords": ["cybersecurity", "zero trust", "SOC"],"cpvPrefixes": ["72", "4873"],"buyerCountries": ["DEU", "NLD", "BEL"],"publicationFrom": "2026-01-01","deadlineFrom": "2026-08-01","minimumValue": 250000,"minimumValueCurrency": "EUR","noticeTypes": ["cn-standard"],"language": "eng","maxItems": 500,"relevance": {"include": [{ "term": "managed service", "fields": ["title"], "weight": 25 },{ "term": "72000000", "fields": ["cpv"], "weight": 12 }],"exclude": [{ "term": "hardware only", "fields": ["title"], "weight": 30 }],"minimumScore": 8,"requireIncludeMatch": false}}
Keywords use OR semantics across title and buyer name. CPV prefixes use OR semantics. Different filter groups combine with AND. Deadline bounds require at least one deadline satisfying each supplied bound. minimumValue and minimumValueCurrency must be supplied together; the filter succeeds only when an estimate in that exact currency reaches the threshold. No different/unknown currencies are compared and no FX conversion is attempted.
Relevance model
The built-in explainable score adds 10 points per matched keyword and 8 per matched CPV prefix. relevance.include rules add their configured weight for each matching field; exclude rules subtract it. Set minimumScore to reject weak opportunities, and requireIncludeMatch when at least one positive profile rule must match.
Rules accept only these normalized fields: title, buyer, cpv, country, place, and procedure. country means buyer country; place covers performance-country codes and raw places. Matching is case-insensitive substring matching after Unicode NFC normalization. Every contribution appears in relevance.signals, so downstream users can audit or tune the ranking.
Safety, traversal, and recovery
The Actor never inserts user text into TED expert-query syntax. The only server-side query is a generated publication-date window made from strictly validated, real-calendar YYYY-MM-DD values; keywords, codes, countries, values, deadlines, notice types, and ranking are client-side filters. TED currently returns publication dates with a numeric UTC offset and deadlines as timestamps. The Actor accepts only those documented forms, validates the calendar date, rejects arbitrary suffixes/impossible dates, and emits canonical YYYY-MM-DD.
TED's official API permits at most 250 notices per page and 10,000 requested fields per page. The Actor requests 20 fields, validates every envelope, row identity, count, iteration token, and timeout flag when present. The official OpenAPI does not require timedOut, so absence is accepted; timedOut: true still fails closed. Requests run sequentially at a conservative minimum 150 ms interval. HTTP 429, 5xx, and network failures receive at most four exponential-backoff retries.
maxItems counts matched opportunities—not raw rows—so a match behind a sparse backlog of rejected notices is still found. maxPages and maxScannedItems bound cost. When a cap lands inside a page, state records the exact query window, run mode, original page size, inbound token, row offset, and compact source ledger. Later runs finish that exact window even on the next day and preserve the saved page contract when caps or page size change. A change between incremental and diff invalidates the token and replays the saved window from its start; the ledger prevents already-handled rows from hiding an unprocessed row. Expired/invalid TED tokens use the same replay path.
The Actor writes dataset rows first, then the OUTPUT summary, and commits cursor/digest state last. A source, dataset, summary, lease, or state-write failure before that state commit therefore leaves work replayable. Replays can duplicate already-written dataset rows; consumers should upsert by noticeId plus fetchedAt/changeType. Conflicting rows with the same TED notice ID and version fail across pages and partial runs. Once a window completes, later content/version changes are handled normally as updates.
TED fetching and writes are sequential within each run. Cross-run state lives in the stable named key-value store tender-opportunity-intelligence-state-v1, not the run-exclusive default store. State commits are immutable, uniquely keyed snapshots. Their generation comes from the Request Queue service's lock-expiry timestamp. After acquiring the queue lock, a run loads every visible candidate and deterministically chooses the greatest document revision tuple: maximum configuration updatedAt, then snapshot generation, then sorted configuration revision content, with the full snapshot key used only when those semantic fields are identical. A stale snapshot therefore cannot win an equal-generation comparison solely because its random UUID sorts later. If the first snapshot listing is empty, the load retries it before reading the legacy TENDER_OPPORTUNITY_STATE_V1 compatibility record; the first successful new commit migrates future visible reads to snapshots without overwriting that legacy record.
A single persistent request in the named Request Queue tender-opportunity-intelligence-lock-v1 provides exclusive writer leases. This queue name is separate from the pre-existing state store and must be creatable on the first upgraded run. Each run acquires the request lock and prolongs it during traversal and around the final state write. Queue lock operations use the queue client's ownership key, so a stale release cannot unlock a later client. Release is best-effort after a successful durable commit: a release 5xx does not mark the committed run failed, and the lock remains unavailable until its server-side lease expires.
The Request Queue provides lock acquisition and lease-expiry semantics; separate dataset, OUTPUT, and state writes are not one transaction. A lock can expire during a slow external operation. The old writer's next ownership check fails, while immutable snapshots and document revision ordering prevent its delayed state from replacing the later owner's visible state. Already-written dataset rows or OUTPUT remain replayable. Keep schedules non-overlapping to avoid duplicate dataset rows and rejected overlapping runs.
Snapshot loads are bounded because the key-value store can be eventually consistent: if listed records disappear during reads, the Actor tries every listed candidate and refreshes the listing before selecting the newest document it actually loaded. Because the runner already holds the queue lock, no conforming writer can commit concurrently during this load. The remaining failure case is storage visibility lag: a durable snapshot omitted from every bounded listing cannot be selected, so the Actor can choose the newest visible snapshot or, after two empty listings, the legacy record. If listings advertise snapshots but none can be loaded during the bounded retries, the run fails closed instead of silently falling back.
Snapshot cleanup targets at most the newest 32 visible records and only deletes excess keys whose parsed generation is strictly less than the current writer's generation. It never deletes a same-generation, concurrent, or newer snapshot. Cleanup is best-effort, so failed cleanup, stale writers, or preservation of unsafe-to-delete generations can leave more than 32 records; the legacy compatibility record is not deleted.
Bounded state
Each configuration retains compact overall/change-field digests for 180 days and at most 5,000 notices by default. Tune stateRetentionDays (7–730) and maxStateEntries (100–12,000). Up to eight recent configurations are normally retained; partial-resume configurations are protected even if that temporarily exceeds eight. Before every write, the entire UTF-8 JSON document is measured against a conservative 3,500,000-byte budget; completed old configurations and then oldest notice digests are pruned. Partial resume windows/source ledgers are never silently discarded. If protected resume data cannot fit, the run fails closed before writing the summary or state. An opportunity/configuration forgotten by retention is classified as new if encountered again.
Run output
The OUTPUT key contains a summary with counts for scanned, matched, emitted, duplicates, and each change type; the stop reason; status (succeeded or partial); configuration hash; query window; dataset ID; timestamp; and attribution. A partial status is expected when an operational cap is reached and carries a safe resume cursor.
Cost expectations
The committed Actor default is 1024 MB. At that allocation, one hour of runtime consumes 1 compute unit (CU), so a 10-minute run consumes about 0.167 CU. At the $0.20/CU observed on the release account, that is about $0.033 of compute for 10 minutes or $0.20 for one hour, before storage operations and data transfer. A one-minute small run is roughly $0.0033 of compute.
These are planning examples, not a quoted price. The actual CU rate and storage/data-transfer charges depend on the runner's Apify plan, discount tier, pricing model, chosen memory override, and run duration. TED response volume and your maxPages, maxScannedItems, and maxItems limits determine how long a run takes. Start with a narrow date window and inspect the run's Usage details before scheduling a large recurring search.
Local development
Requires Node.js 22.
npm cinpm run lintnpm run typechecknpm run buildnpm testnpm run smoke:localnpm run smoke:livenpm audit --omit=devnpm auditapify validate-schema
npm run smoke:live makes a one-item anonymous request to the live TED API. Regular tests, named-store adapter tests, and the local smoke use fixtures and never need network access, cloud storage, or secrets.
The production image compiles TypeScript in an isolated build stage and installs runtime dependencies in the final Node.js 22 image. No API key is required for anonymous TED notice search.
Data source and reuse
Source: TED — Tenders Electronic Daily and the anonymous TED Search API.
TED data may be reused commercially under the TED legal notice. TED editorial documentation is licensed under CC BY 4.0 and metadata is CC0. Preserve the attribution emitted with every row and review the current TED legal notice for your use case. This Actor does not claim ownership of TED source data and does not provide legal or procurement advice.