Spec #1 — Harvest the Ever Jobs Backend
Status: Done (shipped 2026-06-15) · Owner: Hust ← Ever Jobs · Effort: M (integration) · Phase 1 (independent quick win) · Depends on: —
1. Problem & user value
Hust already pays for the Ever Jobs sourcing backend (160+ sources, cross-source dedup, liveness, salary normalization, market analytics), but under-uses it:
- The live user search (
searchJobs) queries Hust's own synced Postgresjobstable, not the full Ever Jobs corpus. - The 15-min sync (
packages/triggers/src/sync-jobs.ts) pinscountry: "USA", rotates a fixed set ofSEARCH_TERMS, and uses an 11-valuesiteTypeenum — a sliver of the 160+ sources. It never passescompanySlug, so the ~170 ATS plugins + hundreds of company-career plugins are dormant. - Ever Jobs's default-on cross-source dedup never sees a multi-source corpus to collapse.
/api/jobs/analyze(market analytics) is wired in thejobs-apiclient (analyzeJobs()) but called nowhere.
This epic unlocks all of that with integration, not new backend — wider supply, better freshness, dedup, liveness, and market analytics for ~zero backend cost.
2. Scope
In:
- Widen the client
siteTypeset (11 → the full Ever Jobs set) and stop hard-coding it. - Pass
companySlugto unlock ATS/company-direct depth. - Drop the hard
country: "USA"pin; make country/locale a parameter (default broad). - Consume
/api/jobs/analyze— surface market analytics (salary ranges, remote %, top companies, per-site comparison) in the UI + as context for evaluation/salary insights. - Broaden the sync coverage (more terms/sites/countries) without overloading the schedule.
Out (other epics / Ever Jobs-side):
- Per-job liveness on the search DTO — that's #4 (small Ever Jobs change + Hust badge).
- Posting legitimacy signals — #7 (net-new Ever Jobs plugin).
- Optionally querying Ever Jobs live per user search (vs. the synced cache) — phase 2 of this epic if the synced approach proves limiting.
3. Design (all Hust-side integration)
siteTypecoverage. Replace the hard-coded 11-value client enum with the full supported set (or a pass-through), so search/sync can request the whole corpus. Keep a sensible default subset for the free tier; allow the full set for sync + pro.companySlug. Thread the optionalcompanySlugparameter through thejobs-apiclient and the sync, so ATS/company-direct sources are reachable. (Dedup activates automatically once multiplesiteTypes flow.)- Drop USA-only. Parameterize
country/locale insync-jobs.tsand the client; default to a broad set (or none) instead of"USA". Geocoding stays Hust-side as today. - Market analytics. Call
EverJobsClient.analyzeJobs()on a schedule (and/or on demand) and cache the result; surface it as: salary-insight enrichment, a "market" panel, and context fed into #3 evaluation (Comp/Demand block) and salary insights. - Sync breadth. Expand
SEARCH_TERMS/ sites / countries with care for rate + schedule; rely on Ever Jobs dedup to collapse overlap rather than de-duping Hust-side.
4. Data / API touchpoints
packages/jobs-api/— widenSiteEnum, addcompanySlug, exposeanalyzeJobs()usage; the Ever Jobs API endpoint stays as configured (EVER_JOBS_API_URL).packages/triggers/src/sync-jobs.ts— dropcountry:"USA", broaden params.- Optional: a small
market_analyticscache table (jsonb snapshot +fetchedAt) or reuse an existing cache; no removal of the existingjobssync. - No competitor anything here — this is entirely our Ever Jobs product surface.
5. Implementation plan
- Audit the current
jobs-apiclient surface (SiteEnum, search params,analyzeJobs). - Widen
siteType+ addcompanySlug(typed, optional) without breaking current callers. - Update
sync-jobs.ts: remove thecountry:"USA"pin, broaden terms/sites; verify dedup is on. - Add an
analyzeJobs()call path + cache; expose via agetMarketInsightstool/route. - Surface market analytics in the salary-insights UI + feed the evaluation Comp/Demand block.
- Backfill/verify geocoding for the wider corpus.
6. Tasks
- Widen
SiteEnum(11 → full) + keep a free-tier default subset. - Add
companySlugthrough client + sync. - Remove hard
country:"USA"; parameterize country/locale. - Wire
analyzeJobs()+ cache (+ tool/routegetMarketInsights). - Feed market analytics into #3 (Comp/Demand) + salary insights UI.
- Tests: client param shaping (unit), sync params (unit), analyze cache (unit), E2E that
/api/jobs/searchreturns multi-source results + a market panel renders.
7. Risks & acceptance
- Risk: wider sync → rate limits / volume → tune schedule + page sizes; lean on backend dedup.
- Risk: non-USA salary/locale normalization edge cases → Ever Jobs handles normalization;
Hust just consumes (
enforceAnnualSalaryetc.). - Acceptance: search returns results from more than the original 11 sites (incl. at least one
ATS/company-direct via
companySlug); non-USA jobs appear;/api/jobs/analyzeis consumed and a market panel + salary insight render; CI green; zero competitor references.
Implementation (shipped)
The market-analytics half of this epic shipped as an AI-native market insights capability that harvests the corpus Hust already syncs from the Ever Jobs API. Real implementation:
- AI tool —
packages/ai/src/tools/market-insights.tsexportsmarketInsightsTool(thegetMarketInsightscapability) plus the pure, unit-tested aggregation corecomputeMarketInsights(). It returns demand count, remote share, salary spread (p25/median/p75), top in-demand skills, top hiring locations, top companies, and the seniority-level mix. - Orchestrator wiring — registered as the
marketInsightstool inpackages/ai/src/agents/orchestrator.ts, re-exported viapackages/ai/src/tools/index.tsand the package barrelpackages/ai/src/index.ts. Surfaced to users through the AI chat (no separate route needed). - Data source — reads Hust's synced Postgres
jobstable directly via Drizzle (@ever-hust/db), filtering by role title (+ optional location) withescapeIlike-guardedilike. Complements the pay-onlysalaryInsightstool (packages/ai/src/tools/salary-insights.ts); both share annualisation helpers inpackages/ai/src/tools/salary-helpers.ts. - Ever Jobs client —
analyzeJobs()(the/api/jobs/analyzeconsumer) is implemented onEverJobsClientinpackages/jobs-api/src/index.ts, behind the same circuit-breaker/retry + timeout path assearchJobs(). - Typed
companySlug— added toScraperInputSchemainpackages/jobs-api/src/types.ts, so ATS/company-direct depth is reachable through the client contract. - Tests —
packages/ai/src/tools/market-insights.test.ts(aggregation core),packages/jobs-api/src/client.test.ts(theanalyzeJobspath), andpackages/jobs-api/src/types.test.ts(SiteEnum/companySlugschema).
Intentionally deferred (not shipped in this pass):
- Widen
SiteEnum— still the original 11-value enum inpackages/jobs-api/src/types.ts; the full-corpus widening is not yet done. - Drop the USA pin —
packages/triggers/src/sync-jobs.tsstill hard-codescountry: "USA", andScraperInputSchema.countrystill defaults to"USA"; parameterising country/locale and broadening sync breadth remains open. - Live
/api/jobs/analyzein the tool —getMarketInsightsaggregates the syncedjobstable rather than calling the liveanalyzeJobs()endpoint; the client method exists but is not yet wired into the tool or a scheduled cache (market_analyticstable not added). companySlugthrough sync/tools is typed/usable at the client layer but not yet threaded into the sync task.- Dedicated market-panel UI (now shipped) — the
marketInsightstool result renders as a standalone, dismissible canvas card viaapps/web/components/canvas/market-insights-card.tsx(demand, remote %, pay spread, top skills/locations/companies, seniority mix), wired throughuseCanvasSync(marketInsightsslot +clearMarketInsights) and the dashboard overlay, mirroring the salary-insights card.