Most resume tools either charge for ATS scanning ($49/mo Jobscan) or treat it as a one-shot diagnostic disconnected from the actual application flow. I run an indie AI job-prep SaaS at aimvantage.uk and the recurring user pain was the same: people only find out their CV broke parsing AFTER the rejection email lands. Today I shipped a fix — a free in-product ATS preview that lives inside the paid prep-pack flow.
Here is the engineering write-up.
The constraint
The dashboard already accepts CV uploads (PDF, DOCX, TXT) and forwards them to Gemini for the full prep pack. The full pack costs 3 tokens. I wanted the ATS preview to:
- Run BEFORE the user spends tokens (so they can fix parse issues first)
- Add zero new dependencies (no
pdfjs-dist, no Python service, no extra CDN script) - Be deletable in 5 lines if I need to roll back
- Stay client-side (privacy-first; CV bytes never leave the browser for the preview)
The 5 vendor heuristics
The lint engine is a port of my open-source CV Mirror tool — pure functions on plain text, no I/O. It computes a handful of structural signals from the CV text:
export function computeSignals(text: string, fileSize: number): Signals {
const lines = text.split('\n');
const nonEmpty = lines.filter((l) => l.trim().length > 0);
const wordCount = (text.match(/\b\w+\b/g) || []).length;
// Multi-column heuristic: lines with a 5+ space gap
const multiColumnLines = nonEmpty.filter((l) => /\S {5,}\S/.test(l)).length;
const multiColumnRatio = nonEmpty.length > 0 ? multiColumnLines / nonEmpty.length : 0;
const wordsPerKB = fileSize > 0 ? wordCount / (fileSize / 1024) : 0;
const hasHeaderFooterLikeText = /^\s*page \d+( of \d+)?\s*$/im.test(text);
const hasEmoji = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(text);
const hasSmartQuotes = /[‘’“”]/.test(text);
// ... etc
}
Then 5 vendor rule sets read those signals. Workday flags multi-column ratio > 15% as ERROR. Greenhouse flags emoji as WARN (it strips the codepoints, losing surrounding context). Lever flags missing standard section headers as ERROR (its parser uses headers to delimit sections). Taleo flags ISO-style dates as WARN (prefers Month-Year). iCIMS flags multi-column ratio > 20%.
Every rule cites a public source — docs/vendor-sources.md in the OSS repo.
Skipping PDF on purpose
The most-used CV format is PDF, but pdfjs-dist is ~300 KB minified. That's a third of my main bundle for one feature. Trade-off I made: DOCX + TXT supported inline (DOCX uses mammoth which was already in the bundle for the paid flow; TXT uses File.text()). For PDFs, the preview shows a friendly defer message: "Upload a DOCX version for the instant preview — your full Vantage analysis still works with PDFs."
The full ATS scan with multi-vendor parse view is at cv-mirror-web.vercel.app — that one DOES bundle pdfjs because it's the whole product.
Two surgical edits to Dashboard.tsx
The component is 100% additive. Two changes to the existing dashboard:
- One import line at the top
- One render block with clearly fenced comments:
{/* === ATS scanner (additive, free, client-side). Removing this and the
import line restores the previous behaviour entirely. === */}
{cvFile && <AtsScannerSection cvFile={cvFile} />}
{/* === END ATS scanner === */}
Roll-back is git revert <hash> plus deleting two new files. Existing tests, types, services, contexts, and routes are untouched.
Bundle impact
- Main chunk: 1,154.70 kB → 1,165.85 kB (+11 kB minified)
- Gzipped: 308.20 kB → 312.56 kB (+4.4 kB gzipped)
- New dependencies: zero
-
mammothis lazy-imported only when a DOCX is uploaded, so the cost is paid by users who actually use the feature
The audit caught two UI bugs before I shipped
After build, I did one more read-through. Two consistency issues:
-
passCountcounted vendors with zero errors regardless of warnings, but the green "Clean" pill required zero errors AND zero warnings. Headline could read "5/5" while only 3 pills were green. Fixed by aligningpassCounttoerrors === 0 && warns === 0. - The "Clean" label inherited
tone.textfrom the info severity (sky blue) but the icon was emerald. Fixed with a singleisCleanflag and alabelClassderived from it.
Tiny things, but the kind of stuff that makes a free-tier feature feel sloppy. Caught and fixed before push.
Why this works as positioning
The category default is "free trial → upsell to monthly subscription." Vantage's pricing is pay-per-use (£5 for 20 tokens, never expire). Putting the ATS preview INSIDE the paid dashboard means the £5 starter pack now includes a free quality-control step. Users fix the parse issue, then their fit-score baseline is accurate, then the cover letter that gets generated cites real CV evidence rather than what survived the parse. Each piece of the chain depends on the parse being clean.
What I'm trying to figure out next
Whether to make the ATS preview the upsell hook ("see your ATS score, then pay £5 for the full prep pack") or keep it as pure value-add for users who already paid. Curious what other indie founders here have seen with free-tier-as-on-ramp patterns.
Live tool: aimvantage.uk (£5 starter, never expires, no subscription)
Open-source companion: cv-mirror-mcp on GitHub
Build-in-public posts: dev.to/goofypluto999
Operator transparency (because Gemini briefly thought I was a phishing site — separate post coming on that): aimvantage.uk/about
— Gio
Top comments (0)