# Wellfound Jobs Scraper (AngelList) (`dami_studio/wellfound-jobs-scraper`) Actor

Wellfound refuses a share of plain HTTP requests, so every page gets up to six attempts from a new address and fingerprint. A page that lands carries every job in full: Markdown description, salary and equity split apart, company badges, exact posting time. $0.085 per 1,000.

- **URL**: https://apify.com/dami\_studio/wellfound-jobs-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Jobs, Lead generation, AI
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.09 / 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.

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

## Wellfound Jobs Scraper (AngelList Talent)

Wellfound refuses a share of plain HTTP requests outright, with a 403 and no explanation, so this actor gives every listing page up to six attempts, each from a new proxy session and a different browser fingerprint. What lands is worth the retries: a role page carries the complete text of every job on it, not a teaser. Checked on 2026-08-21, all 47 jobs on `/role/product-manager` came back with a full Markdown description, median around 5,000 characters, none empty.

A role slug like `software-engineer` is all the input it needs. Each row carries the full job description in Markdown, the compensation string split into salary and equity, the company with its size and Wellfound badges, and the exact posting timestamp.

It runs over plain HTTP. There's no login, no cookie and no headless browser involved, and it never touches residential proxy.

### Why the descriptions come back complete

Wellfound's role pages are Next.js pages that ship their own Apollo cache inside `__NEXT_DATA__`. Every job on the page is already a fully-typed `JobListingSearchResult` with the whole Markdown description attached, and every company is a `StartupResult` linked to its jobs. The actor reads that cache straight off the page.

One page request therefore returns roughly 40 to 49 complete jobs. Nothing has to be fetched a second time to get the description, so pulling 200 jobs is 5 requests rather than 205.

### Output

| Field | Notes |
|---|---|
| `jobId`, `title`, `jobUrl` | the listing |
| `companyName`, `companyUrl`, `companySlug`, `companyTagline`, `companySize`, `companyLogoUrl`, `companyBadges` | company, joined from the same cache. `companySize` is Wellfound's enum (`SIZE_11_50`), `companyBadges` its own labels ("Actively Hiring", "YC Funded", "Top Investors") |
| `jobType` | `full-time`, `internship`, `contract`, `part-time`, `cofounder` |
| `remote`, `remoteKind`, `locations`, `remoteLocations` | `remoteKind` is Wellfound's `ONSITE_OR_REMOTE` / `REMOTE_ONLY` enum |
| `compensation`, `salaryText`, `equityText` | `compensation` is Wellfound's raw string ("$135k – $175k • 0.05% – 0.25%"). The other two are that string split in half, never invented |
| `yearsExperienceMin` | only when the poster set one |
| `description` | the whole description in Markdown, as Wellfound stores it |
| `postedAt` | exact ISO timestamp from `liveStartAt` |
| `autoPosted`, `atsSource` | whether it arrived from an ATS, and which one |
| `sourceUrl`, `page`, `scrapedAt` | provenance |

#### How often each field is filled

From a 200-row platform run on 2026-08-10: roles `software-engineer` and `product-manager`, 3 pages each, 83 distinct companies, 200/200 rows real, 0 diagnostics, 21 seconds.

| Field | Filled |
|---|---|
| `jobId`, `title`, `jobUrl`, `companyName`, `companyUrl`, `companySlug`, `companySize`, `companyLogoUrl`, `companyBadges`, `primaryRoleTitle`, `jobType`, `description`, `postedAt` | 100% |
| `companyTagline` | 97.5% |
| `compensation`, `salaryText` | 92.5% |
| `locations` | 87.0% |
| `atsSource` | 80.0% |
| `remoteKind` | 28.5% |
| `remoteLocations` | 23.5% |
| `yearsExperienceMin` | 21.5% |
| `equityText` | 14.5% |

Median description length was 4,663 characters. Those percentages move with the roles you pick. A later 252-row sample across different roles came back with salary on 83% and equity on 10%. Equity is the field to watch: most Wellfound posters simply leave it blank, and nothing here is filled in with a guess.

### Input

