Skip to content

v0.1.0 — first release

Choose a tag to compare

@jehrr jehrr released this 17 Sep 16:22
· 13 commits to main since this release

First release of wellfound-scraper. Scrapes wellfound.com (formerly AngelList Talent): role landing pages, the /jobs discovery feed, and individual job pages.

Every number below was measured on 2026-09-17.

What you need

A residential exit, and nothing else. Wellfound refuses a datacenter address on every route, robots.txt included — HTTP 403 to plain HTTP, to headless Chromium and to headful Chromium alike. From a 2Captcha residential US exit the same machine got HTTP 200 on /, /jobs, every /role/… shape and every job page, three times out of three.

What it does

  • Three modes. role (a /role/… landing page — full descriptions, company badges, size and tagline, salary and equity; paginates), jobs (the discovery feed, one page), job (one job from its schema.org JobPosting).
  • Four fetchers. Playwright (primary, with --concurrency), Selenium, Puppeteer, and a Scraper API client. All three browser engines were run live against the same listing and returned byte-identical rows — the same 52 skus, 38 columns, and the same position on every row.
  • 38-column JobPosting schema, JSON and CSV, with a .meta.json sidecar per run.
  • 355 offline checks, fixtures cut from real captures and verified to parse identically to their untrimmed originals.

What the site turned out to be

  • Gating is per ROUTE. /role/…, /jobs and /jobs/{id}-{slug} are served to a residential exit; /company/{slug} is refused to one, to plain HTTP and to a real browser alike, 3 of 3 each. There is no --mode company, because there is nothing this repo could honestly offer for it.
  • The obvious challenge marker is inverted here. challenges.cloudflare.com appears once on every page Wellfound serves — the site loads Turnstile as part of its own application, with its sitekey in its own page config — and zero times on either refusal. cf-turnstile appears nowhere at all. The marker set uses _cf_chl_opt and its siblings instead, and the suite fails the build if the inverted one comes back.
  • Walking past the last page does not fail — it repeats. ?page=48 of a 47-page listing answers HTTP 200 with page 1's rows again. Runs plan against the site's own pageCount and read back the page number the server states in its Apollo cache key.
  • A "page" is twenty COMPANIES, not twenty jobs. perPage: 20, pageCount: 47, totalStartupCount: 923, totalJobCount: 1881 — and 32 to 45 job rows per page.
  • /jobs has no addressable pages. ?page=2 returns the identical 46 job ids, so that mode reports single_page_listing.
  • Two structured sources, different applications. A landing page carries zero JSON-LD and a Next.js Apollo cache; a job page carries no __NEXT_DATA__ and one JobPosting. Where both publish a salary they agree.
  • Pay is one rendered string with nine measured shapes: the bullet splits salary from equity and either side may be absent, L is the Indian lakh (1e5), and No equity is a statement rather than a missing value.