# Remote Jobs Aggregator - 6 Job Boards, Ghost-Job Scoring (`wfh_scout/remote-jobs-aggregator`) Actor

Fetches every open remote job from six public job boards, normalises them onto one schema, folds duplicates, and scores each posting for freshness and ghost-job risk.

- **URL**: https://apify.com/wfh\_scout/remote-jobs-aggregator.md
- **Developed by:** [Dave Fergins](https://apify.com/wfh_scout) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job returneds

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/platform/actors/running/actors-in-store#pay-per-event

## 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

## Remote Jobs Aggregator

**Every open remote job from six public job boards, on one schema, with each posting scored for how likely it is to still be real.**

Six boards publish remote jobs in six different shapes. Remote OK calls the role `position` and dates it with a unix `epoch`; Remotive calls it `title` with an ISO string; We Work Remotely ships RSS with the company glued to the front of the title. Before you can search across them you have to reconcile all of that, and then work out which postings are still open.

This Actor does both, and hands you a flat dataset you can query.

***

### What you get

Roughly **600 remote jobs** on default settings, or **~1,000** with deeper paging, refreshed on every run. Each row carries:

| | |
|---|---|
| **Identity** | stable `id` across runs, source board, original posting URL, direct `applyUrl` where the board publishes one |
| **The role** | title, company, plain-text description, category, tags, employment type, seniority |
| **Where** | `remote` flag, the board's own location text, normalised `regions` (worldwide / usa / canada / latam / uk / europe / apac / africa / middle\_east), and any timezone restriction |
| **Pay** | min, max, currency and period — parsed into numbers from whatever prose the board used |
| **When** | posted date, expiry where published, and age in days |
| **Trust** | `freshness` 0–1, `ghostRisk` low/medium/high, and `ghostReason` explaining the verdict in plain words |

#### The trust fields are the point

Aggregated job feeds are full of postings that are still published but no longer open: filled roles left up for pipeline, evergreen "talent pool" adverts, and boards that never expire anything. Paying per row for those is annoying.

Every posting gets a risk band and the reasons behind it:

```json
{
  "title": "Senior Backend Engineer",
  "company": "Acme",
  "freshness": 0.71,
  "ageDays": 10.4,
  "ghostRisk": "low",
  "ghostReason": ["recent, and nothing contradicts it"]
}
```

```json
{
  "title": "General Application — Engineering Talent Pool",
  "company": "Globex",
  "freshness": 0.12,
  "ageDays": 64.2,
  "ghostRisk": "high",
  "ghostReason": [
    "posted 64 days ago",
    "reads as an open/pipeline advert: talent pool"
  ]
}
```

The signals are computed from what the boards actually publish — age, the board's own expiry date, corroboration across boards, missing application links, and evergreen phrasing. Nothing is inferred by a model, and nothing claims certainty. You can set `maxGhostRisk` to filter, or ignore the field entirely and use the reasons yourself.

Freshness decays on a 21-day half-life. A posting with **no** date scores 0.5, not 1.0 — absence of evidence is not evidence of freshness.

***

### Example input

Recent, well-paid, US-or-worldwide backend roles, nothing that smells stale:

```json
{
  "query": ["golang", "backend", "platform"],
  "regions": ["usa"],
  "seniority": ["senior", "lead", "principal"],
  "maxAgeDays": 21,
  "minSalary": 120000,
  "salaryOnly": true,
  "maxGhostRisk": "low",
  "maxItems": 200
}
```

Everything is optional. Run it with no input at all and you get every remote job the six boards currently publish, best-first.

A few behaviours worth knowing:

- **Search terms are OR-ed.** `["go", "rust"]` returns jobs mentioning either, which is what a job search means in practice.
- **Worldwide jobs match every region filter**, because a job open to everyone is open to you.
- **`minSalary` is annualised first**, so hourly and monthly rates compare correctly against it.
- **`maxAgeDays` excludes rows whose age is unknown.** If you ask for jobs under 14 days old, you should not get rows where nobody published a date.
- **`maxItems` is your cost ceiling** — you are billed per job returned.

***

### Sources

| Board | What it adds |
|---|---|
| [Remote OK](https://remoteok.com) | broad, global, tech-weighted |
| [Remotive](https://remotive.com) | curated, strong salary coverage |
| [Himalayas](https://himalayas.app) | the only board publishing explicit expiry dates and numeric timezone restrictions |
| [Arbeitnow](https://www.arbeitnow.com) | Europe-weighted, by far the largest single feed |
| [We Work Remotely](https://weworkremotely.com) | long-running, well-known employers |
| [Jobicy](https://jobicy.com) | cleanest structured salary data |

All six are public, documented, unauthenticated APIs or feeds. This Actor does not scrape HTML, defeat a bot check, or touch personal data — these are employer-published job adverts, fetched the way each board asks to be fetched, with a paced, budgeted request pattern and an honest user agent.

Listings from Remote OK are used under their API terms, which require a followed link back — the run report carries the required credit line for every board included.

#### One thing the data says that you might expect otherwise

**These boards barely overlap.** Measured across a full live run: 1.7% of employers, and exactly **1 of 1,513** company+title pairs, appear on more than one board.

So this is not a tool that saves you from wading through the same job six times — there is almost no duplication to remove. It is a tool that gets you **six boards' worth of distinct jobs** in one shape and one query, which is the more useful thing anyway. Duplicates are still folded when they do occur (including repeats *within* a single board, which are more common), and `seenOn` tells you every board carrying a row.

***

### Output

One dataset item per job. Two prebuilt views: **Jobs** (everything) and **Low ghost-risk only**. Export as JSON, CSV, Excel or via the API like any Apify dataset.

If a board fails, the run does not: the other five still publish, and the failure is named explicitly in the log and the run report rather than quietly reducing your row count.

***

### Not included, deliberately

- **No LinkedIn, Indeed or Glassdoor.** Those need scraping against active bot defences and, for profile data, raise real GDPR and CCPA questions. Everything here is a published open API.
- **No contact details, no candidate data.** Job adverts only.
- **No guess-work in empty fields.** A blank `seniority` means the source never said, not that the job is mid-level. A blank salary means unpublished, not zero.

***

### Running it yourself

The Actor is a single static Go binary with no third-party dependencies, and it runs identically off-platform:

```bash
go test ./...
go run ./cmd/actor              # reads ./INPUT.json if present, writes NDJSON to stdout
go run ./cmd/probe              # per-board health and field coverage
go run ./cmd/probe -overlap     # measure how much the boards actually duplicate each other
```

`cmd/probe` exists because these are six third-party feeds that change without notice. When a board silently renames a field the Actor does not crash — it quietly publishes rows with an empty company, or a zero date that scores as maximally fresh. The probe reports per-field coverage so that gets caught before a buyer pays for it.

# Actor input Schema

## `query` (type: `array`):

Keywords matched against job title, company and tags. Terms are OR-ed, so \["go", "rust"] returns jobs mentioning either. Leave empty to get everything.

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

Restrict to these hiring regions. Jobs open worldwide always match, whatever you pick here.

## `employmentTypes` (type: `array`):

Rows whose type the source never stated are excluded when you set this.

## `seniority` (type: `array`):

Derived from the job title and, where a board publishes one, its own seniority field.

## `maxGhostRisk` (type: `string`):

Drop postings that look stale, expired or evergreen. 'low' is the strictest and keeps only postings nothing contradicts.

## `maxAgeDays` (type: `integer`):

Drop postings older than this. Rows whose posting date no board published are also dropped, because their age cannot be checked. 0 means no age limit.

## `minSalary` (type: `integer`):

Annualised before comparison, so hourly and monthly rates are handled. Only applies to rows that publish pay — combine with 'Only jobs with a published salary' to exclude the rest.

## `salaryOnly` (type: `boolean`):

Roughly a quarter of postings publish pay. This keeps only those.

## `remoteOnly` (type: `boolean`):

One of the six boards also carries on-site roles. Leave on unless you want those too.

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

Leave empty for all six. Fewer boards means a faster, cheaper run.

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

You are billed per job returned, so this is your cost ceiling. 0 means no cap.

## `maxPagesPerSource` (type: `integer`):

How deep to page each board. 10 (the default) returns roughly 600 remote jobs; 35 returns roughly 1,000. Higher costs more time and is heavier on these free public APIs.

## `includeDescription` (type: `boolean`):

Turn off for a much smaller dataset when you only need titles, companies and links.

## `descriptionMaxLen` (type: `integer`):

Characters. Descriptions are plain text, never raw HTML. Use -1 for no cap.

## Actor input object example

```json
{
  "query": [
    "golang",
    "backend"
  ],
  "regions": [],
  "employmentTypes": [],
  "seniority": [],
  "maxGhostRisk": "high",
  "maxAgeDays": 14,
  "minSalary": 0,
  "salaryOnly": false,
  "remoteOnly": true,
  "sources": [],
  "maxItems": 200,
  "maxPagesPerSource": 10,
  "includeDescription": true,
  "descriptionMaxLen": 4000
}
```

# Actor output Schema

## `jobs` (type: `string`):

All matching jobs as JSON. Each row carries the role, company, normalised hiring regions, parsed salary, posting age, and a ghostRisk band with the plain-language reasons behind it.

## `jobsCsv` (type: `string`):

The same rows as CSV, for spreadsheets and tools that do not read JSON.

## `liveJobsOnly` (type: `string`):

Only the postings nothing contradicts: not expired, not stale, not an evergreen talent-pool advert.

# 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 = {
    "query": [
        "golang",
        "backend"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("wfh_scout/remote-jobs-aggregator").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 = { "query": [
        "golang",
        "backend",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("wfh_scout/remote-jobs-aggregator").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 '{
  "query": [
    "golang",
    "backend"
  ]
}' |
apify call wfh_scout/remote-jobs-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,wfh_scout/remote-jobs-aggregator"
        }
    }
}

```

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/Q2GhBvwL0u3R2Je1X/builds/OBg1vmCxbuRtea0zp/openapi.json
