Inspiration I built the first version of this for myself. I was applying to jobs, and the part that wore me down wasn't rejection. It was not knowing whether anything I sent had gone anywhere real. You spend twenty minutes on a Workday form for a req that closed in March. You write a cover letter and quietly suspect nobody opens it. You see "sponsorship available" on a posting with no idea whether that's true or just a checkbox someone ticked in 2023. Then I looked at the tools that promise to fix this, and most of them made it worse. They blast out hundreds of applications with invented experience, spoofed fingerprints, solved captchas. Which is, when you think about it, the same problem from the other side — now the recruiter can't tell what's real either. So the question I actually cared about wasn't "can AI apply to jobs for me." It was: can an agent walk through this funnel in a way both sides can check? No made-up facts. No sneaking past security. No "Applied" badge that means nothing. That's Matatah. It's live at matatah.com. What it does Three things: finds jobs that fit, writes a résumé and cover letter it can defend, and applies for real — pausing to hand me the wheel whenever a human is genuinely needed. Finding: the pool has to delete itself There are 600,000+ live postings in the pool, pulled from 90,000+ verified boards across 30+ ATS families — Greenhouse, Lever, Ashby, Workday, iCIMS, SmartRecruiters, plus plain company career pages. Descriptions get embedded, so matching works on meaning instead of keyword bingo. But honestly, the scraping isn't the interesting part. Anyone can scrape. The interesting part is throwing things away: Every posting gets link-checked, and a 404 is gone. Anything no scrape has re-seen in 22 days gets deleted, because with scraping running continuously, a row that stale means the ATS stopped returning it — the req is closed. That sweep has a guard on it: if the scraper itself has been down for three days, the sweep skips entirely and says so, because otherwise the whole pool ages past the window together and I'd delete every live job at once. I'd rather the feature refuse to run than run wrong. Companies reposting the same req over and over, or sitting on dozens of identical openings, get flagged as likely ghosts. Every listing carries a badge — verified active, stale, dead link — so you see what the system actually thinks instead of a number I laundered. Sponsorship comes from DOL disclosure data, re-synced as the pool grows; when a new quarter publishes and I haven't ingested it yet, the system says the data is aging rather than quietly serving you last year's answer. And anything you've applied to or dismissed never comes back, including reposts under a new title on a different board. Writing: the model isn't allowed to make things up Your facts live in a vault, and each one has an ID. The pipeline figures out the role family, reads the job description, ranks your real evidence against that posting, and writes. The model can reorder things and tighten the language. It cannot add a fact. Employers, titles and dates get rendered straight from the vault — the model never types those. Every bullet cites the ID it came from. Then eight checks run: ATS-safe layout, truth against your record, is the thesis specific, is the best proof first, does the voice match the level, is it actually about this company, is it different from your other letters, is it clean. It's a product, not an average: $$ \mathrm{ship} \iff \prod_{i=1}^{8} c_i = 1, \qquad c_i \in {0,1} $$ One zero and nothing ships. That's deliberate — a weighted score would let a packet squeak through on polish while failing truth, and I'd never see it. The cover letter has its own gate on top: one thesis line, one insight about the company that has to build on something actually written in the posting, exactly two proofs from your real record, voice matched to the level, and a similarity check against your recent letters so two companies never get the same letter with the name swapped. Here's the part people find strange: if a letter fails, nothing ships. No template fallback. It gets saved so you can read it and see what failed, but it never gets attached. That was the hardest call in the whole product and I still think it's right. On my golden set — three personas across three job descriptions — the rubric sits at 10/10 median with factuality at 100%. Those numbers only mean something because failures are allowed to be failures. Applying: it either worked, or you get a reason One engine on the server owns the queue, the state machine, and the evidence rules. The Chrome extension, the desktop app, the cloud session, and the phone are all just workers on top of it. None of them can mark a job Applied on their own — the engine has to see confirmation evidence first. "Fields filled" is not an outcome. It either submitted and I can show you the confirmation, or something stopped it and I can tell you what. And when it hits a captcha, a login, a verification code, or a question only you can answer, it stops and hands you the wheel in your own browser. You clear it, the same application picks up where it left off. No captcha solving. No stealth browsers. No fingerprint spoofing. No residential proxies. It never types your password or your one-time code. The rule I kept coming back to: don't move the user's identity to a bot — move the bot's work to the user's browser. How I built it Next.js 16 on Vercel, Postgres on Neon with pgvector, Drizzle. Claude does the scoring, tailoring and agent passes, with prompt caching on the expensive paths and a cheaper model for extraction. Chrome MV3 extension, an Electron desktop app driving Playwright over CDP, cloud Playwright sessions on Render, Expo for mobile, Stripe, Gmail API, and a stack of Vercel crons for scanning, pruning, embedding, H-1B sync and self-healing. The one architectural idea everything hangs off: all the thinking happens before the browser opens. Fields get resolved at match time — from your profile, from your answer bank, or one batched model call — which handles about 95% of them. Then you get a single confirm screen with whatever's left. Then the browser opens, fills, reads back what it typed, submits, and waits for the confirmation. Under 30 seconds, and it never opens with an unanswered question. All four clients share one form-filler, so when I fix something about Workday's multi-page flow, it's fixed everywhere at once. Challenges I ran into The first version put the thinking inside the browser. LLM calls and stop-and-ask, all running in a metered cloud session. Every session got reaped at five minutes while the job rows showed thirteen and fourteen minutes of runtime. Worse, questions blocked serially at fifteen minutes each, so an n-question form needed $$ T ;\approx; 15n \text{ minutes} ;\gg; T_{\text{cap}} = 5 \text{ minutes} $$ Question two couldn't even be asked until question one had expired. A three-question form was mathematically incapable of finishing. And nothing noticed the browser had died, so the agent kept going blind for nine more minutes and then reported an error that had nothing to do with what actually happened. No faster model fixes that. The thinking had to come out of the session entirely. I was wrong about the ATS APIs and had to publish the correction. My first spec said about half of all matches could be submitted with a plain HTTP call. I spent an afternoon actually probing the endpoints and it fell apart — every ATS submission endpoint needs the employer's key, and none of them are open to someone applying. What is public is Greenhouse's question schema, which covers 21% of matches and lets me pre-resolve every answer with no browser at all. But submission goes through a browser everywhere, full stop. I rewrote the spec with a correction note at the top rather than quietly fixing the numbers. "I consent to receiving text messages" used to halt an entire application. Marketing opt-ins were being classified as attestations, so the agent would stop and wait for a human on a question nobody needs a human for. The prune never actually ran. For months. Both the closed-req delete and the retention sweep sat behind an early return when zero jobs came back, and behind an embedding pass that blew through its 800-second cap. So dead postings piled up while the code that was supposed to remove them sat there doing nothing. That's the one that still bothers me — the trust feature existed, it was in the repo, and in production it was decorative. It has its own hourly cron now, completely independent of how the scraper is doing. Refusing to fall back to a template costs a lot. It's easy to pass a factuality check by writing a letter that says nothing. Getting one that's specific and verifiable, and then accepting that some of them just don't ship, took far more iterations than "generate a cover letter" ever would have. What I'm proud of That "Applied" means something here. The engine has to see proof. That fabrication is blocked by the architecture, not discouraged by a prompt. The model can't reach a fact that isn't in your vault, and every line traces back to an ID. That the pool deletes itself, out loud, with badges you can see. That I shipped something competitive while turning down every shortcut this category treats as normal — no captcha solving, no stealth browser, no proxies, no spoofing — and it's still fast enough that people use it. And one engine behind four clients, including a phone that does real work instead of showing you a status bar. What I learned Trust is a maintenance problem, not a feature. Every trust claim I shipped decays on its own. If a badge refreshes every Δ and the truth underneath changes at rate λ, the chance a candidate is reading something already false is $$ P(\text{stale}) ;=; 1 - e^{-\lambda \Delta} $$ which goes to 1 as Δ goes to infinity — and Δ = infinity is exactly what my broken prune cron was. A confident badge sitting on stale data is worse than no badge at all, because at least the absence of a badge doesn't lie to anyone. The safe design turned out to be the better design. I didn't choose the human hand-off purely on principle. Moving the work into the user's own browser deleted an entire category of detection problems I'd have been fighting forever. Turns out the honest architecture and the durable one were the same architecture, which was a nice thing to find out. Probe before you spec. My ATS analysis was wrong in a way that would have built the product around an endpoint that doesn't exist for me. One afternoon of live testing saved a quarter of work. Refusing is a feature. The moment the system tells you a letter didn't pass and why is the moment you start trusting the ones that do. What's next Showing employers what their own postings look like from the candidate side — how many times reposted, how long open, whether the apply flow still works. Most companies have no idea. Click any résumé bullet and jump to the fact it came from, so a recruiter can audit a packet the same way I can. Turning ghost-job flags into a real calibrated score, validated against outcomes, with the methodology published rather than hidden. Sponsorship data beyond H-1B, under the same rule: cite the source or say it's stale. And campuses, in the US and India — career services, placement cells, student clubs. Ghost jobs and fabricated packets hurt first-time applicants worst, and they're the ones with the least ability to tell.

Built With

Share this project:

Updates

Submission history