Inspiration

Remote work has opened global opportunities — and a parallel market of scams, ghost listings, and low-quality postings designed to harvest applications instead of fill roles. Job seekers waste hours triaging boards with no signal beyond a logo and a job title. Existing platforms optimize for quantity of listings, not quality of evidence. We wanted to flip that: surface fewer postings, but stand behind each one with verifiable, transparent reasoning a candidate can act on.

## What we built

RemoteTrust AI is an AI-powered audit pipeline for remote job postings. The user types a keyword, and we:

  1. Discover — fan out in parallel to multiple live sources (Greenhouse public boards across 20+ remote-friendly employers, plus RemoteOK), merge, deduplicate, and rank by relevance.
  2. Verify — for each posting we extract structured fields via an LLM, then run a five-axis verification stack: external checks (email-domain match, company-website reachability, application-URL whitelist), remote authenticity (geographic / time-zone contradiction patterns), liveness (URL reachability, posting freshness), and recruiter activity (30 / 90-day posting volume).
  3. Shortlist — combine the signals into a Trust Score and Quality Score, route to a verdict (verified_high_quality / legit_low_quality / suspicious / manual_review), and produce a plain-language AI Explanation plus structured evidence the candidate can audit.

Every score links back to the underlying check that produced it. The user never has to take the model's word for anything.

## How we built it

Backend — FastAPI (Python 3.12), Pydantic models throughout, in-memory run store, asyncio.gather for parallel provider fan-out and verification.

Discovery — Greenhouse ?content=true API + RemoteOK JSON, with content-based deduplication ((title, company)), word-boundary token matching, and relevance scoring where title hits weigh 10×, company 5×, and description 1×. LLM extraction — OpenAI-compatible adapter using DeepSeek V4 Flash via OpenRouter, response_format: json_object, with one automatic retry on transient relay failures. HTML job descriptions are decoded (handles double-entity-encoded payloads like <p>) and parsed to bullet-preserving plain text before reaching the model.

Deployment — frontend on Cloudflare Pages with on-push builds, backend on a DigitalOcean VPS deployed via GitHub Actions SSH on every push touching backend/**. Health checks and systemd-managed restart.

Testing — 335+ unit tests covering scoring rules, verification edge cases, HTML preprocessing, retry behavior, and verdict semantics. End-to-end calibration sweeps against real Greenhouse boards (Stripe, GitLab, Figma, Airbnb, Twilio, Elastic, etc.) drove every accuracy-related fix.

## Challenges we faced

The hardest problems were never the obvious ones — they were the class-level false positives that didn't surface until we audited real companies and saw legitimate postings labeled "suspicious."

  • Greenhouse list endpoint returned empty job bodies by default. We added ?content=true and discovered the content was HTML-entity-encoded inside JSON (&lt;p&gt; instead of <p>). The LLM was silently skipping bulleted sections like "What you'll do" because it saw a wall of escaped markup. Fix: decode entities, then parse with a stdlib HTMLParser subclass that converts <li> to dash bullets.
  • Whitelist pinned specific subdomains (boards.greenhouse.io) but GitLab uses job-boards.greenhouse.io, Stripe proxies through stripe.com/jobs/..., and Elastic posts at jobs.elastic.co. Three layered fixes: switch to registered-domain matching, infer the company website from the job URL's registered domain when the URL isn't a generic ATS, and promote URLs hosted on the verified company domain to "whitelisted."
  • Verdict logic fell through to "suspicious" as a default whenever scores didn't fit a positive bucket — turning thin-content legit postings into accusations. Reworked the rules so "suspicious" requires positive evidence (a critical or risk flag); uncertain cases route to "manual_review."
  • Remote authenticity flagged legitimate region-scoped roles ("Must be based in the United States") the same as overseas scam patterns ("Must be in Shenzhen"). Split the trigger: known scam-hub cities always fire, generic restriction phrases only fire when no standard regional context (US / EMEA / APAC / country names) is present.
  • Free LLM relays were unreliable — ~40% transient failure rate on one provider. We added a single automatic retry and migrated to a more stable provider, dropping observed extraction failures to zero in a 10-job sweep.
  • Dark mode looked broken because the <body> had a hardcoded light background overriding the carefully-themed component-level dark variants — light text on a forced-white background ghosted out entirely. Fix in two lines, lesson worth more.

## What we learned

  • Class fixes beat case fixes. When a single posting looks wrong, the temptation is to hardcode an exception. The real win is finding the pattern: "this happens to every company with a non-.com TLD," or "this happens to every ATS that proxies through a vanity URL."
  • Trust is a UX problem, not just a model problem. The same score means different things to a hopeful applicant and a cautious one. Surfacing why a score landed — every check that fired, every URL we tested — is what turns an AI verdict into actual trust.
  • Real-data calibration catches what unit tests can't. Repeated sweeps against real Greenhouse boards (Stripe, GitLab, Elastic, Airbnb) exposed every edge case in this list. Hand-rolled fixtures would have missed all of them.
  • LLM extraction is fragile against unstructured HTML. Cleaning the input matters more than perfecting the prompt.

## What's next

  • Add more ATS sources (Lever, Workday, Ashby, SmartRecruiters)
  • Browser extension that shows the audit verdict inline on existing job boards
  • Multi-language support for non-English job postings
  • Persistent run history + user-owned shortlists

Built With

  • fastapi
  • llm
  • next.js
  • pydantic
  • python
  • remoteok-api
  • sqlite
  • tailwind-css
Share this project:

Updates