<?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: Yurii Liub</title>
    <description>The latest articles on DEV Community by Yurii Liub (@yl_hintder).</description>
    <link>https://dev.to/yl_hintder</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3979974%2F579a71db-bb46-454d-9da0-ac7d1986acd7.png</url>
      <title>DEV Community: Yurii Liub</title>
      <link>https://dev.to/yl_hintder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yl_hintder"/>
    <language>en</language>
    <item>
      <title>The hardest part of my AI dating app wasn't the AI — it was making it not sound like AI</title>
      <dc:creator>Yurii Liub</dc:creator>
      <pubDate>Thu, 11 Jun 2026 17:51:39 +0000</pubDate>
      <link>https://dev.to/yl_hintder/the-hardest-part-of-my-ai-dating-app-wasnt-the-ai-it-was-making-it-not-sound-like-ai-36n9</link>
      <guid>https://dev.to/yl_hintder/the-hardest-part-of-my-ai-dating-app-wasnt-the-ai-it-was-making-it-not-sound-like-ai-36n9</guid>
      <description>&lt;p&gt;I built &lt;a href="https://hintder.ai" rel="noopener noreferrer"&gt;hintder&lt;/a&gt; — you screenshot a dating profile (Tinder, Hinge, Bumble), and it writes 5 openers tuned to what's actually on her profile. Pick a tone, get a reply-worthy first message.&lt;/p&gt;

&lt;p&gt;The interesting engineering wasn't "call an LLM and get text back." That's a weekend. The hard part was the thing every AI-writing product secretly fights: &lt;strong&gt;making the output not instantly readable as AI.&lt;/strong&gt; Here's what actually moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: a screenshot is not a prompt
&lt;/h2&gt;

&lt;p&gt;A dating profile is a messy image — photos, prompts, a bio, age, distance, sometimes Spotify. If you hand the whole thing to a model and say "write an opener," you get something generic, because the model averages over everything.&lt;/p&gt;

&lt;p&gt;The fix was &lt;strong&gt;two stages, not one&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract.&lt;/strong&gt; A vision model reads the screenshot and returns &lt;em&gt;structured hooks&lt;/em&gt; — the specific, openable details. Not "she likes the outdoors" but "her 3rd photo is bouldering, her prompt says she'll lose at Mario Kart."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate.&lt;/strong&gt; A second pass writes openers grounded in those extracted hooks, one hook per opener.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Splitting extraction from generation is what makes openers feel &lt;em&gt;about her&lt;/em&gt; instead of about "a girl who likes hiking."&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="c1"&gt;// stage 1 — structured extraction, not free text&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array of her written prompts, verbatim&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;standout_details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;specific, openable things (a pet, a trip, a joke)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playful | dry | earnest | adventurous&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// stage 2 — generate ONE opener per hook, never averaging&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ANTI_AI_RULES&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem 2: the "obviously AI" tells
&lt;/h2&gt;

&lt;p&gt;This was 80% of the work. Out of the box, the model writes openers that are &lt;em&gt;grammatically perfect and completely dead&lt;/em&gt;. The tells, in order of how badly they out you as a bot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The em-dash.&lt;/strong&gt; Nobody texts an em-dash. It's the #1 giveaway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Ah, a fellow [hobby]"&lt;/strong&gt; / "I see you're a person of culture." Reddit-comment cadence, not flirting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survey questions.&lt;/strong&gt; "What do you enjoy most about hiking?" reads like an onboarding form, not a human who's into her.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-completeness.&lt;/strong&gt; Real openers are a little lazy. A model writes a tidy paragraph; a person writes seven words and a typo's worth of casualness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix wasn't a bigger model — it was &lt;strong&gt;constraints as first-class product logic&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ANTI_AI_RULES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;never use an em-dash or semicolon&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;max ~15 words; lowercase is fine&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reference exactly ONE detail, not a checklist&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no question she can answer in one word&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no "ah, a fellow…", no "I see you…", no "as someone who…"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;write like a confident text, not an essay&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Negative constraints ("never do X") turned out to matter more than positive ones. Telling a model what &lt;em&gt;good&lt;/em&gt; looks like is vague; banning the specific tells is enforceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: the second message is where it really dies
&lt;/h2&gt;

&lt;p&gt;Everyone optimizes the opener. But the actual graveyard is message two: she replied "haha hey", and the guy freezes. So hintder takes the thread context and suggests the next line — keep the thread alive, don't restart it. Solving only the opener would've solved the easy half.&lt;/p&gt;

&lt;h2&gt;
  
  
  On privacy (because it's a screenshot of a real person)
&lt;/h2&gt;

&lt;p&gt;Screenshots are processed for the reading and &lt;strong&gt;auto-deleted after 30 days&lt;/strong&gt;. For a product built on images of real people, that constraint isn't a footnote — it's table stakes, and it shaped the storage design from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate extraction from generation.&lt;/strong&gt; Specificity is an architecture decision, not a prompt adjective.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ban the tells explicitly.&lt;/strong&gt; "Sound human" is unenforceable; "no em-dashes, no survey questions" is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solve the unglamorous half.&lt;/strong&gt; The second message is where retention actually lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it on a real profile at &lt;strong&gt;&lt;a href="https://hintder.ai" rel="noopener noreferrer"&gt;hintder.ai&lt;/a&gt;&lt;/strong&gt; — first 3 openers are free. If you've shipped anything that had to &lt;em&gt;not sound like AI&lt;/em&gt;, I'd love to hear which tells you had to kill. &lt;/p&gt;

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