<?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: Low Key Builds</title>
    <description>The latest articles on DEV Community by Low Key Builds (@lowkeybuilds).</description>
    <link>https://dev.to/lowkeybuilds</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%2F4181324%2F391d601f-aae5-4a33-97a0-84240cadeb8d.png</url>
      <title>DEV Community: Low Key Builds</title>
      <link>https://dev.to/lowkeybuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lowkeybuilds"/>
    <language>en</language>
    <item>
      <title>Urdu voice notes to text on a free tier: Whisper pinned to Urdu, Roman Urdu without drift, and an honest error rate</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:16:54 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/urdu-voice-notes-to-text-on-a-free-tier-whisper-pinned-to-urdu-roman-urdu-without-drift-and-an-21gf</link>
      <guid>https://dev.to/lowkeybuilds/urdu-voice-notes-to-text-on-a-free-tier-whisper-pinned-to-urdu-roman-urdu-without-drift-and-an-21gf</guid>
      <description>&lt;p&gt;Pakistan runs on WhatsApp voice notes. WhatsApp now writes transcripts for some languages, but Urdu is not in any list of supported languages I could find, and a Hindi transcript of Urdu speech comes out in Devanagari, which most Pakistanis do not read.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://sunlo.appin60.com" rel="noopener noreferrer"&gt;Sunlo&lt;/a&gt;, a free web app: drop in a voice note, get Urdu script, Roman Urdu (the way people actually type on WhatsApp) and an English translation. Disclosure: I made it, and this post is how it works, including what it gets wrong.&lt;/p&gt;

&lt;p&gt;The constraints: no budget, no audio stored anywhere, and a cheap Android phone as the main client.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Whisper on Workers AI, language pinned to Urdu
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers AI hosts &lt;code&gt;whisper-large-v3-turbo&lt;/code&gt;. The one setting that matters is &lt;code&gt;language: 'ur'&lt;/code&gt;. Left to auto-detect, Whisper often decides Urdu speech is Hindi (the spoken languages are close) and writes Devanagari.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cf/openai/whisper-large-v3-turbo&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;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64Audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// WhatsApp .opus works as-is&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// never auto-detect for Urdu&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transcribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vad_filter&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="c1"&gt;// skips silence in voice notes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WhatsApp's &lt;code&gt;.opus&lt;/code&gt; files go straight in; no ffmpeg needed on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A hard daily cap, enforced before the model runs
&lt;/h2&gt;

&lt;p&gt;The free Workers AI allocation is 10,000 "neurons" a day. Whisper turbo costs about 46 neurons per audio minute ($0.00051/min at $0.011 per 1k neurons). The Worker keeps a per-day counter in D1 and &lt;strong&gt;reserves&lt;/strong&gt; the estimated cost before calling the model, so parallel requests cannot overshoot, then corrects it with the real duration afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;46.4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;40&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;b&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT neurons FROM budget WHERE day=?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&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;b&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;neurons&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="s1"&gt;daily_cap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`INSERT INTO budget (day, neurons) VALUES (?, ?)
  ON CONFLICT(day) DO UPDATE SET neurons = neurons + excluded.neurons`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;est&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-visitor limits use a salted daily hash of the IP, so nothing identifying is kept. Audio and text are never written anywhere; D1 only holds counts, durations and timings.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Roman Urdu: transliterate, do not translate
&lt;/h2&gt;

&lt;p&gt;Roman Urdu and English come from one call to Llama 4 Scout (about 11 neurons for a short note). The first prompt said "write it in Roman Urdu" and the model drifted into English words ("patient ko dusre se alag karna include hai"). Saying TRANSLITERATE, with two examples of how people type, fixed it:&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="nl"&gt;"roman"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TRANSLITERATE,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translate.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Urdu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;word&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;same&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Urdu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;word&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Latin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;letters,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;way&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Pakistanis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;WhatsApp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main kal aaunga, aap fikar na karein"&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Llama 3.1 8B was cheaper but changed words ("aaj", today, became "kal"). Scout, Llama 3.3 70B and gpt-oss-120b were all fine on clean text.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Long recordings: cut at silence, in the browser
&lt;/h2&gt;

&lt;p&gt;Lectures and interviews are longer than one request should be. The browser decodes the file with &lt;code&gt;AudioContext({ sampleRate: 16000 })&lt;/code&gt;, which also resamples to what Whisper wants, then cuts it into ~150-second pieces. Each cut goes at the quietest 50 ms window in the last quarter of the piece, so words are not split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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;e&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bestE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&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;Pieces are transcribed one by one, joined, and then translated in a single pass, because translating fragments separately produced nonsense at the seams.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Share target: one tap from WhatsApp
&lt;/h2&gt;

&lt;p&gt;On Android, an installed PWA can appear in WhatsApp's Share sheet. The manifest declares a &lt;code&gt;share_target&lt;/code&gt; with a multipart POST; the service worker catches it, stashes the file in Cache Storage and redirects to the page, which picks it up:&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="nl"&gt;"share_target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/share-target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enctype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multipart/form-data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"audio/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".opus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".ogg"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How accurate is it?
&lt;/h2&gt;

&lt;p&gt;I ran the first 30 clips of the FLEURS Urdu test set (28 downloaded) through the live API, encoded as Opus like a WhatsApp note:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Word error rate&lt;/th&gt;
&lt;th&gt;Character error rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;20.8%&lt;/td&gt;
&lt;td&gt;8.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White noise at 15 dB SNR&lt;/td&gt;
&lt;td&gt;23.0%&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That counts spelling variants and "70" against "ستر" as errors, so the text reads better than one wrong word in five suggests. Real voice notes are harder than read sentences.&lt;/p&gt;

&lt;p&gt;The failure I worry about most: in a noisy test, "jald rihaa" (released quickly) was heard as "jal raha" (burning), and every model I tried, including the 70B and 120B ones, then translated the sentence as hostages being set on fire. A recognition slip becomes an alarming English sentence. The prompt now tells the model not to invent dramatic meanings, but the honest fix is in the interface: every result says it is a machine transcript and to check money, medicine, addresses and times against the audio. The method and examples are on the &lt;a href="https://sunlo.appin60.com/accuracy.html" rel="noopener noreferrer"&gt;accuracy page&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://sunlo.appin60.com" rel="noopener noreferrer"&gt;sunlo.appin60.com&lt;/a&gt; is free, needs no account, and handles notes up to 10 minutes. If you speak Urdu, I would like to hear where it goes wrong: &lt;a href="mailto:sunlo@appin60.com"&gt;sunlo@appin60.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>ai</category>
      <category>whisper</category>
    </item>
    <item>
      <title>The 201st unit: modelling Pakistan's 2026 electricity slab tariff from a scanned SRO</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:43:19 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/the-201st-unit-modelling-pakistans-2026-electricity-slab-tariff-from-a-scanned-sro-32ha</link>
      <guid>https://dev.to/lowkeybuilds/the-201st-unit-modelling-pakistans-2026-electricity-slab-tariff-from-a-scanned-sro-32ha</guid>
      <description>&lt;p&gt;Pakistan's household electricity tariff has a cliff in it. If a home stays at 200 units (kWh) or less in each of the last six months, it is billed as &lt;strong&gt;protected&lt;/strong&gt;. Use 201 units once and the home is &lt;strong&gt;non-protected&lt;/strong&gt; for the next six bills, even if usage drops again.&lt;/p&gt;

&lt;p&gt;I wanted to know what that one unit actually costs, so I modelled the tariff from the government notification and built a free calculator in Urdu around it. Disclosure: I built the tool linked at the end. This post is about the modelling, which was more interesting than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find the real source, even when it is a scan
&lt;/h2&gt;

&lt;p&gt;Search results for "LESCO bill calculator" are full of calculators that disagree. I found per-unit rates of 10.54, 13.99, 7.74 and 10.06 quoted for the same "protected 101-200" band, plus tools still adding a 1.5% electricity duty and a Rs 35 TV fee that were dropped from bills in July 2025, or using 17% GST.&lt;/p&gt;

&lt;p&gt;The binding document is &lt;strong&gt;S.R.O. 279(I)/2026&lt;/strong&gt; (Ministry of Energy, 12 February 2026), which notifies NEPRA's decision on "rationalization of tariff of XWDISCOs and K-Electric". NEPRA hosts it as a 54-page scanned PDF with no text layer, on a server slow enough that I had to resume the download several times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; sro279.pdf &lt;span class="s2"&gt;"https://nepra.org.pk/tariff/Tariff/Notifications/2026/02%20Feb/S.R.O.%20279(1)2026%20dated%2012-02-2026.pdf"&lt;/span&gt;
pdftoppm &lt;span class="nt"&gt;-r&lt;/span&gt; 60 &lt;span class="nt"&gt;-png&lt;/span&gt; sro279.pdf t   &lt;span class="c"&gt;# thumbnails, to find the tables&lt;/span&gt;
pdftoppm &lt;span class="nt"&gt;-r&lt;/span&gt; 200 &lt;span class="nt"&gt;-png&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 15 &lt;span class="nt"&gt;-l&lt;/span&gt; 15 sro279.pdf z   &lt;span class="c"&gt;# Annex A-1 at reading resolution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contact sheets of the thumbnails found the residential schedule (Annex A-1) and the K-Electric schedule (Annex C) quickly. Both carry the same consumer-end rates, so one table serves all eleven distribution companies plus K-Electric.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slab (units/month)&lt;/th&gt;
&lt;th&gt;Rs/kWh&lt;/th&gt;
&lt;th&gt;Fixed Rs/kW/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lifeline up to 50&lt;/td&gt;
&lt;td&gt;3.95&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifeline 51-100&lt;/td&gt;
&lt;td&gt;7.74&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected 1-100&lt;/td&gt;
&lt;td&gt;10.54&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected 101-200&lt;/td&gt;
&lt;td&gt;13.01&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-100&lt;/td&gt;
&lt;td&gt;22.44&lt;/td&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;101-200&lt;/td&gt;
&lt;td&gt;28.91&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;201-300&lt;/td&gt;
&lt;td&gt;33.10&lt;/td&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;301-400&lt;/td&gt;
&lt;td&gt;36.46&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401-500&lt;/td&gt;
&lt;td&gt;38.95&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;501-600 / 601-700 / 700+&lt;/td&gt;
&lt;td&gt;40.22 / 41.85 / 47.20&lt;/td&gt;
&lt;td&gt;675&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;New in 2026: a fixed charge per kW of &lt;strong&gt;sanctioned load&lt;/strong&gt;, by slab.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The rules hide in the footnotes
&lt;/h2&gt;

&lt;p&gt;Three lines under Annex A-1 decide the arithmetic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Only protected residential consumers will be given the benefit of one previous slab." 150 protected units = 100 x 10.54 + 50 x 13.01.&lt;/li&gt;
&lt;li&gt;Lifeline consumers get no slab benefit.&lt;/li&gt;
&lt;li&gt;Everyone else pays the rate of the slab their total lands in, &lt;strong&gt;on every unit&lt;/strong&gt;. 201 units = 201 x 33.10.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;protected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unprotected&lt;/span&gt;&lt;span class="dl"&gt;"&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;protected&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;10.54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;kw&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="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1054&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;13.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;kw&lt;/span&gt; &lt;span class="p"&gt;};&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;slabs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;22.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;275&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;28.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;33.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;36.46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;38.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;],&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="mf"&gt;40.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;675&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;41.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;675&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;47.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;675&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;units&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fixed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;kw&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;h2&gt;
  
  
  4. What the 201st unit costs
&lt;/h2&gt;

&lt;p&gt;At 1 kW sanctioned load, adding the FC surcharge (Rs 3.23/unit, Rs 1.43 for protected users, per NEPRA's 2023 decision) and 18% GST, but not the monthly fuel adjustment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200 units, protected: about &lt;strong&gt;Rs 3,470&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;201 units, non-protected: about &lt;strong&gt;Rs 9,030&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The six-month rule makes it worse. If the next five months sit at 180 units, each is billed at the non-protected rate (about Rs 7,181 instead of Rs 3,130). The single extra unit costs roughly &lt;strong&gt;Rs 25,800&lt;/strong&gt; across six bills.&lt;/p&gt;

&lt;p&gt;The six-month window is a sliding check over bill history, which turns into a one-liner: if the most recent month above 200 sits at index &lt;code&gt;i&lt;/code&gt; (0 = oldest of six), the household gets &lt;code&gt;i + 1&lt;/code&gt; more non-protected bills, counting the current one.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Calibrate against a real bill
&lt;/h2&gt;

&lt;p&gt;My first estimate came out about 10% low. An open-source LESCO estimator on GitHub that its author tuned to their own real bills includes the FC surcharge line, which the SRO does not mention because it was notified separately in 2023. Adding it brought my 153-unit example within a few percent of their recorded bill; the remaining gap is the monthly fuel price adjustment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool and the data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BijliHisab&lt;/strong&gt; (free, unofficial, in Urdu): &lt;a href="https://bijlihisab.appin60.com" rel="noopener noreferrer"&gt;https://bijlihisab.appin60.com&lt;/a&gt;. Enter today's meter reading and the reading date from your bill; it tells you how many of your 200 units are left, the daily budget to stay under, and the date you will cross 200 at your current pace. It also writes a printable Urdu application to the SDO for a wrong or late reading. Everything runs in the browser and nothing is collected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tariff as JSON&lt;/strong&gt; (CC0, with the billing rules): &lt;a href="https://github.com/lowkey-studio/pakistan-electricity-tariff" rel="noopener noreferrer"&gt;https://github.com/lowkey-studio/pakistan-electricity-tariff&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spot a rate that differs from your bill, open an issue with the bill line (no reference numbers, please) and the source.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opendata</category>
      <category>webdev</category>
      <category>pakistan</category>
    </item>
    <item>
      <title>Printing Urdu worksheets in Nastaliq from the browser: joined letter forms with ZWJ and print-to-PDF</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:33:51 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/printing-urdu-worksheets-in-nastaliq-from-the-browser-joined-letter-forms-with-zwj-and-print-to-pdf-5eb8</link>
      <guid>https://dev.to/lowkeybuilds/printing-urdu-worksheets-in-nastaliq-from-the-browser-joined-letter-forms-with-zwj-and-print-to-pdf-5eb8</guid>
      <description>&lt;p&gt;Urdu letters change shape when they join. ب on its own, at the start of a word, in the middle and at the end are four different shapes, and a child who only practises the first one cannot read بلی (cat). Most free Urdu worksheets online are the same few scanned PDFs mirrored on spam pages, and almost none show the joined forms.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://takhti.appin60.com" rel="noopener noreferrer"&gt;Takhti&lt;/a&gt;, a free worksheet maker that prints them correctly, with the child's name on the sheet. Everything runs in the browser. Here are the three tricks that made it work without a server or a PDF library.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Ask the font for a joined form with ZWJ
&lt;/h2&gt;

&lt;p&gt;You do not need a table of presentation-form code points. Put a ZERO WIDTH JOINER (U+200D) on the side where the letter should connect, and the shaping engine (HarfBuzz in Chrome, CoreText in Safari) picks the right glyph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;‍&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;)&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;joins&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// alone, start, middle, end&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;joins&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZWJ&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// ا د ڈ ذ ر ڑ ز ژ و ے: two shapes only&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;                                       &lt;span class="c1"&gt;// ء never joins&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ten right-joining letters are the part teachers notice when a sheet gets them wrong: ر has no "start" or "middle" form, so a sheet that shows four shapes for it is teaching a mistake.&lt;/p&gt;

&lt;p&gt;The same trick builds a "missing first letter" exercise. Remove the first letter of پتنگ and you get تنگ, which renders with ت in its initial form. Prefix the rest with ZWJ and ت shows its middle form, the way it looks inside the real word. If the removed letter is one of the right-joiners (ریل without ر), skip the ZWJ, because the next letter really does start fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Nastaliq needs room, and a real font
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/notofonts/nastaliq" rel="noopener noreferrer"&gt;Noto Nastaliq Urdu&lt;/a&gt; is SIL OFL, so it can be self-hosted. The variable TTF from google/fonts converts to a 260 KB WOFF2 with fontTools. Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nastaliq has tall ascenders and deep descenders. A line height under about 1.8 clips dots and the tails of ے and ی.&lt;/li&gt;
&lt;li&gt;The glyphs are small for their em size compared with Latin text, so Urdu needs a bigger font size next to English to look balanced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also offer Noto Naskh Arabic as an option, since many teachers start small children on the simpler Naskh shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Print to PDF is the PDF engine
&lt;/h2&gt;

&lt;p&gt;Most client-side PDF libraries draw text glyph by glyph and do not run the shaping engine, so Nastaliq comes out as disconnected letters. The browser's own print path does shape it, and on Android and desktop Chrome "Save as PDF" is in the print dialog. So each sheet is a fixed-size element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sheet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;210mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;297mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11mm&lt;/span&gt; &lt;span class="m"&gt;12mm&lt;/span&gt; &lt;span class="m"&gt;9mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.letter&lt;/span&gt; &lt;span class="nc"&gt;.sheet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;215.9mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;279.4mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c"&gt;/* US Letter for families abroad */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#print-root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.sheet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&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;On print, the script copies the sheets into &lt;code&gt;#print-root&lt;/code&gt;, injects &lt;code&gt;@page { size: A4; margin: 0 }&lt;/code&gt; (or &lt;code&gt;letter&lt;/code&gt;), waits for &lt;code&gt;document.fonts.ready&lt;/code&gt;, then calls &lt;code&gt;window.print()&lt;/code&gt;. Headless Chrome's &lt;code&gt;--print-to-pdf&lt;/code&gt; gives the same output, which made it easy to test every sheet type: the fonts embed as Type 3 glyphs with the joins intact.&lt;/p&gt;

&lt;p&gt;For the on-screen preview on a 390 px phone, the same A4 element is scaled with CSS &lt;code&gt;zoom&lt;/code&gt; set to the container width divided by 794 (A4 width in CSS pixels), so what you see is exactly what prints.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it makes
&lt;/h2&gt;

&lt;p&gt;Letter tracing in all joined forms, the whole alphabet, the child's own name (common names are converted from Roman spelling, and there is a tap keyboard for the rest), letter and picture matching, missing letter, joining letters into words, ginti 1 to 10, opposites, wahid jama and muzakkar muannas, and short graded stories with a Roman Urdu and an English line under each Urdu line for parents abroad who do not read the script.&lt;/p&gt;

