<?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: Wei Bin</title>
    <description>The latest articles on DEV Community by Wei Bin (@visionwei).</description>
    <link>https://dev.to/visionwei</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%2F4237079%2Fc1708050-a3dc-44d8-a4af-893ce5ac25fb.png</url>
      <title>DEV Community: Wei Bin</title>
      <link>https://dev.to/visionwei</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/visionwei"/>
    <language>en</language>
    <item>
      <title>I mapped a YouTube channel's entire catalog in two runs — for under a cent</title>
      <dc:creator>Wei Bin</dc:creator>
      <pubDate>Sun, 15 Nov 2026 08:09:40 +0000</pubDate>
      <link>https://dev.to/visionwei/i-mapped-a-youtube-channels-entire-catalog-in-two-runs-for-under-a-cent-1k6m</link>
      <guid>https://dev.to/visionwei/i-mapped-a-youtube-channels-entire-catalog-in-two-runs-for-under-a-cent-1k6m</guid>
      <description>&lt;p&gt;Competitor research on YouTube usually dies in one of two places: you click through forty videos by hand and give up, or you start writing a scraper and give up slightly later, somewhere around the third rate-limit.&lt;/p&gt;

&lt;p&gt;I needed a channel's full catalog — titles, URLs, and the view numbers that show which topics actually land — for a content teardown. Instead of building anything, I ran two existing tools back to back. Total elapsed time: under a minute. Total compute cost: less than a cent. Here's the exact pattern, with the real output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Enumerate the catalog (fast, cheap, deliberately shallow)
&lt;/h2&gt;

&lt;p&gt;The first run takes a channel handle and returns its video catalog as structured rows. I pointed it at @OpenAI with a 30-video cap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wall time:&lt;/strong&gt; about 7 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows back:&lt;/strong&gt; 30&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute cost:&lt;/strong&gt; $0.001&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each row carries the skeleton: &lt;code&gt;videoId&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, channel name and channel ID. What it deliberately does &lt;strong&gt;not&lt;/strong&gt; carry: view counts, durations, publish dates. Those fields come back null, and that's a design choice worth understanding — enumerating a catalog and enriching a catalog are different jobs with different costs. The enumeration pass is built to be nearly free, so you can afford to run it on every competitor you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Enrich only what you'll actually read
&lt;/h2&gt;

&lt;p&gt;From the 30 rows, I fed 3 video URLs into the companion details tool. Now the expensive fields show up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Video&lt;/th&gt;
&lt;th&gt;Views&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Likes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How Mozilla uses GPT-6 Cyber to find and fix Firefox vulnerabilities&lt;/td&gt;
&lt;td&gt;14,565&lt;/td&gt;
&lt;td&gt;20:19&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cyber at the Frontier: Introducing Daybreak Red Team &amp;amp; Daybreak Defense Factory&lt;/td&gt;
&lt;td&gt;51,193&lt;/td&gt;
&lt;td&gt;34:36&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What Codex Unlocks for Holiday Extras&lt;/td&gt;
&lt;td&gt;6,772&lt;/td&gt;
&lt;td&gt;2:26&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wall time:&lt;/strong&gt; seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute cost:&lt;/strong&gt; $0.0024 for the 3 videos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the honest gaps: one row came back without a like count. Per-video enrichment reads public pages one by one, and public pages don't always expose every counter. A tool that returns a confident number for every field on every video is inventing some of them; I'd rather get a null I can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the two-step split matters
&lt;/h2&gt;

&lt;p&gt;If you enrich inline during enumeration, a 500-video channel costs 500 detail fetches before you know whether you care about 490 of them. Split the steps and the economics flip:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate everything&lt;/strong&gt; — pennies, seconds. Now you have the full topic map: what they publish, how often, how they title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort and filter locally&lt;/strong&gt; — by title keywords, by series, by recency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enrich the shortlist&lt;/strong&gt; — pay the per-video cost only for the rows that will actually feed a decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same pattern works for TikTok profiles, playlists and search results — enumerate wide and cheap, enrich narrow and deep. It's also how I'd advise evaluating any data tool: ask what the enumeration pass costs before you ask what the detail pass returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it yourself
&lt;/h2&gt;

