init
This commit is contained in:
commit
d295c7f139
1527 files changed
+270726
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# seeds/ — VC Portfolio Seed Fetchers
|
||||
|
||||
A complementary discovery path for startup job-seekers: pull a **public VC portfolio company list** and probe each company's ATS for openings, feeding results into the same pipeline as tracked companies in `portals.yml`.
|
||||
|
||||
## What this does
|
||||
|
||||
`scan-ats-full.mjs` normally discovers companies by walking public ATS directories (Greenhouse, Lever, Ashby, Workday). The `seeds/` layer adds a **high-signal starting point for startup roles**: rather than waiting for companies to appear in ATS directories, we seed the universe from well-known VC portfolios — giving you instant coverage of hundreds of YC/a16z-backed companies.
|
||||
|
||||
Flow:
|
||||
```
|
||||
VC portfolio API/page
|
||||
↓ seeds/vc-portfolios.mjs
|
||||
SeedCompany[]
|
||||
↓ toPortalEntry()
|
||||
PortalEntry (careers_url set to best-guess ATS URL)
|
||||
↓ provider.detect() (same as portals.yml companies)
|
||||
ATS provider fetches jobs
|
||||
↓ title_filter / location_filter / dedup
|
||||
data/pipeline.md
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Via scan-ats-full.mjs (recommended)
|
||||
|
||||
```bash
|
||||
# Seed from Y Combinator portfolio, last 7 days
|
||||
node scan-ats-full.mjs --seeds yc --since 7
|
||||
|
||||
# Seed from both YC and a16z, dry-run preview
|
||||
node scan-ats-full.mjs --seeds yc,a16z --dry-run
|
||||
|
||||
# Combine seeds + regular ATS sources
|
||||
node scan-ats-full.mjs --seeds yc --ats greenhouse,lever --since 5
|
||||
|
||||
# npm shortcuts
|
||||
npm run scan:seeds # yc + a16z
|
||||
npm run scan:yc # YC only
|
||||
```
|
||||
|
||||
### Programmatic
|
||||
|
||||
```js
|
||||
import { fetchYCCompanies, fetchA16zCompanies, toPortalEntry, SEED_SOURCES } from './seeds/vc-portfolios.mjs';
|
||||
|
||||
// Fetch YC companies
|
||||
const companies = await fetchYCCompanies();
|
||||
console.log(companies[0]);
|
||||
// → { name: 'Stripe', slug: 'stripe', url: 'https://stripe.com', source: 'yc', batch: 'W11' }
|
||||
|
||||
// Convert to a PortalEntry for ATS provider.detect()
|
||||
const entry = toPortalEntry(companies[0]);
|
||||
// → { name: 'Stripe', careers_url: 'https://job-boards.greenhouse.io/stripe', source: 'yc' }
|
||||
|
||||
// Using the registry
|
||||
for (const [id, source] of Object.entries(SEED_SOURCES)) {
|
||||
const companies = await source.fetch();
|
||||
console.log(`${source.label}: ${companies.length} companies`);
|
||||
}
|
||||
```
|
||||
|
||||
## Data sources
|
||||
|
||||
| Source | URL | Format | Auth |
|
||||
|--------|-----|--------|------|
|
||||
| Y Combinator | `https://api.ycombinator.com/v0.1/companies` | JSON API | None |
|
||||
| a16z | `https://a16z.com/portfolio/` | Public HTML page | None |
|
||||
|
||||
- **YC**: Fetches up to 3 pages × 1000 companies. Covers all public YC batches.
|
||||
- **a16z**: Parses the public portfolio page. Falls back gracefully if the page structure changes.
|
||||
|
||||
## Security
|
||||
|
||||
- All slugs are validated against `SLUG_RE = /^[A-Za-z0-9._-]+$/` before any URL interpolation — consistent with the guard in `scan-ats-full.mjs`.
|
||||
- Constructed ATS URLs go through the existing `entryOnHost()` SSRF guard before reaching any provider.
|
||||
- No authentication tokens, no headless browser, no LLM API calls.
|
||||
|
||||
## Adding more VC portfolios
|
||||
|
||||
1. Add a `parseXyzPayload(payload)` pure function (no network — testable with inline fixtures).
|
||||
2. Add a `fetchXyzCompanies(opts?)` async function that calls the public endpoint and returns `SeedCompany[]`.
|
||||
3. Register it in `SEED_SOURCES`:
|
||||
|
||||
```js
|
||||
export const SEED_SOURCES = {
|
||||
yc: { fetch: fetchYCCompanies, label: 'Y Combinator Portfolio' },
|
||||
a16z: { fetch: fetchA16zCompanies, label: 'Andreessen Horowitz (a16z) Portfolio' },
|
||||
// Add yours:
|
||||
sequoia: { fetch: fetchSequoiaCompanies, label: 'Sequoia Portfolio' },
|
||||
};
|
||||
```
|
||||
|
||||
4. Add test cases in `test-all.mjs` covering your `parseXyzPayload()` function.
|
||||
|
||||
## Prior art
|
||||
|
||||
The VC-portfolio seeding approach is inspired by [adityachaudhary99/job-hunt](https://github.com/adityachaudhary99/job-hunt) (`02-seeds/fetch_yc.py`, `fetch_a16z.py`), which was the original companion reference cited in issue #1370.
|
||||
@@ -0,0 +1,393 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* seeds/vc-portfolios.mjs — VC portfolio seed fetchers for career-ops.
|
||||
*
|
||||
* Pulls public VC portfolio company lists (Y Combinator, Andreessen Horowitz)
|
||||
* and emits company entries compatible with the existing ATS scan/discovery
|
||||
* path (same shape as tracked_companies entries in portals.yml).
|
||||
*
|
||||
* Design constraints:
|
||||
* - Zero auth — public sources only, no login, no API keys.
|
||||
* - Zero LLM tokens — pure HTTP + JSON / HTML.
|
||||
* - Same SLUG_RE guard used by scan-ats-full.mjs for every slug that reaches
|
||||
* URL interpolation — a tampered or malformed payload can never inject
|
||||
* unexpected characters into a URL.
|
||||
* - `parseSeedEntries()` is a pure, synchronous function (no network) so it
|
||||
* can be unit-tested with inline fixtures without any mocking.
|
||||
*
|
||||
* Typical usage (via scan-ats-full.mjs --seeds flag):
|
||||
* node scan-ats-full.mjs --seeds yc
|
||||
* node scan-ats-full.mjs --seeds yc,a16z --since 7 --dry-run
|
||||
*
|
||||
* Direct usage:
|
||||
* import { fetchYCCompanies, fetchA16zCompanies } from './seeds/vc-portfolios.mjs';
|
||||
* const companies = await fetchYCCompanies();
|
||||
*/
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Safe charset for slug values that will be interpolated into ATS URLs.
|
||||
* Consistent with the SLUG_RE guard in scan-ats-full.mjs.
|
||||
*/
|
||||
export const SLUG_RE = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 20_000;
|
||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; career-ops-seeds/1.0)';
|
||||
|
||||
/**
|
||||
* YC public company API.
|
||||
* Returns paginated JSON with company objects including name, slug, website.
|
||||
* Documentation: https://www.ycombinator.com/companies (public, no auth needed).
|
||||
*/
|
||||
const YC_API_URL = 'https://api.ycombinator.com/v0.1/companies?page=1&per_page=1000';
|
||||
|
||||
/**
|
||||
* a16z public portfolio page.
|
||||
* The portfolio is a publicly accessible HTML page listing all portfolio companies.
|
||||
*/
|
||||
const A16Z_PORTFOLIO_URL = 'https://a16z.com/portfolio/';
|
||||
|
||||
// ── HTTP helper (local — avoids importing providers/_http.mjs to keep seeds/ self-contained) ──
|
||||
|
||||
/**
|
||||
* Minimal fetch wrapper with timeout + user-agent header.
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {{ timeoutMs?: number }} [opts]
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function fetchWithTimeout(url, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'user-agent': DEFAULT_USER_AGENT },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const snippet = await res.text().catch(() => '').then(t => t.slice(0, 200));
|
||||
throw new Error(`HTTP ${res.status}${snippet ? ': ' + snippet.replace(/\s+/g, ' ').trim() : ''}`);
|
||||
}
|
||||
return res;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared types (JSDoc only — no runtime cost) ──────────────────────
|
||||
|
||||
/**
|
||||
* A single VC-portfolio company entry — the output unit of both seed fetchers.
|
||||
*
|
||||
* @typedef {object} SeedCompany
|
||||
* @property {string} name Display name, e.g. "Stripe".
|
||||
* @property {string} slug URL-safe slug, validated against SLUG_RE.
|
||||
* @property {string} url Company website URL.
|
||||
* @property {string} [ats] ATS platform if detectable: 'greenhouse' | 'lever' | 'ashby'.
|
||||
* @property {string} [ats_id] ATS board/org slug for URL construction.
|
||||
* @property {string} [source] Which VC list this came from: 'yc' | 'a16z'.
|
||||
* @property {string} [batch] YC batch label, e.g. "W21" (YC only).
|
||||
*/
|
||||
|
||||
/**
|
||||
* A PortalEntry-compatible object ready to be passed to ATS provider.detect().
|
||||
* Shape matches the PortalEntry typedef in providers/_types.js.
|
||||
*
|
||||
* @typedef {object} SeedPortalEntry
|
||||
* @property {string} name
|
||||
* @property {string} careers_url Best-effort ATS or website URL.
|
||||
* @property {string} [source] Seed origin ('yc' | 'a16z').
|
||||
*/
|
||||
|
||||
// ── Pure parser: YC ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a raw YC API response payload into SeedCompany entries.
|
||||
*
|
||||
* This is the testable unit — pure, no network, no side effects.
|
||||
* The YC API returns a paginated JSON object:
|
||||
* { companies: [{ id, name, slug, website, batch, ... }], ... }
|
||||
*
|
||||
* @param {unknown} payload Parsed JSON from the YC API (or a fixture in tests).
|
||||
* @returns {SeedCompany[]}
|
||||
*/
|
||||
export function parseYCPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
const raw = /** @type {any} */ (payload);
|
||||
const list = Array.isArray(raw.companies) ? raw.companies : (Array.isArray(raw) ? raw : []);
|
||||
/** @type {Map<string, SeedCompany>} */
|
||||
const seen = new Map();
|
||||
|
||||
for (const item of list) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
|
||||
const name = typeof item.name === 'string' ? item.name.trim() : '';
|
||||
if (!name) continue;
|
||||
|
||||
// Prefer explicit slug; derive from name as fallback.
|
||||
const rawSlug = typeof item.slug === 'string' ? item.slug.trim()
|
||||
: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!rawSlug || !SLUG_RE.test(rawSlug)) continue;
|
||||
|
||||
// Deduplicate by slug.
|
||||
if (seen.has(rawSlug)) continue;
|
||||
|
||||
const url = typeof item.website === 'string' && item.website.startsWith('http')
|
||||
? item.website.trim()
|
||||
: (typeof item.url === 'string' && item.url.startsWith('http') ? item.url.trim() : '');
|
||||
|
||||
/** @type {SeedCompany} */
|
||||
const entry = {
|
||||
name,
|
||||
slug: rawSlug,
|
||||
url,
|
||||
source: 'yc',
|
||||
};
|
||||
|
||||
if (typeof item.batch === 'string' && item.batch.trim()) {
|
||||
entry.batch = item.batch.trim();
|
||||
}
|
||||
|
||||
seen.set(rawSlug, entry);
|
||||
}
|
||||
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
// ── Pure parser: a16z ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse raw a16z portfolio HTML into SeedCompany entries.
|
||||
*
|
||||
* This is the testable unit — pure, no network, no side effects.
|
||||
* The a16z portfolio page lists companies in anchor tags with data attributes.
|
||||
* We extract company names and URLs from the HTML without a full DOM parser —
|
||||
* matching patterns like:
|
||||
* <a ... href="https://company.com" ... data-company-name="Stripe" ...>
|
||||
* or <h3 class="...">Stripe</h3> adjacent to a link
|
||||
*
|
||||
* Strategy: look for JSON-LD structured data first (most reliable), then
|
||||
* fall back to pattern-matching anchor/heading text.
|
||||
*
|
||||
* @param {string} html Raw HTML from the a16z portfolio page (or a fixture in tests).
|
||||
* @returns {SeedCompany[]}
|
||||
*/
|
||||
export function parseA16zPayload(html) {
|
||||
if (typeof html !== 'string' || !html.trim()) return [];
|
||||
|
||||
/** @type {Map<string, SeedCompany>} */
|
||||
const seen = new Map();
|
||||
|
||||
// Strategy 1: JSON-LD embedded in the page (structured data block).
|
||||
// a16z sometimes embeds schema.org/Organization blocks — extract if present.
|
||||
const jsonLdMatches = html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi);
|
||||
for (const match of jsonLdMatches) {
|
||||
try {
|
||||
const data = JSON.parse(match[1]);
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
for (const item of items) {
|
||||
if (item?.['@type'] === 'Organization' || item?.['@type'] === 'Corporation') {
|
||||
const name = typeof item.name === 'string' ? item.name.trim() : '';
|
||||
const url = typeof item.url === 'string' ? item.url.trim() : '';
|
||||
if (!name) continue;
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!slug || !SLUG_RE.test(slug) || seen.has(slug)) continue;
|
||||
seen.set(slug, { name, slug, url, source: 'a16z' });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Malformed JSON-LD — skip silently.
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: data-company-name attributes (a16z uses React-rendered data attrs).
|
||||
// Pattern: data-company-name="Stripe" (optionally with data-company-url)
|
||||
const dataAttrRe = /data-company-name=["']([^"']+)["'](?:[^>]*data-company-url=["']([^"']+)["'])?/gi;
|
||||
for (const match of html.matchAll(dataAttrRe)) {
|
||||
const name = match[1]?.trim();
|
||||
const url = match[2]?.trim() || '';
|
||||
if (!name) continue;
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!slug || !SLUG_RE.test(slug) || seen.has(slug)) continue;
|
||||
seen.set(slug, { name, slug, url, source: 'a16z' });
|
||||
}
|
||||
|
||||
// Strategy 3: Portfolio card anchors / heading text fallback.
|
||||
// Matches patterns like: <a href="https://stripe.com" class="...portfolio...">Stripe</a>
|
||||
// or company names in h3/h4 elements within portfolio sections.
|
||||
const portfolioAnchorRe = /<a\s+[^>]*href=["'](https?:\/\/[^"'?\s]+)["'][^>]*class=["'][^"']*(?:portfolio|company|card)[^"']*["'][^>]*>\s*([A-Z][^<]{1,60}?)\s*<\/a>/gi;
|
||||
for (const match of html.matchAll(portfolioAnchorRe)) {
|
||||
const url = match[1]?.trim();
|
||||
const name = match[2]?.trim().replace(/\s+/g, ' ');
|
||||
if (!name || !url) continue;
|
||||
// Filter out nav/generic link text.
|
||||
if (/^(read more|learn more|visit|see all|view|more|news|blog|press|contact)/i.test(name)) continue;
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!slug || !SLUG_RE.test(slug) || seen.has(slug)) continue;
|
||||
seen.set(slug, { name, slug, url, source: 'a16z' });
|
||||
}
|
||||
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
// ── Generic pure parser (entry point for test-all.mjs) ───────────────
|
||||
|
||||
/**
|
||||
* Parse a raw seed payload (either YC JSON or a16z HTML) into validated
|
||||
* SeedCompany entries. This is the universal testable unit cited in the
|
||||
* issue acceptance criteria.
|
||||
*
|
||||
* @param {unknown} payload JSON object (YC) or HTML string (a16z).
|
||||
* @param {'yc'|'a16z'} source Which VC portfolio this payload came from.
|
||||
* @returns {SeedCompany[]}
|
||||
*/
|
||||
export function parseSeedEntries(payload, source) {
|
||||
if (source === 'a16z') {
|
||||
return parseA16zPayload(typeof payload === 'string' ? payload : '');
|
||||
}
|
||||
// Default: YC (also used for unknown sources — parse defensively).
|
||||
return parseYCPayload(payload);
|
||||
}
|
||||
|
||||
// ── toPortalEntry converter ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Convert a SeedCompany into a PortalEntry-shaped object that ATS provider
|
||||
* detect() can consume directly.
|
||||
*
|
||||
* Resolution order for careers_url:
|
||||
* 1. If `company.ats === 'greenhouse'` and `company.ats_id` is set → Greenhouse board URL.
|
||||
* 2. If `company.ats === 'lever'` and `company.ats_id` is set → Lever URL.
|
||||
* 3. If `company.ats === 'ashby'` and `company.ats_id` is set → Ashby URL.
|
||||
* 4. Derive from slug: try Greenhouse, Lever, Ashby URLs (provider.detect() will
|
||||
* validate at scan time; if none match, the entry is skipped with a warning).
|
||||
* 5. Fallback: company website URL (the ATS may be on a custom subdomain).
|
||||
*
|
||||
* @param {SeedCompany} company
|
||||
* @returns {SeedPortalEntry}
|
||||
*/
|
||||
export function toPortalEntry(company) {
|
||||
let careers_url = '';
|
||||
|
||||
// Explicit ATS hint from the YC dataset.
|
||||
const atsId = company.ats_id && SLUG_RE.test(company.ats_id) ? company.ats_id : null;
|
||||
if (atsId) {
|
||||
if (company.ats === 'greenhouse') {
|
||||
careers_url = `https://job-boards.greenhouse.io/${atsId}`;
|
||||
} else if (company.ats === 'lever') {
|
||||
careers_url = `https://jobs.lever.co/${atsId}`;
|
||||
} else if (company.ats === 'ashby') {
|
||||
careers_url = `https://jobs.ashbyhq.com/${atsId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// No explicit ATS: try Greenhouse by slug (most common for YC companies), then
|
||||
// Lever, then Ashby — provider.detect() will confirm or skip at scan time.
|
||||
if (!careers_url && company.slug && SLUG_RE.test(company.slug)) {
|
||||
// Use a format that greenhouse.mjs detect() can match.
|
||||
careers_url = `https://job-boards.greenhouse.io/${company.slug}`;
|
||||
}
|
||||
|
||||
// Last resort: company website (ATS may auto-detect from the domain).
|
||||
if (!careers_url) {
|
||||
careers_url = company.url || '';
|
||||
}
|
||||
|
||||
return {
|
||||
name: company.name,
|
||||
careers_url,
|
||||
source: company.source,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Network fetchers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch the Y Combinator public company list and return parsed SeedCompany entries.
|
||||
*
|
||||
* Uses the public YC API (no auth, no API key). The response is a JSON object
|
||||
* with a `companies` array. We fetch page 1 with a large per_page to get the
|
||||
* most recent batch; subsequent pages can be fetched if needed (most users want
|
||||
* the latest batch anyway).
|
||||
*
|
||||
* @param {{ timeoutMs?: number, maxPages?: number }} [opts]
|
||||
* @returns {Promise<SeedCompany[]>}
|
||||
*/
|
||||
export async function fetchYCCompanies({ timeoutMs = DEFAULT_TIMEOUT_MS, maxPages = 3 } = {}) {
|
||||
/** @type {SeedCompany[]} */
|
||||
const all = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const url = `https://api.ycombinator.com/v0.1/companies?page=${page}&per_page=1000`;
|
||||
let payload;
|
||||
try {
|
||||
const res = await fetchWithTimeout(url, { timeoutMs });
|
||||
payload = await res.json();
|
||||
} catch (err) {
|
||||
if (page === 1) throw new Error(`vc-portfolios: YC API fetch failed — ${err.message}`);
|
||||
break; // Partial data is fine after page 1.
|
||||
}
|
||||
|
||||
const entries = parseYCPayload(payload);
|
||||
if (entries.length === 0) break; // No more pages.
|
||||
|
||||
for (const e of entries) {
|
||||
if (!seen.has(e.slug)) {
|
||||
seen.add(e.slug);
|
||||
all.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
// The YC API pagination: stop when we receive fewer than 1000 companies.
|
||||
const raw = /** @type {any} */ (payload);
|
||||
const batchSize = Array.isArray(raw?.companies) ? raw.companies.length : 0;
|
||||
if (batchSize < 1000) break;
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the a16z public portfolio page and return parsed SeedCompany entries.
|
||||
*
|
||||
* a16z does not expose a public JSON API, so we fetch the HTML portfolio page
|
||||
* and parse it with `parseA16zPayload()`.
|
||||
*
|
||||
* @param {{ timeoutMs?: number }} [opts]
|
||||
* @returns {Promise<SeedCompany[]>}
|
||||
*/
|
||||
export async function fetchA16zCompanies({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||
let html;
|
||||
try {
|
||||
const res = await fetchWithTimeout(A16Z_PORTFOLIO_URL, { timeoutMs });
|
||||
html = await res.text();
|
||||
} catch (err) {
|
||||
throw new Error(`vc-portfolios: a16z portfolio fetch failed — ${err.message}`);
|
||||
}
|
||||
return parseA16zPayload(html);
|
||||
}
|
||||
|
||||
// ── SEED_SOURCES registry ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registry mapping seed source names to their fetch functions.
|
||||
* Consumed by scan-ats-full.mjs --seeds flag and CLI tooling.
|
||||
*
|
||||
* To add a new VC portfolio:
|
||||
* 1. Add a fetchXyzCompanies() function above.
|
||||
* 2. Add an entry here: { fetch: fetchXyzCompanies, label: 'XYZ Portfolio' }
|
||||
*
|
||||
* @type {Record<string, { fetch: (opts?: object) => Promise<SeedCompany[]>, label: string }>}
|
||||
*/
|
||||
export const SEED_SOURCES = {
|
||||
yc: {
|
||||
fetch: fetchYCCompanies,
|
||||
label: 'Y Combinator Portfolio',
|
||||
},
|
||||
a16z: {
|
||||
fetch: fetchA16zCompanies,
|
||||
label: 'Andreessen Horowitz (a16z) Portfolio',
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user