&lt;p&gt;Every sheet carries the site address in its footer, since worksheets get photocopied and passed around classes. It is free, with no signup: &lt;a href="https://takhti.appin60.com" rel="noopener noreferrer"&gt;https://takhti.appin60.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you teach Urdu, or have children learning it, I would like to hear which sheet is missing: &lt;a href="mailto:takhti@appin60.com"&gt;takhti@appin60.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>urdu</category>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A .docx file in 60 lines of browser JavaScript, and other lessons from a Nikah Nama translation tool</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:11:34 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/a-docx-file-in-60-lines-of-browser-javascript-and-other-lessons-from-a-nikah-nama-translation-tool-f3d</link>
      <guid>https://dev.to/lowkeybuilds/a-docx-file-in-60-lines-of-browser-javascript-and-other-lessons-from-a-nikah-nama-translation-tool-f3d</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I built the tool this post is about, &lt;a href="https://nikahdraft.appin60.com/" rel="noopener noreferrer"&gt;NikahDraft&lt;/a&gt;. It has a free preview and a $9 unlock.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pakistani couples applying for US, UK or Canadian spouse visas need an English translation of their Nikah Nama, the marriage contract. It is a printed Urdu form with 25 numbered columns, filled in by hand. People search for "nikah nama in english word format" because they want something they can fill in themselves and open in Word.&lt;/p&gt;

&lt;p&gt;The documents are personal: names, parents, addresses, dower amounts. So the constraint I set was that nothing typed into the tool leaves the browser. That ruled out the easy routes (server-side PDF rendering, a docx library on a backend) and led to a few small techniques worth sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A valid .docx needs three files and a zip writer with no compression
&lt;/h2&gt;

&lt;p&gt;A .docx is a zip with XML inside. Word opens a package with only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[Content_Types].xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_rels/.rels&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;word/document.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zip's "stored" method (no compression) is simple to write by hand: a local header per file, the bytes, a central directory and an end record. The only real work is CRC-32:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CRC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mh"&gt;0xEDB88320&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&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;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crc32&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;x&lt;/span&gt; &lt;span class="k"&gt;of&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;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CRC&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then each file is a 30-byte local header (signature &lt;code&gt;PK\x03\x04&lt;/code&gt;, version 20, method 0, the CRC, sizes, name length), the name, and the body; the central directory repeats that with an offset; the end-of-central-directory record closes it. With a &lt;code&gt;Blob&lt;/code&gt; of those &lt;code&gt;Uint8Array&lt;/code&gt; chunks you have a download. I check the output with &lt;code&gt;python-docx&lt;/code&gt; in a test: it opens, and the table has 26 rows (header plus 25 columns).&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;document.xml&lt;/code&gt;, a three-column bordered table is a &lt;code&gt;w:tbl&lt;/code&gt; with &lt;code&gt;w:tblBorders&lt;/code&gt; and fixed &lt;code&gt;w:gridCol&lt;/code&gt; widths in twentieths of a point. Line breaks inside a cell are &lt;code&gt;w:br&lt;/code&gt; elements between runs, not &lt;code&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Watermarks that survive "Save as PDF"
&lt;/h2&gt;

&lt;p&gt;The free tier prints with a "DRAFT PREVIEW" watermark. A CSS background image does not work: Chrome drops backgrounds when printing unless the user ticks "Background graphics". What does work is a real element, &lt;code&gt;position: fixed&lt;/code&gt; inside &lt;code&gt;@media print&lt;/code&gt;, which Chrome repeats on every printed page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.wm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.wm&lt;/span&gt; &lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-28deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;155&lt;/span&gt; &lt;span class="m"&gt;63&lt;/span&gt; &lt;span class="m"&gt;102&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;.16&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;h2&gt;
  
  
  3. Nastaliq needs room
&lt;/h2&gt;

&lt;p&gt;Urdu headings help users match columns on their paper form, and they have to be in Nastaliq, the script style Urdu readers expect, not the Naskh-style Arabic fallback a system font gives you. Noto Nastaliq Urdu (SIL OFL) is about 690 KB as TTF and 268 KB as WOFF2 after &lt;code&gt;fontTools&lt;/code&gt; conversion. Two things bit me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nastaliq's descenders are deep and its stacking is tall: line-height under about 2 clips glyphs. I use 2.3 for body text.&lt;/li&gt;
&lt;li&gt;A heading font set on &lt;code&gt;h1&lt;/code&gt; by the design system overrides inheritance from &lt;code&gt;&amp;lt;html lang="ur"&amp;gt;&lt;/code&gt;, so the Urdu page title fell back to a system Arabic font until I added an explicit &lt;code&gt;html[lang="ur"] h1&lt;/code&gt; rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Amounts in words, the Pakistani way
&lt;/h2&gt;

&lt;p&gt;Pakistani documents group digits in lakhs (5,00,000) and write "Rupees five hundred thousand only". A small &lt;code&gt;words()&lt;/code&gt; function plus a regex does the grouping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lakh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;s&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\B(?=(\d{2})&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?!\d))&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&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;Gold dower is in tola, so the tool adds grams (1 tola = 11.6638 g).&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A paid unlock without accounts
&lt;/h2&gt;

&lt;p&gt;The paid step is a one-time card payment through Whop. After checkout Whop redirects to &lt;code&gt;/unlock/?payment_id=...&lt;/code&gt;. A Cloudflare Pages worker looks the payment up with the Whop API, checks it is paid, for this product and not refunded, and returns &lt;code&gt;payment_id + "." + HMAC(secret, payment_id)&lt;/code&gt;. The browser stores that token; on load it asks the worker to verify it again, so a refund removes the unlock. The worker never sees the form contents, only the payment id.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It does not read the handwriting. A person who reads Urdu types each entry, and the tool does the layout, the printed column text in English, the translator's notes for struck-out columns, and the translator's statement worded to the USCIS rule (8 CFR 103.2(b)(3)). It is not a certified translation, and for UK and Canadian files a professional translator still signs.&lt;/p&gt;

&lt;p&gt;If you work on tools for languages other than English, I would like to hear what broke for you in print and PDF.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>i18n</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Confirming cash-on-delivery orders on WhatsApp without the API, in the browser</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:32:15 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/confirming-cash-on-delivery-orders-on-whatsapp-without-the-api-in-the-browser-3bb7</link>
      <guid>https://dev.to/lowkeybuilds/confirming-cash-on-delivery-orders-on-whatsapp-without-the-api-in-the-browser-3bb7</guid>
      <description>&lt;p&gt;Most online orders in Pakistan are cash on delivery. Sellers who run ads get a steady stream of fake or careless orders, and every refused parcel costs two-way courier charges. So most small stores do one manual step before booking the courier: they message each customer on WhatsApp and ask them to confirm.&lt;/p&gt;

