My product · Since January 2026 · Product Designer / AI Tools
Ghosted
An AI resume editor that keeps people in control. I designed the product, its proposal-and-review workflow, and the system behind it, and keep improving it with feedback from 30+ users.
- The problem
- AI can tailor a resume quickly. People still need to see and control every change.
- The decision
- Make each edit a yellow proposal on the real resume, ready to keep, reject, or revise.
- What changed
- Used by 30+ users, and their feedback shapes each release. I haven’t measured hiring outcomes.
My role. Product designer and owner. I set the direction, the flows, the product model, the brand, the design system and the architecture, review the code and ship the releases, and make every call about where the AI stops. AI coding agents implemented substantial code from my specs and decisions.
The product: a resume you can talk to
Tailoring a resume for every job is repetitive, and an AI can make it a lot faster. But I only trust that if I can see exactly what it changed.
So Ghosted is built around one surface: a live resume canvas with a chat beside it. You ask for a change, and the change shows up on the page in yellow.
- A live canvas. The resume is drawn in the browser by Typst, and the PDF comes from the same compiler, so what you see is what you download. An edit re-renders in about 7 ms.
- Proposals, not edits. Every change the agent makes is highlighted and saved as a version: who made it, when, and why. You can Keep it, Reject it, or edit it yourself. Undo and Restore only move a pointer, so nothing is ever deleted.
- Click any line to fix it yourself. Clicking a line on the rendered page opens the edit form at that exact field. When the AI misses twice in a row, the chat says so and points there instead of making you argue with it.
- Evidence, not invention. The base resume is a master copy that’s never trimmed. When the agent needs a stronger bullet, it asks for specifics and writes only from what you told it. Each job gets a one-page version cut from it, and the page is measured after tailoring: if it spills, the chat trims the least relevant lines and says what it cut.
- Three levels: base, archetype, job. Jobs are filed into your own job families. Once a family has a few jobs, Ghosted can build an archetype resume for it, and new jobs in that family start from there.
- Intake from the source. A pasted job link is read through the hiring platform’s own API first (Greenhouse, Lever, Ashby, Workday and others) and trimmed to the job itself.
- In Spanish, for Latin America. A CV follows the posting’s language (its headings, dates and A4 paper in Peru), so someone in Lima can apply locally in Spanish and to a US remote job in English. My family in Peru reviews the wording before it opens to everyone.
The launch post walks through the idea screen by screen.
The design system
Three days into building, the app had drifted: 18 font sizes, 19 ways of writing a border radius, 26 shadows, and five different left edges on one page. Coding agents wrote most of that, one reasonable edit at a time.
On October 11 I had it counted, chose the system from a proposal of options in one sitting, and moved the app onto it the same day. Screenshot checks on every screen, in light and dark, proved that each step changed only what it was meant to.
Tokens that carry their own rule
One file, tokens.json, is the source of truth: 59 primitive colors, 43 semantic color tokens in Light and Dark, 14 spacing steps, 7 radii, and 8 type sizes in three Text size modes. Nothing uses a primitive directly. Every semantic token has a one-line rule, and that line is the guidance. Two of them:
ai/highlight · Light #ffe58a · Dark #ffe58a
- Its usage line: “Only ‘the AI changed this’ (plus the logo and the celebration highlighter). Always a background under ink; same in both themes.”
- Never: a button, text, selection, focus or progress.
action/primary · Light #2f4bd1 · Dark #7d93ff
- Its usage line: “Primary buttons and links. Blue only ever means an action or focus.”
- Never: selection, progress, or “the AI changed this”.
The same line shows up in the generated guide, in the Figma variable’s description, and next to the value in code, so a designer in Figma and an agent in code read the same rule.
Components with every state
There are eight components in web/src/ui: Button, ButtonLink, LinkButton, IconButton, Chip, Field, EmptyState and Dialog.
- Button replaced about 48 class combinations. Variants: Primary, Secondary, Quiet, Danger and Danger solid, in two sizes. States: default, hover, pressed, focus, selected, disabled and busy. One Primary per view, and never a yellow button.
- Field is a label, the control (input, textarea or select), and a hint or an error under it. Before the system, no field in the app had an error state. Now an error turns the border red, replaces the hint with what to do next, and is announced: the control gets
aria-invalidand points at the message.
One named exception
The highlight yellow stays a raw #ffe58a in the resume template. The resume is drawn by Typst, not CSS, and the app finds the highlights on the rendered page by that exact fill. So the literal stays, and the guide names it as an exception and says why. Everywhere else, it’s ai/highlight.
How a change gets in
- Edit
tokens.jsonfirst: the value, the alias, or the usage line. - Run
pnpm tokens. It writes the CSS, the motion values and the guide’s token tables. - Then update Figma. Never Figma first. If the two disagree, the JSON wins.
pnpm checkruns before every push. It’s in the instructions every agent session reads, not a git hook. It fails when a generated file is stale, a new raw hex color appears, a button is styled by hand instead of coming fromweb/src/ui, a pill skips<Chip>, a modal skips<Dialog>, or code calls the browser’sconfirm(),alert()orprompt().
This is what an agent sees when it hand-writes a button:
web/src/App.tsx:407: use <Button>, <LinkButton> or <IconButton> from web/src/ui, not a classed <button>
The message names the fix, because the reader is usually an agent, and an agent that’s told which component to use will use it.
To be clear about scale: this system’s users are coding agents and Ghosted’s testers. No partner teams use it yet, and the system itself hasn’t been usability-tested.
Before you apply: Fit and Research
Every tailored job gets a Fit report, written by the same AI call that tailors the resume. The first version was honest and busy: 18 items, all open. I said as much, and we prototyped three directions on a real job. I liked two of them and asked to mix them.
Fit now opens with one line that reads the whole report. Then comes the proof: what’s already strong, with the resume line that proves it, and the posting’s own words with a check where the resume has them. Then what to lead with and the gaps to prepare, each one a box to tick. The ghost keeps score, and the chat can see what you’ve ticked. On the test job it went from about 4,300 characters to 1,100, and from two screens to one.
Fit has to be checkable, too. Every “already strong” point cites a line, and code looks for that line on the job’s resume and the base. Anything it can’t find is flagged, never hidden.
Research started with my own application to Nike. I was looking up their design system by hand and wanted Ghosted to do it for me. The rule I cared about: the AI never types a link. It picks what’s worth reading (what the team has built, recent news, tools the posting names that my resume doesn’t, how they interview) and writes a search for each.
The server runs the searches and keeps only real results. It also strips my name and contact details from every search before it leaves, and the privacy page said so before Research went to everyone. On my Nike job, “Podium, Nike’s design system” came back first. When a link misses, a × hides it and logs it, so I can see which searches need work.
A first resume, with the ghost
Everything above assumes you already have a resume. My brother doesn’t. He’s a high school student applying to college, and I wanted Ghosted to be the place he writes his first one: “like a gamified interview with the ghost.”
So a new account with no resume can build one with the ghost instead of importing. It asks one card at a time beside the live page: a quick hello, then six chapters (School, Activities, Work, Helping out, Wins and Skills). Chips and short facts go straight onto the page with no AI. Each typed answer is one small AI call that returns only the new lines, plus at most one follow-up question.
New lines swipe in yellow with a Fix chip, and the next answer keeps them. It’s the same proposal model as the rest of the app, without stopping to press Keep every time. Activities ask for hours a week and weeks a year, the numbers college applications use.
The resume itself had to grow for this: activities, honors and school details are now real sections, and education moves first when there’s no work on the page yet. The interview ends with “Your first resume is ready”, a Download PDF button and the Offer moment’s highlighter burst. Tested on a made-up student with two clubs, two jobs and a food pantry, it took 26 cards and produced a one-page resume with nothing invented. I’d planned for 15 to 20 cards and kept the longer version, because someone typing all of that in is clearly engaged.
Process and decisions
I decide; agents build. That split only works if the decisions are written down before the code, so the repo carries them: use cases with dated decisions, proposals for anything bigger than a day, ADRs for the things that set direction, and a release log where every production release records what shipped, why, and where the decision lives. That log is the source for this page.
For the bigger calls I get grilled. An agent asks me question after question until the edge cases are settled, and the answers go into the proposal. Two-way sync between the base resume and its copies came out of one of those sessions. Every release follows the same checklist: typecheck and a production build, check whether a tester is mid-session, snapshot the database, then ship.
A few decisions that show how I think about it:
- One style, set from the base. There’s one style for the whole workspace, so a Style button inside a job was quietly restyling every resume. I moved Style to the base resume and archetypes, where the panel can say plainly that it applies to all your resumes.
- Looks are presets, not layouts. When Sam asked for templates, I made five looks of the one ATS-safe template rather than new two-column layouts. Sidebars and columns look nice and break applicant-tracking systems.
- Accept became Keep. “Accept” sounds like signing something. “Keep” is what you’re doing: keeping a change, or not.
- New conversation instead of clearing the chat. Each request re-sent the last 20–40 messages. Now the AI only sees what comes after a “New conversation” divider, and the resume, posting, notes and letter always come along. Nothing gets deleted, so History links and requests keep working.
Early testing with real people
Ghosted went out for early testing on October 8. The first tester used it that day, and her job notes were open in an interview the next morning. From then on, testers set the order of work. Here’s what they said and what changed.
To be clear, this was early testing: me watching and listening to a handful of testers in those first days, not formal research. One example, start to finish:
- What I saw. On a call, Kim dismissed the new “your base resume changed” bar, then couldn’t find her way back to it.
- What I decided. Closing a bar shouldn’t lose anything, and bringing changes in should be a choice, not all or nothing.
- What changed. Dismiss became “Not now”. A “Base updated” button reopens the bar, and “Choose what to bring in” lists the changes in the chat so you pick which ones to keep.
The rest of what testers said, and what changed:
- Jacky couldn’t load her resume. Her PDF had gone from Google Docs through “Microsoft Print to PDF”, so the letters were drawn as shapes with no text to extract, and the app failed silently. Picture-only PDFs are now read with OCR (about 4 seconds for three pages), and a failed upload says why, right under the button.
- Kim’s test call produced the longest list:
- The page break looked like a spacing bug, so each page is now its own sheet labeled “Page 1 of 2”.
- She wanted to know when messages were sent, so chat has timestamps.
- A half-typed message vanished when she switched jobs. Drafts now survive switching and reloading.
- The job agent added an Operations skills group that never reached her base resume, so the next job started without it. Her idea was that the job “sends a request” to the base. That became two-way sync: copies notice when their source changed and can bring the change in, and new facts from a job chat go to the base as requests you can apply or decline.
- She asked for formatted job notes. They shipped that evening.
- She proposed naming downloads by role, since “it’s obviously for the company you’re applying to.” Downloads are now “First Last — Role.pdf”.
- Sam asked for templates after a demo, ones that apply with the live reload. That became Looks: five presets drawn from your own resume, applied live, with “Save as a look” for your own fine-tuning.
- I tested it on my phone and wrote down “we’re getting a bit crowded.” That became the focus pass (below).
- Two of three new testers stopped before their first resume. They signed in and never saved anything. That might just be timing. A small first-run guide went out on October 9, and two days later it became a guided walkthrough (below).
Testers also needed a way in and a way to hear back. The sign-in screen now has “Request an invite”: a short form that pings me in a private Discord channel, with a link that opens the request on my phone, where Approve creates the tester’s code. Every deploy posts its “What’s new” entry to a Discord changelog channel on its own. Every week or so the house AI drafts a roundup in my voice, and I edit it before it goes out.
Identity and motion
By October 9 the product worked and still looked like a placeholder: an emoji for a logo and the system font. A design agent built a brand lab in Figma, with logo studies, a palette, type and a motion board. Claude reviewed it, and I made the calls.
What I realized was that the one thing Ghosted could own was already in the product: the yellow highlight that marks what the AI changed. So the highlighter yellow, #FFE58A, became the brand color.
It only ever means the logo or a pending AI change, always as a background under ink, never as text, and it’s the same in both themes. Blue stays for buttons and links. If the primary button were yellow, yellow would mean both “click here” and “the AI changed this”.
- The mark: the Paper Ghost, simplified until the folded corner is the only detail, so it holds at 16 px. In the app it’s just the ghost shape in ink.
- Type: DM Sans for all app UI and no monospace, so the serif resume reads as the document.
- Rejected: yellow buttons, a 3D ghost mascot (it clashed with the flat mark), and serif headings in the app (they compete with the resume).
Motion only goes where it explains a change. The hero moment: when you keep a proposal, the yellow fades into plain ink in 180 ms while the text stays perfectly still. When a proposal lands, the highlights swipe in left to right behind a small Paper Ghost cursor. My first build ran the swipe in 280 ms, and testing it, I couldn’t actually see the cursor. It now takes about 600 ms, the one deliberate exception to the 220 ms ceiling. The loader is the mark, and it only appears if a wait passes 200 ms. With reduced motion on, every one of these goes direct.
The voice follows the same rule. Ghosted’s chat is a calm editor with a little dry humor, and it never jokes about rejection, the job search, or errors. Resume text, letters and answers stay in the person’s own voice, with no ghost references. Keep gets a short toast: what changed, plus one line like “Called it.” or “Not bad for a ghost.” That reversed an earlier “no jokes” call after I tried it, and I’m watching whether it gets old. The Discord kit has emoji for saved, applied, interviewing and offer, plus Keep and Reject for voting on ideas. There’s deliberately no “rejected” emoji: testers are people actually getting ghosted.
The focus pass
Testing on my phone, I wrote: “we’re getting a bit crowded… keep the functionality but clean up the view so it’s more relaxing and allows you to focus on the current task.” So I measured it: 62% of the first phone screen was chrome before you saw any of the resume.
The rule became: show what the current task needs, keep everything else one tap away, and remove nothing.
The job header became one line. Editing a job moved into the Posting tab. Older chat replies show their buttons only on hover or tap. The resume toolbar shrank to the few things you use. On phones, the resume now starts 233 px down the first screen instead of 522 px. Board cards got a “Move to” menu, because drag never worked on phones at all.
Speed counts as focus too. The Looks grid renders each look from your own resume, and the first build froze for 880 ms when you opened Style. Now the panel paints first and the thumbnails fill in one at a time behind sketch placeholders: 30 ms to open, 55 ms to reopen.
Showing people around
On October 11 I asked for the tutorial to be rebuilt so the ghost would “hold your hand and explain things while trusting your intelligence,” the way Adobe’s hands-on tutorials and Logic’s first run do.
The tour runs in the real app, not a slideshow. The rest of the screen dims, the control that matters sits in a cutout with a blue ring, and the ghost talks from a card beside it. Every step has Next and Dismiss, and doing the real thing counts as Next.
Two bugs taught me something about overlays. Clicking a line on the resume opened the edit form, and then the tour snapped the view back to the chat, because the tour heard the click before the app did. Toasts sat half-dimmed across the cutout’s edge, and making the number bigger didn’t fix it, because a layer only stacks against its siblings. While a step shows, the page now knows it: toasts rise above the dim, and anything that would compete steps aside.
Then I asked what the tour would do to Kim, who already had a base resume, three jobs and applications out. The hands-on version asks you to practice an edit on your base resume and start a new job. For her, that edit would have flagged every job with “your base resume changed.” So anyone who already has jobs gets a shorter look-don’t-touch tour. It opens one of their own jobs, fills in a missing Fit report or research as it goes, and changes nothing else. Its first card says so. A tour left waiting more than a day ends quietly, so a step can’t pop up days later out of context.
Later that night I asked whether the steps were as short as they could be. They weren’t. Two steps pointed at the same page, one repeated what the page already says, and Fit and Research took two clicks each. The tour is now seven steps, or five if you already have jobs, and it ends where an application does: Download PDF, the cover letter and answers, and marking it Applied.
The landing page: the Loop
Testers never see the landing page, but everyone else does first, and a page of screenshots can’t show what a proposal feels like. I asked for something “stupidly flashy… more of an experience,” then reacted to three live prototypes: a scroll loop, a kinetic editorial page and a hands-on playground. I picked the loop and pulled in the playground’s Keep and Reject and the editorial page’s mouse-following ghost.
One fictional resume performs the whole product as you scroll, in six chapters: paste a link, check the posting for ghost-job signs, review the proposal as it swipes in, keep or reject it, check fit and research, and track the job on the board. At the end of each chapter it hands you the controls. You can press Tailor, click a line to edit it, burst the highlights into sparks with Keep or break them into shards with Reject, switch Fit and Research, and drag the card to Applied. If you just keep scrolling, each step plays by itself.
It’s a showcase, so it gets its own motion tier, written down as a decision: no animation library, no scroll hijacking (snap points only catch a fling, so a trackpad can’t skip a chapter), a Pause motion toggle for WCAG 2.2.2, and six still panels with reduced motion.
Splitting the signed-in app into its own bundle took the landing’s JavaScript from 288 kB to 81 kB gzipped. On mobile, Lighthouse performance went from 93 to 98 and page weight from 978 KiB to 349 KiB (the live page against the new build, measured locally).
A phone audit afterwards found taps landing on faded layers and scrolling that re-styled about 300 elements every frame. Moving the scrubbed parts onto one scroll timeline cut frames over 33 ms from 21 to 3 on a throttled desktop, and every hands-on moment now works on a phone.
A leaner, faster AI
Ghosted’s AI runs on my ChatGPT subscription. At first, every request went through the Codex command-line tool. When I added usage counts on October 10, I could finally see what that was costing: the tool added about 17,000 tokens of its own instructions to every call, before Ghosted said anything. I asked whether we could cut it, because “every request is crazy.”
Now the server calls the same Codex endpoint directly, with Ghosted’s own short instructions. A job-chat edit went from 25,470 input tokens to 3,621, 86% less. Chat replies now stream in as they’re written, so you see the answer start instead of waiting for all of it. Resume edits still arrive whole at the end, highlighted for review, because a half-written proposal isn’t something you can keep or reject.
The same day, testers got an Effort picker next to Send (Low by default, then Medium and High for bigger edits) and their own usage under Your data: requests, tokens, and what it would have cost at API prices, which they aren’t charged. The direct request is also the first step toward running Ghosted on a model of its own one day.
By the numbers
- 70 entries in the production release log from October 8 to 11, each with what shipped, why, and who decided.
- 522 → 233 px: where the resume starts on a phone, before and after the focus pass.
- 62% of the first phone screen was chrome before that pass.
- 880 → 30 ms to open Style.
- ~7 ms to re-render the resume after an edit.
- Same evening: Kim’s formatted-notes idea, from request to production.
- 25,470 → 3,621 input tokens for a job-chat edit (86% less) after the AI connection rewrite on October 10.
- 4,300 → 1,100 characters on the Fit tab of the test job, after the redesign.
- ~12 search credits per tailored job for Research, against a plan of 5,000 a month.
- 26 cards to a one-page first resume for a made-up student, with nothing invented.
- 288 → 81 kB of landing-page JavaScript, gzipped; Lighthouse mobile performance 93 → 98.
- 30+ users today. That says who uses it, not how: I don’t have active-use, retention or hiring numbers, and measuring how people actually use it is the next piece of work.
How it was built
I want to be direct about this part, because it’s part of the design. The flow, the chat-plus-canvas model, the choice to render with Typst, the brand, and every product decision above are mine, written down before any code. A coding agent, Claude, built the first working cut in a day from that spec. Since then, AI coding agents have implemented substantial code from my specifications, while I set the direction, tested, reviewed the code and shipped each release.
The product’s own AI is OpenAI’s GPT, running on my ChatGPT subscription, and Research searches go through Firecrawl. That’s useful evidence of how I work with AI tools. It isn’t a claim that I hand-wrote every line. The foundations I’m now taking over by hand are the resume data model, the template, the prompts and version history.
Before this
Ghosted is my third pass at the same problem, in development since January 2026. The first was a Go terminal app with an eight-stage pipeline, where agents drafted documents but nothing touched the tracker without my approval. The second was a Next.js web app where code did everything it could and one bounded model call wrote the prose, checked by 744 tests. The first proved the workflow but only I could use it. The second worked but testers found it too complicated, and its output lived apart from the resume. This version puts the AI on the resume itself, which is why every edit has to be a proposal.
What’s next
- Pick a layout. The three layout studies above (AI-generated concepts from the brand lab, still with the retired 3D ghost) are unselected. The identity reskinned the current layout. Phones still stack three rows of tabs on a job, and that’s the first thing a new layout has to fix.
- Open Spanish to everyone once my family in Peru has been through it.
- Bring Keep’s burst and Reject’s break from the landing page into the app, after testers have seen them.
- Tune Research’s searches from the links testers mark as not useful.
- Check that Fit is right: a way to say an item isn’t true, and tracking whether “strong” fits actually get interviews.
- Archetypes that learn from what you keep across jobs, instead of only being built on request.
- A recognition test for the mark at 16 px before calling it final.
- 1.0, open source, where you sign in with the AI subscription you already pay for.
What I learned
Adding AI to a product is the easy part. The design work is deciding how much the model gets to do, and making sure the person can see, judge and undo every bit of it.
The second lesson: the fastest way to find out what that means is to put it in front of real people on day one and let what they say set the order of work.
The walkthrough added a third. Every first-run decision has to be asked again for the person who already has work in the app.
And the first-resume interview added a fourth. The same proposal model works for someone with nothing on the page yet, as long as the AI only ever writes what they just said.