- `roleSlugs` — `software-engineer`, `product-manager`, `data-scientist`. Plain phrases get slugified ("Data Scientist" becomes `data-scientist`) and full `wellfound.com/role/<slug>` URLs are accepted. `searchQueries` is an alias for the same field.
- `locationSlugs` — optional, such as `san-francisco`, `new-york`, `london`. Each role is then crawled once per location via `wellfound.com/role/l/<role>/<location>`.
- `remoteOnly` — switches to `wellfound.com/role/r/<role>` and keeps only listings flagged remote. Ignored when `locationSlugs` is set.
- `jobTypes`, `locationFilters`, `keywords` — filters applied on our side. Rows they drop are not returned and not charged.
- `maxItems` (default 100), `maxPagesPerSource` (default 5), `maxRetries` (default 6), `requestTimeoutSecs` (default 25).
- `proxyConfiguration` — optional. Leave it empty and the actor uses Apify datacenter proxy with a fresh session per attempt, which is what it's tuned for.

Empty input returns exactly one row labelled `_sample: true` and charges nothing.

```json
{
  "roleSlugs": ["software-engineer", "product-manager"],
  "locationSlugs": ["san-francisco"],
  "jobTypes": ["full-time"],
  "keywords": ["python"],
  "maxItems": 200,
  "maxPagesPerSource": 5
}
```

That crawls `wellfound.com/role/l/software-engineer/san-francisco` and the product-manager equivalent, up to 5 pages each, keeps full-time listings whose title, company or description mentions "python", and stops at 200 rows.

Remote instead of a city:

```json
{ "roleSlugs": ["data-scientist"], "remoteOnly": true, "maxItems": 100 }
```

#### A real output row

```json
{
  "ok": true,
  "_sample": false,
  "jobId": "3317746",
  "title": "Software Engineer",
  "jobUrl": "https://wellfound.com/jobs/3317746-software-engineer",
  "companyName": "Keeper",
  "companyUrl": "https://wellfound.com/company/keeper-tax",
  "companySlug": "keeper-tax",
  "companyTagline": "File your complex taxes confidently",
  "companySize": "SIZE_11_50",
  "companyLogoUrl": "https://photos.wellfound.com/startups/i/keeper-tax-logo.png",
  "companyBadges": ["Actively Hiring", "YC Funded"],
  "primaryRoleTitle": "Software Engineer",
  "jobType": "full-time",
  "remote": true,
  "remoteKind": "ONSITE_OR_REMOTE",
  "locations": ["San Francisco"],
  "remoteLocations": ["United States"],
  "compensation": "$135k – $175k • 0.05% – 0.25%",
  "salaryText": "$135k – $175k",
  "equityText": "0.05% – 0.25%",
  "yearsExperienceMin": 0,
  "description": "### Mission\n\nWe are building…",
  "postedAt": "2026-08-01T00:00:00.000Z",
  "autoPosted": false,
  "atsSource": "greenhouse",
  "sourceUrl": "https://wellfound.com/role/software-engineer",
  "page": 1,
  "scrapedAt": "2026-08-10T09:14:02.113Z"
}
```

Diagnostic rows have the opposite shape: `ok: false`, `_diagnostic: true`, an `errorCode` of `BAD_INPUT`, `BLOCKED`, `RATE_LIMITED`, `NOT_FOUND`, `SERVER_ERROR`, `NETWORK`, `PARSE_ERROR` or `NO_RESULTS`, and a plain-English `error`. Filter on `ok === true` if you only want jobs.

### Reliability

Wellfound 403s a share of requests no matter what address they come from. Rotation is what fixes that, not a better address: every attempt uses a new proxy session over rotating datacenter addresses and a different browser TLS fingerprint. On the 200-row run above, 4 of the 5 listing pages parsed on the first attempt and the fifth on the third. None failed outright. A page that never parses becomes an uncharged `BLOCKED` diagnostic row, so a bad day costs you the start fee and nothing more.

Retries back off, 300 ms doubling to a 5 s ceiling with jitter, so a temporary refusal doesn't turn into a hammering loop. `maxRetries` caps attempts per page and `requestTimeoutSecs` caps a single request, which gives a run a predictable worst case.

An earlier build drove a headless browser and fell back to a per-gigabyte proxy whenever it got blocked. It was slower, much more expensive, and still got refused. Both are gone. The direct HTTP path turned out to be cheaper and more reliable, and that's what makes the price below workable.

### Billing

Pay per event, event name `job`: **$0.085 per 1,000 jobs**, plus a $0.001 start fee per run.

One event per unique job row. Duplicates across pages are charged once, because the runner keeps a set of `jobId`s, so overlapping pages and two roles that surface the same listing bill once rather than twice. Never charged: the empty-input sample row, rows dropped by `jobTypes` / `locationFilters` / `keywords`, and every diagnostic row.