&lt;p&gt;Both tools are public Apify actors, no code required — paste a channel handle, set a cap, download JSON or CSV:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Channel catalog: &lt;a href="https://apify.com/bin_ai_tools/youtube-channel-videos" rel="noopener noreferrer"&gt;https://apify.com/bin_ai_tools/youtube-channel-videos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video details: &lt;a href="https://apify.com/bin_ai_tools/youtube-video-details" rel="noopener noreferrer"&gt;https://apify.com/bin_ai_tools/youtube-video-details&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather automate it, both run through the Apify API with the same inputs, so the two-step pattern drops straight into a script or a scheduled job — a weekly competitor-catalog diff is about twenty lines.&lt;/p&gt;

&lt;p&gt;The teardown I built on top of this took an afternoon. The data collection took a minute. That ratio is the whole point: the analysis is the work; the gathering shouldn't be.&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How to track a live flight with one API call (real ADS-B data, no enterprise feed)</title>
      <dc:creator>Wei Bin</dc:creator>
      <pubDate>Sun, 15 Nov 2026 08:00:14 +0000</pubDate>
      <link>https://dev.to/visionwei/how-to-track-a-live-flight-with-one-api-call-real-ads-b-data-no-enterprise-feed-4fln</link>
      <guid>https://dev.to/visionwei/how-to-track-a-live-flight-with-one-api-call-real-ads-b-data-no-enterprise-feed-4fln</guid>
      <description>&lt;p&gt;Every side project that touches flight data hits the same wall: the well-known flight data feeds are priced for airlines and enterprises, and the "free" tiers are either schedule data dressed up as live, or demos that die the moment you ship.&lt;/p&gt;

&lt;p&gt;I wanted actual live positions — where is this aircraft, right now — for a tracker widget I was building. So I built FlightScope on top of the open ADS-B receiver network (adsb.lol) plus the OurAirports and OpenFlights datasets, and I've been running it as a public API since. This post is a walkthrough of what the data really looks like, with responses I captured live while writing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One call: the complete live profile of a flight
&lt;/h2&gt;

&lt;p&gt;Give the API a flight number — IATA (&lt;code&gt;TS655&lt;/code&gt;) or ICAO (&lt;code&gt;TSC655&lt;/code&gt;) form both work — and you get the operator, the aircraft, and its live position in a single response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"flight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"iata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"TS655"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"callsign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"TSC655"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"airline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Air Transat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"iata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"TS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"icao"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"TSC"&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="nl"&gt;"aircraft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"icao24"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"c051d1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"registration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"C-GEZO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"typeCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A321"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"live"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;40.889648&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;-73.986694&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"altitudeFt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"groundSpeedKt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;464.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"trackDeg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;19.76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"verticalRateFpm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"onGround"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"phase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"airborne"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"squawk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"6166"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"lastSeenSecondsAgo"&lt;/span&gt;&lt;span class="p"&gt;:&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="nl"&gt;"ageSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an Air Transat A321 at 35,025 ft doing 464 knots over New Jersey, climbing gently at 128 feet per minute, captured the second I asked. The fields worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lastSeenSecondsAgo&lt;/code&gt; / &lt;code&gt;ageSeconds&lt;/code&gt;&lt;/strong&gt; — ADS-B data is only honest if it tells you how old it is. Every live response carries its freshness, and positions served from the short edge cache say so. A tracker that hides this is showing you a plane that may have landed ten minutes ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;phase&lt;/code&gt;&lt;/strong&gt; — ground vs airborne, derived from the broadcast itself (the barometric altitude field), and labelled as derived in the full response. It is not the airline's official status, and the API says that in the payload rather than in fine print.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "What's flying over me right now?"
&lt;/h2&gt;

&lt;p&gt;The endpoint I end up demoing most is the radius query. One call over JFK (40.6413, −73.7781, 80 km radius) came back with &lt;strong&gt;30 aircraft&lt;/strong&gt; — callsigns, types, altitudes, speeds, ground flags — everything you need to draw a live map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="na"&gt;GET /flights/near?lat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;40.6413&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mf"&gt;73.7781&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;radiusKm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;
→ &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"flights"&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="s2"&gt;"callsign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"TSC655"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"typeCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A321"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"altitudeFt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"groundSpeedKt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;464.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"onGround"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No polling a firehose, no WebSocket plumbing — a stateless GET you can call from a cron job, a Cloudflare Worker, or a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The batch trick: 100 flights, one call
&lt;/h2&gt;

