A personal job-search system: tailor your resume to any job description, discover remote startup internships automatically, autofill application forms, and keep every application in one pipeline.
Built for one user (you). One password, one database, no multi-tenancy.
| Feature | How it works | |
|---|---|---|
| 1 | Resume tailoring | Upload your master resume once. Paste a JD, get a version rewritten for that specific role — with a before/after match score, the keywords it surfaced, the gaps you genuinely have, and a diff of every change. |
| 2 | Assisted apply | A local Playwright agent opens the posting in your own browser, fills every field it recognises, attaches the tailored resume, and hands it to you to submit. |
| 3 | Application tracking | Kanban pipeline across ten stages, timeline per application, automatic follow-up reminders, interview-rate stats. Agent submissions log themselves. |
| 4 | Job discovery | Nightly pull from RemoteOK, Remotive, Arbeitnow, HN "Who is Hiring" and 29 startup ATS boards, then scored against your resume by an LLM against saved criteria. |
| 5 | CSV outreach | Write the mail once as a template, upload a CSV of recruiters (Company, Recruiter, Email), and get one mail per row with your resume PDF attached. Flip the tailor toggle on and the model adds two or three lines about each company. Read and edit every draft, then send from your own address — throttled and capped. A live preview box shows the exact subject, body and attachment before you import anything. |
LinkedIn is fill-only and always will be. LinkedIn's User Agreement §8.2 prohibits automated interaction, and their detection is good. A permanent account restriction in your final year costs far more than the seconds saved. So the agent opens the posting in your logged-in browser, fills what it can, and stops — you read it and click Submit.
Greenhouse, Lever and Ashby can be fully automated if you want. These are the
employer's own application forms, with stable field names and no bot detection. Set
AUTO_SUBMIT_ATS=1 in agent/.env and queue a task in AUTO mode. It ships off by
default, because reading a form before submitting it is usually the right call.
There is no keyless "apply via API" for applicants on any major ATS — their public APIs are read-only for job data. Anyone claiming otherwise is driving a browser too.
Nothing sends without you reading it. Drafting and sending are separate steps. A contact
has to be APPROVED — which means you opened the draft and cleared it — before the send route
will touch it. There is no "import and blast" path, and that is deliberate.
Mail goes out over SMTP from your own address, not an ESP, so replies land in your inbox and the recipient sees a person. For Gmail that needs an App Password; your normal password will not authenticate.
Sends are spaced OUTREACH_SEND_GAP_MS apart (6s default) and hard-capped at
OUTREACH_DAILY_CAP a day (40 default) across every campaign. Gmail's own ceiling is far
higher, but volume is what gets a personal address flagged, and a burned personal address in
your final year is expensive. Keep it low and keep the list short.
The mail is yours, not the model's. A campaign carries a subject and body template with
{{placeholders}} — {{firstName}}, {{company}}, {{role}}, {{ask}}, {{links}} — filled
from each CSV row. With the tailor toggle off that is the whole mail: pure substitution, no
model call, no cost, and every draft is ready the moment you import. With it on, the model
writes two or three sentences about that specific company into the {{tailored}} slot and
touches nothing else you wrote.
Your resume goes out as an attachment, byte for byte as you uploaded it on the Profile page. If a campaign says it attaches a resume and no file is stored, the send stops rather than quietly mailing a body that claims an attachment that is not there.
Those tailored lines are held to the same fabrication rules as the resumes: no invented skills or projects for you, and no invented facts about them either — no imagined funding rounds, no fake mutual connections, no "great speaking with you" on a first contact. If a CSV row carries nothing specific about the company, the lines talk about your own real work instead.
The prompt forbids inventing employers, dates, degrees, metrics, and technologies. Because a prompt is not a guarantee, every generated resume also passes a deterministic fabrication check: each number and each distinctive term is matched back to your master resume, and anything ungrounded is flagged in red before you download it. Invented metrics are graded high severity — those are what sink you in an interview.
If the JD wants Kubernetes and you have never touched it, Kubernetes goes in "genuine gaps", not on your resume.
Local development:
docker run -d --name jat-pg \
--restart unless-stopped \
-v jat-pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=jat -e POSTGRES_USER=jat -e POSTGRES_DB=jat \
-p 55433:5432 postgres:16-alpineProduction: create a free Postgres at neon.tech and use its pooled connection string.
cp .env.example .envFill in:
| Variable | What it is |
|---|---|
DATABASE_URL |
Postgres connection string |
APP_PASSWORD |
The single password gating the whole site. Make it long. |
SESSION_SECRET |
openssl rand -base64 32 |
AGENT_TOKEN |
openssl rand -hex 24 — the local agent uses this |
OPENAI_API_KEY |
From platform.openai.com |
OPENAI_MODEL |
Defaults to gpt-4.1 |
OPENAI_FAST_MODEL |
Defaults to gpt-4.1-mini, used for bulk job scoring |
Optional, and only needed for CSV outreach:
| Variable | What it is |
|---|---|
SMTP_HOST / SMTP_PORT |
smtp.gmail.com and 587 for Gmail |
SMTP_USER / SMTP_PASS |
Your address, and a Gmail App Password — not your login password |
SMTP_FROM |
Optional. Your Name <you@gmail.com> for a nicer From line. Defaults to SMTP_USER |
OUTREACH_DAILY_CAP |
Hard ceiling on sends per day, all campaigns. Default 40 |
OUTREACH_SEND_GAP_MS |
Delay between sends. Default 6000 |
npm install
npm run db:push # create the tables
npm run db:seed # default search criteria
npm run devOpen http://localhost:3000, sign in with APP_PASSWORD.
- Profile → upload your fullest resume (PDF/DOCX) and fill in the autofill fields. Put everything in the resume — tailoring can only select from what is there.
- Find jobs → Pull new jobs. Takes ~2 minutes, brings in several thousand postings.
- Find jobs → adjust the search criteria, then Score against my resume.
- Tailor on anything shortlisted, or Queue it for the apply agent.
Browser automation cannot run on Vercel — it drives a real Chrome window on your machine.
cd agent
cp .env.example .env # set APP_URL and AGENT_TOKEN
npm install # also downloads Chromium
npm run login # one-time: log into LinkedIn, session saved to agent/.auth
npm startIt polls the queue every 6 seconds and processes tasks as they arrive.
npm i -g vercel && vercelSet the same environment variables in the Vercel dashboard, plus CRON_SECRET for the
nightly ingest (vercel.json already schedules /api/cron/ingest for 03:00 UTC).
Then point the agent at production by setting APP_URL=https://your-app.vercel.app in
agent/.env. The agent keeps running locally; only the web app is deployed.
Only the OpenAI calls cost anything. Everything else is free tier.
| Operation | Model | Rough cost |
|---|---|---|
| Parse master resume | gpt-4.1 |
~$0.02, once |
| Tailor one resume | gpt-4.1 |
~$0.03 |
| Score a job posting | gpt-4.1-mini |
~$0.001 |
A heavy week — 50 tailored resumes, 500 jobs scored — lands around $2.
The keyword prefilter in src/lib/match.ts rejects obviously-irrelevant postings before
they ever reach the model, which is what keeps scoring cheap.
src/lib/sources/startups.ts holds the ATS board slugs. Take the last path segment of a
company's careers URL and verify it returns jobs before adding it:
curl -s "https://api.ashbyhq.com/posting-api/job-board/SLUG" | head -c 200
curl -s "https://boards-api.greenhouse.io/v1/boards/SLUG/jobs" | head -c 200
curl -s "https://api.lever.co/v0/postings/SLUG?mode=json" | head -c 200A wrong slug returns 404 or an empty list and silently wastes a request every night.
src/
app/ pages and API routes
lib/
tailor.ts the tailoring prompts and ground rules
integrity.ts deterministic fabrication check
match.ts LLM scoring of jobs that clear the saved searches
ingest.ts source orchestration
criteria.ts saved-search matching, shared by scoring and the jobs list
sources/ one adapter per job source
csv.ts RFC 4180 parser + HR-sheet header mapping
outreach-template.ts the mail template, placeholders and sample preview (no db, no model)
outreach.ts the tailored-lines prompt and ground rules
mailer.ts SMTP transport, daily cap, send throttle
agent/
src/index.mjs poll loop
src/fillers.mjs per-ATS form fillers
tests/
integrity.test.mjs
csv.test.mjs
template.test.mjs
criteria.test.mjs
npx tsx tests/integrity.test.mjs # fabrication-check tests
npx tsx tests/csv.test.mjs # CSV parsing and contact-import tests
npx tsx tests/template.test.mjs # outreach template filling, both toggle states
npx tsx tests/criteria.test.mjs # saved-search matching rules