<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Payanai</title>
    <description>The latest articles on DEV Community by Payanai (@payanai_dev).</description>
    <link>https://dev.to/payanai_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143542%2Fa56760af-72ab-4266-8693-2c90d22ec75f.png</url>
      <title>DEV Community: Payanai</title>
      <link>https://dev.to/payanai_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/payanai_dev"/>
    <language>en</language>
    <item>
      <title>How I stopped my AI resume writer from inventing achievements</title>
      <dc:creator>Payanai</dc:creator>
      <pubDate>Fri, 25 Sep 2026 22:47:04 +0000</pubDate>
      <link>https://dev.to/payanai_dev/how-i-stopped-my-ai-resume-writer-from-inventing-achievements-ddn</link>
      <guid>https://dev.to/payanai_dev/how-i-stopped-my-ai-resume-writer-from-inventing-achievements-ddn</guid>
      <description>&lt;p&gt;Ask most AI resume tools to "improve" a bullet like &lt;em&gt;"worked on the website"&lt;/em&gt; and you'll get something like &lt;em&gt;"Spearheaded a website redesign that boosted conversions by 35%."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It sounds impressive. It's also made up. And the first time an interviewer asks "how did you measure that 35%?", it falls apart.&lt;/p&gt;

&lt;p&gt;I recently built a free resume builder, and the one rule I cared about most was: &lt;strong&gt;the AI may reword what you wrote, but it may never add facts.&lt;/strong&gt; Here's how I tried to enforce that, what I tested, and a strange PDF bug I found along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: LLMs are trained to be "helpful"
&lt;/h2&gt;

&lt;p&gt;Language models are very good at producing text that &lt;em&gt;sounds&lt;/em&gt; like a strong resume. Strong resumes have numbers, tools and outcomes, so when a model sees a vague bullet, the most "resume-like" completion includes a number, a tool and an outcome, whether or not they're true.&lt;/p&gt;

&lt;p&gt;So "be helpful" and "be honest" pull in different directions. You have to make the honest option the only acceptable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Tell the model exactly what it can't do
&lt;/h2&gt;

&lt;p&gt;Vague instructions like "don't make things up" aren't enough. I listed the specific kinds of invention I'd seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HONESTY_RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Rules you must follow:
- Only use facts the user provided. Never invent employers, job titles, dates, numbers,
  percentages, tools, technologies, awards or achievements.
- If a bullet has no measurable result, do not add one. Make it clear and specific
  with the facts given instead.
- Do not use placeholders like [X%] or "N users".
- Write in plain, professional English. No buzzwords like "synergy", "rockstar"
  or "results-driven".`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details mattered more than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Banning placeholders.&lt;/strong&gt; Without that line, models "politely" add &lt;code&gt;[X]%&lt;/code&gt; for you to fill in, which nudges users to invent a number themselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saying what to do instead.&lt;/strong&gt; "If a bullet has no measurable result, make it clear and specific with the facts given" gives the model a path that isn't "add a number".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 2: Check the output's structure in cod
&lt;/h2&gt;

&lt;p&gt;Prompts are guidance, not guarantees. So the cof the response before showing it to the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bullets&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;improved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bullets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// One output per input, or it's not a safe dro&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;improved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bullets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unexpected AI response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requiring **exactly one rewritten bullet per ins two useful things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model can't sneak in an extra "achieveme&lt;/li&gt;
&lt;li&gt;The model can't merge or drop bullets, so the user can compare each one before and after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the check fails, the user sees an error instead of a questionable rewrite. I'd rather fail loudly than show something plausible but wrong.&lt;/p&gt;

&lt;p&gt;I also use JSON output mode (`response_format: so parsing is reliable, and a low temperature&lt;br&gt;
(0.4) to keep rewrites conservative.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layer 3: Test it with inputs that &lt;em&gt;tempt&lt;/em&gt; invention
&lt;/h2&gt;

&lt;p&gt;The best test cases are vague bullets that almost beg for a made-up number:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;worked on the website&lt;/td&gt;
&lt;td&gt;Developed features for the website.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;helped with customer support tickets&lt;/td&gt;
&lt;td&gt;Assistickets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fixed bugs in the checkout page using React&lt;/td&gt;
&lt;td&gt;Resolved bugs in the checkout page using React.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;made the product page load faster&lt;/td&gt;
&lt;td&gt;Optimizeder loading.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No numbers, no new tools, no invented outcomes.and that's the point: they're clearer, but still&lt;br&gt;
true.&lt;/p&gt;

&lt;p&gt;I also ran an automated check that flags any output containing digits, &lt;code&gt;%&lt;/code&gt; or technology names that weren't in the input.&lt;br&gt;
It's crude, but it catches the most common kind&lt;/p&gt;

&lt;p&gt;The UI still tells users to **read the result bt is perfect, and the person whose name is on&lt;br&gt;
the resume should have the final say.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus bug: why "SKILLS" became "SKI L LS"
&lt;/h2&gt;

&lt;p&gt;The PDF export is simply the browser's print-to-PDF of a single-column HTML preview. That gives real, selectable text, which matters because many employers run resumeng systems (ATS) that read the text.&lt;/p&gt;

&lt;p&gt;To check that, I extracted the text from a gene&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;text&lt;br&gt;
EDUCATION&lt;br&gt;
SKI L LS&lt;br&gt;
CERTI F ICATIONS&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The cause was my section headings: uppercase wi`. With wide tracking, PDF text extraction&lt;br&gt;
sometimes decides the gaps between letters are word breaks. A human sees "SKILLS"; a parser may see three words.&lt;/p&gt;

&lt;p&gt;The fix was one line: remove the letter-spacing from the headings (and add a thin divider so they still stand out). After&lt;br&gt;
that, extraction gave clean "SKILLS" and "CERTI&lt;/p&gt;

&lt;p&gt;Lesson: **if machines will read your output, te, not just what it looks like.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keeping it free without runaway costs
&lt;/h2&gt;

&lt;p&gt;Every AI action costs money, so there's a limit per day. I store the counter in the user's&lt;br&gt;
server-only metadata (Supabase &lt;code&gt;app_metadata&lt;/code&gt;, which users can't edit), and only successful actions count against the&lt;br&gt;
limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;aiUsageToday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You've used all 20 AI actions for today.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...call the model...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordAiUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// only afte&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editing, saving and PDF export aren't limited at all. Only the part that costs money is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building AI writing features
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List the specific ways the model tends to go wrong&lt;/strong&gt;, not just "be accurate".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the model an honest alternative&lt;/strong&gt; to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate structure in code.&lt;/strong&gt; Prompts guide; code enforces.&lt;/li&gt;
&lt;li&gt;**Test with inputs designed to tempt failuremples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test what machines see&lt;/strong&gt;, whether that's parsed PDFs, screen readers or search crawlers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to try it, the resume builder is free at &lt;a href="https://nokker" rel="noopener noreferrer"&gt;nokku.payanai.com/resume-builder&lt;/a&gt;. I'd genuinely like to hear if you can getit to invent something. That's the kind of bug report I want.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
