I Built a Local AI That Fills Job Applications for Me (And It Doesn't Snitch)
After rage-applying to 27 jobs in one night, I built a browser extension that uses a local LLM to auto-fill applications with zero API costs, zero data leaks, and a Notion dashboard to track it all.
I Built a Local AI That Fills Job Applications for Me (And It Doesn't Snitch)
Picture this: it's 2 AM. You've been copy-pasting your name, email, phone, and LinkedIn URL into text fields for the past three hours. Your hand cramps. Your soul is slowly leaving your body. Every single job portal has a slightly different form. Some want "Phone Number", some want "Mobile", and one idiot wants "Primary Contact Telecommunication Identifier."
I snapped.
So I did what any reasonable developer would do: I spent 2 weeks procrastinating and 2 days building a browser extension to automate the entire thing. But I didn't want to send my resume data to OpenAI or some random cloud API because, y'know, it's my data. Enter LoopBack AI: a fully local, privacy-respecting job autofiller that runs a GGUF model on your own machine and syncs everything to Notion.
Act I: The Form Hell That Broke Me
If you've applied to jobs recently, you know the dance:
- Click "Easy Apply"
- Fill in Name, Email, Phone (again)
- Upload resume (again)
- Answer "Do you have a Bachelor's degree?" (again)
- Get ghosted
The worst part? Every single platform like LinkedIn, Greenhouse, Lever, Workday, and Ashby, they all ask the same damn questions in slightly different ways. Some use <input>, some use <div contenteditable>, some use React's synthetic events. It's a mess.
I wanted something that could detect any form field on any portal, fill it intelligently, let me review before injecting (because I don't want to accidentally apply as "Barack Obama"), keep everything local, and track applications so I can actually follow up.
Three components, one local network, zero cloud dependencies.
Act II: The Architecture (Three Local Services That Mind Their Own Business)
[ Job Portal Webpage ]
│
(DOM Scraping)
▼
[ Chrome Extension ] ◄─── (Human Review via Popup)
│
├──────────────────────────────┐
(Port 8080 JSON POST) (Port 1234 JSON POST)
▼ ▼
[ llama-server ] [ Notion Bridge ]
│ │
(Answers Complex Fields) (Formats & Pushes)
│
▼
[ Notion Database ]
Three independent components, all talking over localhost. No cloud, no API keys, no "we'll store your data to improve our models."
The Chrome Extension
Does three things. First, it walks every <input>, <select>, and ARIA role on the page and figures out what the field wants. Second, for basic fields (Name, Email, Phone, LinkedIn), it uses regex patterns, so no need to wake up the LLM for your email address. Third, for the weird stuff ("Describe your experience with cross-functional team collaboration"), it dispatches to the local model.
function detectFieldType(input) {
const label = getLabelFor(input).toLowerCase();
if (/email/i.test(label)) return "email";
if (/phone|mobile|contact/i.test(label)) return "phone";
if (/name|full.name/i.test(label)) return "name";
if (/linkedin/i.test(label)) return "linkedin";
return "complex";
}
The Local LLM
You run llama-server on your machine with any GGUF model. The extension POSTs the field label plus your resume context, and the model returns a prediction. That's it.
llama-server -m "path/to/model.gguf" --port 8080 -ngl 999
Why local? Because I don't trust companies with my resume. That thing has my phone number, my email, my home address, my entire work history, and that one embarrassing internship I'd rather forget. Sending it to an API feels wrong.
Also, it's free. Once you have a model downloaded, inference costs are zero. No per-token billing. No hitting a rate limit mid-application.
The extension also has a Local Knowledge Bank. If you correct a prediction, it remembers and uses that correction next time. So it actually gets smarter the more you use it.
The Notion Bridge
Applying is only half the battle. The other half is actually following up. I built a lightweight Express server that sits on port 1234 and formats application data into Notion's API.
app.post("/sync", async (req, res) => {
const { company, position, platform, jobLink, content } = req.body;
const payload = {
parent: { database_id: process.env.NOTION_DATABASE_ID },
properties: {
Company: { title: [{ text: { content: company } }] },
Position: { rich_text: [{ text: { content: position } }] },
Platform: { multi_select: [{ name: platform }] },
"Application Date": { date: { start: new Date().toISOString().split("T")[0] } },
"Job Link": { url: jobLink }
},
children: buildPageContent(content)
};
await notion.pages.create(payload);
res.json({ ok: true });
});
The bridge has this Dynamic Schema Adapter that queries your Notion database schema and auto-maps columns regardless of what you named them. Call it "Company" or "Company Name" or "Employer", it figures it out.
Act III: The Human-in-the-Loop Review (Because AI Is Confidently Wrong)
Here's the thing about AI: it's confidently wrong sometimes. I didn't want the extension to just blindly fill everything and hit submit while I'm grabbing coffee.
So there's a staging drawer that pops up. Before anything gets written to the DOM, you see all predictions laid out:
┌──────────────────────────────────────────┐
│ 🔍 Review Predictions │
├──────────────────────────────────────────┤
│ Name: ✓ Yumn Gauhar │
│ Email: ✓ yumn@example.com │
│ Phone: ✓ +1-555-1234 │
│ LinkedIn: ✓ /in/yumngauhar │
│ "Cover Letter": [NEED_INPUT] 🔴 │
│ "Experience": ✓ 5 years (from resume) │
├──────────────────────────────────────────┤
│ [ Confirm & Fill Live Form ] │
│ [ Sync Application to Notion ] │
└──────────────────────────────────────────┘
Green is good. Yellow is fast-track. Red is [NEED_INPUT], meaning the AI couldn't figure it out and needs you. You fix what's wrong, click confirm, and then the values get injected into the live DOM. I've had AI auto-fill experiences where it put "John Doe" in the phone number field. Not great.
Act IV: The React/Vue Nightmare (Or: How I Learned to Love Event Spoofing)
If you've ever tried to programmatically set an <input> value in a React app, you know the pain. React uses _valueTracker, an internal property that detects native setter calls and ignores them. Vue does similar things.
The fix? You have to dispatch actual native events after setting the value:
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
if (valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
Took me way too long to figure this out. But once it worked, I felt like a wizard. The extension now spoofs events properly for React 16+, Vue 2/3, and Angular. They all fall for it.
Act V: The Lessons
Form autofill is deceptively hard. Every job portal is a unique snowflake. Some use <form> elements, some use divs with role="form", some dynamically render inputs after you click a button. You need MutationObservers, debounced re-scans, and a lot of patience.
Local LLMs are good enough for structured tasks. You don't need GPT-4 to figure out that "Phone Number" means "put the phone number here." A 3B parameter GGUF model running on CPU handles this perfectly. ~2 second latency on a modern laptop, totally acceptable.
Event handling in SPAs is cursed. The number of hacks I had to layer on to get React to recognize my input changes was absurd. But it works now, and that's what counts.
Notion's API is surprisingly nice. Once you get past the authentication dance, their database query endpoint returns your schema, which made the dynamic adapter straightforward to build. Respect where it's due.
The Results
Since I started using LoopBack AI, I've applied to way too many jobs without losing my sanity. The extension handles about 80% of fields automatically. The remaining 20% are usually cover letters or custom questions that I fill in through the staging drawer.
The Notion dashboard is genuinely useful. I can see at a glance which applications are pending, which got a response, and which went into the void (most of them).
The entire thing is open source under MIT. You'll need a Chrome-based browser, Node.js v18+, a GGUF model (I recommend Llama-3.2-3B-Instruct-Q4_K_M), and optionally a Notion database. Clone it, spin up llama-server, start the bridge, load the extension, and you're off. The README has detailed steps.
Go check it out on GitHub. May the forms be ever in your favor.
Now go check how many times you've typed your phone number this week. I'll wait.