# Wellfound (AngelList) — Startup Jobs & Company Profiles Field-tested against wellfound.com on 2026-04-18. All confirmed via live HTTP probes and response header analysis. --- ## Anti-bot verdict: browser required, no http_get workaround exists **`http_get` returns HTTP 403 on every Wellfound URL without exception** (except `robots.txt`). Tested endpoints (all 403): - `/company/stripe` - `/jobs` - `/jobs?role=engineer&location=remote` - `/company/stripe/jobs` - `/sitemap.xml`, `/sitemap_index.xml` - `/jobs.rss` - `POST /graphql` (HTTP 403, Cloudflare managed challenge) Old AngelList public API (`api.angel.co/1/...`) returns `404 Not Found` — permanently shut down. **Dual anti-bot stack confirmed from response headers:** | Layer | System | Evidence | |-------|--------|----------| | Page GETs | DataDome | `X-DataDome: protected`, `X-DD-B: 2`, `Set-Cookie: datadome=...` | | API POSTs | Cloudflare Bot Management | `Cf-Mitigated: challenge` | The 403 response body contains a DataDome captcha challenge script (`geo.captcha-delivery.com`) AND an embedded Cloudflare challenge (`window.__CF$cv$params`). Both fire simultaneously. Neither cookie can be replayed — both are TLS-fingerprint-bound. **Use `new_tab()` + `wait()` exclusively. Never use `http_get` for Wellfound.** --- ## Tech stack (confirmed from response headers) Wellfound is a **Ruby on Rails + React + Apollo GraphQL** hybrid app — NOT a pure Next.js app. Confirmed headers from `robots.txt` (the only accessible endpoint): ``` x-runtime: 0.006700 → Rails rack middleware timer x-request-id: 4645fd66... → Rails request ID x-xss-protection: 1; mode=block → Rails security defaults Set-Cookie: _wellfound=... → Rails session cookie Server: cloudflare → Cloudflare CDN ``` Implications: - **`__NEXT_DATA__` is NOT present** — not a Next.js app - **`window.__APOLLO_STATE__` or `window.gon` may be present** — check these instead - CSRF token is in a `` tag (Rails default) - Session cookie is `_wellfound=...` for anonymous sessions; login sessions add `_wellfound_session=...` --- ## Do this first: open in new tab, wait for DataDome to resolve ```python new_tab("https://wellfound.com/company/stripe") wait_for_load() wait(5) # DataDome JS fingerprinting runs ~2-4s after readyState=complete ``` Verify you are past the DataDome challenge before extracting: ```python title = js("document.title") url = page_info()["url"] if "wellfound.com" not in url or not title or "Just a moment" in title: # DataDome or CF challenge did not resolve — wait longer wait(8) title = js("document.title") if "Just a moment" in title or not title: capture_screenshot("/tmp/wellfound_block.png") raise RuntimeError("DataDome/CF challenge did not resolve — see screenshot") ``` DataDome resolves **silently** in a real Chrome session via CDP — no user interaction required. The challenge is a JS fingerprint check that passes automatically when running in a real browser. --- ## URL patterns | Goal | URL | |------|-----| | Company profile | `https://wellfound.com/company/{slug}` | | Company jobs | `https://wellfound.com/company/{slug}/jobs` | | Company culture | `https://wellfound.com/company/{slug}/culture` | | Job board (all) | `https://wellfound.com/jobs` | | Job board filtered | `https://wellfound.com/jobs` — then use UI filters (query params are disallowed by robots.txt) | | Investor profile | `https://wellfound.com/investor/{slug}` | | User profile | `https://wellfound.com/u/{username}` (disallowed by robots.txt, login wall) | **Note on query params:** `robots.txt` disallows `?role=*`, `?jobId=*`, `?jobSlug=*`, `?location=*`. Wellfound enforces these with login walls or redirects for most filtered job searches. --- ## Workflow 1: Company profile — name, description, team size, funding, tags Navigate to the company page and extract structured data. Most fields are visible without login. ```python import json new_tab("https://wellfound.com/company/stripe") wait_for_load() wait(5) # Check for Apollo state (Rails + React app, not Next.js) # Wellfound embeds data in window.gon or inline script tags apollo_raw = js(""" (function() { // Try window.__APOLLO_STATE__ (Apollo Client cache) if (window.__APOLLO_STATE__) return JSON.stringify(window.__APOLLO_STATE__); // Try window.gon (Rails Gon gem) if (window.gon) return JSON.stringify(window.gon); // Try inline Just a moment... ``` In a real Chrome browser, both challenges resolve automatically without user interaction. --- ## Minimal working example ```python import json # Open Wellfound company page new_tab("https://wellfound.com/company/openai") wait_for_load() wait(5) # Verify not blocked title = js("document.title") assert "Just a moment" not in (title or ""), f"Still on challenge page: {title}" # Extract company overview data = js(""" (function() { var name = document.querySelector('h1'); var bodyText = document.body.innerText; var sizeMatch = bodyText.match(/(\\d+[-\\u2013]\\d+)\\s+(employees|people)/i); var fundingMatch = bodyText.match(/\\$[\\d,.]+[KMBkm](?:\\s+(?:raised|total))?/i); var stageMatch = bodyText.match(/\\b(Seed|Series [A-Z]\\+?|Pre-seed|Late Stage|Public)\\b/); var tags = Array.from(document.querySelectorAll('a[href*="/jobs?role="]')).map(a => a.innerText.trim()); var locs = Array.from(document.querySelectorAll('a[href*="/location/"]')).map(a => a.innerText.trim()); return JSON.stringify({ name: name ? name.innerText.trim() : null, teamSize: sizeMatch ? sizeMatch[0] : null, funding: fundingMatch ? fundingMatch[0] : null, stage: stageMatch ? stageMatch[0] : null, roles: tags.slice(0, 8), locations: locs.slice(0, 5), }); })() """) print(json.dumps(json.loads(data), indent=2)) ```