Skip to content

piqim/cse-auto-apply

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Handshake Internship Auto-Applicator (UCSD)

Look, I'll be honest. I had 10+ browser tabs open, a growing list of Handshake postings I kept telling myself I'd apply to "later," and a creeping suspicion that manually submitting the same resume and cover letter over and over again was not a great use of my time.

So I built this instead.

This bot is built specifically for UC San Diego students on ucsd.joinhandshake.com. It scrapes Handshake for internship postings across your keyword list — paginating through multiple pages per keyword — deduplicates everything, and applies. Groq (LLaMA 3.3 70B) only kicks in when an application actually needs it: to write a cover letter or answer a free-text question. Every outcome gets logged to a CSV so it never double-applies across runs.

Is this lazy? Yes. Is it also the most efficient thing I've built this semester? Also yes.


What it actually does

  1. Scrapes Handshake for internship postings across your keyword list, paginating through up to MAX_PAGES pages per keyword
  2. Deduplicates results — the same posting can appear across multiple keywords and pages, it only gets applied to once
  3. Detects external postings immediately — jobs that redirect off Handshake are flagged right after page load, before any long waits
  4. Applies to each new job automatically — clicks the Apply button, handles the modal, submits
  5. Generates cover letters on the fly using Groq when an application asks for one — fresh, tailored to the specific job — and waits for Handshake to fully convert the file before submitting
  6. Auto-fills free-text questions ("Why are you interested?", "Describe your experience") using your CANDIDATE_PROFILE + the job description
  7. Pauses for manual fields — if an application requires a transcript, portfolio URL, or other input the bot can't fill, it lists exactly what's needed and waits for you to fill it before submitting
  8. Logs every outcome — applied, already applied, external link, no apply button, error — to applied_jobs.csv

Your resume is already attached from your Handshake profile — no upload needed. For external applications (the ones that redirect off Handshake), the bot flags them in the CSV for manual follow-up.


Setup

1. Install dependencies

pip install -r requirements.txt
playwright install chromium

2. Get a free Groq API key

Head to console.groq.com, sign up, and grab a key. Free tier, no credit card. Add it to your .env file (copy .env.example to get started):

GROQ_API_KEY=gsk_your_key_here

Or export it before running:

export GROQ_API_KEY="gsk_your_key_here"

3. Update the candidate profile

Near the top of bot.py there's a CANDIDATE_PROFILE block — a plain-text summary of who you are, your skills, experience, and projects. Groq reads this when writing cover letters and question answers. The more honest and detailed it is, the better the output sounds. Replace it with your own if you're not me.

4. Make sure your Handshake profile has a default resume set

The bot doesn't upload a resume file — it relies on whatever's already set as your default document in your Handshake profile. Go to your profile → Documents and confirm one is set before running.

5. Run it

python bot.py

A Chrome window opens pointing to ucsd.joinhandshake.com. Log in with your UCSD SSO and complete DUO authentication. The bot watches for you automatically — no need to press ENTER. Once it detects you're on the dashboard, it takes over.


Configuration

All the knobs are at the top of bot.py:

Variable Default What it does
GROQ_API_KEY env var Your Groq key — set via .env or export
KEYWORDS (see file) What to search for on Handshake
MAX_PAGES 5 Pages to scrape per keyword (25 results each)
MAX_APPLICATIONS 25 Safety cap — stops after this many submissions per run
DELAY_BETWEEN_APPS 4 Seconds between each application
DRY_RUN False Set to True to scrape without submitting
TRACKER_FILE "applied_jobs.csv" Where the log lives

On pagination: with the defaults, the bot can scrape up to 12 keywords × 5 pages × 25 results = 1,500 postings per run before deduplication. In practice it'll be far fewer due to keyword overlap. Crank MAX_PAGES up if you want wider coverage, or lower it for faster runs.

First time running? Set DRY_RUN = True. It'll scrape everything and print what it finds, but won't submit anything. Good way to sanity-check your keywords before you start firing off applications.


