Freelance project
Job Fraud Detector
- Problem
- Fake job postings look like real ones and cost applicants money and personal data; a job seeker has no quick way to check a posting before applying.
- Approach
- A posting is checked against a crowd-reported blacklist, scored by a fast text classifier that shows which words drove its verdict, then reviewed by Gemini with a strict output schema; Gemini can overrule the classifier only when it is more than 70% confident.
- Result
- A FastAPI web app that checks a URL or pasted text, keeps a history of checks in Postgres, and drafts an ATS-friendly resume for postings that look legitimate.
- My role
- Freelance AI engineer (2024–2025). Built the system end to end for a client: backend, classifier, LLM integration and UI.
- Stack
- Python
- FastAPI
- BeautifulSoup
- scikit-learn
- Gemini
- Pydantic structured output
- PostgreSQL
- SQLAlchemy + Alembic
- python-docx / WeasyPrint
- Links
01
Architecture
- Job URL or text: the user pastes a link to a posting or the posting's text.
- Crowd blacklist: the posting is first checked against postings other users have reported as fraudulent, the cheapest check of all.
- Scraper: for a URL, the posting's text is fetched from the page; pasted text skips this step.
- SGD classifier: a CountVectorizer bag of words feeds a linear classifier trained with SGD. It is fast, and its top-weighted words are shown as the explanation for its verdict.
- Gemini verifier: Gemini reviews the posting and must answer in a fixed Pydantic schema that includes its verdict and its confidence, so the reply can be checked and used by the code rather than parsed out of free text.
- Verdict: the classifier's call stands unless Gemini disagrees with more than 70% confidence; only then does the LLM override it.
- ATS resume: for postings judged legitimate, the app can generate an ATS-friendly resume for that job.
- Postgres keeps the history of every check.
02
Key decisions
The LLM overrides the classifier only above 70% confidence
The classifier is fast and predictable, but it only sees word counts; Gemini can catch context it misses. Letting Gemini overrule only when it is confident keeps a hesitant LLM from flipping a sound verdict. The cost is that the 70% threshold is a fixed choice, and an LLM's own confidence is not well calibrated.
A simple linear text model with visible top words
CountVectorizer with an SGD-trained linear classifier is cheap to train and run, and shows exactly which words pushed a posting towards fraud, which a user can check. The cost is that it ignores word order and context, which is why the LLM review sits behind it.
Schema-checked LLM output
Requiring a Pydantic schema means a malformed or off-topic reply fails validation instead of silently changing the verdict. The cost is extra failure handling: a reply that does not validate is a separate case the code has to handle.
03
Results
- Checks a posting by URL or pasted text through four layers: blacklist, scraper, classifier and LLM review.
- Every verdict comes with an explanation: the classifier's top words, plus Gemini's structured assessment.
- Keeps a history of checks in Postgres, and drafts an ATS-friendly resume for postings judged legitimate.
04
What I'd do next
- Measure how often Gemini overrides the classifier and whether those overrides are right, to set the 70% threshold from data.
- Replace the bag-of-words model with a small fine-tuned transformer and compare both on a held-out set.
- Add moderation to the crowd blacklist so a single false report cannot mark a real posting as fraud.