<?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>What Google, Amazon, Microsoft and Meta actually say about their interviews (and a free prep site I built from it)</title>
      <dc:creator>Payanai</dc:creator>
      <pubDate>Thu, 01 Oct 2026 01:12:43 +0000</pubDate>
      <link>https://dev.to/payanai_dev/what-google-amazon-microsoft-and-meta-actually-say-about-their-interviews-and-a-free-prep-site-i-3nj0</link>
      <guid>https://dev.to/payanai_dev/what-google-amazon-microsoft-and-meta-actually-say-about-their-interviews-and-a-free-prep-site-i-3nj0</guid>
      <description>&lt;p&gt;Most interview prep online is a mix of leaked questions, hearsay and "Top 75" lists. I wanted the opposite: &lt;strong&gt;what each company says about its own interview process&lt;/strong&gt;, in one place, with practice questions I could trust.&lt;/p&gt;

&lt;p&gt;So I read the official hiring pages for Google, Amazon, Microsoft and Meta, and turned them into free guides on Nokku: &lt;strong&gt;&lt;a href="https://nokku.payanai.com/interview-prep" rel="noopener noreferrer"&gt;nokku.payanai.com/interview-prep&lt;/a&gt;&lt;/strong&gt;. No sign-up.&lt;/p&gt;

&lt;p&gt;Here's what stood out, then how I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the companies actually say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google&lt;/strong&gt; describes structured interviews: every candidate for a role gets the same questions and the same scoring rubric. Its own guide names role-related knowledge, problem solving and leadership. In 2025 Google also said it's bringing back at least one in-person round, citing AI-assisted cheating in virtual interviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon&lt;/strong&gt; runs a phone screen of 30 to 45 minutes, then a loop of &lt;strong&gt;4 to 6 interviews of 45 to 60 minutes each&lt;/strong&gt;, including a &lt;strong&gt;Bar Raiser&lt;/strong&gt;: a trained interviewer from outside the hiring team. Every interview checks for its &lt;strong&gt;16 Leadership Principles&lt;/strong&gt;, and about half of a technical loop is behavioural. Amazon suggests STAR answers of about 2 to 3 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft&lt;/strong&gt; is refreshingly specific: code in your strongest language, &lt;strong&gt;no pseudocode&lt;/strong&gt;, know at least one O(n log n) sort in detail (preferably two), and manage your time in a ~45-minute round. It also says it looks for a &lt;strong&gt;growth mindset&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta&lt;/strong&gt; describes a 45-minute tech screen (roughly 5 minutes of intros, 35 of coding, 5 for your questions), then a loop of coding, design and behavioural interviews. The process usually takes about 2 to 3 months. And the big change: Meta's hiring page says &lt;strong&gt;technical interviews now include an AI assistant&lt;/strong&gt; built into the environment.&lt;/p&gt;

