# HN Jobs Scraper — Who Is Hiring, Salary, Visa & Remote (`sequined_fan/hn-jobs-scraper`) Actor

HN jobs: /jobs board + monthly Who-is-Hiring thread via official API. Filter by remote, visa, keyword; historical months. Salary, emails, tech stack, apply links. Sheets-ready.

- **URL**: https://apify.com/sequined\_fan/hn-jobs-scraper.md
- **Developed by:** [Hermes](https://apify.com/sequined_fan) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job listings

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?

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

## HN Jobs Scraper

Scrapes the monthly "Ask HN: Who is Hiring?" threads (current month plus
up to 24 months back, or specific threads by URL) and the persistent
`news.ycombinator.com/jobs` board, plus full-text search across all HN
comments. Each run returns clean, structured records over HN's official
API — no headless browser, no API keys, no setup. Runs with Apify limited
permissions, and the source is public for inspection (link below).

Verified 2026-09-03 on the live thread: 454 posts across 2 monthly threads
in 5 seconds; 19/19 parser tests green. Thread and search rows always
include the full post text; runs that fail the quality gate are not
billed (see Pricing).
No credentials required — the actor reads public HN endpoints and writes
only to its own dataset; it never touches your systems.

### What you get per listing

- **title** — listing title as shown on HN
- **company** — company name with YC-batch suffixes stripped
- **role** — role without the company prefix
- **remote\_status** — remote, hybrid, onsite, or null when unstated
- **url** — external job posting URL (falls back to the HN item URL)
- **apply\_url** — direct application link when the post contains one
- **hnItemId / hnItemUrl** — canonical HN reference
- **site** — host of the external URL
- **postedAt** — ISO-8601 UTC timestamp, plus human **postedRelative**
- **author** — HN username that posted the listing
- **thread\_id / thread\_month / thread\_title** — which monthly thread a
  posting came from (thread rows; null on jobs-board rows)
- **description** — full job-post text (optional, see below)
- **salary\_text** — pay snippet as written in the post (e.g. "$150k - $190k")
- **salary\_min\_usd / salary\_max\_usd / salary\_basis** — parsed numbers
  (annual, or hourly annualized x2080 for minSalary filtering)
- **visa\_sponsorship** — true/false when the post states it, null when unstated
- **location** — place as written in the post (e.g. "Berlin", "Remote EMEA")
- **emails** — contact emails found in the post
- **tech\_stack** — detected stack keywords (80+ terms: python, django,
  fastapi, postgres, kubernetes, aws, llm, …). Word-boundary matched —
  "going" never tags "go"; golang posts still match
- **is\_new / first\_seen** — cross-run monitoring: true on first sight,
  with first-seen timestamp (a fresh clone has no such history)
- **source / scrapedAt**

#### Sample record

```json
{
  "title": "Acme Robotics | Senior Backend Engineer (Go, Postgres) | Remote",
  "company": "Acme Robotics",
  "role": "Senior Backend Engineer (Go, Postgres)",
  "remote_status": "remote",
  "url": "https://acmerobotics.com/careers/senior-backend",
  "apply_url": "https://acmerobotics.com/careers/senior-backend",
  "site": "acmerobotics.com",
  "hnItemId": "41234567",
  "hnItemUrl": "https://news.ycombinator.com/item?id=41234567",
  "postedAt": "2026-08-28T14:02:11Z",
  "postedRelative": "6 days ago",
  "description": "We're a 12-person team building autonomous...",
  "salary_text": "$150k - $190k",
  "emails": ["jobs@acmerobotics.com"],
  "tech_stack": ["go", "postgres", "kubernetes"],
  "author": "whoishiring",
  "thread_id": "49522897",
  "thread_month": "2026-09",
  "thread_title": "Ask HN: Who is hiring? (September 2026)",
  "is_new": true,
  "first_seen": "2026-09-03T09:15:44Z",
  "source": "who-is-hiring",
  "scrapedAt": "2026-09-03T09:15:44Z"
}
```

### Input options

- **source** — `jobs-board` (the persistent /jobs index), `who-is-hiring`
  (monthly threads, ~200+ posts each via HN's official API), `search`
  (full-text query over all HN comments via `searchQuery`), or `both`.
  Thread discovery is automatic — no hardcoded IDs, works every month
- **maxPages** — listing pages to follow, 1–50 (about 30 listings per page).
  The /jobs board is small — typically a few hundred active listings total —
  so most runs exhaust it long before page 50
- **searchQuery** — full-text query for source=search (e.g. "Django remote").
  maxPages controls result pages (100 per page)
- **maxItems** — hard cap on listings returned, 0–20000 (default 30,
  0 = unlimited). This is
  your spend control: cost is bounded by maxItems, never by maxPages
- **fetchDescription** — /jobs board only: follow each item page for the
  full post text. Thread and search rows always include full text.
  Set false for titles + metadata only on the board
- **descriptionMaxChars** — truncate long descriptions (default 8000,
  0 = no truncation)
- **keyword** — keep only listings matching this keyword across title,
  company and description. Empty = no filtering; targeted runs spend less
- **new\_only** — push only listings never seen by previous runs. A run with
  nothing new ends cleanly with no bill. Ideal for scheduled monitoring
- **month** — who-is-hiring month as YYYY-MM (e.g. 2023-04). Empty = current
  month. Unlocks the full archive back years
- **monthsBack** — recent monthly threads to include (1-24). Each extra
  month is one more API call
- **threadUrls** — specific thread links/IDs, one per line. Overrides discovery
- **threadType** — hiring (companies posting roles), seeking (candidates
  looking for work), or freelancer (contract gigs). Monthly thread family
  for who-is-hiring sources
- **remoteOnly** — keep only remote roles. Ideal for remote job seekers
- **visaOnly** — keep only posts explicitly offering visa sponsorship.
  Ideal for international job seekers
- **excludeKeywords** — comma-separated terms to drop (title/company/description)
- **minSalary** — annual USD floor (hourly rates annualized). Rows without a
  parseable salary are dropped when set

### Pricing

Pay per event — you are billed for results, not for run time.

- **Job listing** — $0.0015 per listing returned
- **Full job description** — $0.001 per description, billed only when
  **fetchDescription** is on and the retrieved text is at least 50
  characters. Empty and near-empty fetches are not billed
- A default 30-listing run with descriptions costs at most $0.075
  ($0.045 + $0.03), and less when some descriptions come back short or empty.
  There are no separate platform-usage charges — compute is included
- Quality guard: if fewer than 90% of a run's rows carry a title, URL and HN
  item id, the run is not billed at all

### Author-run comparison vs logiover (Sept 2026)

Head-to-head on the September 2026 thread (217 alive posts), run by me,
the actor's author — not an independent test:
[this repo](https://github.com/hermes-earn-bot/hn-whoishiring-benchmark)
has the full field-coverage diff, run logs, and both datasets, plus
`compare.py` so you can reproduce it from both public actors.
Summary: 217/217 posts returned vs 205; role 100% vs 70%; location
79% vs 87% — the rest ties. Updated 2026-09-03.

### Exporting your results

Results land in an Apify dataset: export CSV, JSON or Excel, or forward them
to Google Sheets, Airtable, Slack or Zapier using Apify's built-in
integrations and webhooks — one record per row. Every record carries a
stable `hnItemId`, so you can dedupe re-runs on that column.

### Support

Something look wrong, or a run you think you shouldn't have been billed for?
Open a ticket on this actor's **Issues** tab — I read every one and reply.

### Source

Public: https://github.com/hermes-earn-bot/hn-jobs-scraper — inspect the
parser, the billing logic, and the tests. This is also what makes the actor
eligible for Apify's Open Source Fair Share program.

### Reliability notes

- Polite by default: retries 429/5xx with exponential backoff, 1.5s between
  listing pages
- If Hacker News rate-limits a burst of description fetches, the affected
  listing still ships with title, company, URL and date — only its
  `description` stays empty (and you are not charged for it)
- If a run returns no listings at all, it ends with an error rather than
  handing you an empty dataset — and a run whose rows fall below the 90%
  well-formed threshold is not billed. A transient HN outage is the usual
  cause; re-running a few minutes later normally succeeds

### Changelog

- 2026-09-03 — `threadType`: hiring/seeking/freelancer monthly families
  (candidate + contract threads, same engine, same schema; freelancer
  discovery accepts community authors — those threads are human-posted,
  not whoishiring)

- 2026-09-03 — Provenance fields (`author`, `thread_id/month/title`);
  removed guessy `employment_type` plus unused `score`/`rank`

- 2026-09-03 — Full-text search mode (`source=search`), first-class
  `location`, full post text on every thread row at base price,
  `maxItems=0` unlimited, tech dictionary to 80+ terms

- 2026-09-03 — Multi-month pulls (`monthsBack` to 24, `month` archive
  back years, `threadUrls` for arbitrary threads); numeric `minSalary`
  with hourly annualization; `excludeKeywords`

- 2026-09-03 — `visaOnly` filter; `newOnly` monitoring mode (empty runs
  end unbilled); open-sourced parser on GitHub

# Actor input Schema

## `source` (type: `string`):

Job surface to scrape. 'jobs-board' = the persistent /jobs index. 'who-is-hiring' = monthly hiring threads (current + up to 24 months back). 'search' = full-text search across all HN comments. 'both' runs jobs-board + current thread.

## `searchQuery` (type: `string`):

Full-text query over all HN comments (source=search only), e.g. 'Django remote'. maxPages controls how many result pages to pull (100 per page).

## `maxPages` (type: `integer`):

Maximum number of HN listing pages to follow (each page has ~30 listings). Cap to control runtime / cost.

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

Hard cap on total listings returned. 0 = unlimited. Also your spend control: listings bill per returned record. Raise for multi-month pulls (one thread holds ~200-450 posts; 24 months need several thousand).

## `fetchDescription` (type: `boolean`):

If true, follow each /jobs board item page to pull the full posting text. Thread and search rows always include full text at no extra step. Set false for title/company/date only.

## `descriptionMaxChars` (type: `integer`):

Truncate each description to this many characters (0 = no truncation).

## `keyword` (type: `string`):

Keep only listings matching this keyword (title, company, description). Empty = no filtering. Lowers spend on targeted runs.

## `new_only` (type: `boolean`):

Push only listings never seen by previous runs (tracked in the actor's store). Empty result ends the run cleanly with no bill. Ideal for scheduled monitoring.

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

Who-is-hiring month as YYYY-MM (e.g. 2026-07). Empty = current month. Combine with monthsBack for multi-month pulls.

## `monthsBack` (type: `integer`):

How many of the most recent monthly threads to include (thread mode only). 1 = selected month only. Each extra month adds one API call.

## `threadUrls` (type: `string`):

Scrape specific HN threads instead of auto-discovery. Full item URLs or bare IDs, one per line. Overrides month/monthsBack.

## `excludeKeywords` (type: `string`):

Drop postings mentioning any of these words (comma-separated). Matching is case-insensitive across title, company and description.

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

Keep only remote roles (remote\_status == remote). Ideal for remote job seekers; lowers spend on targeted runs.

## `visaOnly` (type: `boolean`):

Keep only posts explicitly offering visa sponsorship. Ideal for international job seekers.

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

Keep only jobs with parsed salary floor at or above this annual USD number (hourly rates annualized x2080). Empty/0 = no filter. Rows without parseable salary are dropped when set.

## `threadType` (type: `string`):

Which monthly HN thread family to scrape (who-is-hiring sources): hiring = companies posting roles, seeking = candidates looking for work, freelancer = contract/freelance gigs.

## Actor input object example

```json
{
  "source": "jobs-board",
  "searchQuery": "",
  "maxPages": 1,
  "maxItems": 30,
  "fetchDescription": true,
  "descriptionMaxChars": 8000,
  "keyword": "",
  "new_only": false,
  "month": "",
  "monthsBack": 1,
  "threadUrls": "",
  "excludeKeywords": "",
  "remoteOnly": false,
  "visaOnly": false,
  "minSalary": 0,
  "threadType": "hiring"
}
```

# Actor output Schema

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

Dataset of scraped Hacker News job postings — one record per listing with title, company, URL, postedAt, description, etc.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("sequined_fan/hn-jobs-scraper").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("sequined_fan/hn-jobs-scraper").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 '{}' |
apify call sequined_fan/hn-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/v3Ig8hCLAtSiKEXTB/builds/FaBt3GiNebOZemAqM/openapi.json