&lt;p&gt;If you're building a fleet board or a multi-flight tracker, per-flight polling burns quota fast. The batch endpoint takes up to &lt;strong&gt;100 flight numbers in one POST&lt;/strong&gt; and returns one result per flight plus a summary — and the whole batch counts as &lt;strong&gt;one call&lt;/strong&gt; against your quota:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST &lt;span class="s2"&gt;"https://flightscope-live-flight-tracking-and-airport-data-api.p.rapidapi.com/flights/batch"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"X-RapidAPI-Key: YOUR_KEY"&lt;/span&gt; -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"flights": ["TS655", "AAL751", "DL123"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pricing-of-attention detail — batch = 1 call — is deliberate. I'd rather you build the dashboard than ration refreshes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Airports without a second API
&lt;/h2&gt;

&lt;p&gt;Trackers always end up needing airport context too, so the same API carries profiles for &lt;strong&gt;3,270 commercial airports&lt;/strong&gt; from OurAirports (public domain): codes, coordinates, elevation, municipality, and links. &lt;code&gt;GET /airport/KJFK&lt;/code&gt; returns John F. Kennedy International with its IATA/ICAO codes, position and elevation in the same response shape as everything else. There's also &lt;code&gt;/airports/search&lt;/code&gt; for name/city/code lookup and &lt;code&gt;/aircraft/{icao24}&lt;/code&gt; for looking an airframe up by transponder hex.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits of ADS-B
&lt;/h2&gt;

&lt;p&gt;Three things this kind of data cannot do, which I learned to state up front because users discover them anyway:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If it isn't broadcasting, it isn't there.&lt;/strong&gt; A flight with no live ADS-B signal returns &lt;code&gt;404 FLIGHT_NOT_LIVE&lt;/code&gt;. That means "no receiver heard it just now" — not "cancelled", not "doesn't exist". Military traffic and some regions are simply dark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No schedules, no gates, no delay minutes.&lt;/strong&gt; ADS-B tells you where an aircraft physically is. Departure boards, terminals and baggage belts live in airline systems, not in radio broadcasts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage follows the receiver network.&lt;/strong&gt; Over the US and Europe it's dense; over oceans it's sparse. The &lt;code&gt;ageSeconds&lt;/code&gt; field is how you stay truthful about that in your UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Within those limits, though, the data is genuinely live and genuinely open — the sources (adsb.lol, OurAirports, OpenFlights) are credited in every response, as their licences require.&lt;/p&gt;

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

&lt;p&gt;FlightScope is on RapidAPI with a free tier that's enough to wire up and test a real integration, plus paid tiers if your tracker takes off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RapidAPI: &lt;a href="https://rapidapi.com/binbin7418/api/flightscope-live-flight-tracking-and-airport-data-api" rel="noopener noreferrer"&gt;https://rapidapi.com/binbin7418/api/flightscope-live-flight-tracking-and-airport-data-api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it — a spotted-over-my-house bot, a fleet board, a spotter app — I'd love to see it. And if there's a field you wish the response had, tell me; the schema is versioned precisely so it can grow without breaking you.&lt;/p&gt;

</description>
      <category>api</category>
      <category>aviation</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I tested 3 ways to verify catch-all email addresses — here's what actually works</title>
      <dc:creator>Wei Bin</dc:creator>
      <pubDate>Sun, 15 Nov 2026 07:11:09 +0000</pubDate>
      <link>https://dev.to/visionwei/i-tested-3-ways-to-verify-catch-all-email-addresses-heres-what-actually-works-551m</link>
      <guid>https://dev.to/visionwei/i-tested-3-ways-to-verify-catch-all-email-addresses-heres-what-actually-works-551m</guid>
      <description>&lt;p&gt;Somewhere between a quarter and a third of the addresses on a typical B2B list sit on catch-all domains. I know this because I build an email validation API, and catch-all is the question every user eventually asks:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Your API says this address is fine. But the domain is catch-all — so is the mailbox real or not?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is that nobody can tell you that from the outside. But "nobody can" is not what the verification industry advertises, so I spent some time testing the three approaches that are actually sold as catch-all verification, using real domains and my own API's live output. Here's what each one really does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "catch-all" means, in one paragraph
&lt;/h2&gt;

&lt;p&gt;A catch-all (accept-all) mail server is configured to accept mail for &lt;strong&gt;any&lt;/strong&gt; local part at the domain — real employees, people who left years ago, and addresses that never existed. It's a deliberate, legitimate setup, common at companies that would rather receive a mis-addressed message than lose it. Google Workspace and Microsoft 365 both let admins enable it, which is why so much of B2B email lives behind it.&lt;/p&gt;

