How I Built One A developer built **One**, an AI platform that parses resumes to generate personalized coding assessments and career roadmaps. The platform uses OpenAI's GPT-4o and Files API with a two-step prompt that validates whether a document is a genuine developer resume before extracting only skills the candidate personally used. One also integrates OpenAI's Realtime API for voice mentoring via WebRTC, keeping the server out of the audio path to minimize latency. AI Platform That Turns Your Resume Into Proof that You Can Actually Code so Resume says "React developer." One proves it. Every developer has been there. You spend 3 hours polishing your resume, listing every framework you've ever opened a tutorial for, and then a recruiter spends 6 seconds scanning it. Interviews test LeetCode algorithms you'll never use on the job. Nobody actually verifies if you can build things. I got tired of that. So I built One an AI platform that parses your resume, generates a personalized Proof-of-Work assessment from your actual stack, gives you a real-time voice mentor, and builds a 6-month career roadmap from a conversation. All in one workspace. One is a full-stack AI developer career platform. Here's the core loop: Tech stack: gpt-4o-realtime-preview over WebRTC for voice email confirm: true proxy.ts verifies Supabase session before any page rendersThe naive approach "here's a PDF, what are the skills?" doesn't work. GPT-4o will happily extract every technology mentioned in every job description the candidate ever worked near. That's not the candidate's skill set, that's their employer's stack. I used OpenAI's Files API to upload the PDF, then hit gpt-4o with a two-step prompt: js const PROMPT = You are analyzing a document to determine if it is a DEVELOPER/ENGINEER technical resume and generate a personalized coding assessment. STEP 1 — Is this a developer resume? A valid developer resume MUST contain ALL of the following: - Personal work experience jobs, internships, freelance OR personal projects built by the candidate - At least 3 distinct programming languages, frameworks, libraries, or technical tools used BY THE CANDIDATE If NOT a valid developer resume, return ONLY: { "not resume": true, "skills": , "title": "", "questions": } STEP 2 — If valid, return ONLY: { "not resume": false, "skills": "skill1", "skill2", ... , "title": "TopSkill · SecondSkill", "questions": { "type": "mcq", "skill": "React", "q": "...", "opts": ... , "a": 0 }, { "type": "coding", "skill": "JavaScript", "q": "...", "answer": "..." }, { "type": "system-design", "skill": "System Design", "q": "...", "answer": "..." }, { "type": "workflow", "skill": "Architecture", "q": "...", "answer": "..." } } ; Step 1 is a gate. If the document doesn't have personal work experience AND 3+ personal skills, it short-circuits and never generates questions. Step 2 has an explicit instruction to extract only skills the candidate personally used. Then I added a server-side guard on top of the model's own judgment: // Treat as not-a-resume if: model flagged it, too few skills, or no questions generated if parsed.not resume || skills.length < 3 || questions.length < 5 { return NextResponse.json { not resume: true, skills: , title: "", questions: } ; } The model can lie. The guard catches it. One more gotcha: OpenAI's response format: { type: "json object" } throws a 400 if the word "json" doesn't appear anywhere in your messages. Took me longer than I want to admit to figure that one out. Keri uses OpenAI's Realtime API for sub-second voice responses. The architecture matters here — you do not want your server in the audio path. That adds latency and cost. The right flow is: Browser → SDP offer → Next.js server → OpenAI /v1/realtime OpenAI → SDP answer → Next.js server → Browser Browser ←→ OpenAI direct WebRTC audio, server out of the loop The session endpoint creates a short-lived token: js // /api/realtime/session const res = await fetch "https://api.openai.com/v1/realtime/sessions", { method: "POST", headers: { Authorization: Bearer ${key} , "Content-Type": "application/json", }, body: JSON.stringify { model: "gpt-4o-realtime-preview", voice: "shimmer", } , } ; The SDP endpoint forwards the browser's WebRTC offer to OpenAI and returns the answer: js // /api/realtime/sdp const sdpOffer = await req.text ; const res = await fetch "https://api.openai.com/v1/realtime?model=gpt-realtime-2", { method: "POST", body: sdpOffer, headers: { Authorization: Bearer ${key} , "Content-Type": "application/sdp", }, } ; const answerSdp = await res.text ; return new Response answerSdp, { status: 200, headers: { "Content-Type": "application/sdp" } } ; After the SDP handshake, the server is completely out of the audio path. The browser and OpenAI talk directly over WebRTC. That's why the latency is low — there's no proxy in the middle. The thing that makes Keri feel different from a generic chatbot is that she has real data about you injected into every conversation: js const ctxLines: string = ; if context.skills?.length { ctxLines.push Resume skills: ${context.skills.join ", " } ; } if context.skillScores?.length { const sorted = ...context.skillScores .sort a, b = b.v - a.v ; ctxLines.push PoW test scores: ${sorted.map s = ${s.k} ${s.v}% .join ", " } ; } if context.roadmap?.length { const rm = context.roadmap.map m = { const tag = m.active ? " ← CURRENT" : m.done ? " done " : ""; return Month ${m.month} · ${m.title}: ${m.topics.join ", " }${tag} ; } .join "\n" ; ctxLines.push 6-month roadmap:\n${rm} ; } Every message to GPT-4o carries the user's actual resume skills, their exact test scores sorted by performance, and their current roadmap status. When you ask "what should I work on?", Keri doesn't guess — she looks at your lowest score and your next roadmap month and gives you a specific answer. Supabase's default signup flow sends a confirmation email before the user can log in. That's terrible UX for an assessment tool where you want people in the app immediately. Supabase used to have a UI toggle for this. They removed it. The workaround is the admin API: js // /api/auth/register const supabase = createClient process.env.NEXT PUBLIC SUPABASE URL , process.env.SUPABASE SERVICE ROLE KEY ; const { data, error } = await supabase.auth.admin.createUser { email, password, email confirm: true, // skip verification entirely user metadata: { full name: name, organization: org ?? "" }, } ; The register page calls this server-side endpoint, then immediately signs in the user with signInWithPassword . No email. No waiting. Just in. One stores resume data, test scores, and roadmap state in localStorage. The problem: if user A logs in after user B on the same browser, they'd see B's data. The fix is a user ID sentinel key: js const storedUserId = localStorage.getItem "one-current-user" ; if storedUserId == currentUserId { // Different user — clear everything const keysToRemove = Object.keys localStorage .filter k = k.startsWith "one-" ; keysToRemove.forEach k = localStorage.removeItem k ; localStorage.setItem "one-current-user", currentUserId ; } And to make sure no React state from the previous user survives, the entire dashboard remounts using a key prop tied to the user ID: