An autonomous placement agent that finds, ranks, and alerts you to jobs — so you don't have to keep checking.
Waypost is a full-stack job placement agent built for people who are tired of refreshing job boards. It runs continuously in the background, pulling fresh postings from public job-board APIs, scoring each one against a candidate's actual resume, and pushing a notification — by email or Telegram — the moment something worth applying to appears.
It's not a job board. It's the layer that sits on top of job boards and does the searching for you.
In one sentence: register, upload a resume, and Waypost quietly watches the market and taps you on the shoulder when it finds something that fits.
- Core features
- Architecture
- Tech stack
- Project structure
- Getting started
- Environment variables
- API reference
- Security model
- Design decisions
- Honest limitations
- Roadmap
- License
| 🔐 Authentication | Email/password accounts secured with bcrypt hashing and JWT bearer tokens; full forgot/reset password flow with single-use, time-limited, hashed reset tokens |
| 🧭 Continuous discovery | A background scheduler polls Greenhouse, Lever, and Adzuna every hour on behalf of every registered user's stated roles and locations — no manual searching required |
| 📄 Resume intelligence | Upload a PDF or DOCX; the agent extracts raw text, skills, and estimated experience automatically |
| 🎯 Smart matching | TF-IDF relevance ranking with location-aware filtering, including city-alias resolution (Bangalore ↔ Bengaluru, Bombay ↔ Mumbai, etc.) and fuzzy string matching for spelling variants |
| 📊 ATS scoring | Scores a resume against any job description the way an applicant-tracking system would, combining keyword/formatting checks with LLM-generated qualitative feedback |
| 🔔 Proactive alerts | Push notifications — not pull — via email (Resend) and/or Telegram the moment a new posting clears the user's match threshold, with automatic de-duplication |
| 🤖 Conversational agent | A natural-language endpoint lets users say things like "find me remote data analyst roles in India" and have the agent decide which tools to call |
flowchart LR
subgraph Sources["External sources"]
GH[Greenhouse API]
LV[Lever API]
AZ[Adzuna API]
end
subgraph Backend["Waypost backend (FastAPI)"]
ING[Ingestion pipeline]
DB[(PostgreSQL / SQLite)]
MATCH[Matcher — TF-IDF + location + alias]
SCHED[APScheduler — hourly]
AUTH[Auth — JWT + bcrypt]
NOTIFY[Notifier]
LLM[LLM layer — Gemini / Anthropic]
end
subgraph Client["Client"]
UI[Web dashboard]
end
GH --> ING
LV --> ING
AZ --> ING
SCHED --> ING
ING --> DB
DB --> MATCH
MATCH --> NOTIFY
NOTIFY -->|email| Resend
NOTIFY -->|message| Telegram
UI <-->|JWT| AUTH
AUTH <--> DB
UI <-->|search / upload| Backend
Backend <--> LLM
The scheduler is the heart of the "agent" behavior: it doesn't wait to be asked. Every hour, it re-fetches postings for every distinct role/location combination any registered user cares about, scores them against each profile, and fires a notification for anything new that clears that user's match threshold — all without a human triggering it.
| Layer | Choice | Why |
|---|---|---|
| API framework | FastAPI | Async-first, automatic OpenAPI docs, strong typing via Pydantic |
| Database | PostgreSQL (Neon) / SQLite | Postgres for persistence in production; SQLite for zero-setup local dev |
| ORM | SQLAlchemy 2.0 | Dialect-agnostic — same models work against both databases unchanged |
| Auth | JWT (python-jose) + bcrypt (passlib) | Stateless tokens, industry-standard password hashing |
| Scheduling | APScheduler | In-process background jobs, no external infra needed for MVP scale |
| Matching | scikit-learn (TF-IDF) | Lightweight, dependency-free relevance scoring without needing a vector DB |
| LLM | Google Gemini (default) / Anthropic (fallback) | Free tier covers ATS feedback + conversational agent at MVP scale |
| Resend (HTTPS API) | SMTP ports are blocked on many free hosting tiers (Render included); HTTPS is not | |
| Messaging | Telegram Bot API | Zero-cost, zero-infra push notification channel |
| Frontend | Vanilla HTML/CSS/JS | Served directly by FastAPI — one deployable unit, no separate build pipeline |
placement_agent/
├── app/
│ ├── main.py FastAPI app — all HTTP endpoints
│ ├── agent.py Claude/Gemini tool-calling orchestrator
│ ├── scheduler.py Background ingestion + notification loop
│ ├── db.py Models (Job, UserProfile, MatchResult) + auto-migration
│ │
│ ├── core/
│ │ ├── auth.py Password hashing, JWT issue/verify, reset tokens
│ │ ├── notifier.py Email (Resend) + Telegram delivery
│ │ ├── matching_notify.py Detects new high-score matches, triggers alerts
│ │ ├── matcher.py TF-IDF ranking + location/alias/fuzzy filtering
│ │ ├── resume_parser.py PDF/DOCX → text → skills extraction
│ │ ├── ats_scorer.py Keyword + LLM-based ATS scoring
│ │ ├── ingest.py Multi-source fetch, dedup, persist
│ │ └── llm.py Provider-agnostic LLM layer (Gemini/Anthropic)
│ │
│ ├── sources/
│ │ ├── greenhouse.py Public Greenhouse board API
│ │ ├── lever.py Public Lever board API
│ │ └── adzuna.py Adzuna aggregator API
│ │
│ ├── data/
│ │ ├── companies.json Fallback Greenhouse/Lever board list
│ │ └── city_aliases.json Location alias map for matching
│ │
│ └── static/
│ └── index.html Login/dashboard SPA (served at `/`)
│
├── requirements.txt
├── runtime.txt Pinned Python version for deployment
└── .env.example
- Python 3.11
- A free Neon Postgres database (or use SQLite locally with zero setup)
git clone <your-repo-url>
cd placement_agent
pip install -r requirements.txt
cp .env.example .env
# fill in .env — see Environment variables below
uvicorn app.main:app --reload --port 8000Open http://localhost:8000 for the dashboard, or drive the API directly (see API reference).
No API keys needed to see jobs. On startup (and then every hour) the scanner pulls live postings from the public Greenhouse, Lever and Ashby board APIs, so the pool fills itself. Adzuna is optional and needs its own key. There is deliberately no "load sample data" button or endpoint.
Waypost is a single deployable service — the dashboard is served by the same FastAPI process that runs the API, so there's nothing to build or host separately.
| Setting | Value |
|---|---|
| Build command | pip install -r requirements.txt |
| Start command | uvicorn app.main:app --host 0.0.0.0 --port $PORT |
| Runtime | Pinned via runtime.txt (Python 3.11.9) |
Required
| Variable | Description |
|---|---|
JWT_SECRET_KEY |
Signs login tokens. Generate with python -c "import secrets; print(secrets.token_hex(32))" |
DATABASE_URL |
sqlite:///./placements.db locally, or a Postgres connection string in production |
AI features
| Variable | Description |
|---|---|
GEMINI_API_KEY |
Free tier, no card required — aistudio.google.com/apikey |
GEMINI_MODEL |
Defaults to gemini-2.5-flash |
ANTHROPIC_API_KEY |
Optional fallback if Gemini isn't configured |
Notifications
| Variable | Description |
|---|---|
RESEND_API_KEY |
Free tier email delivery via HTTPS — resend.com |
RESEND_FROM |
Defaults to onboarding@resend.dev (sends to your own verified address only, until a custom domain is verified) |
TELEGRAM_BOT_TOKEN |
Created via @BotFather |
FRONTEND_BASE_URL |
Your deployed URL, used to build password-reset links |
Job sources (optional — fallback list used if blank)
| Variable | Description |
|---|---|
GREENHOUSE_BOARDS |
Comma-separated company board tokens |
LEVER_BOARDS |
Comma-separated company board tokens |
ADZUNA_APP_ID / ADZUNA_APP_KEY |
Free signup at developer.adzuna.com |
Full reference with inline comments lives in .env.example.
All endpoints except /auth/register, /auth/login, /auth/forgot-password,
/auth/reset-password, and /health require Authorization: Bearer <token>.
Authentication
# Register
curl -X POST /auth/register \
-F "name=Jane Doe" -F "email=jane@example.com" -F "password=supersecret123" \
-F "job_titles=Software Engineer,Data Analyst" -F "locations=Bangalore,Remote"
# Log in
curl -X POST /auth/login -F "email=jane@example.com" -F "password=supersecret123"
# Forgot password
curl -X POST /auth/forgot-password -F "email=jane@example.com"
# Reset password
curl -X POST /auth/reset-password -F "token=<from email link>" -F "new_password=newpass123"Resume & matching
# Upload resume (PDF or DOCX)
curl -X POST /resume/upload -H "Authorization: Bearer $TOKEN" -F "file=@resume.pdf"
# Search jobs (ranked against your uploaded resume automatically)
curl -X POST /jobs/search -H "Authorization: Bearer $TOKEN" \
-F "job_titles=Software Engineer" -F "locations=Bangalore,Remote"
# ATS score against a specific job description
curl -X POST /resume/ats-score -H "Authorization: Bearer $TOKEN" \
-F "job_description=We need a Python developer with Django and AWS..."Agent & notifications
# Natural-language agent
curl -X POST /agent/chat -H "Authorization: Bearer $TOKEN" \
-F "message=Find me remote data analyst internships in India"
# Manually trigger a fetch cycle
curl -X POST /jobs/ingest -H "Authorization: Bearer $TOKEN" \
-F "search_query=data analyst" -F "search_location=Bangalore"
# Link Telegram
curl -X POST /notifications/telegram/link -H "Authorization: Bearer $TOKEN" \
-F "telegram_chat_id=123456789"- Passwords — hashed with bcrypt, never stored or logged in plaintext
- Sessions — stateless JWTs, expire after
JWT_EXPIRE_MINUTES(default 24h) - Password reset — a random token is generated per request; only its SHA-256 hash and a 30-minute expiry are ever persisted, mirroring how the password itself is handled. Tokens are single-use and consumed on successful reset.
- User enumeration resistance —
/auth/forgot-passwordreturns an identical response whether or not the email is registered - Schema drift protection —
db.pyruns a lightweight auto-migration on startup that diffs live database columns against the model and adds anything missing, preventing "column does not exist" errors after a model change ships without a corresponding manual migration - Connection resilience — the database engine uses
pool_pre_pingand a 3-minutepool_recycle, since managed Postgres providers (Neon included) silently close idle connections that a naive pool would otherwise try to reuse
Why Greenhouse, Lever, and Adzuna, and not LinkedIn/Naukri?
Scraping either violates their Terms of Service and reliably gets IPs
banned. Greenhouse and Lever expose genuinely public, unauthenticated
board APIs by design — many of the startups and scale-ups that post
"off-campus" opportunities students otherwise miss are on these
platforms. Adzuna adds broad aggregator coverage with a real,
compliant free-tier API. New sources should follow the same
fetch_jobs() -> list[dict] contract in app/sources/.
Why email over Resend's HTTPS API instead of SMTP? Several hosting providers — Render's free tier among them — block outbound traffic on SMTP ports (25/465/587) entirely to prevent spam abuse. That's a platform-level restriction no amount of correct SMTP configuration can work around. Resend's API is a normal HTTPS POST, which is unaffected.
Why TF-IDF instead of embeddings? TF-IDF has zero external dependencies, zero cost, and no added infrastructure — a deliberate choice to validate the product with real usage before introducing a vector database and embeddings API. See Roadmap.
This is an MVP. Read this before showing it to real users.
- ATS scoring is an estimate, not a simulation of any specific
platform (Workday, Taleo, iCIMS all use proprietary parsing). The
API response includes a
disclaimerfield for this reason. - Matching uses TF-IDF, not semantic embeddings. Good enough to validate the product; a vector-based approach would meaningfully improve match quality at scale.
- No LinkedIn/Naukri coverage, by design — see Design decisions.
- Single-process scheduler. Fine for MVP/small-user-base load; move to Celery beat + workers (or a managed cron) before scaling past one instance.
- Telegram linking is manual (the user pastes their own chat ID).
A bot
/starthandler could auto-capture this instead.
- Replace TF-IDF with embeddings + a vector store once usage data justifies it
- Add RemoteOK / WeWorkRemotely as additional free sources
- Auto-capture Telegram chat ID via a bot
/starthandler - Move scheduling to a distributed worker for multi-instance deployments
MIT — use freely, attribution appreciated.