&lt;p&gt;The consequence: from the outside, every address at that domain looks identical. And that breaks the standard verification playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 1: SMTP probing — the server says yes to everything
&lt;/h2&gt;

&lt;p&gt;The classic verification trick is to open an SMTP conversation with the receiving server, start a delivery to the address in question, and read the response code — then hang up before sending anything. A &lt;code&gt;250&lt;/code&gt; means "accepted", a &lt;code&gt;550&lt;/code&gt; means "no such mailbox".&lt;/p&gt;

&lt;p&gt;On a normal domain this works reasonably well. On a catch-all domain it proves nothing, by definition: the server answers &lt;code&gt;250&lt;/code&gt; for the CEO's real address and for &lt;code&gt;totally-invented-zzq@&lt;/code&gt; with exactly the same enthusiasm. The probe tells you the server accepts mail — which you already knew from its MX records.&lt;/p&gt;

&lt;p&gt;There's also a practical problem nobody mentions: outbound port 25 is blocked or throttled in most cloud and corporate networks, so running your own probes at scale means fighting your infrastructure provider before you fight the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; on catch-all domains, an SMTP probe returns a confident-looking answer that contains zero information about the mailbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 2: Big-vendor scoring — better, but look at the resolution rates
&lt;/h2&gt;

&lt;p&gt;The large verification vendors know probing fails on catch-all, so they layer scoring on top: domain age, provider patterns, and — the part you can't replicate — engagement data from their own sending networks (did this address open, click, bounce somewhere else before?).&lt;/p&gt;

&lt;p&gt;Some of that is genuinely useful. But published benchmark figures for how often vendors actually &lt;em&gt;resolve&lt;/em&gt; a catch-all address to a firm verdict are sobering: roughly &lt;strong&gt;12% for ZeroBounce, 8% for NeverBounce, 5% for MillionVerifier&lt;/strong&gt;. In other words, the best-known vendor in the industry settles about one catch-all address in eight. The other seven come back as some flavour of "unknown" — usually at the same per-address price as a resolved one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; real signal, but you're paying full price for a coin that lands on its edge 7 times out of 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 3: Domain-level signals — grade the risk, don't fake the verdict
&lt;/h2&gt;

