Qhaphela ("watch out!" / "be careful!" in Zulu/Xhosa) is an AI-powered job posting fraud detection tool and media literacy resource for South African job seekers. It ships two surfaces:
extension/- the real product: a Chrome extension that runs on job platforms and shows a live risk verdict directly on the page.- The web app (
src/,server.ts) - a secondary demo/testing surface for showing the same scoring engine in a browser tab, without installing the extension.
The scoring engine itself is a real trained model, not a hardcoded rules engine: a scikit-learn Random Forest + TF-IDF pipeline (qhaphela/) trained on labelled SA job posting data, with SHAP explanations for every score. Both surfaces call the same FastAPI service, so they always agree.
Qhaphela helps you check a job before you apply, in two ways:
- No install needed - open the web app and paste a job posting, or a recruiter's message forwarded over WhatsApp, SMS or email. You get a plain-language safety read: a risk level, why it was flagged, and what to do next.
- Best experience - install the free Chrome extension. It runs quietly and only appears when you open a real job posting, showing a safety panel right on the page. On ordinary pages it stays completely invisible.
Inside the web app you will also find:
- Red Flags - short, shareable lessons on how job scams actually work.
- Verify a Recruiter - polite, copy-ready messages to confirm an employer before you share anything.
- Been scammed? - calm, step-by-step help if something has already gone wrong, pointing you to real South African channels (your bank, SAPS, SAFPS).
Three rules that keep you safe: never pay to get a job, never send your ID or banking details before a signed offer, and verify the employer yourself. Qhaphela gives you an informed opinion, never a guarantee - a low score is not proof a job is safe.
python3 -m venv venv # first time only
source venv/bin/activate # for Linux/macOS
.\venv\Scripts\Activate.ps1 # for Windows
pip install -r requirements.txt
cd qhaphela
uvicorn app:app --port 8000This must be running before the extension or the web app can score anything - both are thin clients over this service.
In a second terminal, from the project root:
npm install # first time only
npm run devOpen http://localhost:3000.
- Run in Production Mode:
npm run build npm run start
- Check for Code Errors:
npm run lint
- Make sure the ML model service from Step 1 above is running on port 8000.
- Open Google Chrome and go to
chrome://extensions/. - Turn on Developer mode in the top-right corner.
- Click Load unpacked.
- Select the
extension/folder inside this project directory.
The extension runs on any job board, not a fixed list. It loads on all sites but stays
completely inert unless the page is genuinely a job posting - detected via schema.org
JobPosting metadata, job-shaped URLs, or several independent hiring phrases. No page text
leaves the browser otherwise, and it is switched off entirely on banking, tax and webmail
sites. It then docks a panel into the page layout:
- On a listing/search page: shows aggregate stats across every job card currently on screen (how many scanned, how many flagged high/medium risk), with a "click for more" list of the specific flagged postings.
- When you open one specific posting: the panel automatically switches focus to that posting's own score, tier, and plain-language advice, with the full SHAP reasons breakdown tucked behind "click for more" so the primary view stays uncluttered.
- Safe alternatives: real low-risk postings scanned on the same page, with their real links and real scores. Never invented listings.
- Search everywhere: pre-filled searches across 12 South African job platforms for the role you are viewing.
You can also click the toolbar icon for the same verdict in a popup, or paste text manually (useful for postings forwarded via WhatsApp).
Fraud signals (itemised with fixed, disclosed weights so the score is checkable): upfront payment / registration fees, ID & banking detail requests, passport requests, tax/SARS document requests, proof-of-residence requests, WhatsApp off-platform migration, salary far above the market band for the stated role, fake POPIA clauses used to sound official, unverifiable B-BBEE claims, urgency/scarcity pressure, free-email recruiters, and youth programmes (learnerships/internships/graduate programmes) advertised with a "no experience needed" hook.
Identity-theft layer: sensitive-document requests are surfaced as their own warning, separate from the fraud score, because the harm (identity theft, SIM-swap fraud, credit taken out in your name) is different in kind from "this job isn't real".
Contact & domain checks: whether the recruiter's email uses a company domain or a free provider, whether a company website is linked, and whether the email domain actually matches the company name being advertised (a common impersonation signal). These are observations from the posting text - deliberately not labelled "verified", because no company registry (CIPC) is queried.
CV guidance: qualifications, certifications, skills and experience requirements are extracted from the posting being viewed, and turned into advice on mirroring those exact terms if you genuinely hold them (ATS software matches on literal terms).
Accessibility: Atkinson Hyperlegible throughout, light/dark themes with every colour pair measured for WCAG 2.2 contrast, risk tiers carry a symbol and text label as well as colour (never colour alone), and the entire interface is available in all 11 official South African languages: English, isiZulu, isiXhosa, Sesotho, Sepedi, Setswana, Xitsonga, siSwati, Tshivenda, isiNdebele and Afrikaans.
source venv/bin/activate
pip install -r requirements.txt
cd qhaphela && pytest test_app.py -vcd qhaphela && pytest -q # runs both suites72 tests, across three suites.
test_app.py (38) covers detection accuracy on real scam and real legitimate
postings, including a regression test for every false positive found during live
testing: fairness toward small informal employers, excerpt anonymisation,
multi-class scam typing, explainability output, the identity-theft layer,
contact checks, CV matching, input validation and reporting.
test_ai.py (19) covers the assistant's security properties: that the API key
can never appear in any response, that prompt-injection attempts are detected
while ordinary recruiter wording is not, that fabricated evidence quotes are
discarded, that a malformed chat history is rejected rather than crashing the
route, and that scoring is completely unaffected when the AI is unreachable.
test_recon.py (15) covers employer verification, and two of its tests pin
bugs found in live testing that had the same shape: a scam posting being shown
reassuring green ticks. Infrastructure checks must never run against a free
email provider (checking gmail.com returns "registered 30 years ago, valid
SPF, valid certificate" - all true, all about Google, none of it evidence about
the employer), and a near-miss domain must never be reported as matching the
company, because that is precisely the domain an impersonator registers.
The AI and recon tests stub the network, so they keep protecting you with no internet and no key.
python qhaphela/evaluate.py12 of 12 correct · 6 of 6 scams caught · 0 false alarms, measured through the full production pipeline on hand-labelled real postings.
The
1.0figures inmodels/metadata.jsonare deliberately not quoted anywhere: they come from a held-out split of the synthetic training data, where the classes are trivially separable. A small honest sample beats an inflated one - and the tooling reports a fraction with its caveat rather than a percentage that implies more than was shown.
Both the web app (localhost:3000) and the model service directly (localhost:8000) expose the same shape:
- Health Check:
GET /api/health(web app, proxied) orGET /health(model service directly) - Score Job Posting:
POST /api/score(web app, proxied) orPOST /score(model service directly)- Body:
{"text": "Job text here...", "company_name": "optional"} - Returns: score, tier, itemised
rule_reasonswith points,identity_theft_signals,contact_checks,cv_guidance,highlights(literal phrases for in-page underlining), and raw SHAPtop_reasons
- Body:
- Report a posting:
POST /report-{"url", "domain", "category", "excerpt", "score"} - Report counts:
GET /report-stats?url=…&domain=… - CV match (text):
POST /match-{"cv_text", "job_text"} - CV match (file):
POST /match-file- PDF, .docx, .txt or .md, up to 5 MB - Model evaluation:
GET /metrics- real-posting results with their caveat - Local impact figures:
GET /impact- counted, never estimated
Requests are capped at 20,000 characters and rate-limited to 60/minute per client. CORS is restricted to extension and localhost origins.
Privacy note: reports are stored in a local SQLite file (
qhaphela/reports.db, gitignored) and record only a hash of the posting URL, its domain, a category, and a short excerpt - never any reporter identity. Excerpts are anonymised before storage - SA ID numbers, phone numbers, email addresses and account numbers are stripped - per the research protocol's POPIA commitment. Report counts are shown as "recorded on this device" because the store is local; they are never presented as community-wide figures.
| Document | What it covers |
|---|---|
| Privacy Policy | What is processed and where. Issued under section 18 of POPIA and meeting the Chrome Web Store user-data rules in force since 1 August 2026. |
| Terms of Use | What Qhaphela is, what it cannot do, and how to use it fairly. |
| Security Policy | How to report a vulnerability, and the design constraints we hold to. |
| Contributing | How to help, and the one rule: never show a user something untrue. |
| Licence | MIT. |
All of these are also reachable inside the extension under Privacy & terms, alongside a My data page that lists everything stored on your device and erases it on request.
A low score does not mean a job is safe. A high score does not mean a company is dishonest. Always verify an employer yourself before sharing anything personal.
No account, no analytics, no tracking. Fraud detection runs entirely on your own computer, so the jobs you look at never leave your machine.
The single exception is Qhaphela AI: text you actively choose to give the assistant is sent to Groq for processing. Nothing else is, no identifier for you is attached, and the extension never holds the API key. Everything except the assistant works with no internet connection at all. Full detail in PRIVACY.md.