Ashby Salary Scraper - Startup Job Pay Ranges
Ashby job postings with normalized annual salary ranges, equity flags and new-since-last-run detection.
datagrit › Jobs & salaries › Hacker News Who Is Hiring: Jobs & Hiring History
Jobs & salariesTurn Hacker News Who is hiring threads into job rows with salary, stack, work mode and visa, plus how many months each company has been hiring.
Hacker News Who Is Hiring: Jobs & Hiring History turns the monthly "Ask HN: Who is hiring?" thread into a clean table of job posts. Every top-level comment becomes one row with the company, roles, location, work mode, employment type, stated pay, visa sponsorship, technologies, apply link and the full post text. What other scrapers of this thread do not give you is company history: the Actor compares every company with the earlier monthly threads and tells you how many months it has been hiring, when it first and last posted before, and whether it is posting for the first time. It reads the public Hacker News search API, needs no login or API key, and exports to JSON, CSV or Excel.
2026-03.For each post the Actor normalises the company name (lower case, letters and digits only, legal suffixes removed) and, when the post links the company's own website, also keeps the domain (acme.com for Acme, factory.ai for Factory AI). It then looks for the name or the domain in the earlier threads, so a company that links its site in one month and not in the next is still one company. A link to a news article, a LinkedIn short link or a file host inside the post text is never taken for the company's domain. The companyKey column is the normalised name alone (acme, factoryai), so it does not change with the link; two spellings of one company (Factory and Factory AI) give two keys, and the history columns still treat them as one when the website domain is the same. You get monthsHiring (this month included), firstSeenMonth, previousSeenMonth, consecutiveMonths, isNewCompany and postsThisMonth. Posts where no company could be read from the header, or only a placeholder such as Stealth, Confidential or Startup, have no key and carry null history, monthsChecked included, rather than a guess. A header that starts with a place (Boston, MA - Full Time, NYC | Engineer; a name with a place after a comma, such as adapptiv Labs, Switzerland or LiquidFi (Miami, FL), stays a company), a role (Software Engineer | Acme) or an employment phrase gives no company either, and those rows are delivered free of charge, as are rows with a placeholder name. Older threads (before about 2016) were written free-form, so many of their posts have no readable company.
History is only computed from threads whose posts can be read reliably: every thread of the window, and the thread you ask for, must have a standard Company | role | location header in at least 75% of its posts. Before 2016 that is not the case (see "Good to know"), so a run that asks for history on such a month, or on a month whose window reaches one, stops with a message instead of returning rows with empty history. Set Months of history to 0 to read those months as plain posts: you then get the posts without history fields, and rows without a company name are free.
You pay per job post returned. Posts removed by your filters, posts skipped as already delivered, posts without a usable company name, placeholders such as Stealth included (the run status counts them) and a run that finds nothing are not charged: a search without matches returns a single status row with found: false that is free. Reading earlier threads for the history costs nothing extra. Set a maximum charge per run in the Apify console to cap spending; the Actor stops cleanly when it is reached.
Company | role | location format only settled around 2016. Measured on every thread in October 2026: 2011 and 2012 between 2% and 27%, 2013 between 10% and 35%, 2014 between 22% and 41%, 2015 from 35% in January to 77% in December, 2016 between 79% and 92%, 2017 to 2026 between 89% and 96%. With history switched off, posts without a company name are returned but free. With history on, a thread below 75% stops the run (see Company history); with the default 12 months of history that means asking for a month before December 2016 needs Months of history set to 0.History window 12 months (12 read). The Actor lists threads from the official whoishiring accounts and then looks up by title every month that list does not contain, or contains only as a thread with fewer than 20 comments, and keeps the thread with the most comments, so threads posted from another account (July 2011) and stub threads next to the real one (December 2011) are read correctly. A month is reported as with no Hacker News thread only when that title search finds nothing; such a month is skipped without breaking consecutiveMonths. If a month cannot be confirmed, the run fails instead of marking companies as new by mistake, and running it again fixes it.Every result is one flat record, so it drops straight into a spreadsheet, a database or a CRM.
| Field | Type | Description | Example |
|---|---|---|---|
found | boolean | False only on the single status row returned when nothing matched your filters. Normal rows do not carry this field. | false |
month | string | Status row only: the month you asked for in the Month input, or null when you asked for the latest threads. | 2026-09 |
postId | string | Hacker News comment ID of the job post. Unique, stable and safe to use as a key. | 49522903 |
sourceUrl | string | Link to the job post on news.ycombinator.com, where replies and the author profile live. | https://news.ycombinator.com/item?id=49522903 |
threadId | string | Hacker News ID of the monthly Who is hiring thread the post belongs to. | 49522897 |
threadMonth | string | Month of the thread as YYYY-MM, taken from the thread title. | 2026-09 |
postedAt | string | When the post was written, ISO 8601 in UTC. | 2026-09-01T15:01:54.000Z |
author | string | Hacker News username of the person who wrote the post. Usually a founder or recruiter of the company. | jobposter |
company | string | Company name read from the first part of the header line, without funding or batch notes. Null when the post has no header in the usual Company | role | place form, when that first part is a place, a role or an employment phrase, or when it is a placeholder such as Stealth or Confidential (the name is kept but there is no company key); such rows are delivered free of charge. | Modash.io |
companyWebsite | string | Company homepage: the URL in the header, or a link in the post whose domain carries the company name (not a news article, social or file-host link). Null when the post has none. | https://modash.io |
companyKey | string | The normalised company name: lower case, letters and digits only, legal suffixes such as Inc or GmbH removed. It is built from the name alone, so the same spelling always gives the same key whether or not the post links a website. Two different spellings of one company (Factory and Factory AI) give two keys; the history columns still match them through the website domain. Null when the post has no company name. | modashio |
headerLine | string | First line of the post exactly as written, with links shown as plain URLs. It is where company, roles, place and pay usually sit. | Modash.io | Senior Product Engineer | Remote (Europe) | Full-time | €75k–110k |
headerParsed | boolean | True when the header had the usual "Company | role | location" form with a company name. False means roles and location are best guesses or empty. | true |
roles | array | Job titles or role groups listed in the header. Empty when the header names none. | ["Senior Product Engineer"] |
location | string | Place or region from the header, for example "Remote (Europe)" or "London, UK". Null when the header gives none. | Remote (Europe) |
workModes | array | Any of remote, hybrid, onsite found in the header, or in the start of the post when the header says nothing. | ["remote"] |
employmentTypes | array | Any of full-time, part-time, contract, freelance, internship, temporary found in the header. | ["full-time"] |
salaryText | string | The pay segment exactly as written, cut at 120 characters. Null when the post states no pay in the header or in a Salary line. | €75k–110k |
salaryMin | number | Lower end of the stated pay in the currency and period given. Equal to the maximum when a single figure is stated. | 75000 |
salaryMax | number | Upper end of the stated pay in the currency and period given. | 110000 |
salaryCurrency | string | ISO currency code (USD, EUR, GBP and so on) inferred from the symbol or code next to the figures. Null when it cannot be told. | EUR |
salaryPeriod | string | year, month, day or hour. Figures of 1,000 or more with no period are read as yearly. | year |
salaryYearlyMin | number | Salary minimum converted to a year (monthly x12, daily x220, hourly x2080) so posts can be compared. Still in the posted currency; null without a known period. | 75000 |
salaryYearlyMax | number | Salary maximum converted to a year the same way as the yearly minimum. | 110000 |
visaSponsorship | boolean | True when the post offers visa or work-permit sponsorship, false when it says it does not, null when it does not mention it. | true |
technologies | array | Languages, frameworks, databases and cloud tools named anywhere in the post, from a fixed list of about 100 names. | ["TypeScript","PostgreSQL"] |
applyUrl | string | First link in the post that points to a job page or an applicant tracking system. Null when there is none. | https://jobs.ashbyhq.com/modash/123 |
links | array | Every link found in the post. | ["https://modash.io","https://jobs.ashbyhq.com/modash/123"] |
emails | array | Email addresses written in the post, lower-case. | ["jobs@modash.io"] |
descriptionText | string | Full text of the post with HTML removed; paragraphs are separated by blank lines. | Modash.io | Senior Product Engineer | Remote (Europe) | Full-time
Modash helps |
monthsChecked | integer | Number of monthly threads compared for company history, this one included. At most Months of history plus one; fewer when HN has no thread for some months (the run status then lists the missing months). Null when history is off or when the row has no company key (no earlier thread was compared). | 13 |
monthsHiring | integer | In how many of the checked threads the company posted, this one included. Null when history is off or the post has no company key. | 7 |
firstSeenMonth | string | Earliest checked month in which the company posted (YYYY-MM). Equals the thread month for a company first seen now. Null when history is off or the post has no company key. | 2026-01 |
previousSeenMonth | string | Latest earlier month in which the company posted (YYYY-MM). Null for a company first seen now, when history is off or when the post has no company key. | 2026-08 |
consecutiveMonths | integer | How many months in a row, counting back from this thread, the company has posted. Counted only within the months checked. A month in which Hacker News published no thread is skipped without ending the streak; a month with a thread in which the company did not post ends it. Null when history is off or the post has no company key. | 3 |
isNewCompany | boolean | True when the company did not post in any of the earlier checked threads. Null when history is off or the post has no company key. | false |
postsThisMonth | integer | How many posts the company has in this thread; some companies post one per role. Null when history is off or the post has no company key. | 1 |
scrapedAt | string | When the row was produced, ISO 8601 in UTC. | 2026-10-01T06:00:00.000Z |
{
"found": false,
"month": "2026-09",
"postId": "49522903",
"sourceUrl": "https://news.ycombinator.com/item?id=49522903",
"threadId": "49522897",
"threadMonth": "2026-09",
"postedAt": "2026-09-01T15:01:54.000Z",
"author": "jobposter",
"company": "Modash.io",
"companyWebsite": "https://modash.io",
"companyKey": "modashio",
"headerLine": "Modash.io | Senior Product Engineer | Remote (Europe) | Full-time | €75k–110k",
"headerParsed": true,
"roles": [
"Senior Product Engineer"
],
"location": "Remote (Europe)",
"workModes": [
"remote"
],
"employmentTypes": [
"full-time"
],
"salaryText": "€75k–110k",
"salaryMin": 75000,
"salaryMax": 110000,
"salaryCurrency": "EUR",
"salaryPeriod": "year",
"salaryYearlyMin": 75000,
"salaryYearlyMax": 110000,
"visaSponsorship": true,
"technologies": [
"TypeScript",
"PostgreSQL"
],
"applyUrl": "https://jobs.ashbyhq.com/modash/123",
"links": [
"https://modash.io",
"https://jobs.ashbyhq.com/modash/123"
],
"emails": [
"jobs@modash.io"
],
"descriptionText": "Modash.io | Senior Product Engineer | Remote (Europe) | Full-time\n\nModash helps brands find and manage creators.",
"monthsChecked": 13,
"monthsHiring": 7,
"firstSeenMonth": "2026-01",
"previousSeenMonth": "2026-08",
"consecutiveMonths": 3,
"isNewCompany": false,
"postsThisMonth": 1,
"scrapedAt": "2026-10-01T06:00:00.000Z"
}
| Field | Name | Type | What it does |
|---|---|---|---|
monthsBack | Latest threads to read | integer | How many of the most recent monthly threads to read, newest first. 1 is the current month. Ignored when Month is set. A thread with fewer than 10 comments (the first hours of a month) is skipped. |
month | Specific month | string | Read one month instead, written as YYYY-MM, for example 2026-03. The run fails when neither the whoishiring accounts nor a title search on Hacker News finds a Who is hiring thread for that month. Before 2016 most posts have no standard header, so such a month needs Months of history set to 0. |
historyMonths | Months of history | integer | How many earlier monthly threads to compare every company against, for months hiring, first seen, previous seen and new company. 0 switches history off and makes the run faster; it is also the setting for months before December 2016, where company history cannot be computed and a run with history fails. More months mean more threads to download: each one adds about a second. |
keywords | Post text contains | array | Keep posts whose full text contains at least one of these words or phrases (case-insensitive), for example climate or "machine learning". |
roleContains | Role contains | array | Keep posts whose header line contains at least one of these words, for example designer or "data engineer". The header is where roles are listed. |
locationContains | Location contains | array | Keep posts whose header line contains at least one of these words, for example berlin, europe or "new york". |
technologies | Technologies (any of) | array | Keep posts that name at least one of these technologies anywhere in the text, for example python, rust or kubernetes. Names are matched against the Technologies column, case-insensitive. |
workMode | Work mode | string | Keep posts whose header mentions this mode. Posts that do not state a mode are dropped when you choose one. |
employmentType | Employment type | string | Keep posts whose header mentions this type. Posts that do not state one are dropped when you choose one. |
onlyWithSalary | Only posts with a salary | boolean | Keep only posts that state pay in the header or in a Salary line. |
minSalary | Minimum yearly salary | integer | Keep posts whose top stated pay, converted to a year, is at least this amount. Needs Salary currency, because pay is posted in different currencies. 0 means no minimum. |
salaryCurrency | Salary currency | string | Currency that Minimum yearly salary is measured in. Posts in other currencies are dropped when a minimum is set. |
visaSponsorshipOnly | Only with visa sponsorship | boolean | Keep only posts that offer visa or work-permit sponsorship. |
onlyNewCompanies | Only first-time companies | boolean | Keep only companies that did not post in any of the earlier threads covered by Months of history. Needs history. |
minMonthsHiring | Minimum months hiring | integer | Keep companies that posted in at least this many of the checked threads, this one included. Use it to find companies that hire all the time. Needs history. 0 means no minimum. |
onlyNewSinceLastRun | Only posts not delivered before | boolean | Return only posts that no earlier run with the same filters has delivered. Schedule the Actor during the month to get each new post once. Runs with different filters keep separate memory. |
maxItems | Maximum posts | integer | Stop after this many posts in total across all threads read. A monthly thread holds 250 to 450 posts. |
proxyConfiguration | Proxy configuration | object | Optional proxy. Leave disabled unless the Hacker News search API rate-limits the shared platform IP (HTTP 429); a proxy raises the platform cost of the run. |
Run the Actor and get the results in one request. Replace YOUR_APIFY_TOKEN with the token from your Apify account settings.
curl -X POST "https://api.apify.com/v2/acts/datagrit~hacker-news-hiring-history/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"monthsBack":1,"historyMonths":12,"technologies":["python"],"maxItems":100}'import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('datagrit/hacker-news-hiring-history').call({
"monthsBack": 1,
"historyMonths": 12,
"technologies": [
"python"
],
"maxItems": 100
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]);Install with npm i apify-client.
from apify_client import ApifyClient
import os
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("datagrit/hacker-news-hiring-history").call(run_input={
"monthsBack": 1,
"historyMonths": 12,
"technologies": [
"python"
],
"maxItems": 100
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items), items[0] if items else None)Install with pip install apify-client.
The Actor reads the public Algolia search API that Hacker News itself links to, for public job posts. It collects no data behind a login.
Once a month, on the first working day. Schedule the Actor weekly with "Only posts not delivered before" switched on to collect the new posts as they appear during the month.
Up to 36 earlier months, as long as those threads are readable (see Good to know). Each earlier thread adds about two seconds to the run.
Its header gave no company name, or it posted under a different name. Rows without a company key have null history.
The Hacker News search API limits requests per IP address and the Actor shares an IP address with other runs on the platform. Failed attempts and pauses between attempts draw from one 75-second budget for the whole run, and a request that gets no answer is cut off after 25 seconds (also when a proxy accepts the connection and stays silent). When the budget is used up the Actor stops with an error instead of running past the five-minute limit of a scheduled test; successful requests do not use it. Run again in a few minutes, or switch on the proxy in the input.
Open an issue on the Actor page with the input you used and the run link.
Ashby job postings with normalized annual salary ranges, equity flags and new-since-last-run detection.
Open jobs from Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Recruitee and Personio in one schema with normalized salaries.
Greenhouse job postings with published pay ranges by zone, annualised pay, filters and new-since-last-run detection.
Find EU public contracts approaching expiry from TED award notices: incumbent, buyer, value, end date and renewal options.