# Canada Opportunity Feed – Tenders, Permits, Leads (`loudtzu/canada-opportunity-feed`) Actor

Only NEW Canadian opportunities since your last call: NS tender awards, large Halifax and Calgary permits, new federal corporations, Government of Canada recalls. One schema, 0–100 score with reason, top-N digest. Open-licensed, no personal data. $0.05/run + $0.01/item.

- **URL**: https://apify.com/loudtzu/canada-opportunity-feed.md
- **Developed by:** [Travis M](https://apify.com/loudtzu) (community)
- **Categories:** Business, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Canada Opportunity Feed

**Returns only NEW Canadian business signals since your last call** — Nova Scotia tender awards, large Halifax/Calgary building permits, new federal corporations, Government of Canada recalls — in one schema, each scored 0–100 with a one-line reason, plus a top-N digest record.

- **Per item:** id, type, region, postal FSA, sector, value (CAD), date, URL, source, score, reason.
- Repeat calls never return the same item twice (state store).
- **Output:** structured JSON/CSV.

**Why not build it yourself?** No four source integrations, de-duplication state or scoring to build.

**Price (live, pay per event):** $0.05 per run for the digest + $0.01 per item. `digestOnly` = $0.05; 20 items = $0.25.

### Example inputs

```json
{}
```

All sources, all of Canada, last 7 days on the first run, up to 100 items + digest.

```json
{"regions": ["NS"], "minValueCad": 100000, "maxItems": 20}
```

Nova Scotia only, awards/permits ≥ $100k.

```json
{"sectors": ["construction", "energy"], "sinceDays": 30, "digestOnly": true, "topN": 10}
```

Cheap peek: one digest of the top 10 construction/energy items from the last 30 days. State is not changed.

```json
{"sources": ["federal-corporations"], "regions": ["NS", "NB", "PE", "NL"], "stateStore": "atlantic-newco"}
```

New Atlantic federal corporations, with their own "seen" cursor.

### Input fields

| Field | Default | Meaning |
|---|---|---|
| `sources` | all 5 | `ns-tenders`, `halifax-permits`, `calgary-permits`, `federal-corporations`, `recalls` |
| `regions` | `[]` (all) | 2-letter province codes; `CA` = national items (recalls) |
| `sectors` | `[]` (all) | construction, health, food, technology, energy, professional_services, transport, manufacturing, retail, real_estate, finance, other |
| `minValueCad` | 0 | Drops awards/permits below this value (or with no value). Does not affect companies or recalls. |
| `permitMinValueCad` | 250000 | Permits below this are never fetched (only "large" permits) |
| `sinceDays` | 7 | First-run lookback (1-90). Later runs cover the time since the last run + 3 days, at least `sinceDays`, at most 90 |
| `maxItems` | 100 | Max items returned (1–50,000), highest score first. Anything over the cap is held for the next run. |
| `fetchAll` | false | Return every new item, capped at 50,000. $0.01 per item (50,000 ≈ $500); set a max cost per run in Apify. |
| `topN` | 10 | Items listed in the digest |
| `digestOnly` | false | Return only the digest and leave the state unchanged |
| `markSeen` | true | false = don't update the state (repeatable test) |
| `stateStore` | `canada-opportunity-feed-state` | Named key-value store holding seen IDs (key `SEEN_IDS`) |

### Output

Dataset item 1 is always the **digest** (`type: "digest"`), which is also saved as `DIGEST` in the run's default key-value store. The items follow, sorted by score.

#### Item schema (every source)

| Field | Type | Notes |
|---|---|---|
| `id` | string | Stable: `<source>:<native id>`, e.g. `calgary-permits:BP2026-13695`, `federal-corporations:18302308`, `ns-tenders:<tender id>:<vendor hash>` |
| `type` | string | `tender_award`, `building_permit`, `new_company`, `market_signal` |
| `title`, `summary` | string | Plain English, no personal data |
| `region` | string | Province code or `CA` |
| `postalFsa` | string/null | First 3 postal characters only (companies) |
| `sector` | string | See the sector rules below |
| `valueCad` | number/null | Award amount or estimated project value |
| `date` | YYYY-MM-DD | Award, issue, incorporation or last-updated date |
| `url` | string | Record page when one exists, otherwise the dataset page |
| `source` | string | Dataset name |
| `licence`, `licenceUrl` | string | Required attribution line + licence link |
| `score` | 0-100 | See scoring |
| `reason` | string | One line, with the points breakdown |

#### Example output (real local run, 2026-10-16, second call, so the top 5 from the first call are excluded)

```json
{
  "id": "digest:2026-10-16:193847",
  "type": "digest",
  "title": "Canada Opportunity Feed digest: top 3 of 1432 new items since 2026-10-09",
  "summary": "115 building_permit, 27 market_signal, 1172 new_company, 118 tender_award",
  "date": "2026-10-16",
  "windowStart": "2026-10-09",
  "newItems": 1432,
  "fetchedPerSource": {
    "ns-tenders": 118,
    "federal-corporations": 1172,
    "recalls": 19793,
    "halifax-permits": 17,
    "calgary-permits": 103
  },
  "top": [
    {
      "rank": 1,
      "score": 86,
      "reason": "$3.2M construction permit, 2d old, AB (value 33+recency 28+type 20+detail 5)",
      "id": "calgary-permits:BP2026-13695",
      "type": "building_permit",
      "title": "Calgary permit: 3199 - Trade/Retail/Wholesale Misc. – New in Sage Hill",
      "region": "AB",
      "valueCad": 3162810.0,
      "url": "https://data.calgary.ca/Business-and-Economic-Activity/Building-Permits/c2es-76ed"
    },
    {
      "rank": 2,
      "score": 86,
      "reason": "$2.6M construction permit, 1d old, AB (value 32+recency 29+type 20+detail 5)",
      "id": "calgary-permits:BP2026-14747",
      "type": "building_permit",
      "title": "Calgary permit: 5109 - School, Public – Alteration in North Glenmore Park",
      "region": "AB",
      "valueCad": 2600000.0,
      "url": "https://data.calgary.ca/Business-and-Economic-Activity/Building-Permits/c2es-76ed"
    },
    {
      "rank": 3,
      "score": 85,
      "reason": "$2.5M construction permit, 2d old, AB (value 32+recency 28+type 20+detail 5)",
      "id": "calgary-permits:BP2026-17977",
      "type": "building_permit",
      "title": "Calgary permit: 1606 - Ths Townhouse – New in Mahogany",
      "region": "AB",
      "valueCad": 2544548.18,
      "url": "https://data.calgary.ca/Business-and-Economic-Activity/Building-Permits/c2es-76ed"
    }
  ],
  "scoring": "score = value(0-40, log10 $10k..$10M) + recency(0-30, linear over 30 days) + type(permit 20, award 16, new company 12, recall 8) + detail(sector known 5, record URL 5)",
  "licence": "Each top item's licence is on its full record; sources: OGL-Canada, OGL-Nova Scotia, OGL-Halifax, OGL-City of Calgary."
}
{
  "id": "calgary-permits:BP2026-13695",
  "type": "building_permit",
  "title": "Calgary permit: 3199 - Trade/Retail/Wholesale Misc. – New in Sage Hill",
  "summary": "Permit BP2026-13695 (Commercial / Multi Family Project) issued 2026-10-14, status Issued Permit. Lead for trades, suppliers and services. Address and contractor removed.",
  "region": "AB",
  "postalFsa": null,
  "sector": "construction",
  "valueCad": 3162810.0,
  "date": "2026-10-14",
  "url": "https://data.calgary.ca/Business-and-Economic-Activity/Building-Permits/c2es-76ed",
  "source": "City of Calgary Building Permits (data.calgary.ca c2es-76ed)",
  "licence": "Contains information licensed under the Open Government Licence – City of Calgary. Licence: https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa",
  "licenceUrl": "https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa",
  "score": 86,
  "reason": "$3.2M construction permit, 2d old, AB (value 33+recency 28+type 20+detail 5)"
}
```

### Scoring (deterministic, no AI)

`score = value + recency + type + detail`, capped at 0-100:

- **value (0-40):** `round(40 × log10(valueCad / 10,000) / 3)`, so $10k or less (or no value) = 0, $100k ≈ 13, $1M ≈ 27, $10M+ = 40.
- **recency (0-30):** `round(30 × (1 − ageDays/30))`, so today = 30 and 30+ days old = 0.
- **type:** building_permit 20 (an active site that needs trades and suppliers), tender_award 16 (shows who buys what), new_company 12 (new buyer of services), market_signal 8 (an indirect signal).
- **detail (0-10):** +5 when a sector was assigned, +5 when `url` is a record-level page.
  Ties are broken by value, then date, then id. `reason` reads, for example, `"$3.2M construction permit, 2d old, AB (value 33+recency 28+type 20+detail 5)"`.

**Sector rules:** permits = construction. NS awards flagged construction = construction. Recalls map by agency (TC → transport, CFIA → food, Health Canada drugs/devices → health, Consumer product safety → retail). Everything else takes the first matching keyword group in `src/core.py:SECTOR_RULES`, checked against the title, description or company name, otherwise `other`.

### Sources and licences

| Source | Licence (quoted) | Access / robots.txt |
|---|---|---|
| NS Tender Awards, data.novascotia.ca `m6ps-8j6u` | Open Government Licence – Nova Scotia: "licence to use the Information, including for commercial purposes"; attribution "Contains information licensed under the Open Government Licence – Nova Scotia" | Socrata API. robots.txt `Crawl-delay: 1`, so 1 s between pages. The dataset is refreshed roughly monthly, so this source looks back an extra 60 days; seen-ID dedupe stops repeats. |
| Federal Corporations (Corporations Canada), open.canada.ca | OGL – Canada: "including for commercial purposes"; "Contains information licensed under the Open Government Licence – Canada" | One official CSV download per run (active CBCA, ~100 MB, streamed). open.canada.ca robots.txt `Crawl-delay: 20`. The Actor makes a single request to this source. |
| Recalls and Safety Alerts, recalls-rappels.canada.ca | OGL – Canada (as above) | One JSON file under `/sites/default/files/`, which robots.txt does not disallow |
| Halifax building permits, data-hrm.hub.arcgis.com | Open Government Licence—Halifax v1.0: "licence to use the Information, including for commercial purposes"; "Contains information licenced under the Open Government Licence—Halifax" | ArcGIS REST. The host serves no robots.txt. 1 s between pages. |
| Calgary building permits, data.calgary.ca `c2es-76ed` | Open Government Licence – City of Calgary v2.1: "including for commercial purposes" | Socrata API. robots.txt `Crawl-delay: 1`; `/resource` is allowed. |

**Excluded: CanadaBuys tender and award notices.** They are OGL-Canada, but `canadabuys.canada.ca/robots.txt` has `User-agent: * / Disallow: /` (only Googlebot and Bingbot are allowed), and the CSVs exist only on that host. Tender descriptions also carry named contacts.

This Actor is independent and not endorsed by any government.

### Privacy

- No person names, emails, phones or street addresses.
- Companies: street lines dropped, postal code cut to 3 characters (FSA), city cleaned. Any corporation whose name may be a person's (e.g. "Jane Doe Professional Corporation") is dropped entirely.
- NS awards: the vendor is named only if it carries a legal-form suffix (Inc, Ltd, ...) or is a public body and doesn't look like a person. Otherwise the summary says "vendor name withheld".
- Permits: no address, PID, applicant, contractor or description fields are read.

### State and dedupe

The named key-value store `stateStore` holds `SEEN_IDS = {version, lastRunDate, seen: {id: firstSeenDate}}`. Only returned items are marked seen. IDs older than 400 days are pruned. Delete the store or change `stateStore` to start over.

### Pricing events (`.actor/pay_per_event.json`)

`result` $0.01 per returned item (`Actor.charge("result", count=n)`), `digest` $0.05 once per run (`Actor.charge("digest")`). If the caller's spending limit is lower, the item count is cut to fit. This pricing is live on Apify.

# Actor input Schema

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

Which feeds to read. Any of: "ns-tenders" (Nova Scotia awarded tenders), "halifax-permits" and "calgary-permits" (large building permits), "federal-corporations" (new federal incorporations), "recalls" (Government of Canada recalls as market signals). Empty = all.

## `regions` (type: `array`):

Keep only items in these 2-letter province/territory codes, e.g. \["NS"] or \["NS","NB","PE"]. "CA" = national items (recalls). Empty = all of Canada.

## `sectors` (type: `array`):

Keep only these sectors: construction, health, food, technology, energy, professional_services, transport, manufacturing, retail, real_estate, finance, other. Empty = all. Sector is assigned by fixed keyword rules (see README).

## `minValueCad` (type: `integer`):

Drop tender awards and permits below this dollar value (and those with no value). Items that never have a value (new companies, recalls) are not affected. 0 = no minimum. Example: 100000.

## `permitMinValueCad` (type: `integer`):

Building permits below this estimated project value are never fetched, so only large permits appear. Default 250000.

## `sinceDays` (type: `integer`):

On the first run (empty state) return items dated in the last N days. Later runs cover the time since the previous run (+3 days overlap) and never less than N days. Items already returned are never repeated. 1-90, default 7.

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

Maximum new items to return (highest score first). Each is a billable 'result' event. Items over the cap stay unseen and come back next run. 1-50,000, default 100. Ignored when 'Fetch all' is on.

## `fetchAll` (type: `boolean`):

Opt in to return every new item, capped at 50,000 per run. Each item is a billable result ($0.01 each), so 50,000 costs about $500. Tip: set a maximum cost per run in Apify run options.

## `topN` (type: `integer`):

How many top-scored items to list in the digest record. 1-50, default 10.

## `digestOnly` (type: `boolean`):

true = return only the single digest record (one 'digest' event, no 'result' events) and do NOT mark anything as seen (a cheap peek). Default false.

## `markSeen` (type: `boolean`):

true (default) = remember returned item IDs so the next run returns only newer items. false = do not change the state (repeatable test run).

## `stateStore` (type: `string`):

Name of the key-value store that holds seen item IDs (key SEEN_IDS). Use a different name per agent or per use case to keep separate 'new since last run' cursors. Letters, digits, hyphens.

## Actor input object example

```json
{
  "sources": [
    "ns-tenders",
    "halifax-permits",
    "calgary-permits"
  ],
  "regions": [],
  "sectors": [],
  "minValueCad": 0,
  "permitMinValueCad": 250000,
  "sinceDays": 7,
  "maxItems": 100,
  "fetchAll": false,
  "topN": 10,
  "digestOnly": false,
  "markSeen": true,
  "stateStore": "canada-opportunity-feed-state"
}
```

# Actor output Schema

## `results` (type: `string`):

Default dataset, overview table view.

# 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 = {
    "sources": [
        "ns-tenders",
        "halifax-permits",
        "calgary-permits"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("loudtzu/canada-opportunity-feed").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 = { "sources": [
        "ns-tenders",
        "halifax-permits",
        "calgary-permits",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("loudtzu/canada-opportunity-feed").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 '{
  "sources": [
    "ns-tenders",
    "halifax-permits",
    "calgary-permits"
  ]
}' |
apify call loudtzu/canada-opportunity-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,loudtzu/canada-opportunity-feed"
        }
    }
}
```

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/0N8gq8o85qCUUKGpR/builds/C41hOhmbOdvmpVuvO/openapi.json