&lt;p&gt;The third approach gives up on the mailbox question entirely and answers a different, answerable one: &lt;em&gt;what kind of domain is this, and how much risk does an address on it carry?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is what I built into my own API (EmailCheck), so let me show you real output instead of describing it. I ran one batch call with 10 addresses chosen to cover the signal space — consumer providers, corporate domains, a disposable service, a typo, a domain that doesn't exist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;catchAllRisk&lt;/th&gt;
&lt;th&gt;Domain age (days)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:ceo@gmail.com"&gt;ceo@gmail.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;11,417&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:anyone@outlook.com"&gt;anyone@outlook.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;11,777&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:support@google.com"&gt;support@google.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;10,653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:randomperson98765@google.com"&gt;randomperson98765@google.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;10,653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:person@microsoft.com"&gt;person@microsoft.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;12,981&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:hello@stripe.com"&gt;hello@stripe.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;11,387&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:hi@linear.app"&gt;hi@linear.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;null (no RDAP record)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:someone@mailinator.com"&gt;someone@mailinator.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;risky&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;8,536&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:user@gamil.com"&gt;user@gamil.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;risky&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;10,366&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:test@nonexistent-domain-xyz-123.com"&gt;test@nonexistent-domain-xyz-123.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;invalid&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four rows in that table taught me more than the marketing pages of every vendor combined:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The invented address passes.&lt;/strong&gt; &lt;code&gt;randomperson98765@google.com&lt;/code&gt; is an address I made up; it scores 100. Domain-level checks verify the &lt;em&gt;domain&lt;/em&gt; — its syntax, its MX records, its age — not the mailbox. Any tool that implies otherwise for a catch-all domain is selling you a score dressed up as a verdict. The correct output is the graded signal in the &lt;code&gt;catchAllRisk&lt;/code&gt; column: &lt;code&gt;possible&lt;/code&gt; for business-mail patterns where catch-all is commonly enabled (Microsoft 365 tenants, Google Workspace domains like stripe.com and linear.app), &lt;code&gt;low&lt;/code&gt; for the big consumer providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The typo domain accepts mail too.&lt;/strong&gt; &lt;code&gt;user@gamil.com&lt;/code&gt; is a typo of Gmail — and gamil.com has its own MX records and has existed for 10,366 days. Typosquatter domains accept mail (that's the point of them), so "the domain accepts mail" can never be your only check. The API flags it as a likely typo, caps the score at 60, and returns the correction (&lt;code&gt;user@gmail.com&lt;/code&gt;) in the &lt;code&gt;suggestion&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disposable services are true catch-alls.&lt;/strong&gt; Mailinator accepts literally everything — that's its product. It comes back &lt;code&gt;risky&lt;/code&gt; at 50: disposable flag set, catch-all risk &lt;code&gt;possible&lt;/code&gt;. Both signals are true at once, and both matter for a signup form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing data is returned as missing.&lt;/strong&gt; &lt;code&gt;hi@linear.app&lt;/code&gt; has &lt;code&gt;domainAgeDays: null&lt;/code&gt; because RDAP has no registration record the API could read. The field doesn't estimate, doesn't interpolate — it says &lt;code&gt;null&lt;/code&gt; and names its source when it has one. I'd rather ship a null than a confident guess, because every downstream decision (send / segment / drop) inherits whatever the API claims.&lt;/p&gt;

&lt;p&gt;Here's the raw response for the typo row, so you can see the full shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user@gamil.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"risky"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"syntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"domainExists"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"mxFound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"disposable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"roleAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"freeProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"mxRecords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mail.gamil.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"typoLikely"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"domainAgeDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10366&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"domainAgeSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"RDAP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"catchAllRisk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"possible"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; you don't get a yes/no on the mailbox — nobody honestly has one — but you get an auditable risk picture per address, which is what list decisions actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do with catch-all addresses
&lt;/h2&gt;

&lt;p&gt;After all this testing, my working rules are boring and practical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't delete them.&lt;/strong&gt; If 30% of your B2B list is catch-all, deleting them deletes your market. The big vendors' own numbers say most of those addresses are simply &lt;em&gt;unresolvable&lt;/em&gt;, not fake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment them.&lt;/strong&gt; Send to catch-all addresses as their own cohort, after your clean addresses, so a bad pocket can't drag your main sender reputation down with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let bounces teach you.&lt;/strong&gt; A hard bounce from a catch-all domain is real information — that mailbox genuinely doesn't exist. Feed those back into your list; it's the one ground truth this problem gives you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At signup, act on the other signals.&lt;/strong&gt; Disposable flag, two-day-old domain, typo suggestion — those are actionable in real time, catch-all or not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool, briefly
&lt;/h2&gt;

&lt;p&gt;EmailCheck is live on RapidAPI with a free tier (100 calls/month), single and batch endpoints (up to 100 addresses per call, with a summary report), and the fields you saw above on every response. There's also a free bulk checker and a $9.99 pack of 3,500 checks if you'd rather not think about subscriptions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EmailCheck on RapidAPI: &lt;a href="https://rapidapi.com/binbin7418/api/emailcheck-email-validation-api" rel="noopener noreferrer"&gt;https://rapidapi.com/binbin7418/api/emailcheck-email-validation-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free bulk checker: &lt;a href="https://email.newkwlab.com/bulk-email-checker/" rel="noopener noreferrer"&gt;https://email.newkwlab.com/bulk-email-checker/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The longer catch-all write-up: &lt;a href="https://email.newkwlab.com/questions/can-a-catch-all-email-address-be-verified/" rel="noopener noreferrer"&gt;https://email.newkwlab.com/questions/can-a-catch-all-email-address-be-verified/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The $9.99 pack (3,500 checks): &lt;a href="https://binbinster.gumroad.com/l/newkwlab-credits-3500" rel="noopener noreferrer"&gt;https://binbinster.gumroad.com/l/newkwlab-credits-3500&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you verify email for a living, I'd genuinely like to hear how you treat catch-all addresses. The industry mostly pretends this problem is solved. It isn't, and the pretence is priced per address.&lt;/p&gt;

</description>
      <category>email</category>
      <category>api</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I built a tool that puts two people into one Christmas photo — then tested whether the AI keeps their faces</title>
      <dc:creator>Wei Bin</dc:creator>
      <pubDate>Sun, 15 Nov 2026 06:43:22 +0000</pubDate>
      <link>https://dev.to/visionwei/i-built-a-tool-that-puts-two-people-into-one-christmas-photo-then-tested-whether-the-ai-keeps-4cmk</link>
      <guid>https://dev.to/visionwei/i-built-a-tool-that-puts-two-people-into-one-christmas-photo-then-tested-whether-the-ai-keeps-4cmk</guid>
      <description>&lt;p&gt;Every December, the same small problem shows up in my messages: couples who don't have a single good photo together. One of them travels for work, they live in different cities, or they simply never got around to taking a proper holiday picture. The photos exist — just never in the same frame.&lt;/p&gt;

&lt;p&gt;So I built a small tool for exactly that. It's called SnapDuo. You upload one portrait of each person, pick a style, and it generates one photo of the two of you together.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Two photos in, one photo out. There are three styles right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Christmas Couple&lt;/strong&gt; — the classic: matching red sweaters in front of a Christmas tree, mugs of cocoa, holiday-card energy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Christmas Market&lt;/strong&gt; — a candid-style shot at a Christmas market, Santa hats on, hot drinks in hand, crowds blurred behind you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny Boyfriend&lt;/strong&gt; — the meme one: she holds a miniature version of him standing in her palm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, generation runs on the nano-banana-2-1 model through the Kie API, at 1K resolution (about 3 credits per image). Before paying for any production generations, I validated the prompts with a free image tool first — I didn't want to burn credits discovering a prompt was broken.&lt;/p&gt;

&lt;p&gt;The flow is deliberately boring: upload two portraits, get a watermarked preview for free, and only pay if the result actually looks like you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I actually cared about: does it keep their faces?
&lt;/h2&gt;

&lt;p&gt;AI couple photos have one failure mode that kills the whole product: the output looks nice, but it doesn't look like &lt;em&gt;them&lt;/em&gt;. A pretty picture of two strangers is worthless here.&lt;/p&gt;

&lt;p&gt;So before calling this done, I ran a stability test. Three templates, two generations each, six images total. Same input portraits every time. For each output I checked three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Faces&lt;/strong&gt; — do both people still look like the people in the input photos?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hands&lt;/strong&gt; — the usual AI disaster zone. Any extra fingers, melted grips on the mugs, broken palms?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composition&lt;/strong&gt; — does the same template produce a comparable shot twice, or is it a slot machine?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: 6 out of 6 passed. Faces were recognizable in every generation, no hand or face failures in any of the six, and each template's two runs were consistent in composition and quality — similar framing, similar lighting, no wild swings. That consistency matters more to me than one lucky great shot, because users will re-roll and compare.&lt;/p&gt;

&lt;p&gt;Each photo also gets 3 free re-rolls, because "usually consistent" is not "always," and nobody should pay to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: flat pack, no credits
&lt;/h2&gt;

&lt;p&gt;I went back and forth on pricing and landed on the simplest thing I could: previews are free (watermarked), and a single &lt;strong&gt;$9.99 pack unlocks any 3 photos in full HD without the watermark&lt;/strong&gt;. No subscription, no expiring coins, no credit math.&lt;/p&gt;

&lt;p&gt;The reasoning: the preview &lt;em&gt;is&lt;/em&gt; the product demo. If the free preview doesn't look like you, you shouldn't pay — and if it does, $9.99 for three finished photos is an easy yes or no. I didn't want a pricing page that needs its own tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong (so far)
&lt;/h2&gt;

&lt;p&gt;The honest flaw: background signage. In the Christmas Market shots, text on the market stall signs in the background comes out garbled — readable-looking at a glance, nonsense up close. It's blurred background detail, so it doesn't ruin the photos, but it's there and I'm not going to pretend otherwise. Faces, hands, and clothing rendered clean; distant text did not.&lt;/p&gt;

&lt;p&gt;Privacy-wise, I kept it strict: uploaded photos are used only to generate your picture and are automatically deleted within 7 days. They're never sold or used for training.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;It's Christmas season, which is either perfect timing or a deadline, depending on how you look at it. The product is live and the templates are tested. What I don't have yet is proof that strangers want this — so the real test for me is whether real users show up and unlock photos before the end of November. If they do, I'll add more scenes. If they don't, I'll have learned something cheap.&lt;/p&gt;

&lt;p&gt;If you want to try it: &lt;a href="https://api.newkwlab.com/duo/" rel="noopener noreferrer"&gt;SnapDuo&lt;/a&gt; — the preview is free. There's also a short guide on &lt;a href="https://snapduo-guide.pages.dev/how-it-works/" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;, including the photo rules that make results look most like you.&lt;/p&gt;

&lt;p&gt;I'd genuinely like feedback on two things: does the face consistency hold up with &lt;em&gt;your&lt;/em&gt; photos, and is the flat-pack pricing clearer than the credit systems other tools use? Building this solo, so outside eyes help.&lt;/p&gt;

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