So 1,000 jobs in one run costs $0.001 + $0.085 = $0.086. A run that finds nothing costs the $0.001 start fee alone. Because descriptions arrive with the listings, there's no second charge for "detail" rows.

### What this does not do

- **No company-jobs pages.** `wellfound.com/company/<slug>/jobs` sits behind Wellfound's login wall and returned 403 on 8 straight attempts across 4 fingerprints, so that input isn't offered rather than offered and broken.
- **No per-job detail requests.** They'd be pointless; the listing cache already has the full description.
- **No recruiter contact details, no candidate profiles, and it doesn't apply to anything.**
- **No salary normalisation into numbers.** Wellfound publishes a string, so you get the string plus its salary and equity halves.
- **No residential proxy.**

### FAQ

**What's a role slug?**
The last path segment of a Wellfound role page. `wellfound.com/role/software-engineer` gives you `software-engineer`. Pasting the whole URL works too.

**How many jobs per page?**
Roughly 40 to 49. `maxPagesPerSource` sets how deep it goes, and deep pagination does work: page 20 and page 60 both returned jobs in testing.

**Do I really get the full description?**
Yes, in Markdown, on every row. Median length in the measured run was 4,663 characters.

**Can I filter to remote jobs?**
Set `remoteOnly: true`. That switches to Wellfound's remote role pages and drops any listing not flagged remote.

**Can I search one city?**
Put the city slug in `locationSlugs`. Use `locationFilters` instead if you'd rather do a plain substring match over whatever locations a job lists.

**What if Wellfound blocks it?**
Each page is retried up to `maxRetries` times on a fresh session and fingerprint. If every attempt fails you get one uncharged `BLOCKED` row explaining what happened, and no job charges.

**Is the sample row charged?**
No. Empty input returns one `_sample: true` row and zero billable events.

# Actor input Schema

## `roleSlugs` (type: `array`):

Wellfound role slugs, for example software-engineer, product-manager, data-scientist. Plain phrases are slugified, and wellfound.com/role/<slug> URLs are accepted.

## `searchQueries` (type: `array`):

Alias for role slugs. 'Data Scientist' becomes data-scientist.

## `locationSlugs` (type: `array`):

Optional Wellfound location slugs, for example san-francisco, new-york, london. Each role is crawled once per location using wellfound.com/role/l/<role>/<location>.

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

Use Wellfound's remote role pages (wellfound.com/role/r/<role>) and keep only listings flagged remote. Ignored when location slugs are supplied.

## `jobTypes` (type: `array`):

Keep only these Wellfound job types. Observed values: full-time, part-time, contract, internship, cofounder.

## `locationFilters` (type: `array`):

Keep only jobs whose location or accepted remote location contains one of these strings. Case-insensitive substring match.

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

Keep only jobs whose title, company or description contains one of these strings. Case-insensitive substring match.

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

Maximum unique job rows to return and charge for.

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

Listing pages to walk for each role (or role and location pair). Each page carries roughly 40 to 49 jobs.

## `maxRetries` (type: `integer`):

Wellfound 403s a share of requests. Each attempt uses a new proxy session and a different browser fingerprint.

## `requestTimeoutSecs` (type: `integer`):

Maximum duration of each network request.

## `proxyConfiguration` (type: `object`):

Optional. Leave empty to use Apify datacenter proxy with a fresh session per attempt, which is what this actor is tuned for. Supply proxyUrls to use your own addresses; they are used exactly as given. Residential (and any other per-gigabyte group) is never used: if you select one it is swapped for datacenter and the run tells you so.

## Actor input object example

```json
{
  "roleSlugs": [
    "software-engineer"
  ],
  "remoteOnly": false,
  "maxItems": 100,
  "maxPagesPerSource": 5,
  "maxRetries": 6,
  "requestTimeoutSecs": 25
}
```

# Actor output Schema

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

Results in the default dataset.

# 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 = {
    "roleSlugs": [
        "software-engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/wellfound-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 = { "roleSlugs": ["software-engineer"] }

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/wellfound-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 '{
  "roleSlugs": [
    "software-engineer"
  ]
}' |
apify call dami_studio/wellfound-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/wellfound-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/NJrUx268U5fguif3m/builds/K7cKXnzB0VkJiuBji/openapi.json
