# Hacker News Jobs Scraper: Who is Hiring, Classified (`constanciodev/hacker-news-who-is-hiring`) Actor

Hacker News Who is Hiring jobs as structured data: company, role, location, salary, seniority, area, remote scope and remote open to Brazil. Official HN API, monthly thread, monitor mode for new posts only.

- **URL**: https://apify.com/constanciodev/hacker-news-who-is-hiring.md
- **Developed by:** [Constancio Dev](https://apify.com/constanciodev) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 job postings

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

## Hacker News Jobs Scraper: Who is Hiring, Classified

Get every job from Hacker News Who is Hiring, structured and classified: seniority, area,
salary, remote, remote-open-to-Brazil. Point it at a month (or leave it on `latest`) and get one
clean row per job comment instead of 300 free-text comments to read by hand.

Built by **Constancio Dev**, a small engineering studio focused on data pipelines and B2B
automation. [constanciodev.com.br](https://constanciodev.com.br)

### Why this Actor

- **The classification is the point.** Other Who is Hiring scrapers return the comment text
  split on `|`. This one also tells you the `seniority`, the `area` (backend, frontend, data,
  ML/AI, DevOps and more), the `remote_scope` and, for people who live in Brazil,
  `remote_open_to_brazil`. You can filter on all of them, so a run returns only the jobs you
  could actually take.
- **Official API, no browser, no proxy.** It reads the public
  [Hacker News API](https://github.com/HackerNews/API) (MIT licensed, Firebase). No login, no
  scraping of web pages.
- **Any month.** `latest` finds the newest "Ask HN: Who is hiring?" thread by itself, and
  `YYYY-MM` reads an older one.
- **Tolerant parser.** The usual format is `Company | Role | Location | Remote/Onsite | Salary |
  URL`, but people write it in any order, with dashes instead of pipes, as `Role: ...` lines or in
  plain prose. Each piece of the header is classified by what it says, not by its position.
- **Monitor mode for schedules.** Run it daily and pay only for comments that are new since the
  last run. The Actor remembers each comment for 90 days.
- **Safe to share.** Contact e-mails inside comments are masked (`***@company.com`).

### Input

| Field | What it does |
|---|---|
| `month` | `latest` (default) or `YYYY-MM`, for example `2026-09`. |
| `keywords` | Keep jobs whose role, company or text contains at least one of these words. |
| `remote` | Keep only jobs that offer remote work. |
| `remoteOpenToBrazil` | Keep only remote jobs a person living in Brazil can take (see [Classification](#classification)). |
| `seniority` | `intern`, `junior`, `mid`, `senior`, `staff_plus`, `manager`, `unknown`. Empty keeps all. |
| `area` | `backend`, `frontend`, `fullstack`, `data`, `ml_ai`, `devops_sre`, `mobile`, `security`, `qa`, `product`, `design`, `other`. Empty keeps all. |
| `country` | ISO codes or names (`US`, `Germany`). Matches any country the post lists. |
| `maxItems` | Stop after this many jobs (default 1,000; a month has about 300). 0 means no limit. |
| `includeDescription` | Include the cleaned comment text (default: true). |
| `monitorMode` | Deliver and charge only comments not delivered by an earlier monitor run. |

#### Example 1: remote jobs open to Brazil, newest month

```json
{
  "month": "latest",
  "remoteOpenToBrazil": true
}
```

#### Example 2: senior backend and DevOps jobs in September, mentioning Rust or Go

```json
{
  "month": "2026-09",
  "seniority": ["senior", "staff_plus"],
  "area": ["backend", "devops_sre"],
  "keywords": ["rust", "go"]
}
```

#### Example 3: a daily alert with only the new remote jobs

```json
{
  "month": "latest",
  "remote": true,
  "monitorMode": true,
  "includeDescription": false
}
```

Schedule it once a day. The first run delivers the month so far, and every later run delivers
only what was posted since. If you schedule it across a month change, the new thread is picked
up by `latest` on the day it appears.

### Output

One dataset item per job comment. Real item from the October 2026 thread (description cut):

```json
{
  "id": "hn:49925063",
  "hn_comment_id": 49925063,
  "hn_thread_id": 49922569,
  "hn_thread": "2026-10",
  "company": "Discord",
  "title": "Senior Software Engineer, Application Security",
  "url": "https://news.ycombinator.com/item?id=49925063",
  "company_url": null,
  "links": ["https://discord.com/jobs/8656854002"],
  "location": "REMOTE (US West Coast) or San Francisco, CA",
  "country": "US",
  "countries": ["US"],
  "state": "CA",
  "work_model": "remote",
  "employment_type": null,
  "posted_at": "2026-10-01T18:09:56+00:00",
  "salary_min": 196000.0,
  "salary_max": 245000.0,
  "salary_currency": "USD",
  "salary_period": "year",
  "seniority": "senior",
  "area": "security",
  "remote_scope": "remote_country_specific",
  "remote_open_to_brazil": false,
  "years_experience": null,
  "scraped_at": "2026-10-23T16:29:24.655730+00:00",
  "description": "Discord | Senior Software Engineer, Application Security | REMOTE (US West Coast) or San Francisco, CA | $196K-$245K base + equity\nDiscord is where ..."
}
```

| Field | Meaning |
|---|---|
| `hn_comment_id`, `url` | The comment on Hacker News. Apply links the post cites are in `links`. |
| `company`, `title` | From the header. `title` is `null` when the post names no role in its header. |
| `location`, `country`, `countries`, `state` | Location as written; the first country found, every country found, and the US state. |
| `work_model` | `remote`, `hybrid` or `onsite`. `remote` when remote is one of the options the post offers. |
| `employment_type` | `full-time`, `part-time`, `contract` or `internship`, when the post says. |
| `salary_*` | Range as the post states it, with currency and period. `null` when it does not say. |
| `seniority` | `intern`, `junior`, `mid`, `senior`, `staff_plus`, `manager` or `unknown`. |
| `area` | `backend`, `frontend`, `fullstack`, `data`, `ml_ai`, `devops_sre`, `mobile`, `security`, `qa`, `product`, `design` or `other`. |
| `remote_scope` | `remote_global`, `remote_latam_or_americas`, `remote_country_specific`, `hybrid`, `onsite` or `unknown`. |
| `remote_open_to_brazil` | `true`, `false`, or `null` when the post does not say. |
| `years_experience` | Years of experience required, when stated. |

Fields are never guessed: no evidence means `null` (or `unknown`).

#### What the October 2026 thread gave

The thread had 319 top-level comments; **256 were job posts** (34 were deleted, 22 flagged dead
and 7 were meta comments or off topic). In those 256: 85 state a salary, 144 offer remote work and
19 are remote and open to Brazil. 65 name no role in the header (`title: null`), and 176 have
`seniority: unknown` because the post lists several roles or states no level.

### Classification

Deterministic rules over the role, location and text. No paid API, no AI model: the same post
always gets the same answer.

- **`seniority`** reads the role first (`Senior`, `Sr.`, `Staff`, `Principal`, `Lead`, `Head of`,
  `Director`, `Intern`, `Junior`, `II`). With no level in the role, the years of experience the
  post asks for decide: up to 1 is `junior`, 2 to 4 is `mid`, 5 or more is `senior`. A comment
  that lists several roles at different levels is `unknown`.
- **`area`** reads the role. Sales, support, recruiting and other non engineering roles are
  `other`, and so is an engineering role that names no specialty (plain "Software Engineer") or a
  comment that lists roles from several areas.
- **`remote_scope`** and **`remote_open_to_brazil`** combine the header, the location and
  phrases in the text. `remote_open_to_brazil` is `true` for remote jobs open worldwide, in Latin
  America or the Americas, in Brazil, or in a time zone range that includes UTC-3. It is `false`
  for hybrid and onsite jobs and for remote jobs limited to another country or region. A bare
  "Remote" stays `null`.

Measured on real comments from the October 2026 thread, labeled by hand reading the text and not
the Actor's output. 52 comments guided the rules; 30 more were labeled afterwards and measured
before any adjustment.

| Field | 30 unseen comments | All 82 |
|---|---|---|
| `company` | 96.7% | 98.8% |
| role (`title`) | 96.7% | 98.8% |
| `location` | 96.7% | 97.6% |
| `seniority` | 90.0% | 91.5% |
| `area` | 96.7% | 96.3% |
| `remote_open_to_brazil` | 90.0% | 96.3% |

The sample is small and from one month, so treat the numbers as a guide: other months may have
more unusual posts. The test suite fails if any field drops below 80%.

### Pricing

Pay per event: you pay only for jobs delivered to the dataset. Set a maximum cost per run in the
run options and the Actor stops when it reaches it.

- **$1.80 per 1,000 jobs** on the Free plan, and less on paid Apify plans ($1.60 Bronze, $1.10
  Silver and above).
- **$0.00005** per run start.

A whole monthly thread (about 250 job posts) costs about $0.45 on the Free plan. Monitor mode
charges only new comments.

### Limits

- One thread per run. Run it twice to compare two months.
- The Actor reads top-level comments only; replies are not jobs.
- Posts written in free prose parse worse than the `Company | Role | Location` format: the
  company, location or role may be missing, and then the field is `null`.
- Many posts list several roles. Their `seniority` is `unknown` and their `area` is `other` by
  design, because one value cannot describe all of them. The roles are in `title` and the text.
- Only the header is read for the role. A post that names its roles only in the body has
  `title: null`.
- A comment edited after it was delivered is not delivered again in monitor mode.
- The comment text is the author's. Do not use the data to contact people who did not ask to be
  contacted; contact e-mails are masked on purpose.

### Use cases

- Job seekers outside the US who want only remote jobs they can legally take.
- Recruiters and staffing agencies tracking which companies hire, and for what.
- Newsletters and job boards that need a clean feed of Who is Hiring.
- Hiring trend research: how many senior ML roles, how many remote roles, month by month.

### Data source

[Hacker News API](https://github.com/HackerNews/API), MIT licensed. The Actor identifies itself
with its own user agent and reads one item per comment.

***

Questions or a field you need: open an issue on this Actor's page, or write to
<suporte@constanciodev.com.br>.

# Changelog

This Actor's version history is a separate document: https://apify.com/constanciodev/hacker-news-who-is-hiring/changelog.md

# Actor input Schema

## `month` (type: `string`):

Which 'Ask HN: Who is hiring?' thread to read: 'latest' for the most recent one, or a month as 'YYYY-MM' (for example '2026-09'). The thread is posted on the first weekday of each month.

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

Keep jobs whose title, company or text contains at least one of these words (case and accents ignored), for example 'rust', 'kubernetes', 'fintech'. Empty keeps every job.

## `remote` (type: `boolean`):

Keep only jobs that offer remote work (remote is one of the options the post lists).

## `remoteOpenToBrazil` (type: `boolean`):

Keep only remote jobs that a person living in Brazil can take: remote worldwide, remote in Latin America or the Americas, remote in Brazil, or a time zone range that includes UTC-3. Jobs with no clear evidence are left out.

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

Keep only jobs classified with one of these levels (from the role in the post header, or from the years of experience the post asks for). A comment that lists several roles with different levels is 'unknown'. Empty keeps every level.

## `area` (type: `array`):

Keep only jobs classified in one of these areas, read from the role in the post header. A comment that lists roles from several areas is 'other'. Empty keeps every area.

## `country` (type: `array`):

Keep jobs located in one of these countries, as ISO codes or names: 'US', 'Germany', 'GB'. Matches any country the post lists, and the text of the location. Empty keeps every location.

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

Stop after delivering this many jobs in the run. A monthly thread has about 300 job comments. 0 means no limit.

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

Include the cleaned comment text (contact e-mails are masked). Turn off for a smaller dataset with only the structured fields.

## `monitorMode` (type: `boolean`):

For scheduled runs: deliver and charge only comments that were not delivered by a previous monitor run in your account. The Actor remembers each comment for 90 days. Edited comments are not delivered again.

## Actor input object example

```json
{
  "month": "latest",
  "remote": false,
  "remoteOpenToBrazil": false,
  "maxItems": 1000,
  "includeDescription": true,
  "monitorMode": false
}
```

# Actor output Schema

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

Every job comment delivered in this run, as dataset items.

# 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 = {
    "month": "latest"
};

// Run the Actor and wait for it to finish
const run = await client.actor("constanciodev/hacker-news-who-is-hiring").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 = { "month": "latest" }

# Run the Actor and wait for it to finish
run = client.actor("constanciodev/hacker-news-who-is-hiring").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 '{
  "month": "latest"
}' |
apify call constanciodev/hacker-news-who-is-hiring --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,constanciodev/hacker-news-who-is-hiring"
        }
    }
}
```

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/lWizXr3NgXOOVE9yT/builds/63oHLnl0eqJ9PcYcJ/openapi.json
