Skip to main content

Spec #7 — Posting-Legitimacy / "Ghost-Job" Radar

Status: Done (shipped 2026-06-15) · Owner: Ever Jobs (signal) → Hust (badge) · Effort: M (EJ) + S–M (Hust) · Phase 2 · Depends on: #3 (renders alongside)

1. Problem & user value

Many postings aren't real opportunities — perpetual reqs, reposts, vague comp, off-platform redirects, "evergreen" pipelines. Liveness (#4) tells you a posting is reachable; legitimacy tells you whether it's worth trusting. This is the one net-new data capability in the roadmap, and a real trust differentiator: warn users before they invest in a likely ghost job.

2. Scope

In: a corpus-level legitimacy signal computed in Ever Jobs (new feature plugin, alongside dedup/liveness/merge) + a Hust trust badge that renders it. Signal is orthogonal to the fit score — never folded into #3's number (it's "is this real?", not "is this good for me?").

Out: the fit evaluation itself (#3).

3. Design

3.1 Ever Jobs — legitimacy-corpus plugin (corpus, anonymous)

Computes a reliability-weighted legitimacy score from corpus + history signals:

SignalWeightSource
Apply control inactive / off-platform redirecthighpage/apply analysis
Perpetual / very-old req still openmediumposting age + recheck history
Reposting pattern (same role re-listed repeatedly)mediumdedup/recheck ledger
Vague or absent compensationlowsalary extraction
Employer reputation / red flagslowfirmographics

Emits a tier + reasons: active / caution / suspicious (+ contributing reasons). Exposed on the search/by-id DTO like liveness. Lives behind the Ever Jobs API (corpus-level, same for all users → belongs in Ever Jobs per the partition).

3.2 Hust — trust badge (Block G host)

Renders the tier as a benign, explained badge — never red, never the word "scam": e.g. "Verified-active" / "Worth a quick check" / "Some signals to review", each with a one-line "why". Surfaces on the card + job detail, and as the Block G section of the #3 evaluation (orthogonal — shown beside the fit score, not inside it).

4. Data / API

  • Ever Jobs: new legitimacy-corpus plugin + legitimacy: { tier, score, reasons[] } on the DTO.
  • Hust: packages/jobs-api DTO type + nullable jobs.legitimacy* columns; badge components; evaluation Block-G renderer reads it if present.

5. Plan & tasks

  1. EJ: scaffold legitimacy-corpus plugin (reuse dedup/recheck ledger + liveness inputs); emit tier + reasons; add to DTO (additive).
  2. Hust: thread DTO field + jobs columns; trust badge (benign copy); wire Block G in #3.
  3. Tests: EJ signal unit tests (fixtures per tier); Hust badge + Block-G render (E2E).

6. Acceptance

  • A fixture posting with off-platform redirect + perpetual age yields suspicious with reasons; Hust shows a benign explained badge; the signal never alters the #3 fit number; CI green; zero competitor references.

Implementation (shipped)

The tier vocabulary shipped as verified / likely / uncertain (not the spec's draft active / caution / suspicious). The signal stays strictly orthogonal to the #3 fit number.

Ever Jobs — corpus signal (Spec 740 legitimacy-detector plugin):

  • packages/plugins/legitimacy-detector/src/legitimacy-detector.service.ts — deterministic, pure/in-memory legitimacy scorer (+ legitimacy-detector.module.ts, index.ts).
  • packages/models/src/interfaces/legitimacy-checker.interface.tsILegitimacyChecker, LegitimacyInput, and LEGITIMACY_CHECKER_TOKEN contract consumed by the API.
  • apps/api/src/jobs/jobs.controller.ts — opt-in ?legitimacy=true query param; enrichLegitimacy() attaches legitimacy: { state, reasons[] } to each JobPostDto (folds in the liveness off-platform-redirect signal when liveness ran first). Tested in apps/api/__tests__/jobs/corpus-signals.spec.ts.

Hust — trust badge + corpus consumption (now wired end-to-end):

  • packages/jobs-api/src/index.ts — the client opts into ?legitimacy=true (alongside ?liveness=true) by default; per-call signals:false or EVER_JOBS_REQUEST_SIGNALS=false opt out.
  • packages/jobs-api/src/types.ts — DTO carries the optional corpus signal legitimacy?: { state, reasons[] } (forward-compatible; Hust derives a heuristic when absent).
  • packages/db/src/schema/jobs.ts — nullable legitimacy + legitimacy_reasons columns (migration drizzle/0002_jittery_talisman.sql); packages/triggers/src/map-job.ts persists them.
  • apps/web/lib/legitimacy.tsassessLegitimacy() + LegitimacyLevel / LegitimacyAssessment; an explicit corpus signal (with reasons) overrides the Hust-side heuristic. Tested in apps/web/lib/legitimacy.test.ts.
  • Read paths select the signal: AI searchJobs, /api/jobs/search, /api/jobs/[id], /api/user/favorites/list.
  • apps/web/components/canvas/job-card.tsx — "Verify posting" outline badge when the assessment is uncertain (reasons in tooltip) and a positive "Verified" badge when the corpus confirms; never auto-hides.
  • Block G (evaluation drawer) — packages/ai/src/evaluation/legitimacy.ts + assembleEvaluation attach an optional blocks.legitimacy (level/reasons + a fixed "orthogonal to fit" note); apps/web/components/canvas/evaluation-card.tsx renders the "Posting legitimacy" section (auto-open when uncertain). Tests assert legitimacy never moves the fit score/band.
  • E2E: tests/e2e/corpus-signals.spec.ts asserts the signal reaches the read endpoints; the seed (packages/db/src/seed.ts) populates a deterministic spread.