The output CSV

Every job the bot encounters gets a row in applied_jobs.csv:

Column Description
job_id Handshake's internal job ID
title Job title
company Company name
status applied, submitted_unconfirmed, already_applied, external_link, no_apply_button, error:*
applied_at Timestamp

Re-run the bot anytime — it reads the CSV on startup and skips anything already logged.


Run summary

At the end of each run the bot prints a breakdown:

══════════════════════ RUN COMPLETE ══════════════════════
  ✓  Applied               : 25
  →  Skipped (already done): 5
  ↗  External (manual)     : 8
  ⊘  No apply button       : 2
  ✗  Errors                : 1
  ✦  Model used            : Groq / llama-3.3-70b-versatile
  📄 Full log              : applied_jobs.csv
══════════════════════════════════════════════════════════

External includes both postings that were detected as external before clicking (e.g. "Apply on employer's website") and ones where clicking Apply opened an external tab instead of a Handshake modal. No apply button means no recognisable apply button was found at all — usually a Handshake eligibility mismatch or an unusual posting layout, worth checking manually.


A note on Groq and token usage

Groq is only called when the application modal actually needs something written — a cover letter field or a free-text question. If a job has neither, no API call is made at all. This keeps usage well within the free tier across a normal run.

The CANDIDATE_PROFILE is what Groq draws from when writing. Generic input → generic output. The more specific and honest the profile, the more the answers sound like you and less like a ChatGPT hallucination of a computer science student.


Troubleshooting

Bot is stuck on the login check — The bot polls every 10 seconds waiting for a logged-in Handshake tab to appear. If it's been more than a minute, make sure you fully completed DUO and can see the Handshake dashboard in the browser window. It detects login automatically — no ENTER needed.

Bot finds 0 jobs — Handshake occasionally updates their frontend, which can break the card selector (data-hook^="job-result-card | "). Open devtools on the job search page, inspect a card, and check if the data-hook format has changed. Let me know and I'll update the selector.

"no_apply_button" in the CSV — Either the job is external but used unusual button text that slipped past the detector, or your eligibility for the posting was flagged (e.g. graduation year mismatch) and Handshake rendered a different UI. Check the job manually.

"submitted_unconfirmed" in the CSV — The bot submitted but couldn't find a success indicator. Check your Handshake Applications tab to verify. This usually means it went through — Handshake's confirmation UI is inconsistent across postings.

Cover letter field not detected — The bot looks for a fieldset with "cover letter" in its legend text. If a job uses different label text, it'll be skipped. Not a dealbreaker — the application still submits, just without the cover letter attached.

Cover letter conversion warning in the terminal — Handshake converts uploaded files server-side after upload. The bot waits up to 30 seconds for conversion to finish before submitting. If you see a timeout warning, it submitted anyway — check your Applications tab to confirm the cover letter came through.

Bot paused asking for manual input — Some applications require a transcript, GitHub URL, or portfolio upload that the bot can't fill automatically. It'll list exactly which fields need attention and wait. Fill them in the browser, then press ENTER in the terminal to let the bot click Submit.


Limitations worth knowing

This is built and tested on ucsd.joinhandshake.com. If you're at a different university, update HANDSHAKE_BASE_URL in bot.py to your school's subdomain (e.g. ucla.joinhandshake.com) — that's the only change needed.

This isn't magic. Handshake's DOM changes, some postings have non-standard flows, and the bot will occasionally log an error on a job that it just couldn't navigate cleanly. Spot-check applied_jobs.csv after a run to see what went through and what needs a manual follow-up.

It removes the tedious parts. The judgment still has to be yours.


Built by Piqim — because manually applying to 30 internships is not a personality trait I'm willing to develop.

About

This bot scrapes Handshake for internship postings that match your keywords, scores each one against your profile using Groq API LLM model, auto-fills free-text application questions with tailored responses, attaches your resume, and submits.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages