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:
| Signal | Weight | Source |
|---|---|---|
| Apply control inactive / off-platform redirect | high | page/apply analysis |
| Perpetual / very-old req still open | medium | posting age + recheck history |
| Reposting pattern (same role re-listed repeatedly) | medium | dedup/recheck ledger |
| Vague or absent compensation | low | salary extraction |
| Employer reputation / red flags | low | firmographics |
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-corpusplugin +legitimacy: { tier, score, reasons[] }on the DTO. - Hust:
packages/jobs-apiDTO type + nullablejobs.legitimacy*columns; badge components; evaluation Block-G renderer reads it if present.
5. Plan & tasks
- EJ: scaffold
legitimacy-corpusplugin (reuse dedup/recheck ledger + liveness inputs); emit tier + reasons; add to DTO (additive). - Hust: thread DTO field +
jobscolumns; trust badge (benign copy); wire Block G in #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
suspiciouswith 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.ts—ILegitimacyChecker,LegitimacyInput, andLEGITIMACY_CHECKER_TOKENcontract consumed by the API.apps/api/src/jobs/jobs.controller.ts— opt-in?legitimacy=truequery param;enrichLegitimacy()attacheslegitimacy: { state, reasons[] }to eachJobPostDto(folds in the liveness off-platform-redirect signal when liveness ran first). Tested inapps/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-callsignals:falseorEVER_JOBS_REQUEST_SIGNALS=falseopt out.packages/jobs-api/src/types.ts— DTO carries the optional corpus signallegitimacy?: { state, reasons[] }(forward-compatible; Hust derives a heuristic when absent).packages/db/src/schema/jobs.ts— nullablelegitimacy+legitimacy_reasonscolumns (migrationdrizzle/0002_jittery_talisman.sql);packages/triggers/src/map-job.tspersists them.apps/web/lib/legitimacy.ts—assessLegitimacy()+LegitimacyLevel/LegitimacyAssessment; an explicit corpus signal (with reasons) overrides the Hust-side heuristic. Tested inapps/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 isuncertain(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+assembleEvaluationattach an optionalblocks.legitimacy(level/reasons + a fixed "orthogonal to fit" note);apps/web/components/canvas/evaluation-card.tsxrenders the "Posting legitimacy" section (auto-open when uncertain). Tests assert legitimacy never moves the fit score/band. - E2E:
tests/e2e/corpus-signals.spec.tsasserts the signal reaches the read endpoints; the seed (packages/db/src/seed.ts) populates a deterministic spread.