Kenya-first AI-powered job intelligence platform. Scrapes, normalizes, deduplicates, risk-scores, and matches jobs across multiple sources — with a Telegram bot for real-time alerts and a web dashboard for tracking.
| Layer | Technology |
|---|---|
| Backend | FastAPI + SQLAlchemy 2.0 (async) + SQLite/PostgreSQL |
| Frontend | Next.js 15 + React 19 + TypeScript + Tailwind CSS 4 |
| Bot | python-telegram-bot v22 (integrated into FastAPI) |
| Scraping | httpx + BeautifulSoup4 + lxml |
| Risk | Deterministic rule engine (35+ patterns) |
| Matching | 3-stage: keyword → fuzzy → structural |
| Charts | Recharts |
| Animations | Framer Motion |
| UI | Radix UI + Lucide icons |
git clone <repo>
cd work-hunter-bot
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Copy env and add your Telegram token
cp .env.example .env
# Start API + Telegram bot
python run.pyAPI: http://localhost:8000 Docs: http://localhost:8000/docs
cd frontend
npm install
npm run devDashboard: http://localhost:3000
cp .env.example .env
# For Docker Compose, set POSTGRES_PASSWORD, SECRET_KEY and ENCRYPTION_KEY, then set
# DATABASE_URL / DATABASE_URL_SYNC to PostgreSQL URLs using host `db`.
docker compose up -dThe Compose stack refuses to start without database credentials, a JWT signing
key and the CV encryption key. Generate SECRET_KEY with
python -c 'import secrets; print(secrets.token_urlsafe(48))'. Generate
ENCRYPTION_KEY with
python -c 'import base64,secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())'.
Keep both values in a deployment secret manager and retain the CV key backups
separately; losing the CV key makes encrypted documents unrecoverable.
New CV uploads are encrypted with AES-256-GCM before they are written to the
persistent cv_data volume. Before upgrading an installation with existing CVs,
provision ENCRYPTION_KEY, stop uploads, run
python scripts/encrypt_legacy_cvs.py --dry-run, review the count, then run
python scripts/encrypt_legacy_cvs.py. The script writes and verifies encrypted
replacements before updating candidate paths and retains the plaintext originals
under their old filenames. Verify authorized downloads and encrypted backups
before securely removing those plaintext originals. Back up both the database
and CV volume before conversion; rollback is restoring that paired snapshot so
the old database paths still point to their original plaintext files. Legacy
files are not served as plaintext by the new endpoint; they return HTTP 409
until migrated.
Match scores are stored per candidate in candidate_job_scores. Existing
jobs.match_score and jobs.match_breakdown columns remain for compatibility,
but historical values are not returned as personalized scores. Existing
candidate profiles can be backfilled one at a time with
python scripts/score_jobs.py --candidate-id <candidate-id>; use --only-empty
to preserve already calculated candidate/job rows. New profile saves rescore
that profile, and ingestion calculates independent scores for current profiles.
The local development configuration generates an ephemeral signing key when
SECRET_KEY is empty, so development tokens expire when the server restarts.
Docker Compose runs the Alembic revisions before starting the API. Local SQLite
startup retains the existing create_all plus additive migration behavior.
work-hunter-bot/
├── backend/
│ ├── api/ FastAPI app, routes, schemas, deps
│ ├── models/ SQLAlchemy models (8 tables)
│ ├── scrapers/ 5 job source adapters
│ ├── services/ Normalizer, deduplicator, scorer
│ ├── matching/ 3-stage matching engine
│ ├── risk/ Scam/fraud detection engine
│ ├── telegram/ Bot + handlers + auto-alerts
│ ├── security/ JWT auth, bcrypt
│ ├── parsers/ CV and description parsing
│ ├── ai/ LLM integration (planned)
│ └── workers/ Celery background tasks
├── frontend/
│ └── src/
│ ├── app/ 7 pages (Dashboard, Jobs, Detail, Applications, Analytics, CV, Settings)
│ ├── components/ Reusable UI (MatchScore, RiskBadge, StatCard, JobCard, Sidebar, Header)
│ ├── data/ Mock data for development
│ ├── services/ API service layer
│ ├── types/ TypeScript interfaces
│ └── lib/ Utilities
├── tests/unit/ 29 tests
├── migrations/ Alembic
├── src/ Legacy Telegram bot (deprecated)
├── run.py Server entry point
└── docker-compose.yml
| Source | Tier | Focus | Type |
|---|---|---|---|
| BrighterMonday | 1 | Kenya | Job board |
| MyJobsKenya | 1 | Kenya | Job board |
| Jobicy | 2 | Global | Remote API |
| Greenhouse | 2 | Global | ATS boards |
| Ashby | 2 | Global | ATS boards |
Scrapers → Normalizer → Deduplication → Risk Analysis → Matching → Store → API / Telegram Alerts
Each job goes through:
- Scrape — fetch from source APIs/pages
- Normalize — standardize fields, extract skills, parse salary
- Dedup — fingerprint + similarity check (rapidfuzz)
- Risk — 10 checks, 35+ patterns, scored 0-100
- Match — 3-stage: keyword → fuzzy → structural (8 dimensions)
- Store — SQLite/PostgreSQL with full audit trail
- Alert — Telegram push to matching users (every 5 min)
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /jobs |
List jobs with filters |
| GET | /jobs/{id} |
Job detail + analysis |
| POST | /jobs/search |
Search jobs |
| GET | /candidate/profile |
Get candidate profile |
| POST | /candidate/profile |
Create/update profile |
| POST | /candidate/cv |
Upload CV |
| GET | /applications |
List applications |
| POST | /applications |
Create application |
| PATCH | /applications/{id} |
Update status |
| GET | /analytics/overview |
Dashboard stats |
| GET | /analytics/match-distribution |
Score distribution |
| GET | /analytics/top-sources |
Source performance |
| GET | /analytics/risk-summary |
Risk breakdown |
| GET | /analytics/activity |
Recent activity |
The bot runs automatically with the API server. No separate process needed.
| Command | Description |
|---|---|
/start |
Register for job alerts |
/stop |
Unsubscribe |
/top |
View top 5 highest-match jobs |
/search <query> |
Search by title/company/skills |
/apply <job_id> |
Track an application |
/my_applications |
List your tracked apps |
/update_status <id> <status> |
Change app status |
/settings |
View preferences |
/set_keywords python,react |
Set match keywords |
/set_exclude intern,junior |
Set exclusions |
/set_salary 200000 |
Set min salary (KES) |
/stats |
Job statistics |
/sources |
List active sources |
The bot scans every 5 minutes for new jobs matching each user's preferences:
- Jobs scoring ≥60% match with <40% risk are pushed automatically
- Filtered by user's keywords, exclusions, and salary threshold
- Requires the Telegram profile to be linked to a web candidate; ranking uses that candidate's scores
- No manual intervention needed — just
/startand set your preferences
Each user/job alert has a durable delivery record, bounded retries and an expiring send claim so concurrent workers do not normally send the same alert. Delivery is at least once: if Telegram accepts a message but the process dies before recording success, retrying the expired claim can still send a duplicate. Ambiguous outcomes are delayed and logged; exactly-once delivery is not available through Telegram's send operation and the local database alone.
| Route | Page | Description |
|---|---|---|
/dashboard |
Overview | Stat cards, top matches, activity feed |
/jobs |
Discovery | Search, filter, sort job listings |
/jobs/[id] |
Detail | Match breakdown, risk analysis, skills |
/applications |
Tracker | Kanban board with 6 status columns |
/analytics |
Analytics | Funnel, charts, source performance |
/cv |
CV Manager | Upload, download and delete encrypted CV files |
/settings |
Settings | Profile, hunter config, privacy |
All config via environment variables. See .env.example.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite+aiosqlite:///./work_bot_hunter.db |
Database connection |
TELEGRAM_BOT_TOKEN |
— | Telegram bot token from @BotFather |
SECRET_KEY |
change-me |
JWT signing key |
SCRAPE_INTERVAL_MINUTES |
60 |
How often to scrape |
REQUEST_DELAY_SECONDS |
2 |
Delay between requests |
WEIGHT_SKILLS |
0.30 |
Skill match weight |
WEIGHT_EXPERIENCE |
0.20 |
Experience match weight |
RISK_BLOCK |
70 |
Risk score to block |
RISK_REVIEW |
40 |
Risk score to flag |
CV uploads are stored as files under CV_STORAGE_PATH. The application does
not encrypt those files itself; configure encrypted storage at the host or
volume layer if that is required by the deployment.
# Unit tests
pytest tests/unit/ -v
# Integration test (full pipeline)
python test_integration.py- Message @BotFather →
/newbot - Copy the token to
.envasTELEGRAM_BOT_TOKEN - Message your bot →
/start - Set keywords:
/set_keywords python,django,react - Jobs auto-delivered every 5 minutes
# Database (SQLite for local dev)
DATABASE_URL=sqlite+aiosqlite:///./work_bot_hunter.db
# Telegram
TELEGRAM_BOT_TOKEN=your-bot-token
TELEGRAM_CHAT_ID=your-chat-id
# Auth
SECRET_KEY=your-secret-key
# Scraping
SCRAPE_INTERVAL_MINUTES=60
REQUEST_DELAY_SECONDS=2Private — not for distribution.