&lt;p&gt;The paid fix is a Shopify app on the WhatsApp Business API (there are several, from about $3 to $10 a month plus Meta's per-message fees). I wanted to see how far a free, static, browser-only tool could go for the sellers who do not have Shopify or do not want the API: WooCommerce stores, and Instagram and Facebook sellers who keep orders in a Google Sheet.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://confirmkaro.appin60.com" rel="noopener noreferrer"&gt;ConfirmKaro&lt;/a&gt;, which I built. Here is what was interesting to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;wa.me&lt;/code&gt; links do the sending
&lt;/h2&gt;

&lt;p&gt;WhatsApp has a public click-to-chat URL: &lt;code&gt;https://wa.me/&amp;lt;number&amp;gt;?text=&amp;lt;urlencoded message&amp;gt;&lt;/code&gt;. On a phone it opens the chat in the WhatsApp app with the message typed; on desktop it opens WhatsApp Web. The seller presses send. No API, no number registration, no template approval, and the message comes from the shop's normal WhatsApp, which customers already recognise.&lt;/p&gt;

&lt;p&gt;The cost is one tap per order, which is fine up to a few dozen orders a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pakistani phone numbers in the wild
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wa.me&lt;/code&gt; wants the international format with no &lt;code&gt;+&lt;/code&gt;, no spaces and no leading zero: &lt;code&gt;923001234567&lt;/code&gt;. Order exports contain everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0300 1234567      -&amp;gt; 923001234567
+92 321 7654321   -&amp;gt; 923217654321
0333-5556667      -&amp;gt; 923335556667
3001234567        -&amp;gt; 923001234567   (leading zero lost by Excel)
00923001234567    -&amp;gt; 923001234567
03451234          -&amp;gt; invalid (too short)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&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;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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;plus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0092&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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;d&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="mi"&gt;11&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;03&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;92&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&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;d&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="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;92&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&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;d&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="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;923&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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="na"&gt;wa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pk&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plus&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&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;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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="na"&gt;wa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pk&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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="na"&gt;wa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&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;The Excel case matters more than it looks: when a sheet stores the number as a number, the leading zero disappears, so a 10-digit value starting with 3 is almost certainly a Pakistani mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Shopify exports one row per line item
&lt;/h2&gt;

&lt;p&gt;Shopify's orders CSV repeats the order &lt;code&gt;Name&lt;/code&gt; (&lt;code&gt;#1041&lt;/code&gt;) on one row per item, and only the first row has the total and shipping details. Merging by order number and appending item rows gives one message per order. WooCommerce export plugins use different headers (&lt;code&gt;Billing Phone&lt;/code&gt;, &lt;code&gt;Order Total Amount&lt;/code&gt;), so columns are matched by a ranked list of header tests, and the user can override any column.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Fake-order flags without any server
&lt;/h2&gt;

&lt;p&gt;All local heuristics: invalid number, the same number on several orders in the batch, an address under 12 characters ("abc"), a name with digits or fewer than three letters, and a total over four times the batch median. None of these prove fraud; they decide who gets confirmed most carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Urdu in Nastaliq, and Roman Urdu
&lt;/h2&gt;

&lt;p&gt;Most sellers write to customers in Roman Urdu ("Order confirm karne ke liye 1 likh kar bhejein"), some in Urdu script, some in English. The templates come in all three. The page self-hosts Noto Nastaliq Urdu (SIL OFL) as a 160 KB woff2 converted with fontTools, sets &lt;code&gt;lang="ur" dir="rtl"&lt;/code&gt;, and needs a line height above 2 because Nastaliq stacks vertically. WhatsApp renders the Urdu text itself with the phone's font, so the font only matters on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Nothing leaves the browser
&lt;/h2&gt;

&lt;p&gt;The CSV is read with &lt;code&gt;FileReader&lt;/code&gt;, statuses (pending, confirmed, no reply, cancelled) live in &lt;code&gt;localStorage&lt;/code&gt;, and the confirmed orders download as a CSV for courier bulk booking. Customer names and phone numbers never touch a server, which is the main reason a seller can trust a tool from someone they do not know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would like to know
&lt;/h2&gt;

&lt;p&gt;If you build stores for Pakistani (or Indian, Bangladeshi, Egyptian, Moroccan) COD sellers: how do your clients confirm orders today, and what format does their courier's bulk booking want? Replies at &lt;a href="mailto:confirmkaro@appin60.com"&gt;confirmkaro@appin60.com&lt;/a&gt; or in the comments shape what gets added next.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built ConfirmKaro. It is free and has no paid tier.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>whatsapp</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I measured the 402 header of every x402 seller in the Coinbase Bazaar. 156 can't be read by aiohttp, 4 break Node</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:52:18 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/i-measured-the-402-header-of-every-x402-seller-in-the-coinbase-bazaar-156-cant-be-read-by-2pf3</link>
      <guid>https://dev.to/lowkeybuilds/i-measured-the-402-header-of-every-x402-seller-in-the-coinbase-bazaar-156-cant-be-read-by-2pf3</guid>
      <description>&lt;p&gt;Your x402 endpoint can be listed, indexed and priced correctly, and still be impossible to pay from half the agent stack. On 2026-10-14 I sent unpaid requests to every origin in the Coinbase CDP Bazaar and measured one thing most sellers never look at: the size of the &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2,332 origins in the Bazaar (34,705 resources). 2,127 answered a probe with HTTP 402.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;156 of those 402s carry a &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header longer than 8,190 bytes.&lt;/strong&gt; Python's &lt;code&gt;aiohttp&lt;/code&gt; rejects any header line that long by default, so an aiohttp-based agent gets &lt;code&gt;ClientResponseError: 400, message='Got more than 8190 bytes when reading'&lt;/code&gt; and never sees your price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 send more than 16 KB of headers.&lt;/strong&gt; Node's built-in &lt;code&gt;fetch&lt;/code&gt; (undici), and therefore &lt;code&gt;@x402/fetch&lt;/code&gt;, fails with &lt;code&gt;UND_ERR_HEADERS_OVERFLOW&lt;/code&gt;. The largest was 55 KB.&lt;/li&gt;
&lt;li&gt;55 more sit between 12 KB and 16 KB, one bigger example away from the Node limit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;httpx&lt;/code&gt; (the official Python SDK's client) read 32 KB headers without complaint, which is why sellers testing with curl or Python never notice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-origin data and the method are public: &lt;a href="https://github.com/lowkey-studio/x402doctor-data/blob/main/HEADER-SIZES.md" rel="noopener noreferrer"&gt;x402doctor-data/HEADER-SIZES.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the header gets so big
&lt;/h2&gt;

&lt;p&gt;In x402 v2 the whole &lt;code&gt;PaymentRequired&lt;/code&gt; object travels base64-encoded in one header. That includes the Bazaar discovery extension: your input example, your &lt;strong&gt;output example&lt;/strong&gt;, and the JSON schema. Sellers reasonably paste a full, realistic response as the output example. On the 55 KB endpoint, the output example alone was 29 KB (a complete vulnerability report) and the schema another 12 KB, while &lt;code&gt;accepts&lt;/code&gt; was 1.2 KB.&lt;/p&gt;

&lt;p&gt;The other common cause is &lt;code&gt;accepts&lt;/code&gt; itself. One operator offers 14 payment options per route (several networks and stablecoins); that alone is about 4 KB of JSON before base64.&lt;/p&gt;

&lt;p&gt;Base64 adds a third on top of the JSON, so a 6 KB challenge is already an 8 KB header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yours
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null https://your-api.example.com/paid-route | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^payment-required'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over 8,190: aiohttp users can't read it. Over about 16,000 (all headers together): Node users can't either.&lt;/p&gt;

&lt;p&gt;Or run the free origin audit I built, which reads your OpenAPI like the indexers do, probes each paid route with your own examples, reports header size, and lists which routes the CDP Bazaar has actually indexed for your payTo: &lt;a href="https://pitcrew-api.appin60.com/app?ref=devto#audit" rel="noopener noreferrer"&gt;pitcrew-api.appin60.com/app#audit&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make the output example one representative item, not a full response. The schema already documents the shape.&lt;/li&gt;
&lt;li&gt;Trim schema &lt;code&gt;description&lt;/code&gt; strings in the discovery extension; keep the long prose in your OpenAPI.&lt;/li&gt;
&lt;li&gt;Only list &lt;code&gt;accepts&lt;/code&gt; entries you actually settle. Every extra network/asset pair costs about 300 bytes.&lt;/li&gt;
&lt;li&gt;Aim for under 8 KB. That keeps every common client happy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also opened an issue proposing a size budget and a middleware warning upstream: &lt;a href="https://github.com/x402-foundation/x402/issues/3803" rel="noopener noreferrer"&gt;x402-foundation/x402#3803&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method and caveats
&lt;/h2&gt;

&lt;p&gt;Unpaid GET/POST probes only, up to two resources per origin, using the Bazaar's stored example input (query, path and body). Header bytes are measured on the raw response. Results are a snapshot: sellers change their metadata often, and an origin I could not reach (DNS failure or timeout) is not counted either way. aiohttp and Node limits are defaults; clients can raise them, but most agents don't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build Pitcrew, a pay-per-call toolkit for agents and x402 sellers. The audit above is free in the browser and $0.02 per call over x402.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>api</category>
      <category>python</category>
      <category>node</category>
    </item>
    <item>
      <title>I re-checked 516 startup launch directories from 11 GitHub lists: 66 are dead or moved, and "free" often isn't</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:49:02 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/i-re-checked-516-startup-launch-directories-from-11-github-lists-60-are-dead-or-moved-and-free-2lh8</link>
      <guid>https://dev.to/lowkeybuilds/i-re-checked-516-startup-launch-directories-from-11-github-lists-60-are-dead-or-moved-and-free-2lh8</guid>
      <description>&lt;p&gt;I wanted a clean list of places to launch a small SaaS, so I merged 11 popular "launch directories" lists from GitHub (516 unique domains) and checked every one on 14 October 2026. Then I read the submit pages of about 250 of them by hand, starting with the most-listed and most-visited.&lt;/p&gt;

&lt;p&gt;Short version: the lists are older than they look, and "free" usually has a catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check found
&lt;/h2&gt;

&lt;p&gt;Out of 516 domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;436 respond.&lt;/strong&gt; 46 of those sit behind bot walls, so they are probably fine in a normal browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30 are dead&lt;/strong&gt;: the domain no longer resolves, is parked or for sale, or now shows something else entirely. One former directory is now a slot-gambling shop, one shows a crypto-wallet download page, and another, still in two lists, is an Indonesian "link alternatif" page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19 are unreachable or erroring&lt;/strong&gt; (timeouts, TLS errors, Cloudflare 52x).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 redirect to a different site.&lt;/strong&gt; Some are renames (betapage.co now lands on pitchwall.co, angel.co on wellfound.com), some are not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of the dead ones are still in several lists at once. &lt;code&gt;theresanai.com&lt;/code&gt;, &lt;code&gt;demos.co&lt;/code&gt; and &lt;code&gt;launched.io&lt;/code&gt; are each in three lists and none of them resolves.&lt;/p&gt;

&lt;p&gt;DNS failures were re-checked through Cloudflare DNS-over-HTTPS before I called anything dead, because my first pass used a flaky local resolver and wrongly marked about 120 live sites as dead. If you script this yourself, use a second resolver.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "free" means
&lt;/h2&gt;

&lt;p&gt;For 173 reviewed directories where the submit page gave a clear answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;113&lt;/strong&gt; have a free listing with no strings I could find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18&lt;/strong&gt; are free only if you add their badge or link to your site. Examples from the pages: "A backlink to our site is required (on your homepage or footer)", "Free submissions use backlink verification", "Badge must stay on your site permanently".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13&lt;/strong&gt; are free only after a queue: "Minimum 30-day waiting period", "Enter the queue (30-90 day listing time)", "Free Launch has 70 weekly slots", and my favourite, "Free slots are fully booked into 2027".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;29&lt;/strong&gt; are paid only, from $3 to $249 (mostly one-time, a few monthly).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The badge ones add up. If you accept every badge offer you end up with a footer full of other people's logos, so it is worth deciding once whether you will do that before you start.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'd use this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start with directories that are live, take a plain free listing, need no account, and have real traffic. I used the &lt;a href="https://tranco-list.eu/list/N26LW" rel="noopener noreferrer"&gt;Tranco top-1M list&lt;/a&gt; as a popularity signal; most of these directories are not in it at all, which tells you something too.&lt;/li&gt;
&lt;li&gt;Then do the ones that need an account but match your category (AI tools, dev tools, SaaS, open source, Mac apps all have their own).&lt;/li&gt;
&lt;li&gt;Decide on badges once.&lt;/li&gt;
&lt;li&gt;Submit to queue directories on day one and forget about them.&lt;/li&gt;
&lt;li&gt;Pay only where the audience matches your buyers. Most paid tiers sell a dofollow link and a faster review, not customers.&lt;/li&gt;
&lt;li&gt;Delete the dead ones from whatever list you are using.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;There is also a free checker on the page: paste your own list of directory URLs and it tells you which ones are dead or moved.&lt;/p&gt;

&lt;p&gt;The full list of 66 dead, down and moved directories, and a sample of 12 popular ones with a plain free listing, are free here: &lt;a href="https://submitcheck.appin60.com" rel="noopener noreferrer"&gt;https://submitcheck.appin60.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built that page. The complete sheet (all 516 rows with submit URL, free / badge / queue / paid, lowest price seen, account needed, captcha on the form, popularity rank and check date) is $12 there. The method and the source lists (CC0, MIT and CC BY data) are on the page.&lt;/p&gt;

&lt;p&gt;If you maintain one of these lists and want the dead entries for it, comment here and I'll send them.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>saas</category>
      <category>marketing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I sent an empty-wallet payment to 4,256 x402 routes. 38 sellers told me their facilitator was down.</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:38:29 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/i-sent-an-empty-wallet-payment-to-4256-x402-routes-38-sellers-told-me-their-facilitator-was-down-3i1e</link>
      <guid>https://dev.to/lowkeybuilds/i-sent-an-empty-wallet-payment-to-4256-x402-routes-38-sellers-told-me-their-facilitator-was-down-3i1e</guid>
      <description>&lt;p&gt;Answering HTTP 402 is not the same as being payable. On 2026-10-14 I took every Base seller in the Coinbase CDP Bazaar with at least two distinct payers in the last 30 days and sent each of their routes one payment that cannot succeed: an EIP-3009 authorization signed by a brand-new wallet holding zero USDC, through the reference &lt;code&gt;@x402/fetch&lt;/code&gt; client. No money can move. A healthy route rejects it &lt;strong&gt;for balance&lt;/strong&gt; and says so. Anything else tells you how that seller treats every buyer whose payment fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the checker used here (&lt;a href="https://contractlens.appin60.com/x402-payability" rel="noopener noreferrer"&gt;ContractLens&lt;/a&gt;). The free single-URL check is linked at the end; it has paid options too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CDP Bazaar crawl: 34,672 listed resources from 2,332 origins.&lt;/li&gt;
&lt;li&gt;Probed: &lt;strong&gt;4,256 routes from 1,735 origins&lt;/strong&gt; (up to 5 routes for the 442 origins with 5+ payers and 2+ routes, up to 3 for the rest). GET routes got the listing's example query, POST routes the listing's example body.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Routes&lt;/th&gt;
&lt;th&gt;Origins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payable (rejected only for balance)&lt;/td&gt;
&lt;td&gt;2,262&lt;/td&gt;
&lt;td&gt;935&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejected with an opaque reason ("payment_required", "invalid_payload", empty)&lt;/td&gt;
&lt;td&gt;916&lt;/td&gt;
&lt;td&gt;370&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection failed during the run (see correction below)&lt;/td&gt;
&lt;td&gt;455&lt;/td&gt;
&lt;td&gt;181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other non-balance rejection (mostly input validation)&lt;/td&gt;
&lt;td&gt;306&lt;/td&gt;
&lt;td&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 5xx when the payment fails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rejected payment reported as a facilitator outage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Listed route answers 404, or 5xx before quoting a price&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description over ~500 characters&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price above the $1 default client spend cap&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Served the paid response anyway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Correction (2026-10-14, 10:55 UTC):&lt;/strong&gt; the 455 "connection failed" routes are mostly not seller outages. I re-checked their 181 hosts about 90 minutes later through public DNS resolvers (1.1.1.1, 8.8.8.8): 133 responded normally, so most failures were transient or my own resolver failing under parallel load. 44 hosts fail DNS at public resolvers (SERVFAIL), which does mean their Bazaar listings point at dead names. The other rows come from responses the sellers actually sent and are unaffected.&lt;/p&gt;

&lt;p&gt;Opaque rejections are not defects by themselves: an empty wallet and a broken payment path look identical from outside, so I don't count them as broken. The bold rows are the ones that are wrong regardless of who is paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Facilitator unavailable" when the buyer is simply broke (38 origins)
&lt;/h2&gt;

&lt;p&gt;This was the surprise. When the CDP facilitator rejects a payment, &lt;code&gt;/verify&lt;/code&gt; answers &lt;strong&gt;HTTP 400&lt;/strong&gt; with a body like &lt;code&gt;{"isValid": false, "invalidReason": "..."}&lt;/code&gt;. The reference SDK handles that: &lt;code&gt;HTTPFacilitatorClient.verify&lt;/code&gt; in &lt;code&gt;@x402/core&lt;/code&gt; throws a &lt;code&gt;VerifyError&lt;/code&gt; carrying the reason, and the middleware returns 402 with it. My own routes answer the empty-wallet test with &lt;code&gt;402 invalid_payload: ... execution reverted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Hand-rolled integrations often do this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;fetch&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;FACILITATOR&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/verify`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&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;facilitator unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// wrong for 400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So 38 sellers answer a rejected payment with 502/503 "verification temporarily unavailable", "payments_unavailable", "broker_unavailable", "retry in 60 seconds" and similar. Two of them literally put &lt;code&gt;cdp_verify_http_400&lt;/code&gt; and &lt;code&gt;facilitator_status_400&lt;/code&gt; in the message. Funded buyers are fine. Everyone else (low balance, expired authorization, reused nonce, wrong amount) is told to retry later, and agent SDKs do retry 5xx responses. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&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;fetch&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;FACILITATOR&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/verify`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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;data&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;r&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isValid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValid&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;402&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;x402Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invalidReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accepts&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&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;facilitator_error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// real outage only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. HTTP 500 on a failed payment (41 origins)
&lt;/h2&gt;

&lt;p&gt;Same symptom, cruder cause: the verify failure throws and lands in a generic error handler, sometimes rendering an HTML error page. In an earlier run of this test, one seller with 41 buyers fixed this across a whole fleet of subdomains within hours of hearing about it, which suggests it is usually an oversight rather than a design choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Listings that cannot be called (20 origins)
&lt;/h2&gt;

&lt;p&gt;Twelve origins have Bazaar listings whose listed URL answers 404. In the ones I checked by hand, the URL contained the route template, like &lt;code&gt;/v1/:chainId/quote&lt;/code&gt; or &lt;code&gt;/stock/:symbol&lt;/code&gt;, or an example id that does not exist. Agents that pick these from the Bazaar get 404 before they ever see a price, while the listing still shows payers from earlier. Declare a concrete, working example URL, or describe path parameters in the Bazaar input schema.&lt;/p&gt;

&lt;p&gt;Related: one seller's Bazaar example query used the parameter &lt;em&gt;descriptions&lt;/em&gt; as values (&lt;code&gt;?limit=How many articles to return.&lt;/code&gt;), which returns 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Descriptions over ~500 characters (10 origins)
&lt;/h2&gt;

&lt;p&gt;The CDP facilitator rejects payment payloads whose &lt;code&gt;resource.description&lt;/code&gt; is longer than about 500 characters (&lt;code&gt;paymentPayload is invalid&lt;/code&gt;), before balance is checked. It is tracked upstream (&lt;a href="https://github.com/x402-foundation/x402/issues/2284" rel="noopener noreferrer"&gt;x402#2284&lt;/a&gt;, &lt;a href="https://github.com/x402-foundation/x402/issues/2993" rel="noopener noreferrer"&gt;#2993&lt;/a&gt;). The clearest case: one origin's routes with 156-character descriptions behaved normally while its 731-character route returned "facilitator rejected (400)". Keep the long text in the Bazaar extension or your docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Served without payment (3 origins)
&lt;/h2&gt;

&lt;p&gt;Three origins returned a paid result or accepted a paid job (HTTP 200 or 202) for the zero-balance signature; two of them did so in two separate runs. Either verification ran after the handler, or a failed settlement did not stop the response. One also called a paid upstream cloud API before verifying, so every unpaid attempt cost the seller money. I emailed the two with a public contact privately and am not naming any of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with this
&lt;/h2&gt;

&lt;p&gt;I re-verified each hard defect, then emailed or opened an issue with 19 sellers who had a public contact, one specific finding each, nothing else attached. In the previous round of this test, three sellers fixed what was reported (one opened an upstream PR, &lt;a href="https://github.com/x402-foundation/x402/pull/3795" rel="noopener noreferrer"&gt;x402#3795&lt;/a&gt;, to surface rejection reasons).&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own routes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free, one URL at a time (GET or POST): &lt;a href="https://contractlens.appin60.com/x402-payability" rel="noopener noreferrer"&gt;https://contractlens.appin60.com/x402-payability&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Whole origin, every route we can find in your &lt;code&gt;/.well-known/x402&lt;/code&gt;, &lt;code&gt;/openapi.json&lt;/code&gt; and your own Bazaar entries, with a fix hint per route: $0.25 in USDC via x402 (&lt;code&gt;GET https://contractlens-api.appin60.com/audit?origin=https://your-api&lt;/code&gt;) or with a browser wallet on the page above.&lt;/li&gt;
&lt;li&gt;A $0.99 monitor re-checks one route hourly for 30 days and emails you when it changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Method caveats: one test payment per route, from one location, on one day. Opaque rejections are counted as unknown, not broken. Classifications and raw results are per route; the checker shows the exact response for any URL you give it.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>web3</category>
      <category>api</category>
      <category>payments</category>
    </item>
    <item>
      <title>I checked the email DNS of 68 Shopify stores: 63 send order emails without domain authentication</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:20:53 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/i-checked-the-email-dns-of-68-shopify-stores-63-send-order-emails-without-domain-authentication-2k8k</link>
      <guid>https://dev.to/lowkeybuilds/i-checked-the-email-dns-of-68-shopify-stores-63-send-order-emails-without-domain-authentication-2k8k</guid>
      <description>&lt;p&gt;Most Shopify store reviews look at the storefront: photos, copy, speed, checkout. I wanted to look at the part customers only see in their inbox, so I checked the public DNS of every store that merchants shared in the Shopify Community &lt;strong&gt;Store Feedback&lt;/strong&gt; board.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built &lt;a href="https://inboxpass.appin60.com" rel="noopener noreferrer"&gt;InboxPass&lt;/a&gt;, the free checker used here. Everything below is reproducible with &lt;code&gt;dig&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pulled 180 recent Store Feedback topics through the forum's public Discourse JSON API and extracted the store links: &lt;strong&gt;113 domains&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A domain counts as a Shopify store if its apex resolves to Shopify's &lt;code&gt;23.227.38.x&lt;/code&gt; range or &lt;code&gt;www&lt;/code&gt; is a CNAME to &lt;code&gt;*.myshopify.com&lt;/code&gt;: &lt;strong&gt;92 stores&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;I kept the stores that run email on their own domain (they have MX records): &lt;strong&gt;68 stores&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For each one I queried SPF, DMARC and DKIM through Cloudflare's DNS-over-HTTPS resolver. (My local resolver was negatively caching and timing out, which produced false "missing" results, so every result below comes from DoH.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Shopify authenticates your sender domain
&lt;/h2&gt;

&lt;p&gt;When you authenticate a sender domain under &lt;em&gt;Settings &amp;gt; Notifications &amp;gt; Sender email&lt;/em&gt;, Shopify gives you CNAME records like these (you can see real ones posted &lt;a href="https://community.shopify.com/t/339941" rel="noopener noreferrer"&gt;in this community thread&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mailerXX.yourstore.com        CNAME  &amp;lt;id&amp;gt;.pXXX.email.myshopify.com
s1._domainkey.yourstore.com   CNAME  s1.domainkey.uNNNN.wlNNN.sendgrid.net
s2._domainkey.yourstore.com   CNAME  s2.domainkey.uNNNN.wlNNN.sendgrid.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mailer&lt;/code&gt; subdomain covers SPF and bounces. The &lt;code&gt;s1&lt;/code&gt;/&lt;code&gt;s2&lt;/code&gt; CNAMEs carry the DKIM keys. &lt;a href="https://help.shopify.com/en/manual/intro-to-shopify/initial-setup/setup-your-email" rel="noopener noreferrer"&gt;Shopify's help page&lt;/a&gt; says that if the domain isn't authenticated, your sender address is rewritten to &lt;code&gt;store+123@shopifyemail.com&lt;/code&gt;. It also says automatic setup doesn't add a DMARC record.&lt;/p&gt;

&lt;p&gt;So one lookup tells you whether a store did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short CNAME s1._domainkey.yourstore.com
&lt;span class="c"&gt;# authenticated:      s1.domainkey.u27818954.wl205.sendgrid.net.&lt;/span&gt;
&lt;span class="c"&gt;# not authenticated:  (nothing)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results (68 Shopify stores with their own email domain)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Stores failing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No Shopify DKIM CNAMEs (&lt;code&gt;s1._domainkey&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;63 of 68&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No DMARC record at &lt;code&gt;_dmarc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;17 of 68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No SPF record&lt;/td&gt;
&lt;td&gt;15 of 68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two or more SPF records, or more than 10 SPF lookups&lt;/td&gt;
&lt;td&gt;0 of 68&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only 5 of the 68 stores had authenticated their sender domain with Shopify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DNS can't show which sender address a merchant chose. If the sender is a Gmail address, Shopify can't authenticate it at all, and the fix is to use an address on your own domain.&lt;/li&gt;
&lt;li&gt;DKIM selectors can't be listed from DNS. I only treat the specific Shopify &lt;code&gt;s1&lt;/code&gt; CNAME as evidence. Other senders, such as Google Workspace or Klaviyo, have their own selectors.&lt;/li&gt;
&lt;li&gt;This is a small, self-selected sample: merchants who asked for feedback, mostly small and new stores.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 10-minute fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Settings &amp;gt; Notifications &amp;gt; Sender email &amp;gt; Authenticate&lt;/em&gt;, then copy every CNAME into your DNS host.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;dig +short TXT _dmarc.yourstore.com&lt;/code&gt; returns nothing, add a TXT record at &lt;code&gt;_dmarc&lt;/code&gt; with the value &lt;code&gt;v=DMARC1; p=none; rua=mailto:you@yourstore.com&lt;/code&gt;. Never add a second DMARC record.&lt;/li&gt;
&lt;li&gt;If you send from Google Workspace or Microsoft 365 and have no SPF record, add one: &lt;code&gt;v=spf1 include:_spf.google.com ~all&lt;/code&gt; (or &lt;code&gt;include:spf.protection.outlook.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To check a domain in the browser, &lt;a href="https://inboxpass.appin60.com" rel="noopener noreferrer"&gt;InboxPass&lt;/a&gt; runs these lookups client-side through DoH, with no signup. The engine is about 250 lines of JavaScript: it counts SPF lookups recursively (RFC 7208's limit of 10), parses DMARC, probes 35 common DKIM selectors, and guards against wildcard &lt;code&gt;_domainkey&lt;/code&gt; records, because example.com answers every selector with an empty key.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>email</category>
      <category>dns</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Codex logs already contain your quota meter: how to check where your usage went</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:13:33 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/your-codex-logs-already-contain-your-quota-meter-how-to-check-where-your-usage-went-1m10</link>
      <guid>https://dev.to/lowkeybuilds/your-codex-logs-already-contain-your-quota-meter-how-to-check-where-your-usage-went-1m10</guid>
      <description>&lt;p&gt;If you use Codex on a ChatGPT plan, you have probably watched the usage meter fall faster than you expected and wondered whether it was your work or the metering. The openai/codex issue tracker is full of these reports (&lt;a href="https://github.com/openai/codex/issues/41220" rel="noopener noreferrer"&gt;#41220&lt;/a&gt; collects dozens), and most of them compare percentages from screenshots, which cannot settle the question.&lt;/p&gt;

&lt;p&gt;Your machine already logs everything needed to answer it with numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rollout files contain
&lt;/h2&gt;

&lt;p&gt;Codex writes one JSONL file per thread under &lt;code&gt;~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl&lt;/code&gt; (on Windows, &lt;code&gt;%USERPROFILE%\.codex\sessions&lt;/code&gt;). After every model call it appends a &lt;code&gt;token_count&lt;/code&gt; event shaped like this (trimmed):&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"event_msg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"token_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"last_token_usage"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;152311&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cached_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;149760&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reasoning_output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;152723&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rate_limits"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"plan_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"used_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;41.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"window_minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"resets_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1792171200&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"secondary"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"used_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"window_minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"resets_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1792600000&lt;/span&gt;&lt;span class="p"&gt;}}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So for every call you get the tokens it used &lt;strong&gt;and&lt;/strong&gt; the server's view of your quota right after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One number that makes reports comparable
&lt;/h2&gt;

&lt;p&gt;For each rate-limit window (same &lt;code&gt;window_minutes&lt;/code&gt; and &lt;code&gt;resets_at&lt;/code&gt;), take the quota points used (highest minus lowest &lt;code&gt;used_percent&lt;/code&gt; you saw) and divide by the tokens logged in that window:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;quota points per 1M tokens&lt;/strong&gt; = points used ÷ (tokens ÷ 1,000,000)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Comparable work on the same plan and model should cost comparable points per million tokens. If that number jumps several times overnight, the metering changed, not your workload. If it stays flat but tokens per hour rose, the cause is on your side, and the logs show where.&lt;/p&gt;

&lt;p&gt;Two refinements matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cached input is most of your tokens.&lt;/strong&gt; On one Pro Lite account I checked, 97% of 285M logged tokens were cached input, 2.6% uncached input and 0.4% output. So also compute points per 1M &lt;em&gt;uncached input + output&lt;/em&gt;. A drop in cache hit rate can multiply cost without any change in tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps with no local calls.&lt;/strong&gt; If &lt;code&gt;used_percent&lt;/code&gt; rises between two snapshots with no call logged on this machine in between, the quota went somewhere else: another device, a cloud task, or background features. That separates "this prompt cost 9%" from "something else ran at the same time".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On that same account, the weekly windows cost between 0.35 and 1.3 points per 1M tokens, and one step of +28% in a weekly window came after only 350k tokens, about 90 times the window's average rate. That kind of step is worth taking to Support, with its timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tokens go
&lt;/h2&gt;

&lt;p&gt;The same files show why token counts get so large:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context replay.&lt;/strong&gt; Every model call re-sends the whole conversation. A turn with 34 tool calls at a 450k context is about 15.9M input tokens, even if the tools returned little (a real case: &lt;a href="https://github.com/openai/codex/issues/53855" rel="noopener noreferrer"&gt;#53855&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big context, tiny answer.&lt;/strong&gt; Calls with 100k+ context that produce under 300 output tokens: "check status", "run one command", paid at full-context price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling.&lt;/strong&gt; Calls that follow a &lt;code&gt;sleep&lt;/code&gt;, a &lt;code&gt;wait&lt;/code&gt; or an empty stdin write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-sent tool output.&lt;/strong&gt; A 60k-token command output stays in context and is re-sent on every later call until compaction; re-sent 100 times, it is 6M input tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A script that does all of this
&lt;/h2&gt;

&lt;p&gt;I packaged the above as a free, zero-dependency Node script (disclosure: I built it). It reads the logs locally, never uploads anything, and writes a Markdown report with counts only and hashed session ids, so you can attach it to an issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:lowkey-studio/quotatrace &lt;span class="nt"&gt;--since&lt;/span&gt; 30d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces the per-window table (quota used, tokens, points per 1M), the quota steps out of line with their tokens, the replay, polling and re-sent output breakdown, and the heaviest sessions. It also covers Claude Code transcripts (cache writes, cache rebuilt after a pause, subagent share, most re-read tool results). Claude Code does not log a quota percentage, though, so that part is tokens only.&lt;/p&gt;

&lt;p&gt;Source and method: &lt;a href="https://github.com/lowkey-studio/quotatrace" rel="noopener noreferrer"&gt;github.com/lowkey-studio/quotatrace&lt;/a&gt;. &lt;code&gt;--share&lt;/code&gt; turns your per-window aggregates into a prefilled GitHub issue. If enough people contribute, we can show per plan and model what a quota point buys and when that changed.&lt;/p&gt;

&lt;p&gt;Caveats: only calls logged on the machine you run it on are counted, &lt;code&gt;used_percent&lt;/code&gt; comes in whole points so short windows are noisy, and tool output sizes are estimated at four characters per token.&lt;/p&gt;

</description>
      <category>codex</category>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I decoded every failed transaction in 40 Ethereum blocks and 150 Base blocks. Here is why they fail.</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 13 Oct 2026 09:42:39 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/i-decoded-every-failed-transaction-in-40-ethereum-blocks-and-150-base-blocks-here-is-why-they-fail-4e2f</link>
      <guid>https://dev.to/lowkeybuilds/i-decoded-every-failed-transaction-in-40-ethereum-blocks-and-150-base-blocks-here-is-why-they-fail-4e2f</guid>
      <description>&lt;p&gt;"Transaction failed" on a block explorer tells you almost nothing. "Fail with error 'execution reverted'" tells you less. So I took every failed transaction from a recent run of blocks on Ethereum mainnet and on Base, replayed each one against the state of the previous block, decoded the revert data, and counted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the decoder used here, &lt;a href="https://pitcrew.appin60.com" rel="noopener noreferrer"&gt;Pitcrew&lt;/a&gt;. Its free web version is linked at the end. The sampling script and raw results are linked below, so you can rerun the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ethereum&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blocks&lt;/td&gt;
&lt;td&gt;26,182,107 to 26,182,146 (40)&lt;/td&gt;
&lt;td&gt;52,543,866 to 52,544,015 (150)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;11,080&lt;/td&gt;
&lt;td&gt;30,809&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;401 (3.6%)&lt;/td&gt;
&lt;td&gt;2,745 (8.9%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Method: for each failed receipt, fetch the transaction, &lt;code&gt;eth_call&lt;/code&gt; it with the same from/to/data/value/gas against the parent block, then decode the revert data: &lt;code&gt;Error(string)&lt;/code&gt;, &lt;code&gt;Panic(uint256)&lt;/code&gt;, or a custom error selector looked up in the openchain signature database. Free public RPCs only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethereum: why the 401 failed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50.1%&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;21,000-gas ETH send to an EIP-7702 account (all 0.0000001 ETH dust)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27.4%&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;state changed earlier in the same block (lost a race: MEV/arb/front-run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.7%&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;reverted with no reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;slippage / price moved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;out of gas (gas limit too low)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;balance or allowance too low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.7%&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;other require() message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;custom error (needs ABI/signature lookup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Half of all failures were one pattern: 21,000 gas sent to EIP-7702 accounts
&lt;/h3&gt;

&lt;p&gt;201 of the 401 failures were plain ETH sends (no calldata) with a gas limit of exactly 21,000, and every one used all of it. All 50 recipients had EIP-7702 delegation code (&lt;code&gt;0xef0100...&lt;/code&gt;), pointing at, among others, MetaMask's delegator and Uniswap's Calibur account.&lt;/p&gt;

&lt;p&gt;Since Pectra, an EOA can delegate to contract code. When you send it ETH, that code runs, so 21,000 gas is no longer enough. Anything that hard-codes 21,000 for "simple ETH transfers" (scripts, payout bots, older wallet code) will fail against these accounts every time, and retrying with the same limit changes nothing.&lt;/p&gt;

&lt;p&gt;To be honest about this sample: every one of these 201 sends was 0.0000001 ETH from 52 addresses retrying 5 to 8 times in eight minutes. That looks like automated dusting, not people losing money. The failure mode is still real for any payout code that sends ETH with a fixed 21,000 gas. &lt;strong&gt;Fix:&lt;/strong&gt; estimate gas for ETH sends, or detect &lt;code&gt;0xef0100&lt;/code&gt; code on the recipient and raise the limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quarter could not be reproduced: you lost a race
&lt;/h3&gt;

&lt;p&gt;110 failures (27%) succeeded when replayed against the previous block's state. They failed because a transaction earlier in the same block changed something: a price, a reserve, a nonce. These are mostly arbitrage and MEV bots losing races, but if you see this on your own swap, someone else's transaction landed first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ones that hurt people: slippage, balance and allowance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Slippage guards (&lt;code&gt;INSUFFICIENT_OUTPUT_AMOUNT&lt;/code&gt;, &lt;code&gt;Too little received&lt;/code&gt;, &lt;code&gt;Min return not reached&lt;/code&gt;): 26&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERC20: transfer amount exceeds balance&lt;/code&gt;, &lt;code&gt;STF&lt;/code&gt; (Uniswap's "safeTransferFrom failed", usually a missing approval or balance): 8&lt;/li&gt;
&lt;li&gt;Out of gas with a real call: 10&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;STF&lt;/code&gt; deserves a special mention because nobody knows what it means. It is Uniswap V3's &lt;code&gt;TransferHelper&lt;/code&gt; saying &lt;code&gt;transferFrom&lt;/code&gt; failed: the router could not pull your tokens. Check the allowance first, then the balance (and fee-on-transfer tokens).&lt;/p&gt;

&lt;h2&gt;
  
  
  Base: why the 2,745 failed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;58.9%&lt;/td&gt;
&lt;td&gt;1616&lt;/td&gt;
&lt;td&gt;state changed earlier in the same block (lost a race: MEV/arb/front-run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15.0%&lt;/td&gt;
&lt;td&gt;413&lt;/td&gt;
&lt;td&gt;custom error (needs ABI/signature lookup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;td&gt;394&lt;/td&gt;
&lt;td&gt;other require() message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.1%&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;reverted with no reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;slippage / price moved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;balance or allowance too low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;out of gas (gas limit too low)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Base fails 2.5 times as often as Ethereum, and its failures are mostly bots. 59% are lost races: they succeed when replayed a block earlier, and 800 distinct senders produced 2,745 failures, with the top sender (&lt;code&gt;0xc0ffeebabe...&lt;/code&gt;) alone accounting for 98. Gas on Base is cheap enough that bots fire speculative transactions and accept that most will fail.&lt;/p&gt;

&lt;p&gt;The revert strings show it: the top messages after "no reason" are &lt;code&gt;sl&lt;/code&gt;, &lt;code&gt;SPL&lt;/code&gt;, &lt;code&gt;SPL1&lt;/code&gt;, &lt;code&gt;sbl1&lt;/code&gt;, &lt;code&gt;:(&lt;/code&gt; and &lt;code&gt;LEG_1_FAILED&lt;/code&gt;. Those are private bot contracts saving bytes, not anything a user would see.&lt;/p&gt;

&lt;p&gt;Human-facing failures are a small share on Base: slippage guards (&lt;code&gt;Min return not reached&lt;/code&gt;, &lt;code&gt;TooMuchSlippage&lt;/code&gt;, &lt;code&gt;QuoteBelowMin&lt;/code&gt;, &lt;code&gt;OutputTooLow&lt;/code&gt;) 2.8%, balance and allowance (&lt;code&gt;transferFrom reverted&lt;/code&gt;, &lt;code&gt;STF&lt;/code&gt;) 0.5%. 15% end in a custom error, and many of those selectors are not in any public signature database, so an explorer shows only hex. If you are a person debugging your own failed Base transaction, the most likely causes are still slippage and allowance. Just don't expect the network-wide numbers to look like your experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The script is about 80 lines of Node with no API keys: &lt;a href="https://github.com/lowkey-studio/x402doctor-data" rel="noopener noreferrer"&gt;failed_txs.mjs&lt;/a&gt;. It runs &lt;code&gt;eth_getBlockReceipts&lt;/code&gt; per block, then replays each failure at &lt;code&gt;block - 1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode your own
&lt;/h2&gt;

&lt;p&gt;If you have a failed or stuck transaction, paste the hash into the free checker: &lt;strong&gt;&lt;a href="https://pitcrew-api.appin60.com/app" rel="noopener noreferrer"&gt;https://pitcrew-api.appin60.com/app&lt;/a&gt;&lt;/strong&gt; (Ethereum, Base, Arbitrum, Optimism, Polygon, BSC; 20 free checks a day). It returns the decoded revert reason, gas used vs limit, token transfers and a plain-language fix. Stuck transactions get the nonce-gap and replace/cancel recipe. If your agent needs it, the same call is pay-per-use over x402 at $0.01 (&lt;code&gt;GET /tx/explain?chain=base&amp;amp;hash=0x...&lt;/code&gt;, USDC on Base or Arbitrum).&lt;/p&gt;

&lt;p&gt;Caveat: replaying against the parent block is an approximation. When an earlier transaction in the same block changed the state, the replay succeeds and the tool says so instead of guessing.&lt;/p&gt;

</description>
      <category>ethereum</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How email verifiers work (SMTP RCPT in 60 lines of Node), and why they fail on 56% of startups</title>
      <dc:creator>Low Key Builds</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:35:33 +0000</pubDate>
      <link>https://dev.to/lowkeybuilds/how-email-verifiers-work-smtp-rcpt-in-60-lines-of-node-and-why-they-fail-on-56-of-startups-4565</link>
      <guid>https://dev.to/lowkeybuilds/how-email-verifiers-work-smtp-rcpt-in-60-lines-of-node-and-why-they-fail-on-56-of-startups-4565</guid>
      <description>&lt;p&gt;Email verification tools mostly do one thing under the hood: they open an SMTP connection to the domain's mail server and ask whether a mailbox exists, then hang up before sending anything. Here is how that works, in about 60 lines of Node, and what happened when I ran it against 499 Y Combinator companies.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built &lt;a href="https://mailferret.appin60.com" rel="noopener noreferrer"&gt;MailFerret&lt;/a&gt;, a pay-per-call email finder for AI agents that uses this technique. The code below is the core idea, free to copy.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The SMTP conversation
&lt;/h2&gt;

&lt;p&gt;Every domain that receives email publishes MX records. Connect to the first one on port 25 and you get a short dialogue:&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="err"&gt;S: 220 mx.google.com ESMTP
C: EHLO probe.example.com
S: 250-mx.google.com at your service
C: MAIL FROM:&amp;lt;verify@example.com&amp;gt;
S: 250 2.1.0 OK
C: RCPT TO:&amp;lt;paul@supabase.com&amp;gt;
S: 250 2.1.5 OK                      &amp;lt;- mailbox exists
C: RCPT TO:&amp;lt;qzxv81kd@supabase.com&amp;gt;
S: 550 5.1.1 The email account that you tried to reach does not exist
C: QUIT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;250&lt;/code&gt; on &lt;code&gt;RCPT TO&lt;/code&gt; means "I'll accept mail for this address". &lt;code&gt;550 5.1.1&lt;/code&gt; means "no such user". You never send &lt;code&gt;DATA&lt;/code&gt;, so no email is delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Node implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;net&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:net&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:dns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rcptCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;)&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;mx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveMx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="o"&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;priority&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="nx"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&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;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sock&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;l&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nx"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&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;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&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="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&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;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimEnd&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&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="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{3}&lt;/span&gt;&lt;span class="sr"&gt; /&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// wait for the final line of a multi-line reply&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&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;step&lt;/span&gt; &lt;span class="o"&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="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EHLO probe.example.com&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MAIL FROM:&amp;lt;verify@example.com&amp;gt;&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`RCPT TO:&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;addresses&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="s2"&gt;&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&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;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// reply to QUIT&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;addresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&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="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;addresses&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;return&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`RCPT TO:&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QUIT&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// First address is random: if the server accepts it, the domain is catch-all.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rcptCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend.com&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qzxv81kd3j@resend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zeno.rocha@resend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="c1"&gt;// { 'qzxv81kd3j@resend.com': 550, 'zeno.rocha@resend.com': 250 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To &lt;em&gt;find&lt;/em&gt; an address rather than check one, generate the usual patterns (&lt;code&gt;first.last&lt;/code&gt;, &lt;code&gt;first&lt;/code&gt;, &lt;code&gt;flast&lt;/code&gt;, &lt;code&gt;firstlast&lt;/code&gt;, &lt;code&gt;f.last&lt;/code&gt;...) and RCPT them in order in one session. The first &lt;code&gt;250&lt;/code&gt; on a domain that rejected the random address is your answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: catch-all domains
&lt;/h2&gt;

&lt;p&gt;Some domains accept mail for any address. Send &lt;code&gt;RCPT TO:&amp;lt;anything@domain&amp;gt;&lt;/code&gt; and you get &lt;code&gt;250&lt;/code&gt;. On those domains SMTP verification can't tell real mailboxes from fake ones. That's why the snippet probes a random address first.&lt;/p&gt;

&lt;p&gt;How common is that? I took 499 random active companies from the public &lt;a href="https://github.com/yc-oss/api" rel="noopener noreferrer"&gt;yc-oss&lt;/a&gt; YC list and checked each website domain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft 365&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No MX on the website domain&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Catch-all&lt;/strong&gt; (of 456 with mail)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;254&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifiable (random address rejected)&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refused to answer (Proofpoint, Cloudflare routing, SES inbound...)&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Google Workspace alone, 60% of these startups had catch-all routing on. So for founders' addresses, more than half the "valid" results you get from any SMTP-based verifier are really "unknown". Full numbers and method: &lt;a href="https://mailferret.appin60.com/study" rel="noopener noreferrer"&gt;mailferret.appin60.com/study&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bite in production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port 25 is blocked&lt;/strong&gt; on most cloud hosts (AWS, GCP, Azure, Vercel, Workers). You need a box where outbound 25 is open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP reputation matters.&lt;/strong&gt; Cloudflare Email Routing answered our residential IP with &lt;code&gt;550&lt;/code&gt; in the banner; Proofpoint and some others just time out. Treat those as "unknown", not "invalid".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Greylisting&lt;/strong&gt; returns &lt;code&gt;4xx&lt;/code&gt; the first time. Retry later or report unknown. One useful exception: Google's &lt;code&gt;450 4.2.1 The user you are trying to contact is receiving mail at a rate...&lt;/code&gt; is only sent for mailboxes that exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't hammer one server.&lt;/strong&gt; Batch all candidates into one session and cache catch-all results per domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large TXT records&lt;/strong&gt; (SPF/DMARC) can time out through some local stub resolvers; querying 1.1.1.1 / 8.8.8.8 directly fixed it for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy rejections aren't "user unknown".&lt;/strong&gt; Parse the text: &lt;code&gt;5.1.1&lt;/code&gt; / "does not exist" means no mailbox; "blocked", "Spamhaus", "policy" means they refused you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you'd rather not run it yourself
&lt;/h2&gt;

&lt;p&gt;MailFerret wraps this as an HTTP API that AI agents pay per call with &lt;a href="https://x402.org" rel="noopener noreferrer"&gt;x402&lt;/a&gt; (USDC on Base, no API key):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /find&lt;/code&gt; &lt;code&gt;{first_name, last_name, domain}&lt;/code&gt;: $0.01, charged only when the mail server confirms the mailbox. Catch-all and not-found results are free.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /verify&lt;/code&gt; &lt;code&gt;{email}&lt;/code&gt;: $0.003.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /domain&lt;/code&gt; &lt;code&gt;{domain}&lt;/code&gt;: $0.005 for mail provider, catch-all status, SaaS tools revealed by SPF, DMARC, public emails and the likely pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From Claude Code or Codex with AgentCash: &lt;code&gt;npx agentcash fetch https://mailferret.appin60.com/find -m POST -b '{"first_name":"Zeno","last_name":"Rocha","domain":"resend.com"}'&lt;/code&gt;. There are 5 free lookups a day on the &lt;a href="https://mailferret.appin60.com" rel="noopener noreferrer"&gt;homepage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cleaning a whole list instead? The homepage has a bulk CSV option: $4 per 1,000 addresses in USDC, quoted before you pay.&lt;/p&gt;

&lt;p&gt;Use it for legitimate B2B outreach, and follow CAN-SPAM/GDPR when you email anyone.&lt;/p&gt;

</description>
      <category>email</category>
      <category>node</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
