@hasna/experts
v0.0.8
Published
Crawl expert marketplaces (intro.co and more) into a local store, then query them via CLI or a remote HTTP API.
Maintainers
Readme
@hasna/experts
Crawl expert marketplaces into a local store, then query them from the command line or over HTTP. intro.co ships built-in; the architecture is source-agnostic, so other marketplaces are added as small adapters.
$ experts list --topic "Career & Business" --limit 3
$350 ★5.00 Raad Mobrem ✓ CEO of Intro. 2x Entrepreneur
$125 ★5.00 David Greenfeld TOP ✓ Founder & CEO of Dream Pops
$275 ★5.00 Jason Feifer TOP ✓ Editor in Chief of Entrepreneur MagazineWhy
Expert/advisor marketplaces hide their full roster behind infinite-scroll UIs.
experts pulls the whole catalog once (politely — throttled, with backoff),
stores it in SQLite, and gives you fast local search, filtering and stats so you
can actually understand who is on a platform, what they charge, and how they
rank — without clicking through hundreds of profiles.
Install
bun install -g @hasna/expertsRequires Bun ≥ 1.0. Two binaries are installed:
experts— the CLIexperts-serve— a read-only HTTP API over the same store
Quick start
experts crawl intro # fetch the full intro.co catalog (~2,300 experts)
experts stats # summary: counts, price range, avg rating
experts list --top # featured experts, highest-rated first
experts search "growth" # full-text search over name, title and bio
experts show AlexisOhanianCommands
| Command | What it does |
| --- | --- |
| crawl [source] | Fetch experts from a source into the store (default: intro) |
| enrich [source] | Enrich via X/Twitter — profile, recent tweets, avatar |
| enrich-youtube [source] | Fetch recent YouTube videos for experts with a YT handle |
| enrich-linkedin [source] | LinkedIn headline/company/about (needs linkedin connector auth) |
| enrich-sites [source] | Fetch experts' personal sites → text summary |
| avatars [source] | Download + properly name profile pictures (no API) |
| find-contacts [source] | Discover email + phone via Exa.ai websets |
| verify-contacts [source] | Validate contacts → working/not status |
| embed [source] | Build the semantic search index |
| ask <query…> | Natural-language semantic search |
| brief <text…> | Ranked, de-duplicated shortlist for a problem statement |
| find <needs…> | Knowledge-graph search — experts who cover specific needs |
| related <id\|slug> | Experts who share the most expertise with this one |
| list / search <q…> | Filtered listing / full-text search |
| show <id\|slug> | Full profile (X, tweets, videos, contacts, LinkedIn, site) |
| tweets <id\|slug> | Recent tweets (--live to fetch fresh) |
| videos <id\|slug> | Recent YouTube videos |
| contacts <id\|slug> | Discovered emails/phones with working/not status |
| graph / persons / changes / stale | Graph, identity, change-log, staleness overviews |
| topics / tags / stats | Categories, tags, aggregate statistics |
| sync-contacts <ids…> | Export experts + contacts to the contacts system |
| reindex | Rebuild graph + authority + persons (no network) |
| export / sources | Dump store (JSON/CSV) / list marketplace adapters |
Global options
--db <path>— use a specific database file (default~/.hasna/experts/experts.db, or$OPEN_EXPERTS_DB)--json— machine-readable output forlist,search,show,topics,tags,stats
crawl
experts crawl intro # full catalog + topic membership + tags
experts crawl intro --no-topics # catalog only (fewer requests)
experts crawl intro --max 100 # cap for a quick run
experts crawl intro --delay 1000 # 1s between requests (default 500ms)The crawler throttles every request and backs off on 429/5xx, so it stays a
polite guest on the upstream API.
list
experts list --topic "Wellness" --verified --sort price --asc -n 20
experts list --min-price 100 --max-price 500 --min-rating 4.9Filters: --source, --topic, --verified, --top, --min-price,
--max-price, --min-rating. Sort with --sort rating|price|name|reviews
(--asc to reverse), limit with -n.
Enrichment — X/Twitter profiles, tweets, avatars
enrich augments each expert that has a Twitter handle with live data the
marketplace doesn't expose: their current X bio ("what they work on"), follower
counts, their last-30-day tweets, and a properly-named profile picture
downloaded to ~/.hasna/experts/avatars/<source>/<name>.jpg.
experts enrich intro # all experts with a handle (resumable)
experts enrich --max 50 # process 50 (resume later to continue)
experts enrich --refresh # re-enrich already-done experts
experts enrich --since-days 14 # tweet window
experts enrich --no-avatars # skip image downloads
experts tweets AlexisOhanian # show stored tweetsIt runs through the @hasna/connectors
x connector, throttles requests, and stops gracefully when rate-limited so
you can re-run the same command to pick up where it left off. Enriched data
shows up in show and feeds the knowledge graph (X bios sharpen tag inference).
Knowledge graph — search by what you need
Every crawl builds a small graph in SQLite: expert, topic and tag nodes
joined by IN_TOPIC and HAS_TAG edges. Per-expert expertise tags are inferred
by matching the marketplace's tag vocabulary against each expert's title and bio,
so you can search by capability — not just keywords.
experts find Fundraising Startups # experts who cover BOTH (ranked)
experts find SEO Growth --any # experts who cover EITHER
experts find Wellness Yoga --limit 10 # topics and tags are interchangeable needs
experts related AlexisOhanian # peers who share the most expertise
experts graph # node/edge counts + most-covered expertise
experts reindex # rebuild the graph without re-crawlingfind prints how many of your needs each expert matched ([2/2]). The graph is
rebuilt automatically at the end of every crawl; reindex regenerates it from
the stored data when you tweak the vocabulary.
export
experts export --format csv -o experts.csv
experts export --format json --source intro > experts.jsonHTTP API
Run the server to query the store remotely:
experts-serve # listens on :7077 (override with PORT)| Endpoint | Description |
| --- | --- |
| GET /health | Service status, expert count, last crawl time |
| GET /sources | Available adapters and how many experts are stored |
| GET /experts | List/filter (source, topic, verified, top, min_price, max_price, min_rating, sort, asc, limit) |
| GET /experts?q=… | Full-text search |
| GET /experts?needs=a,b&match=all | Need-based graph search |
| GET /experts/:source/:idOrSlug | One expert |
| GET /find?needs=a,b&match=all | Need-based graph search (with match counts) |
| GET /related/:source/:idOrSlug | Experts sharing the most expertise |
| GET /graph | Knowledge-graph overview |
| GET /topics | Topics with counts |
| GET /tags | Expertise tags |
| GET /stats | Aggregate statistics |
| POST /crawl/:source | Trigger a crawl (disabled unless EXPERTS_ALLOW_CRAWL=1) |
curl "http://localhost:7077/experts?topic=Wellness&min_rating=4.9&limit=5"
curl "http://localhost:7077/experts?q=reddit"Config via env: PORT, OPEN_EXPERTS_DB, EXPERTS_ALLOW_CRAWL.
SDK & SaaS
A typed HTTP client ships at @hasna/experts/sdk for consuming a running
experts-serve instance from another service:
import { ExpertsClient } from "@hasna/experts/sdk";
const experts = new ExpertsClient({ baseUrl: "http://localhost:7077" });
const matches = await experts.find(["Fundraising", "Startups"], { limit: 10 });
const profile = await experts.expert.get("intro", "AlexisOhanian");The core is built to be wrapped by a separate SaaS repo (platform-experts,
mirroring platform-todos / @hasna/todos). See
docs/PLATFORM.md for the open-core ↔ platform architecture
and the three integration surfaces (library, HTTP API, SDK).
What gets stored
Each expert is normalized to a common, source-agnostic shape:
{
"source": "intro",
"sourceId": "177804",
"slug": "AlexisOhanian",
"url": "https://intro.co/AlexisOhanian",
"fullName": "Alexis Ohanian",
"title": "Tech Entrepreneur + Investor",
"headline": "Founder of Reddit, Initialized & 776",
"bio": "Co-founder of Reddit…",
"price": 2000,
"priceCurrency": "USD",
"priceUnit": "15 min video call",
"rating": 4.98,
"ratingCount": 50,
"verified": true,
"featured": true,
"topics": ["Career & Business", "Top Experts"],
"tags": [],
"socials": { "instagram": "…", "twitter": "…", "tiktok": "…" },
"extra": { "charityName": "776 Foundation", "charityPercent": 100, "exampleQuestions": ["…"] }
}Anything marketplace-specific (tiered pricing, charity split, example questions,
…) lives under extra, keeping the common schema clean across sources.
Adding a new source
Implement the Source interface and register it — that's the whole extension
surface:
import { registerSource, type Source } from "@hasna/experts";
class MyMarketplace implements Source {
name = "mysite";
description = "My expert marketplace";
website = "https://mysite.example";
async crawl(opts) {
// fetch + map to the common Expert shape
return { experts, topics, tags, total };
}
}
registerSource(new MyMarketplace());The CLI (experts crawl mysite) and HTTP API pick it up automatically.
Development
bun install
bun test # 102 tests
bun run typecheck
bun run build # → dist/
bun run dev list # run the CLI from sourceLicense
Apache-2.0 © Hasna