&lt;p&gt;The common thread across all four: &lt;strong&gt;think out loud, get to working code, then improve it.&lt;/strong&gt; Meta says it plainly: non-optimal but working code beats just an idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;A few decisions mattered more than the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Official sources only.&lt;/strong&gt; Every guide lists its sources and a "facts checked" date. If something is widely reported but not official (for example Google's "Googleyness" or its hiring committee), the guide says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original practice questions.&lt;/strong&gt; No leaked or copied problems. Each one is written in the style of common interview questions, with hints you reveal one at a time and a full solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every solution is tested.&lt;/strong&gt; Each one is che version on thousands of random inputs before it
ships. Writing the test caught one of my own wrong "expected answers".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content as Markdown.&lt;/strong&gt; Guides are plain `.m rendered with the same components as the site's
courses: flashcards, quick checks, step-throughs, and a new "practice question" block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infographics from data.&lt;/strong&gt; Each guide's "at rs, a timeline, how time splits) is generated
from frontmatter. The facts live next to their sources, and the hub builds a side-by-side comparison of all four
companies from the same data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's inside each guide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The process as a step-by-step timeline&lt;/li&gt;
&lt;li&gt;What interviewers score you on&lt;/li&gt;
&lt;li&gt;Pattern flashcards (two pointers, sliding winn's Leadership Principles&lt;/li&gt;
&lt;li&gt;Behavioural scenarios with feedback on each choice&lt;/li&gt;
&lt;li&gt;3 original practice questions with tested sol&lt;/li&gt;
&lt;li&gt;A 4-week prep plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://nokku.payanai.com/interview-prep" rel="noopener noreferrer"&gt;nokku.payanai.com/interview-prep&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's free, with no account. I'd love feedback: which company or topic should I add next? And if you've interviewed at one of these recently, does the process match what&lt;/p&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I had 30 remote jobs and 2,000 more I wasn't showing. Here's what fixing it took</title>
      <dc:creator>Payanai</dc:creator>
      <pubDate>Tue, 29 Sep 2026 15:01:55 +0000</pubDate>
      <link>https://dev.to/payanai_dev/i-had-30-remote-jobs-and-2000-more-i-wasnt-showing-heres-what-fixing-it-took-566e</link>
      <guid>https://dev.to/payanai_dev/i-had-30-remote-jobs-and-2000-more-i-wasnt-showing-heres-what-fixing-it-took-566e</guid>
      <description>&lt;p&gt;I run &lt;a href="https://nokku.payanai.com" rel="noopener noreferrer"&gt;Nokku.ai&lt;/a&gt;, a free site for job-market trends, courses and remote jobs. Its remote jobs page had a problem I didn't notice for weeks: &lt;strong&gt;it listed 30 jobs from the last 30 days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That looked like a quiet market. It wasn't. We collect from three job boards (RemoteOK, JSearch and Arbeitnow), but the page only read from one of them. The other remote listings were sitting in the database, unused.&lt;/p&gt;

&lt;p&gt;Here's what it took to turn that into a page with &lt;strong&gt;~200 live remote jobs from 150 companies&lt;/strong&gt;, and to make it something Google can actually index.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Merge the sources, then deal with the mess
&lt;/h2&gt;

&lt;p&gt;Reading all three boards is one query each. The work is in what comes back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same job shows up twice.&lt;/strong&gt; A company posts on LinkedIn, and two boards pick it up. I remove duplicates by company and title, and keep the copy that lists pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Remote" isn't always remote.&lt;/strong&gt; Boards marked as remote-only still return titles like "Senior Developer – DC + Hybrid". If the title says hybrid or on-site, it's out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some links aren't links.&lt;/strong&gt; Anything that isn't &lt;code&gt;http(s)&lt;/code&gt; is dropped before it reaches the page.
&lt;/li&gt;
&lt;/ul&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;NOT_REMOTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;hybrid|on-&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;site|in&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;- &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;office&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RemoteJob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&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="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="c1"&gt;// Keep one copy per job, preferring the one that lists pay&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;byKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The bug that made "Go" the hottest skill
&lt;/h2&gt;

&lt;p&gt;Once everything was merged, &lt;strong&gt;Go&lt;/strong&gt; was tagged on 102 of 201 jobs. That's not a market signal, it's a bug.&lt;/p&gt;

&lt;p&gt;Older rows had been tagged with substring matching, so "Go" matched "good", "AI" matched "email" and "Java" matched "JavaScript". I'd already switched to word-boundary patterns, but rows scraped before the fix still carried the old tags.&lt;/p&gt;

&lt;p&gt;Rather than rewrite the database first, the pagfix rows when it reads them, from the title and&lt;br&gt;
the board's own tags:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scraped_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;SKILL_FIX_DEPLOYED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;returreturn&lt;/span&gt; &lt;span class="nf"&gt;extractSkillsFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;boardTags&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Go" dropped from 102 jobs to 11. That's why I til I've looked at the rows behind a surprisingnumber.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Google was seeing an empty page
&lt;/h2&gt;

&lt;p&gt;The old page was a client component that fetched jobs after the page loaded. Google got a spinner.&lt;br&gt;
Now &lt;code&gt;/jobs&lt;/code&gt; is rendered on the server and refreshed every 30 minutes, so the first 25 listings are in the HTML. Filters&lt;br&gt;
live in the URL (&lt;code&gt;/jobs?skill=Python&lt;/code&gt;), so filtand the canonical still points at &lt;code&gt;/jobs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. One page per role and skill, but only whe
&lt;/h2&gt;

&lt;p&gt;People don't search for "remote jobs". They seas" or "remote data analyst jobs". So every roleand skill gets its own page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/jobs/remote-python-jobs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/jobs/remote-software-engineer-jobs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/jobs/remote-jobs-with-salary&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The risk with generated pages is thin content. Two rules keep it honest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A page with fewer than 5 listings is &lt;code&gt;noindex&lt;/code&gt; and left out of the sitemap.&lt;/strong&gt; It still works for visitors, and it
goes back into search on its own when jobs arri.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every intro is written from that page's data&lt;/strong&gt;, not a template with a keyword swapped in:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;18 remote jobs from the last 30 days ask for Python, at 17 companies. The skills most often listed alongside it are AI,&lt;br&gt;
AWS and Java.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Only show numbers that can bear the weigh
&lt;/h2&gt;

&lt;p&gt;My first header showed &lt;strong&gt;"Median listed pay: $1t came from **3 listings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A median of three is an anecdote, so the page nngs with pay before it shows one. Until then it&lt;br&gt;
just says "With pay listed: 3". It's less impressive, and it's true.&lt;/p&gt;

&lt;p&gt;The same goes for the "New" badge. With a two-day window, 13 of the first rows had it, so it meant nothing. Now it marks&lt;br&gt;
only the newest day's listings.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Small things that made the list easier to
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company first, in colour.&lt;/strong&gt; Each row leads the same colour as its initials tile, then the
title, location and up to four skills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where you'll apply.&lt;/strong&gt; "via LinkedIn", "via te", with the site's favicon. The icons are
served from our own &lt;code&gt;/public&lt;/code&gt; folder instead of a favicon service, which would send every visitor's IP address to a third
party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Links out are &lt;code&gt;nofollow&lt;/code&gt;&lt;/strong&gt;, and each board is credited on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building a job aggregator
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count what you show against what you store.&lt;/strong&gt; My problem wasn't a lack of data.&lt;/li&gt;
&lt;li&gt;**Treat surprising numbers as bugs until prov&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated SEO pages need a quality floor&lt;/strong&gt;, and a way to get back out of the index.&lt;/li&gt;
&lt;li&gt;**Don't headline a statistic you wouldn't def&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's free, with no sign-up: &lt;strong&gt;[nokku.payanai.conai.com/jobs)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next I'm adding more free job-board feeds, and . If you've built something similar, I'd like to hear how you handle duplicates across boards. Mine is still fairly simple.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Restaurant Finder With $0 in API Costs (OpenStreetMap, Open-Meteo, No Google Maps)</title>
      <dc:creator>Payanai</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:43:54 +0000</pubDate>
      <link>https://dev.to/payanai_dev/building-a-restaurant-finder-with-0-in-api-costs-openstreetmap-open-meteo-no-google-maps-2mlp</link>
      <guid>https://dev.to/payanai_dev/building-a-restaurant-finder-with-0-in-api-costs-openstreetmap-open-meteo-no-google-maps-2mlp</guid>
      <description>&lt;p&gt;VibeEats is a small food-discovery app: pick a mood ("cozy," "date night,"&lt;br&gt;
"quick bite"), or search for what you're craving, and it matches nearby&lt;br&gt;
places that are actually open, weighted by the weather and time of day.&lt;/p&gt;

&lt;p&gt;I want to write about the part that was actually interesting to build: it&lt;br&gt;
runs on &lt;strong&gt;$0 in API costs&lt;/strong&gt;. Here's how, and what broke along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The first version used Google Places for search, Google Geocoding for&lt;br&gt;
locations, and Google Weather. A single user search fanned out into a&lt;br&gt;
Nearby Search call plus ~20 Place Details calls (for photos, hours, ratings)&lt;br&gt;
plus photo fetches. That's the standard Google Places pattern, and it adds&lt;br&gt;
up fast — the bill got bad enough that I turned the whole discovery feature&lt;br&gt;
off for a few months and left up a bare coupon page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rebuild: free, public data only
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Places&lt;/strong&gt;: &lt;a href="https://www.openstreetmap.org" rel="noopener noreferrer"&gt;OpenStreetMap&lt;/a&gt; via the
&lt;a href="https://overpass-api.de/" rel="noopener noreferrer"&gt;Overpass API&lt;/a&gt; — free, keyless, and the data is
often better-tagged than I expected (more on that below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geocoding&lt;/strong&gt;: &lt;a href="https://photon.komoot.io/" rel="noopener noreferrer"&gt;Photon&lt;/a&gt; with
&lt;a href="https://nominatim.org/" rel="noopener noreferrer"&gt;Nominatim&lt;/a&gt; as a fallback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weather&lt;/strong&gt;: &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; (free tier is licensed
for non-commercial use, worth knowing if you're building something you'll
monetize)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No API keys anywhere in that stack. The only paid things left in the whole&lt;br&gt;
app are hosting (Vercel) and the database (Supabase), both well within free&lt;br&gt;
tiers at this scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: no ratings, no photos, no prices
&lt;/h2&gt;

&lt;p&gt;Google/Yelp give you a star rating, photos, and a price level out of the&lt;br&gt;
box. OpenStreetMap gives you none of that. So instead of faking it, I built&lt;br&gt;
the ranking entirely from what's actually tagge&lt;br&gt;
features (outdoor seating, takeaway, wheelchair access...), distance, and&lt;br&gt;
current weather/time of day. Roughly:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
// simplified — the real scoring blends ~6 weighted signals&lt;br&gt;
function scorePlace(place, vibe, weather, hour)&lt;br&gt;
  const kindFit = KIND_WEIGHTS[vibe][place.kind] ?? 0.1;&lt;br&gt;
  const cuisineFit = bestCuisineMatch(place.cuiutral, not punished, if untagged&lt;br&gt;
  const weatherFit = place.hasOutdoorSeating &amp;amp;&amp;amp; isNiceOut(weather) ? 0.35 : 0;&lt;br&gt;
  const openBonus = isOpenNow(place) ? 8 : -25;&lt;br&gt;
  return blend([kindFit, cuisineFit, weatherFit]) + openBonus + proximityScore(place.distance);&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I did the same thing for price. There's a &lt;code&gt;$/$$/$$$&lt;/code&gt; filter in the app now,&lt;br&gt;
and before building it I actually checked: I pus&lt;br&gt;
across three cities (Austin, Bengaluru, New York) and looked for any&lt;br&gt;
price-related OSM tag. &lt;strong&gt;Zero had one.&lt;/strong&gt; Not pathe&lt;br&gt;
price filter is a heuristic from venue type + cuisine + chain status,&lt;br&gt;
labeled "estimated" everywhere it shows up, nev&lt;br&gt;
number. It's a small thing, but I think "check before you build" mattered&lt;br&gt;
more here than usual — it would've been easy to&lt;br&gt;
levels exist somewhere in OSM and ship something that quietly lied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching, because Overpass is slow
&lt;/h2&gt;

&lt;p&gt;Public Overpass instances are free but not fast — a cold query can take&lt;br&gt;
10–30 seconds, and they rate-limit. Calling tha&lt;br&gt;
non-starter. So places are cached by geo-cell: the world's cut into ~2km&lt;br&gt;
tiles (falling back to ~9km in sparse areas), e&lt;br&gt;
served to everyone after that. Cache layers are memory → local disk →&lt;br&gt;
Supabase, in that order, so a serverless cold sond&lt;br&gt;
wait for every visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search over tagged data has a fun problem
&lt;/h2&gt;

&lt;p&gt;Free-text search ("biryani near me") searches within the places already&lt;br&gt;
loaded for the area — no extra API call. The inap&lt;br&gt;
tags dishes more literally than I expected. Querying restaurants near&lt;br&gt;
Hyderabad, India, for &lt;code&gt;cuisine=biryani&lt;/code&gt; returns&lt;br&gt;
not everywhere — so there's a small synonym table (&lt;code&gt;biryani&lt;/code&gt; → also match&lt;br&gt;
&lt;code&gt;indian&lt;/code&gt;/&lt;code&gt;pakistani&lt;/code&gt;/&lt;code&gt;mughlai&lt;/code&gt; cuisine tags) as&lt;br&gt;
specific dish isn't a literal tag. Tested against real data, it correctly&lt;br&gt;
surfaced well-known local biryani spots that weot&lt;br&gt;
&lt;code&gt;biryani&lt;/code&gt; — the fallback earning its keep, not just theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone trying this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OSM data quality varies a lot by city — dense in places with an active
local mapping community, thin elsewhere. Buil
(fall back to a wider search radius) rather than assuming uniform density.&lt;/li&gt;
&lt;li&gt;Don't call public Overpass instances per-reqund
consider hedging across mirrors since any single instance can be flaky.&lt;/li&gt;
&lt;li&gt;If a data source doesn't have a field you wan,
resist the urge to synthesize something that looks authoritative. Label
estimates as estimates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://vibeeats.app" rel="noopener noreferrer"&gt;https://vibeeats.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about any of this, es&lt;br&gt;
built on top of Overpass/OSM data before — curious what I'm missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>api</category>
    </item>
    <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>
