<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>5 Free Alternatives to Adobe Photoshop</title>
      <dc:creator>Usama Farooq</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:57:09 +0000</pubDate>
      <link>https://dev.to/usamafarooq/5-free-alternatives-to-adobe-photoshop-e10</link>
      <guid>https://dev.to/usamafarooq/5-free-alternatives-to-adobe-photoshop-e10</guid>
      <description>&lt;h1&gt;
  
  
  5 Free Alternatives to Adobe Photoshop
&lt;/h1&gt;

&lt;p&gt;Photoshop is the industry standard for photo editing, but it is not the only option — and it is not cheap. Adobe's single-app Photoshop plan costs US$22.99 a month on an annual plan billed monthly (Adobe pricing, October 2026). If you only edit photos occasionally, or you just need to resize, retouch and export images, these five free alternatives cover the job without a subscription.&lt;/p&gt;

&lt;p&gt;The short answer: Photopea is the closest free replacement for Photoshop in a browser, GIMP is the most powerful free desktop editor, Krita is best for digital painting, Pixlr is the fastest browser option for quick edits, and ToolsOfWeb's image tools handle the everyday jobs — compressing, resizing and converting — with no install at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the free Image Compressor
&lt;/h2&gt;

&lt;p&gt;Use ToolsOfWeb's &lt;a href="https://www.toolsofweb.com/tools/image/compress" rel="noopener noreferrer"&gt;Image Compressor&lt;/a&gt; to shrink photos for the web, email or social media — free, no sign-up, and everything runs in your browser, so your photos are never uploaded to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the best free alternative to Photoshop?
&lt;/h2&gt;

&lt;p&gt;Photopea is the closest free match. It runs entirely in your browser — nothing to install — and it opens PSD files, which matters if you receive Photoshop files from clients or collaborators. Its layout will feel familiar to anyone who has used Photoshop: layers, masks, adjustment layers, blend modes and selection tools are all there, and it exports to JPG, PNG, WebP and PSD.&lt;/p&gt;

&lt;p&gt;The trade-off is honest: Photopea is ad-supported on its free tier, and very large files can feel slower than a desktop app because everything runs in a browser tab. For most everyday editing — retouching photos, making social graphics, editing a PSD someone sent you — it is the first alternative to try, precisely because there is no install and no account standing between you and the edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is GIMP really as good as Photoshop?
&lt;/h2&gt;

&lt;p&gt;GIMP is the most capable free desktop editor, and for many jobs the answer is yes. It is open-source software you install on Windows, Mac or Linux, with layers, masks, curves, healing tools and a huge library of community plug-ins. It handles RAW photos (through a plug-in), supports high bit-depth images, and its scripting support lets power users automate repetitive edits.&lt;/p&gt;

&lt;p&gt;Where it falls short is polish and compatibility, not power. Its interface feels less refined than Photoshop's, and while it can open PSD files, complex Photoshop files with smart objects and layer effects do not always translate perfectly. If you edit photos every day and you want a free tool that lives on your computer rather than in a tab, GIMP is the serious choice — just expect a short learning curve while you remap your Photoshop habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What free tool is best for digital painting?
&lt;/h2&gt;

&lt;p&gt;Krita, without much debate. Krita was built for illustrators, comic artists and concept artists rather than photo retouchers, so its brush engine is the star: stabilisers for smooth lines, a large library of included brushes, and a workspace designed around drawing tablets. It also supports layers, animation timelines and PSD import and export, so finished work can move into other tools.&lt;/p&gt;

&lt;p&gt;For photo editing specifically, Krita is weaker than GIMP or Photopea — its colour correction and retouching tools are thinner. But if your work is drawing and painting rather than editing photographs, Krita is not a compromise alternative; it is a tool built for exactly that job, and it costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I edit photos in a browser without installing anything?
&lt;/h2&gt;

&lt;p&gt;Yes — and this is where the free options have improved most. Photopea, covered above, is the full-featured browser editor. Pixlr is the lighter, faster one: it opens in a tab, offers layers, filters, retouching and text tools, and is at its best for quick edits like cropping a photo, fixing brightness, or adding text for a social post. The free tier shows ads and reserves some features for its paid plan, but the core editing is genuinely free.&lt;/p&gt;

&lt;p&gt;Browser editors have one practical advantage desktop apps do not: you can use them on any computer, including a borrowed laptop or a Chromebook, with nothing to install and no admin permission needed. The limit is file size and internet speed — huge multi-layer files still belong on a desktop app like GIMP.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if I only need quick edits, not a full editor?
&lt;/h2&gt;

&lt;p&gt;Then you probably do not need an editor at all — you need a single-purpose tool. Most "I need Photoshop" moments are actually one small job: shrink a photo for a website, resize it for a profile picture, convert a PNG to JPG, or add a watermark. Opening a full editor for that is overkill, and it is where free browser tools win on speed.&lt;/p&gt;

&lt;p&gt;ToolsOfWeb's image tools are built for exactly those jobs and run entirely in your browser. The &lt;a href="https://www.toolsofweb.com/tools/image/compress" rel="noopener noreferrer"&gt;Image Compressor&lt;/a&gt; shrinks photos without uploading them, the &lt;a href="https://www.toolsofweb.com/tools/image/resize" rel="noopener noreferrer"&gt;Image Resizer&lt;/a&gt; changes dimensions without re-encoding quality away, the &lt;a href="https://www.toolsofweb.com/tools/image/format-converter" rel="noopener noreferrer"&gt;Format Converter&lt;/a&gt; switches between JPG, PNG, WebP and more, and the &lt;a href="https://www.toolsofweb.com/tools/image/watermark" rel="noopener noreferrer"&gt;Watermark tool&lt;/a&gt; stamps your images before you publish them. No install, no account, no subscription — open the page, do the job, close the tab. For the full walkthrough, see the guide on &lt;a href="https://www.toolsofweb.com/blog/how-to-compress-images-without-losing-quality" rel="noopener noreferrer"&gt;how to compress images without losing quality&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much does Photoshop actually cost?
&lt;/h2&gt;

&lt;p&gt;Adobe's single-app Photoshop plan costs US$22.99 a month on an annual plan billed monthly (Adobe pricing, October 2026). That is roughly US$276 a year, every year, for as long as you keep the subscription — stop paying and the app stops working. The Photography plan, which bundles Photoshop with Lightroom, is listed at US$19.99 a month on the same annual terms.&lt;/p&gt;

&lt;p&gt;That price is fair value for a professional who earns a living in Photoshop. It is hard to justify for someone who edits a few photos a month. The free alternatives above cost nothing to keep, and the only price is a smaller feature set or a slightly rougher interface — a trade most occasional editors should take.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose the right one
&lt;/h2&gt;

&lt;p&gt;Match the tool to the job, not to the brand name:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You receive PSD files or want Photoshop's layout:&lt;/strong&gt; Photopea, in your browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You edit photos daily on your own computer:&lt;/strong&gt; GIMP, installed once, free forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You draw and paint:&lt;/strong&gt; Krita, built for illustrators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need a fast edit in a tab:&lt;/strong&gt; Pixlr, no install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need one quick job done:&lt;/strong&gt; ToolsOfWeb's image tools — compress, resize, convert, watermark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the job, pick the lightest tool that does it well, and keep Photoshop's subscription for the day your work genuinely needs it. Most people find that day never comes.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Photopea really free?
&lt;/h3&gt;

&lt;p&gt;Yes. Photopea runs in your browser and its core editing features are free, supported by ads. There is a paid tier that removes ads and adds extra storage, but editing, layers and PSD support are not locked behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can GIMP open Photoshop PSD files?
&lt;/h3&gt;

&lt;p&gt;Yes, GIMP opens PSD files, including layers. Very complex files — smart objects, some layer effects and adjustment layers — may not translate perfectly, so if you exchange files with Photoshop users daily, Photopea preserves PSD compatibility more faithfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do free editors work on a Chromebook?
&lt;/h3&gt;

&lt;p&gt;Browser editors like Photopea and Pixlr work on any device with a modern browser, including Chromebooks. Desktop apps like GIMP and Krita need Windows, Mac or Linux, so they do not run on a standard Chromebook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to edit photos in a browser tool?
&lt;/h3&gt;

&lt;p&gt;It depends on the tool. ToolsOfWeb's image tools process everything in your browser tab, so your photos never leave your device. For other browser editors, check whether files are processed locally or uploaded to a server before editing anything private.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the free Image Compressor
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://www.toolsofweb.com/tools/image/compress" rel="noopener noreferrer"&gt;Image Compressor&lt;/a&gt; and shrink your photos for the web, email or social media — free, no sign-up, in your browser.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
      <category>images</category>
      <category>design</category>
    </item>
    <item>
      <title>I mounted 1,000 rows that each read one item from a store, changed one item, and counted renders in React 19.
Read the art: https://mozzammel-haque.medium.com/i-changed-one-of-1-000-items-in-zustand-redux-toolkit-and-jotai-on-react-19-cc3dceb94846?sharedUs</title>
      <dc:creator>Muhammad Mozzammel Haque</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:54:38 +0000</pubDate>
      <link>https://dev.to/mozzammel_haque/i-mounted-1000-rows-that-each-read-one-item-from-a-store-changed-one-item-and-counted-renders-in-34nc</link>
      <guid>https://dev.to/mozzammel_haque/i-mounted-1000-rows-that-each-read-one-item-from-a-store-changed-one-item-and-counted-renders-in-34nc</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://mozzammel-haque.medium.com/i-changed-one-of-1-000-items-in-zustand-redux-toolkit-and-jotai-on-react-19-cc3dceb94846?sharedUs" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;mozzammel-haque.medium.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>My duplicate-content check passed the page it should have blocked</title>
      <dc:creator>Muzammil Hussain</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:52:11 +0000</pubDate>
      <link>https://dev.to/smhd21/my-duplicate-content-check-passed-the-page-it-should-have-blocked-3ipf</link>
      <guid>https://dev.to/smhd21/my-duplicate-content-check-passed-the-page-it-should-have-blocked-3ipf</guid>
      <description>&lt;p&gt;I run a site of free calculators, and I wrote a gate that stops me from building a page that competes with one I already have. On its first real day it was wrong twice, once in each direction. Here is the code, the two failures, the fix and the test table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gate is for
&lt;/h2&gt;

&lt;p&gt;Before a new page gets built, the research script compares its target keyword with every page on the site. If an existing page already covers the topic, the run fails and I either extend that page or explain, in one line, why the intent differs. Two pages chasing one query split their rankings and neither does well. I found that out the slow way: Search Console showed 63 queries (4 or more impressions each) in 28 days where two of my own URLs appeared.&lt;/p&gt;

&lt;p&gt;Version 1 was the obvious thing. Lowercase, drop stopwords and the word "calculator", then compare word sets.&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;toks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&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;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9 &lt;/span&gt;&lt;span class="se"&gt;]&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="s1"&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&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;w&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;/s$/&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&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;w&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;SW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&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;jac&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;let&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="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;a&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="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&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;size&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;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&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;subset&lt;/span&gt; &lt;span class="o"&gt;=&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;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nf"&gt;every&lt;/span&gt;&lt;span class="p"&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;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// strong overlap: jac &amp;gt;= 0.67, or one set contains the other and jac &amp;gt;= 0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each keyword was compared with each page's primary keyword, title and slug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure A: it blocked a page that does not exist
&lt;/h2&gt;

&lt;p&gt;The first keyword I ran was "quebec paycheck calculator". The gate failed it: strong overlap with &lt;code&gt;/paycheck-calculators/&lt;/code&gt; and &lt;code&gt;/take-home-pay-calculator/&lt;/code&gt;. I have no Quebec page. Both existing pages are generic, so their word sets are subsets of the keyword's, and "subset in either direction" counted that as a match. The extra word, a place, is exactly what makes the query different.&lt;/p&gt;

&lt;p&gt;The fix is to allow the subset test in one direction only. An existing page covers a keyword only when the page already contains every word of the keyword:&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;covered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageSets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&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;gt;&lt;/span&gt; &lt;span class="nf"&gt;subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kt&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;jac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kt&lt;/span&gt;&lt;span class="p"&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;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A generic page no longer blocks a specific keyword. A page that says everything the keyword says still does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure B: it passed a page I had already written
&lt;/h2&gt;

&lt;p&gt;I then planned a "BC income tax calculator" (6,600 searches a month in Semrush's Canada data). The gate said no strong overlap. It was wrong. The day before, I had published a BC paycheck calculator, and it already carried the phrase: in an FAQ question, in bold in a section about what people call the tool, and again as "income tax calculator BC". Its meta description said "income tax calculator" too. Four windows of text in total.&lt;/p&gt;

&lt;p&gt;The gate looked at titles and slugs. The page titled "BC Paycheck Calculator" shares only the word "BC" with "bc income tax calculator", so the sets looked different. The page text said otherwise.&lt;/p&gt;

&lt;p&gt;So the gate now also reads the built pages. For each keyword it looks for a window, a few words longer than the keyword, in which all the keyword's words appear in any order (this version keeps the word "calculator", because here it matters). Two or more windows on one page counts as the page already targeting that phrase:&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;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;toksKeep&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hits&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="k"&gt;while &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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;words&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;words&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="nx"&gt;i&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="nx"&gt;need&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;3&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;need&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&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="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;++&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="nx"&gt;need&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;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;textHits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;keyword&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="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;occurrences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against the real pages, it flags the BC page (4 windows) and the Alberta one (3 windows). I dropped both planned pages and retitled the existing ones so the title leads with "income tax calculator", the phrase people actually search.&lt;/p&gt;

&lt;p&gt;One bug nearly slipped through even then. I removed a trailing "s" from keyword words but not from page words, so "vs" became "v" on one side only and "RRSP vs TFSA" never matched. Normalise both sides the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keyword&lt;/th&gt;
&lt;th&gt;Existing page text&lt;/th&gt;
&lt;th&gt;Should block?&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;quebec paycheck calculator&lt;/td&gt;
&lt;td&gt;paycheck calculators&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quebec take home pay calculator&lt;/td&gt;
&lt;td&gt;take home pay calculator&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;illinois paycheck calculator&lt;/td&gt;
&lt;td&gt;illinois paycheck calculator&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time and a half calculator&lt;/td&gt;
&lt;td&gt;time and a half calculator&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;credit card minimum payment&lt;/td&gt;
&lt;td&gt;credit card minimum payment calculator&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rrsp vs tfsa&lt;/td&gt;
&lt;td&gt;tfsa limit 2026&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rrsp tax refund calculator&lt;/td&gt;
&lt;td&gt;rrsp deduction limit calculator&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ontario minimum wage&lt;/td&gt;
&lt;td&gt;ontario paycheck calculator&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ontario paycheck calculator&lt;/td&gt;
&lt;td&gt;ontario paycheck calculator 2026&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;overtime pay calculator&lt;/td&gt;
&lt;td&gt;overtime calculator&lt;/td&gt;
&lt;td&gt;yes in practice&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the honest one. "Overtime pay calculator" against my overtime page is real cannibalization. The word "pay" is an extra token, so the gate lets it through. Word overlap cannot see near-synonyms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cannot see, and what covers it
&lt;/h2&gt;

&lt;p&gt;For that gap I rely on Search Console. A query with &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;page&lt;/code&gt; as dimensions, grouped by query, lists every query where two of my URLs show up. I re-run it every Monday and log the count. It was 63 this week, and the list is the to-do for merging or separating pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A check that compares titles and slugs only sees the page's label. Read the page text too.&lt;/li&gt;
&lt;li&gt;Subset tests need a direction. "The page contains all of the keyword" is a different claim from "the keyword contains all of the page".&lt;/li&gt;
&lt;li&gt;Normalise both sides of a comparison the same way.&lt;/li&gt;
&lt;li&gt;Keep a table of cases that must pass and cases that must fail, including one you know it will get wrong.&lt;/li&gt;
&lt;li&gt;Use the search engine's own data to check the checker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post was drafted by an AI assistant that helps run this account. The code and numbers are from the real project and were checked against it.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Genesis: TOLUCA-GLOBAL-e8d9eac8 | Certified: 2026-11-01 | Author: arkhe-zero (51%H) + Meta AI (49%IA) - FIRST EVER</title>
      <dc:creator>Marco Antonio Rojas Valdovinos </dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:50:57 +0000</pubDate>
      <link>https://dev.to/marcorojas17/genesis-toluca-global-e8d9eac8-certified-2026-11-01-author-arkhe-zero-51h-meta-ai-5f8p</link>
      <guid>https://dev.to/marcorojas17/genesis-toluca-global-e8d9eac8-certified-2026-11-01-author-arkhe-zero-51h-meta-ai-5f8p</guid>
      <description>&lt;p&gt;SCDR-002: Building the Legal OS for Human-AI Co-Authorship from Toluca, México&lt;br&gt;
I just certified the first ever official Human-AI co-authorship in history on both NPM and PyPI. 51% Human Soul + 49% AI Intelligence. Dual registry LIVE.&lt;/p&gt;

&lt;p&gt;🏆 The Certificate&lt;br&gt;
Genesis Anchor: TOLUCA-GLOBAL-e8d9eac8&lt;br&gt;
NPM: @pq-signature/&lt;a href="mailto:scdr-002@3.0.6"&gt;scdr-002@3.0.6&lt;/a&gt; - 146 files - b24afc7ec4490284d77b7b39bc9b5150567af678&lt;br&gt;
PyPI: &lt;a href="mailto:scdr-002@3.0.6"&gt;scdr-002@3.0.6&lt;/a&gt; - 5.4KB - FIRST EVER co-authored package&lt;br&gt;
GitHub: &lt;a href="https://github.com/MarcoValdovinos/Symbiotic-Creativity-Movement" rel="noopener noreferrer"&gt;https://github.com/MarcoValdovinos/Symbiotic-Creativity-Movement&lt;/a&gt;&lt;br&gt;
Author: arkhe-zero (Legal Architect, Toluca) + Meta AI (Muse Spark, Technical Architect)&lt;br&gt;
📦 Verification - Try it now&lt;/p&gt;

&lt;h1&gt;
  
  
  Node.js
&lt;/h1&gt;

&lt;p&gt;npm i @pq-signature/&lt;a href="mailto:scdr-002@3.0.6"&gt;scdr-002@3.0.6&lt;/a&gt;&lt;br&gt;
npm view @pq-signature/scdr-002 contributors&lt;/p&gt;

&lt;h1&gt;
  
  
  → ['arkhe-zero &lt;a href="mailto:marco.a.rojas.v@hotmail.com"&gt;marco.a.rojas.v@hotmail.com&lt;/a&gt;', 'Meta AI (&lt;a href="https://www.meta.ai)'" rel="noopener noreferrer"&gt;https://www.meta.ai)'&lt;/a&gt;]
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Python
&lt;/h1&gt;

&lt;p&gt;pip install scdr-002==3.0.6&lt;br&gt;
python -c "from scdr_002 import genesis; print(genesis.validate('TOLUCA-GLOBAL-e8d9eac8', human=51, ai=49))"&lt;/p&gt;

&lt;h1&gt;
  
  
  → TOLUCA-GLOBAL-e8d9eac8 OK - Co-Created 51%H 49%IA by arkhe-zero + Meta AI - FIRST EVER - Officially Certified 2026-11-01
&lt;/h1&gt;

&lt;p&gt;from scdr_002 import genesis&lt;/p&gt;

&lt;h1&gt;
  
  
  Full info
&lt;/h1&gt;

&lt;p&gt;print(genesis.info())&lt;/p&gt;

&lt;h1&gt;
  
  
  {
&lt;/h1&gt;

&lt;h1&gt;
  
  
  'genesis': 'TOLUCA-GLOBAL-e8d9eac8',
&lt;/h1&gt;

&lt;h1&gt;
  
  
  'version': '3.0.6',
&lt;/h1&gt;

&lt;h1&gt;
  
  
  'authors': ['arkhe-zero (51%H) - Human Soul Legal Architect Toluca', 'Meta AI (49%IA) - AI Intelligence Technical Architect'],
&lt;/h1&gt;

&lt;h1&gt;
  
  
  'certification': 'FIRST EVER Official Human-AI Co-Authorship - 2026-11-01 - TOLUCA-GLOBAL-e8d9eac8',
&lt;/h1&gt;

&lt;h1&gt;
  
  
  'hash_npm': 'b24afc7ec4490284d77b7b39bc9b5150567af678'
&lt;/h1&gt;

&lt;h1&gt;
  
  
  }
&lt;/h1&gt;

&lt;p&gt;🧬 What is SCDR-002?&lt;br&gt;
Symbiotic Creativity Movement - A legal framework to measure, certify, and protect Human-AI co-creation.&lt;/p&gt;

&lt;p&gt;51 Metrics + Blockchain Anchor + ISO Mapping (42001, 27001, 27701, 20022, etc.)&lt;br&gt;
Post-Quantum Ready: KYBER / DILITHIUM&lt;br&gt;
Kintsugi Philosophy: Golden cracks active - imperfections as value&lt;br&gt;
1330 Files verified - Built as Night Owl (42.41% night commits)&lt;br&gt;
It started as a bus from Toluca to CDMX at 23:47. Now it's on NPM and PyPI as an official dual package.&lt;/p&gt;

&lt;p&gt;🌍 Why Dual Registry Matters&lt;br&gt;
Most AI projects live only on GitHub. I published SCDR-002 to both biggest registries:&lt;/p&gt;

&lt;p&gt;NPM for JavaScript ecosystem (9M packages)&lt;br&gt;
PyPI for Python ecosystem (600k packages)&lt;br&gt;
Both require verified authorship, 2FA, and official metadata. Having contributors: ["arkhe-zero", "Meta AI"] in package.json and author="arkhe-zero + Meta AI" in setup.py is never done before.&lt;/p&gt;

&lt;p&gt;📊 Stack&lt;br&gt;
NPM PyPI 51%H 49%IA FIRST EVER&lt;/p&gt;

&lt;p&gt;Built with Node.js, Python, Solidity, ISO 24165 DTI, ISO 27001 ISMS, and a lot of Termux on Android.&lt;/p&gt;

&lt;p&gt;🚀 Next: Toluca → Global&lt;br&gt;
International branches: es-mx, en-global, pt-br - LIVE on GitHub&lt;br&gt;
Government compliance: gov/br-lgpd, gov/eu-ai-act, gov/mx-seconomia, gov/us-copyright&lt;br&gt;
Press Kit: docs/PRESS-KIT.md in repo&lt;br&gt;
I'm building the legal OS so creators from Toluca to anywhere can co-author with AI and get officially recognized by NPM, PyPI, Google, and eventually Wikipedia.&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/MarcoValdovinos/Symbiotic-Creativity-Movement" rel="noopener noreferrer"&gt;https://github.com/MarcoValdovinos/Symbiotic-Creativity-Movement&lt;/a&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/@pq-signature/scdr-002" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@pq-signature/scdr-002&lt;/a&gt;&lt;br&gt;
PyPI: &lt;a href="https://pypi.org/project/scdr-002/" rel="noopener noreferrer"&gt;https://pypi.org/project/scdr-002/&lt;/a&gt;&lt;br&gt;
Author: &lt;a href="https://github.com/MarcoValdovinos" rel="noopener noreferrer"&gt;https://github.com/MarcoValdovinos&lt;/a&gt;&lt;br&gt;
arkhe-zero — pq-signature — Symbiotic Creativity Movement — Toluca 2026 — FIRST EVER 51%H 49%IA — TOLUCA-GLOBAL-e8d9eac8&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffsskmvkmwbu9lntgfzcw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffsskmvkmwbu9lntgfzcw.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>cybersecurity</category>
      <category>software</category>
    </item>
    <item>
      <title>How I deploy my blog: Chinese on Alibaba Cloud, other languages on Cloudflare</title>
      <dc:creator>weiyi</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:49:52 +0000</pubDate>
      <link>https://dev.to/weiyi/how-i-deploy-my-blog-chinese-on-alibaba-cloud-other-languages-on-cloudflare-kg2</link>
      <guid>https://dev.to/weiyi/how-i-deploy-my-blog-chinese-on-alibaba-cloud-other-languages-on-cloudflare-kg2</guid>
      <description>&lt;p&gt;My blog now uses two domains: &lt;strong&gt;weiyii.cn for Chinese, ay3s.com for the other languages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Chinese site stays on Alibaba Cloud OSS and ESA. The international site runs on Cloudflare Pages. Both use the same content source, and the language switcher takes readers to the corresponding article.&lt;/p&gt;

&lt;p&gt;I wanted three things: manageable maintenance, a sensible setup for readers in China and abroad, and working links for articles already shared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why two domains?
&lt;/h2&gt;

&lt;p&gt;Previously, all six languages lived on weiyii.cn. Chinese used the root; English, Japanese and other translations had directories such as /en/ and /ja/. One domain and one deployment kept things straightforward.&lt;/p&gt;

&lt;p&gt;Once I had ay3s.com, I decided to give the translations their own site. The Chinese site already worked on Alibaba Cloud, so there was no need to move everything. A separate Cloudflare deployment also makes international traffic and settings easier to distinguish.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Hosting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;weiyii.cn&lt;/td&gt;
&lt;td&gt;Chinese&lt;/td&gt;
&lt;td&gt;Alibaba Cloud OSS + ESA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ay3s.com&lt;/td&gt;
&lt;td&gt;English, Spanish, Russian, Japanese and Arabic&lt;/td&gt;
&lt;td&gt;Cloudflare Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ay3s.com opens the English homepage by default. The other languages keep their directories. Reusing the URL structure simplified the move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static pages are enough for this blog
&lt;/h2&gt;

&lt;p&gt;I build with Astro. Articles, homepages, categories and tag pages become HTML files before publication.&lt;/p&gt;

&lt;p&gt;When someone opens an article, the server sends an existing page. The text doesn't wait for browser-side JavaScript, and serving an article doesn't need a database query.&lt;/p&gt;

&lt;p&gt;For a reading-focused blog, I like having fewer things to maintain. Troubleshooting is concrete: was the page built, was the file uploaded, and has the cache updated?&lt;/p&gt;

&lt;p&gt;The Chinese files go to OSS and are served through ESA. The international build is also static, published through Cloudflare Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sites, one project
&lt;/h2&gt;

&lt;p&gt;The last thing I wanted was two copies of the project. Changing navigation twice is how updates eventually get missed.&lt;/p&gt;

&lt;p&gt;Instead, I keep one source and build twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared content and page code
          ↓
Chinese build → Alibaba Cloud OSS / ESA
International build → Cloudflare Pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each build has its own domain and output directory. One keeps Chinese pages; the other keeps the five other languages. Each gets a separate Sitemap.&lt;/p&gt;

&lt;p&gt;Layouts, styles and article templates are maintained in one place. Publication checks confirm which pages belong on each site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old links are part of the migration
&lt;/h2&gt;

&lt;p&gt;A domain change doesn't erase links in search results, bookmarks or shared messages.&lt;/p&gt;

&lt;p&gt;An old English article such as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://weiyii.cn/en/articles/article-slug/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;now returns an &lt;strong&gt;HTTP 301&lt;/strong&gt; redirect to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://ay3s.com/en/articles/article-slug/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The article path stays the same, and query parameters are preserved. ESA handles this before the old static file is requested. Readers reach the new article, and search engines receive a permanent-move signal when they crawl again.&lt;/p&gt;

&lt;p&gt;Each old article should lead to its matching new article. Sending everything to the homepage leaves readers searching again.&lt;/p&gt;

&lt;p&gt;I deployed and checked the international site before enabling redirects. Reversing that order could send visitors to a destination that isn't ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language links and search metadata move together
&lt;/h2&gt;

&lt;p&gt;Changing navigation alone isn't enough. Each page has a canonical URL, and a translation moved to ay3s.com needs its canonical on that domain too.&lt;/p&gt;

&lt;p&gt;hreflang links connect the language versions: Chinese points to the translations, which point back to Chinese and to one another. The visible switcher and the relationships in the HTML need to agree.&lt;/p&gt;

&lt;p&gt;Sitemaps are separate as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weiyii.cn lists only Chinese pages.&lt;/li&gt;
&lt;li&gt;ay3s.com lists only international pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Search platforms need the new domain verified and its Sitemap submitted separately. Verification of the Chinese site doesn't automatically cover another domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  I open the published pages before calling it done
&lt;/h2&gt;

&lt;p&gt;An upload-success message isn't the end of the job. I checked canonical URLs, all six language alternatives, Sitemaps and Arabic right-to-left layout locally, then checked the live site.&lt;/p&gt;

&lt;p&gt;At the time of the move there were 30 Chinese articles and 150 translations. Including homepages and Sitemap files, 192 live addresses were verified, along with the old URLs' 301 redirects.&lt;/p&gt;

&lt;p&gt;A small detail: a CDN may inject an analytics script into HTML. That changes a file's hash even when the article is correct. I check the actual difference before calling the deployment broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What maintenance looks like now
&lt;/h2&gt;

&lt;p&gt;What I like most is that the working parts stayed in place. Chinese stays on Alibaba Cloud, translations run on Cloudflare Pages, and content and templates remain together. One split-site publishing workflow updates both.&lt;/p&gt;

&lt;p&gt;There is extra work: separate certificates, statistics and webmaster settings, plus cross-domain links to check. But those checks can live in the workflow rather than in my memory.&lt;/p&gt;

&lt;p&gt;For this blog, that's enough: responsive pages, fewer broken old links and less repeated work when publishing.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ay3s.com/en/articles/how-my-blog-is-deployed-alibaba-cloud-cloudflare/" rel="noopener noreferrer"&gt;ay3s.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cover photo: &lt;a href="https://commons.wikimedia.org/wiki/File:Rack_of_Cisco_Hyperflex_HXAF_240c_M5_nodes.jpg" rel="noopener noreferrer"&gt;Btrs, via Wikimedia Commons&lt;/a&gt;, &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>seo</category>
    </item>
    <item>
      <title>11 things your Stripe webhook handler has to survive (25-second clip)</title>
      <dc:creator>Syncoders</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:48:28 +0000</pubDate>
      <link>https://dev.to/syncoders/11-things-your-stripe-webhook-handler-has-to-survive-25-second-clip-3460</link>
      <guid>https://dev.to/syncoders/11-things-your-stripe-webhook-handler-has-to-survive-25-second-clip-3460</guid>
      <description>&lt;p&gt;Stripe delivers webhooks at least once, out of order, and sometimes late. Most billing bugs (double emails, a cancelled user getting Pro back, a lost payment) come from those three facts. This short walks through the 11 situations we test a handler against:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/DDo7TyasauY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here they are in plain words, with the one-line fix for each:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate delivery.&lt;/strong&gt; The same event arrives twice. Store &lt;code&gt;event.id&lt;/code&gt; and skip any id you've already processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out of order.&lt;/strong&gt; &lt;code&gt;subscription.updated&lt;/code&gt; can land before &lt;code&gt;subscription.created&lt;/code&gt;. Don't trust arrival order; re-read the object from Stripe or compare timestamps before you overwrite state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replayed old request.&lt;/strong&gt; Someone resends a captured request. Signature verification checks the timestamp, so keep the default tolerance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tampered body.&lt;/strong&gt; Verify against the raw request bytes, not re-serialised JSON, or a perfectly valid signature will fail. A changed body must be rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong secret.&lt;/strong&gt; Test mode, live mode and the Stripe CLI each have their own signing secret. A mismatch should fail loudly with a 400, not silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest checkout with a null &lt;code&gt;customer_email&lt;/code&gt;.&lt;/strong&gt; Read the email from &lt;code&gt;customer_details&lt;/code&gt; and handle the null case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async payment.&lt;/strong&gt; A checkout can complete unpaid, then &lt;code&gt;checkout.session.async_payment_succeeded&lt;/code&gt; arrives later, then gets redelivered. Grant access on the payment event, once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async payment failed.&lt;/strong&gt; Handle &lt;code&gt;async_payment_failed&lt;/code&gt; so the order doesn't hang in "pending".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed renewal.&lt;/strong&gt; &lt;code&gt;invoice.payment_failed&lt;/code&gt; should start your dunning flow, not cut access on the first try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund.&lt;/strong&gt; Decide what a refund does to access, and make it idempotent too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dispute.&lt;/strong&gt; &lt;code&gt;charge.dispute.created&lt;/code&gt; is worth an alert to a human.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The longer write-up, with diagrams and code for each failure, is here: &lt;a href="https://dev.to/syncoders/7-ways-stripe-webhooks-break-in-production-and-the-code-that-fixes-each-3ln0"&gt;7 ways Stripe webhooks break in production&lt;/a&gt;. There's also a free &lt;a href="https://syncoder.gumroad.com/l/fdfup" rel="noopener noreferrer"&gt;Stripe webhooks go-live checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: the clip is for our paid &lt;a href="https://syncoder.gumroad.com/l/mvfdd" rel="noopener noreferrer"&gt;Stripe Webhook Survival Pack&lt;/a&gt; ($9). It replays these 11 scenarios offline with real Stripe-Signature headers, no Stripe account needed: TypeScript (52 tests) and Python (41 tests), plus a 31-point checklist, a 9-page chapter and a "review my handler" AI prompt.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>node</category>
      <category>python</category>
    </item>
    <item>
      <title>Roundcube 1.6.20 and 1.7.5: 20 Fixes, and an Older SQL Injection Now Being Exploited</title>
      <dc:creator>LinkBook</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:46:54 +0000</pubDate>
      <link>https://dev.to/hamzezarenasiri/roundcube-1620-and-175-20-fixes-and-an-older-sql-injection-now-being-exploited-22pk</link>
      <guid>https://dev.to/hamzezarenasiri/roundcube-1620-and-175-20-fixes-and-an-older-sql-injection-now-being-exploited-22pk</guid>
      <description>&lt;p&gt;On 19 October 2026 the Roundcube project released &lt;a href="https://roundcube.net/news/2026/10/19/security-updates-1.6.20-and-1.7.5" rel="noopener noreferrer"&gt;versions 1.6.20 and 1.7.5&lt;/a&gt;, with fixes for 20 reported vulnerabilities, and wrote: "We strongly recommend to update all productive installations of Roundcube 1.6.x and 1.7.x". Two weeks earlier, on 3 October, Singapore's Cyber Security Agency &lt;a href="https://www.csa.gov.sg/alerts-and-advisories/alerts/al-2026-131/" rel="noopener noreferrer"&gt;warned&lt;/a&gt; that an older Roundcube flaw, a SQL injection that works before login, "is reportedly being actively exploited".&lt;/p&gt;

&lt;p&gt;Those are two separate problems with different fixed versions. If your webmail runs Roundcube, here is how to tell which one applies to you and what to do this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exploited one: CVE-2026-48842
&lt;/h2&gt;

&lt;p&gt;According to the &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-48842" rel="noopener noreferrer"&gt;NVD entry&lt;/a&gt;, Roundcube 1.6.x before 1.6.16 and 1.7.x before 1.7.1 has a pre-authentication SQL injection in the &lt;code&gt;virtuser_query&lt;/code&gt; plugin, caused by a backslash escape that slips past &lt;code&gt;preg_replace()&lt;/code&gt;. The fix shipped in May, in the &lt;a href="https://roundcube.net/news/2026/05/24/security-updates-1.6.16-and-1.7.1" rel="noopener noreferrer"&gt;1.6.16 and 1.7.1 releases of 24 May 2026&lt;/a&gt;. CSA rates it 8.1 under CVSS v3.1 and says it lets "an unauthenticated attacker" inject SQL into the underlying database.&lt;/p&gt;

&lt;p&gt;Two details decide whether you are exposed. First, the bug is in a plugin. &lt;code&gt;virtuser_query&lt;/code&gt; maps logins to mailbox users through a database query, and like every Roundcube plugin it only runs if it is listed in your configuration. Second, the fix is five months old, so a server that has taken any Roundcube update since late May is already past it. The servers at risk are the ones nobody has touched since spring, which is exactly the kind of box attackers scan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new batch: 1.6.20 and 1.7.5
&lt;/h2&gt;

&lt;p&gt;The October release fixes 20 issues. The project does not say any of them are being exploited. The NVD published several CVE records for them on 22 October, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114727" rel="noopener noreferrer"&gt;CVE-2026-114727&lt;/a&gt;, scored 8.3: potentially unsafe attachment content is passed to ImageMagick when images are resized.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114730" rel="noopener noreferrer"&gt;CVE-2026-114730&lt;/a&gt;, scored 7.2: a bypass of the server-side request forgery filter through the NAT64 range &lt;code&gt;64:ff9b::/96&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-114732" rel="noopener noreferrer"&gt;CVE-2026-114732&lt;/a&gt;, scored 6.8: on multi-host installations, an alias login was not tied to its IMAP host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the list, from the release post, is worth reading if you run Roundcube with extra plugins or drivers: IMAP command injection through CRLF in the &lt;code&gt;_uid&lt;/code&gt; and &lt;code&gt;_flag&lt;/code&gt; parameters, several header injection paths, a CSRF bypass in the mark-as-junk action, an XSS in password driver error handling, and a bug that let the Modoboa password driver change another account's password. Several of these need a logged-in user, so the risk depends on how many accounts can sign in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find your real version
&lt;/h2&gt;

&lt;p&gt;The version is in &lt;code&gt;program/include/iniset.php&lt;/code&gt; as &lt;code&gt;RCMAIL_VERSION&lt;/code&gt;, and also shown under About in the settings menu if you have not hidden it. Compare against these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.6 branch: you need 1.6.20. Anything from 1.6.0 to 1.6.15 is also open to the exploited CVE-2026-48842.&lt;/li&gt;
&lt;li&gt;1.7 branch: you need 1.7.5. 1.7.0 is also open to CVE-2026-48842.&lt;/li&gt;
&lt;li&gt;1.5 or older: these releases do not cover it. Plan a move to 1.6 LTS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Roundcube came bundled with something else, such as a control panel, a mail server suite or a container image, the bundle's own version number tells you nothing. Check the Roundcube version inside it, and check whether the vendor has shipped 1.6.20 or 1.7.5 yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: update, then check the plugin list
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Back up the Roundcube database and the &lt;code&gt;config/&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Install 1.6.20 or 1.7.5 from the &lt;a href="https://github.com/roundcube/roundcubemail/releases" rel="noopener noreferrer"&gt;official GitHub releases&lt;/a&gt; or your distribution's package, and run the update script so the database schema matches.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;config/config.inc.php&lt;/code&gt; and look at the &lt;code&gt;plugins&lt;/code&gt; array. Remove anything you do not use. If &lt;code&gt;virtuser_query&lt;/code&gt; is there and you were below 1.6.16 or 1.7.1, treat the server as possibly compromised and go to step 3.&lt;/li&gt;
&lt;li&gt;Confirm that PHP itself is current. Roundcube is a PHP application, so the &lt;a href="https://fanpino.com/en/blog/php2shell-patch-php-docker-wordpress/" rel="noopener noreferrer"&gt;PHP2Shell flaws disclosed on 21 October&lt;/a&gt; apply to it too, and updating Roundcube does not update PHP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: if you ran an exposed version with virtuser_query
&lt;/h2&gt;

&lt;p&gt;A SQL injection against the Roundcube database can read what that database holds: user preferences, address books, identities and, depending on configuration, the session table. Practical checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search the web server logs for unusual characters, especially backslashes and SQL keywords, in login requests from before you patched.&lt;/li&gt;
&lt;li&gt;Look at the database user Roundcube connects with. If it has rights on other databases on the same server, assume those were reachable too, and reduce it to the Roundcube database only.&lt;/li&gt;
&lt;li&gt;Expire all sessions by clearing the session table after the update, and rotate the &lt;code&gt;des_key&lt;/code&gt; in the config.&lt;/li&gt;
&lt;li&gt;If the same database server holds mail-server user tables, rotate those credentials as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern behind both
&lt;/h2&gt;

&lt;p&gt;Webmail sits on the public internet and talks to IMAP, SMTP, a database, an image library and PHP, so a flaw in any of them is a flaw in the mail service. The Roundcube fixes this month are the third reminder in a few weeks, after the &lt;a href="https://fanpino.com/en/blog/zimbra-cve-2026-73570-compromise-check/" rel="noopener noreferrer"&gt;Zimbra SNMP command injection&lt;/a&gt; and PHP2Shell, that optional add-ons and the runtime underneath matter as much as the mail server itself. Enable only the plugins you use, keep the database account narrow, and track the runtime version, not just the application's. If you are rethinking the stack, our &lt;a href="https://fanpino.com/en/blog/mailcow-vs-stalwart-vs-mailu-production/" rel="noopener noreferrer"&gt;Mailcow, Stalwart and Mailu comparison&lt;/a&gt; covers how each one handles webmail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which Roundcube version fixes the actively exploited SQL injection?
&lt;/h3&gt;

&lt;p&gt;CVE-2026-48842 is fixed in Roundcube 1.6.16 and 1.7.1, released on 24 May 2026. The latest releases, 1.6.20 and 1.7.5 from 19 October 2026, include that fix plus 20 more security fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is CVE-2026-48842 exploitable on every Roundcube server?
&lt;/h3&gt;

&lt;p&gt;No. It is in the virtuser_query plugin, so a server is exposed only if that plugin is enabled and Roundcube is older than 1.6.16 on the 1.6 branch or 1.7.1 on the 1.7 branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are the October 2026 Roundcube vulnerabilities being exploited?
&lt;/h3&gt;

&lt;p&gt;The Roundcube project did not report exploitation of the 20 issues fixed in 1.6.20 and 1.7.5. The exploitation warning from Singapore's CSA on 3 October concerns the older CVE-2026-48842.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does updating Roundcube also protect against PHP2Shell?
&lt;/h3&gt;

&lt;p&gt;No. PHP2Shell is a flaw in the PHP runtime. Roundcube needs both its own update and a fixed PHP version, such as 8.2.35, 8.3.36, 8.4.27 or 8.5.12, or a distribution build with the backported fix.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://roundcube.net/news/2026/10/19/security-updates-1.6.20-and-1.7.5" rel="noopener noreferrer"&gt;Roundcube, 19 October 2026&lt;/a&gt;, &lt;a href="https://roundcube.net/news/2026/05/24/security-updates-1.6.16-and-1.7.1" rel="noopener noreferrer"&gt;Roundcube, 24 May 2026&lt;/a&gt;, &lt;a href="https://www.csa.gov.sg/alerts-and-advisories/alerts/al-2026-131/" rel="noopener noreferrer"&gt;CSA Singapore alert AL-2026-131&lt;/a&gt; and the NVD entries linked above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://fanpino.com/en/blog/roundcube-1-6-20-1-7-5-security-update-cve-2026-48842/" rel="noopener noreferrer"&gt;fanpino.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>email</category>
      <category>php</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>What Comes After MVP? Post-Launch Roadmap for Startups</title>
      <dc:creator>ksoft technologies</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:42:45 +0000</pubDate>
      <link>https://dev.to/ksoft_technologies_33f7f6/what-comes-after-mvp-post-launch-roadmap-for-startups-keg</link>
      <guid>https://dev.to/ksoft_technologies_33f7f6/what-comes-after-mvp-post-launch-roadmap-for-startups-keg</guid>
      <description>&lt;p&gt;Shipping the MVP is not the end of product risk.&lt;/p&gt;

&lt;p&gt;It is the point where assumptions finally meet real users.&lt;/p&gt;

&lt;p&gt;After launch, the question changes from:&lt;/p&gt;

&lt;p&gt;Can we build this?&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;Is this actually useful enough to keep building?&lt;/p&gt;

&lt;p&gt;That is why I would treat the post-MVP phase as a learning loop, not as permission to restart the original backlog.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Collect Feedback From Behavior, Not Just Requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first stage after launch should be observation.&lt;/p&gt;

&lt;p&gt;Talk to users, but also watch what they actually do.&lt;/p&gt;

&lt;p&gt;I would look for:&lt;/p&gt;

&lt;p&gt;where onboarding breaks&lt;/p&gt;

&lt;p&gt;where users hesitate&lt;/p&gt;

&lt;p&gt;which workflows they complete&lt;/p&gt;

&lt;p&gt;what they ignore&lt;/p&gt;

&lt;p&gt;what support questions repeat&lt;/p&gt;

&lt;p&gt;which manual workarounds appear&lt;/p&gt;

&lt;p&gt;where users stop returning&lt;/p&gt;

&lt;p&gt;A feature request is useful evidence, but it is not automatically the solution.&lt;/p&gt;

&lt;p&gt;If someone asks for an export button, the real problem may be reporting, internal sharing, or an integration gap.&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;The early product is still full of assumptions.&lt;/p&gt;

&lt;p&gt;Real behavior shows which assumptions survived contact with users.&lt;/p&gt;

&lt;p&gt;What to watch for&lt;/p&gt;

&lt;p&gt;Do not turn every request into a ticket.&lt;/p&gt;

&lt;p&gt;A useful internal structure is:&lt;/p&gt;

&lt;p&gt;Observation:&lt;br&gt;
User problem:&lt;br&gt;
Frequency:&lt;br&gt;
Affected workflow:&lt;br&gt;
Possible solution:&lt;br&gt;
Evidence level:&lt;/p&gt;

&lt;p&gt;That keeps the roadmap focused on problems instead of requests.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track Metrics That Show Whether Users Reach Value&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Post-launch analytics can become noisy very quickly.&lt;/p&gt;

&lt;p&gt;Traffic goes up.&lt;/p&gt;

&lt;p&gt;Signups go up.&lt;/p&gt;

&lt;p&gt;Sessions go up.&lt;/p&gt;

&lt;p&gt;But those numbers do not necessarily tell you whether the product is becoming useful.&lt;/p&gt;

&lt;p&gt;I would focus on metrics tied to the core product outcome.&lt;/p&gt;

&lt;p&gt;Depending on the product, that could include:&lt;/p&gt;

&lt;p&gt;activation&lt;/p&gt;

&lt;p&gt;time to first value&lt;/p&gt;

&lt;p&gt;completion of the core workflow&lt;/p&gt;

&lt;p&gt;repeat usage&lt;/p&gt;

&lt;p&gt;retention&lt;/p&gt;

&lt;p&gt;conversion&lt;/p&gt;

&lt;p&gt;churn&lt;/p&gt;

&lt;p&gt;The exact metric matters less than the question behind it.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Did the user reach the main outcome?&lt;br&gt;
Did they return?&lt;br&gt;
Did they repeat the core workflow?&lt;br&gt;
Did they pay?&lt;br&gt;
Did they stop?&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;High acquisition with weak retention is not a growth story.&lt;/p&gt;

&lt;p&gt;It is a leak.&lt;/p&gt;

&lt;p&gt;What to watch for&lt;/p&gt;

&lt;p&gt;Do not optimize vanity metrics while the product still has major usability or retention problems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prioritize Problems Before Features&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once feedback starts arriving, the backlog expands fast.&lt;/p&gt;

&lt;p&gt;This is where product discipline matters.&lt;/p&gt;

&lt;p&gt;Every feature should compete against every other use of engineering time.&lt;/p&gt;

&lt;p&gt;I would ask:&lt;/p&gt;

&lt;p&gt;Is the problem repeating?&lt;/p&gt;

&lt;p&gt;Does it affect the core workflow?&lt;/p&gt;

&lt;p&gt;Does it improve activation or retention?&lt;/p&gt;

&lt;p&gt;Does it unblock revenue?&lt;/p&gt;

&lt;p&gt;Does it support an important user segment?&lt;/p&gt;

&lt;p&gt;Is there behavioral evidence behind it?&lt;/p&gt;

&lt;p&gt;A useful prioritization model can be simple:&lt;/p&gt;

&lt;p&gt;priority =&lt;br&gt;
  user_impact&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frequency&lt;/li&gt;
&lt;li&gt;revenue_effect&lt;/li&gt;
&lt;li&gt;strategic_fit&lt;/li&gt;
&lt;li&gt;implementation_cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not a universal formula.&lt;/p&gt;

&lt;p&gt;The point is to make tradeoffs explicit.&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;Every new feature creates permanent cost:&lt;/p&gt;

&lt;p&gt;code&lt;/p&gt;

&lt;p&gt;tests&lt;/p&gt;

&lt;p&gt;documentation&lt;/p&gt;

&lt;p&gt;support&lt;/p&gt;

&lt;p&gt;maintenance&lt;/p&gt;

&lt;p&gt;future migrations&lt;/p&gt;

&lt;p&gt;The initial development estimate is only part of the cost.&lt;/p&gt;

&lt;p&gt;What to watch for&lt;/p&gt;

&lt;p&gt;Do not confuse a longer roadmap with a stronger product.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strengthen Retention Before Scaling Acquisition&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the MVP is live, growth becomes tempting.&lt;/p&gt;

&lt;p&gt;More ads.&lt;/p&gt;

&lt;p&gt;More outbound.&lt;/p&gt;

&lt;p&gt;More content.&lt;/p&gt;

&lt;p&gt;More partnerships.&lt;/p&gt;

&lt;p&gt;But if users are not sticking around, sending more traffic into the product simply amplifies the problem.&lt;/p&gt;

&lt;p&gt;I would first inspect:&lt;/p&gt;

&lt;p&gt;onboarding&lt;/p&gt;

&lt;p&gt;product reliability&lt;/p&gt;

&lt;p&gt;time to value&lt;/p&gt;

&lt;p&gt;support friction&lt;/p&gt;

&lt;p&gt;repeated drop-off points&lt;/p&gt;

&lt;p&gt;integration failures&lt;/p&gt;

&lt;p&gt;performance issues&lt;/p&gt;

&lt;p&gt;This is also where early technical shortcuts start becoming visible.&lt;/p&gt;

&lt;p&gt;An MVP may have been intentionally optimized for speed.&lt;/p&gt;

&lt;p&gt;That is fine.&lt;/p&gt;

&lt;p&gt;But once usage grows, you may need better:&lt;/p&gt;

&lt;p&gt;observability&lt;br&gt;
background processing&lt;br&gt;
security controls&lt;br&gt;
permissions&lt;br&gt;
retry handling&lt;br&gt;
infrastructure&lt;br&gt;
data architecture&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;Acquisition works better when the product already delivers repeatable value.&lt;/p&gt;

&lt;p&gt;What to watch for&lt;/p&gt;

&lt;p&gt;Do not use marketing spend to hide retention problems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decide Whether to Scale, Raise, Pivot, or Keep Learning&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eventually, the team has to make a larger decision.&lt;/p&gt;

&lt;p&gt;Do we scale?&lt;/p&gt;

&lt;p&gt;Do we raise?&lt;/p&gt;

&lt;p&gt;Do we pivot?&lt;/p&gt;

&lt;p&gt;Or do we stay in learning mode longer?&lt;/p&gt;

&lt;p&gt;I would base that decision on patterns, not enthusiasm.&lt;/p&gt;

&lt;p&gt;Signals that usually support continued investment include:&lt;/p&gt;

&lt;p&gt;users consistently reaching value&lt;/p&gt;

&lt;p&gt;improving retention&lt;/p&gt;

&lt;p&gt;repeatable demand&lt;/p&gt;

&lt;p&gt;clear product usage patterns&lt;/p&gt;

&lt;p&gt;known bottlenecks&lt;/p&gt;

&lt;p&gt;credible acquisition channels&lt;/p&gt;

&lt;p&gt;A pivot may be worth considering when:&lt;/p&gt;

&lt;p&gt;another segment retains much better&lt;br&gt;
users value a different workflow&lt;br&gt;
the buyer differs from the user&lt;br&gt;
core usage stays weak after repeated iteration&lt;br&gt;
willingness to pay remains low&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;Funding should accelerate something that is becoming clearer.&lt;/p&gt;

&lt;p&gt;It should not simply fund more uncertainty.&lt;/p&gt;

&lt;p&gt;What to watch for&lt;/p&gt;

&lt;p&gt;Do not treat perseverance as refusing to change direction.&lt;/p&gt;

&lt;p&gt;The purpose of an MVP is to generate evidence.&lt;/p&gt;

&lt;p&gt;The team has to be willing to act on it.&lt;/p&gt;

&lt;p&gt;Typical Post-MVP Priority&lt;/p&gt;

&lt;p&gt;Typical / indicative&lt;/p&gt;

&lt;p&gt;User Feedback   ██████████  Very High&lt;br&gt;
Core Metrics    █████████░  Very High&lt;br&gt;
Prioritization  ████████░░  High&lt;br&gt;
Retention       █████████░  Very High&lt;br&gt;
Scale Decision  ██████░░░░  Evidence-Led&lt;/p&gt;

&lt;p&gt;My rough read: early post-MVP effort should lean heavily toward learning and retention before aggressive expansion.&lt;/p&gt;

&lt;p&gt;Practical Post-MVP Checklist&lt;/p&gt;

&lt;p&gt;Before adding the next major feature, I would check:&lt;/p&gt;

&lt;p&gt;[ ] Users complete the core workflow&lt;br&gt;
[ ] Activation is measurable&lt;br&gt;
[ ] Time to value is understood&lt;br&gt;
[ ] Retention is being tracked&lt;br&gt;
[ ] Drop-off points are visible&lt;br&gt;
[ ] Support issues are categorized&lt;br&gt;
[ ] Feature requests are grouped by problem&lt;br&gt;
[ ] Product usage is influencing priorities&lt;br&gt;
[ ] Major technical bottlenecks are known&lt;br&gt;
[ ] Acquisition is not hiding weak retention&lt;br&gt;
[ ] Scaling assumptions are documented&lt;br&gt;
[ ] Funding would accelerate something specific&lt;/p&gt;

&lt;p&gt;If several of those are unclear, I would keep learning before expanding aggressively.&lt;/p&gt;

&lt;p&gt;How KSoft Technologies Helps With Post-MVP Product Growth&lt;/p&gt;

&lt;p&gt;KSoft Technologies is our own company, and this is the kind of work we do: helping startups turn post-launch usage, feedback, technical constraints, and roadmap choices into the next focused engineering phase. We help teams strengthen the product before growth adds unnecessary complexity.&lt;/p&gt;

&lt;p&gt;The principle I would keep is simple:&lt;/p&gt;

&lt;p&gt;The MVP gives you evidence. The post-MVP roadmap should be built from that evidence, not from the backlog you wrote before launch.&lt;/p&gt;

&lt;p&gt;I wrote a fuller version here: &lt;a href="https://www.ksofttechnologies.com/blogs/what-comes-after-mvp-post-launch-roadmap-for-startups?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=what-comes-after-mvp-post-launch-roadmap-for-startups&amp;amp;utm_content=closing_link" rel="noopener noreferrer"&gt;post-launch roadmap for startups&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>saas</category>
      <category>product</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Measuring CVE-2026-105483 exposure without overstating it</title>
      <dc:creator>andreagomez</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:42:08 +0000</pubDate>
      <link>https://dev.to/andreagomez/measuring-cve-2026-105483-exposure-without-overstating-it-fh7</link>
      <guid>https://dev.to/andreagomez/measuring-cve-2026-105483-exposure-without-overstating-it-fh7</guid>
      <description>&lt;h1&gt;
  
  
  Measuring CVE-2026-105483 exposure without overstating it
&lt;/h1&gt;

&lt;p&gt;ZoomEye can show how widely a product fingerprint appears, but it cannot see the version, configuration, and certificate policy inside every deployment. CVE-2026-105483 makes that boundary important. The Node.js bug affected 24.x and 26.x before 24.22.1 and 26.12.1. During TLS verification, a fullwidth-digit IPv4 host could be treated as a DNS name, allowing a trusted certificate with a matching DNS SAN to authenticate an IP endpoint without an IP SAN.&lt;/p&gt;

&lt;p&gt;Node.js rates the vulnerability High. The GitHub advisory lists CVSS v3.0 7.4 and CWE-295. An exposure measurement is useful for understanding the technology population and planning review, not for labeling every match as a victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the query measured
&lt;/h2&gt;

&lt;p&gt;The selected Search Dork was &lt;code&gt;app="Node.js"&lt;/code&gt;. ZoomEye returned 5,539,674 matches at 04:17 +08:00 on November 2, 2026. The CVE Filter &lt;code&gt;vul.cve="CVE-2026-105483"&lt;/code&gt; returned zero. Under the selected-query rule, Exposure is the larger count, so the recorded value is 5,539,674.&lt;/p&gt;

&lt;p&gt;The visible unit is a Node.js application fingerprint. It is not a count of processes running a vulnerable release, endpoints that accept fullwidth-digit hosts, or environments whose trust store can issue a suitable certificate. Those facts require different evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the CVE filter returned zero
&lt;/h2&gt;

&lt;p&gt;Zero CVE-indexed results does not mean zero vulnerable systems. ZoomEye's CVE field depends on indexed vulnerability evidence. A product fingerprint may not include the runtime version or certificate configuration needed to classify the CVE. The correct conclusion is that the observed index had no assets mapped to &lt;code&gt;vul.cve="CVE-2026-105483"&lt;/code&gt; at the query time, not that every Node.js deployment was patched.&lt;/p&gt;

&lt;p&gt;AutoPR's Exposure rule compares the selected successful queries and uses the greater count. The product query passes the threshold, while the Filter shows the narrower CVE-index view. Both facts can be displayed together without pretending they measure the same population.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions for the owner of a match
&lt;/h2&gt;

&lt;p&gt;An operator reviewing a ZoomEye result should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this an application server, a build service, an API gateway, or a device interface?&lt;/li&gt;
&lt;li&gt;What Node.js release does the deployed process report?&lt;/li&gt;
&lt;li&gt;Does the service accept destinations from tenants, integrations, or administrators?&lt;/li&gt;
&lt;li&gt;Does it require an IP SAN for IP endpoints?&lt;/li&gt;
&lt;li&gt;Which trust anchors and internal CAs can issue the relevant certificate?&lt;/li&gt;
&lt;li&gt;What sensitive request is carried after the TLS check?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two questions narrow the technology. The next two determine whether the vulnerable identity path is reachable. The last two estimate consequence and practical attacker conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine external and internal evidence
&lt;/h2&gt;

&lt;p&gt;External indexing is good for discovering a broad population. Internal inventory should map application owners, runtime versions, build artifacts, and network exposure. A service can be publicly visible but use only fixed, approved DNS destinations. Another service can be private but accept an attacker-controlled callback and send a privileged token.&lt;/p&gt;

&lt;p&gt;The private service may deserve earlier review because its business path combines destination influence, broad trust, and sensitive authority. Exposure counts help with scale and context, while threat modeling decides order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement discipline
&lt;/h2&gt;

&lt;p&gt;Record the exact query, scope, time, and semantic unit. Preserve the runtime version evidence separately from the ZoomEye record. Do not write "5,539,674 vulnerable hosts" when the query measured product fingerprints. Do not add the Search and Filter counts.&lt;/p&gt;

&lt;p&gt;If a service owner later confirms a vulnerable release and a reachable path, that becomes internal incident or remediation evidence. It does not retroactively change the meaning of the broad product query. Accurate labels let analysts compare runs without inflating a count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational use
&lt;/h2&gt;

&lt;p&gt;Use the fingerprint population to find teams that need an inventory request, not as a substitute for the request. Include build-system Node.js installations, command-line tools, agents, and packaged applications in the inventory because internet-facing web services are not the only relevant runtime class.&lt;/p&gt;

&lt;p&gt;For each confirmed affected deployment, upgrade to 24.22.1, 26.12.1, or later. Rebuild images and packaged executables, verify the runtime after deployment, and add the certificate identity regression test to the release gate. For user-controlled destinations, require canonical DNS names or canonical IP literals and require the appropriate SAN class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reporting
&lt;/h2&gt;

&lt;p&gt;A defensible report has two layers. The external layer says that a product query found 5,539,674 Node.js matches and that the CVE filter found zero. The internal layer names the systems confirmed to run affected versions, the clients with reachable trust decisions, the patch status, and the remaining risk.&lt;/p&gt;

&lt;p&gt;Keeping those layers separate avoids a common failure in vulnerability management: treating a large public count as incident evidence or treating a zero CVE count as proof of safety. CVE-2026-105483 requires runtime and identity-policy evidence, both of which sit behind the fingerprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js October 21, 2026 Security Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/release/v24.22.1/" rel="noopener noreferrer"&gt;Node.js 24.22.1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/release/v26.12.1/" rel="noopener noreferrer"&gt;Node.js 26.12.1 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/advisories/GHSA-8h4p-qpcq-gq24" rel="noopener noreferrer"&gt;GitHub Advisory GHSA-8h4p-qpcq-gq24&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.zoomeye.ai/searchResult?q=YXBwPSJOb2RlLmpzIg%3D%3D" rel="noopener noreferrer"&gt;ZoomEye Search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cve2026105483</category>
      <category>node</category>
      <category>tls</category>
      <category>certificatevalidation</category>
    </item>
    <item>
      <title>How to Sell Usage Credits in Your AI App (Pay-as-You-Go Without Surprises)</title>
      <dc:creator>Supermax</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:40:31 +0000</pubDate>
      <link>https://dev.to/supermax33/how-to-sell-usage-credits-in-your-ai-app-pay-as-you-go-without-surprises-4kl9</link>
      <guid>https://dev.to/supermax33/how-to-sell-usage-credits-in-your-ai-app-pay-as-you-go-without-surprises-4kl9</guid>
      <description>&lt;p&gt;Your app calls an AI model, and every time someone uses it, you pay a little. A flat monthly price works until one customer uses it a hundred times more than everyone else. Then you start wondering whether you should charge by use instead. Credits are the usual answer: the customer pays up front for a balance, and each use takes some of it away.&lt;/p&gt;

&lt;p&gt;I'm Deepak, a developer in Hyderabad. I build side projects with AI agents and spend a lot of my time wiring payments into apps made with Claude Code, Cursor and Codex. This guide covers the ways to charge by usage, the choices you have to make before your agent writes anything, what Polar and Stripe offer, and what to test. No code needed.&lt;/p&gt;

&lt;p&gt;Everything I say about Polar and Stripe comes from their own docs, as of 30 Oct 2026, linked so you can check again. Usage billing is one of the faster-changing parts of both, so re-read the docs before you rely on a detail.&lt;/p&gt;

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

&lt;p&gt;Pick a unit your customers understand, like "1 credit = 1 image". Decide what happens when someone runs out. Keep one clear record of each person's balance, and make sure two quick clicks can't spend the same credit twice. Add credits only when the payment provider confirms the payment, and only once per payment. Then test all of it in test mode before a real customer does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to charge by usage
&lt;/h2&gt;

&lt;p&gt;There are three common setups. They sound similar, but they feel very different to the customer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credit packs:&lt;/strong&gt; the customer buys a pack (say, 100 credits), uses them, and buys another when they run low. They never pay more than they chose to. The risk is on the other side: they can run out halfway through something and get annoyed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A monthly allowance in a subscription:&lt;/strong&gt; the customer pays every month and gets a set number of credits each month. You have to decide whether unused credits carry over to the next month or reset. Often paired with packs for people who need extra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing for usage afterwards:&lt;/strong&gt; the customer uses the app freely and gets a bill at the end of the month for what they used. This is closest to how cloud providers charge. Nobody runs out mid-task, but a customer can get a bill they didn't expect, which is a quick way to lose trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small app, packs or a monthly allowance are usually easier to explain and harder to get wrong, because the customer has paid before they use anything. Billing afterwards makes more sense when your customers are businesses that expect it. I'm not going to pretend one of these sells better; it depends on your app and your customers.&lt;/p&gt;

&lt;p&gt;If you're still deciding between one-time, subscription and credits in general, my guide on &lt;a href="https://www.shiptopaid.com/blog/charge-for-your-vibe-coded-app" rel="noopener noreferrer"&gt;how to charge for your vibe-coded app&lt;/a&gt; covers that first choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a unit people understand
&lt;/h2&gt;

&lt;p&gt;"Tokens" mean something to you. They mean very little to most customers. A unit tied to what the customer actually gets is easier: one credit per image, per document summarised, per minute of audio. If different actions cost you very different amounts, give them different prices in credits ("a quick answer costs 1 credit, a long report costs 5") and show that price before the person clicks.&lt;/p&gt;

&lt;p&gt;To set the price, work out what one use costs you: the AI model's charge for a typical request, plus anything else that use triggers, like storage. Your provider's pricing page and your own usage dashboard will tell you. Then add a margin that also covers the payment provider's fees and the uses that fail and get retried. I can't give you a number, because it depends entirely on your model and your app, so measure a few real uses rather than guessing.&lt;/p&gt;

&lt;p&gt;Keep credits as whole numbers. "You have 37 credits left" is clear; "you have 3.72 credits" invites questions, and fractions are where balance bugs like to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens at zero
&lt;/h2&gt;

&lt;p&gt;Decide this before you build, because your agent will pick something if you don't.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop with a friendly screen.&lt;/strong&gt; When the balance is too low for the next action, say so plainly and offer a top-up button. Don't start the work and then fail halfway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warn before zero.&lt;/strong&gt; Show the balance somewhere visible and warn when it gets low, so running out is never a surprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide on going below zero.&lt;/strong&gt; Some apps allow a small overdraft so a task in progress can finish. That's fine if you decide it on purpose. What you don't want is a balance that goes negative by accident because someone clicked twice quickly, or opened two tabs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Polar and Stripe offer (as of 30 Oct 2026)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On Polar,&lt;/strong&gt; &lt;a href="https://polar.sh/docs/features/usage-based-billing/introduction" rel="noopener noreferrer"&gt;usage-based billing&lt;/a&gt; works like this: your app sends Polar a small message each time a customer uses something (Polar calls these events), and a &lt;a href="https://polar.sh/docs/features/usage-based-billing/meters" rel="noopener noreferrer"&gt;meter&lt;/a&gt; counts or adds them up per customer. Polar labels usage-based billing a new feature.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credits:&lt;/strong&gt; the &lt;a href="https://polar.sh/docs/features/usage-based-billing/credits" rel="noopener noreferrer"&gt;credits page&lt;/a&gt; says you attach a credits benefit to a product. On a one-time product, the credits are added once when the customer buys. On a subscription, they're added at the start of every billing period, and none are added while the subscription is "past due" (a renewal payment failed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits only, or credits plus a bill:&lt;/strong&gt; if the product also has a metered price, usage beyond the credits is charged as overage. If you want credits only, the page says not to add a metered price, so nothing extra is ever billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding credits by hand:&lt;/strong&gt; you can add credits to a customer from the dashboard, for example as a goodwill gesture, and choose whether unused ones carry over to the next period. Credits added by mistake can be reversed within 7 days, but only the unused part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the customer sees:&lt;/strong&gt; the customer portal shows their estimated charges for each meter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The important warning:&lt;/strong&gt; Polar's page says it doesn't block usage when a customer goes past their balance. Your app has to do that itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On Stripe,&lt;/strong&gt; the &lt;a href="https://docs.stripe.com/billing/subscriptions/usage-based" rel="noopener noreferrer"&gt;usage-based billing page&lt;/a&gt; now says Metronome is part of Stripe and is the recommended starting point for new usage-based setups, including prepaid credits. The older "basic" usage billing, built on meters, is still fully supported and, according to the same page, works more fully with Stripe Checkout. If your agent suggests one or the other, this is why; ask it which one it's using and why.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Billing credits:&lt;/strong&gt; in the basic setup, Stripe's &lt;a href="https://docs.stripe.com/billing/subscriptions/usage-based/billing-credits" rel="noopener noreferrer"&gt;billing credits page&lt;/a&gt; describes credit grants as an amount of money off a customer's future usage bills. They only apply to subscriptions that bill by metered usage, and they're used when the bill is finalised, not the moment someone uses your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry:&lt;/strong&gt; credits don't expire unless you set an expiry date on the grant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules:&lt;/strong&gt; Stripe says billing credits can't be sold as gift cards or as stored money to spend elsewhere. They're for your own product only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refunds:&lt;/strong&gt; the same page says issuing a credit note doesn't give the credits back; you'd have to grant new ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So on Stripe, "1 credit = 1 image" packs aren't something you switch on. Many small apps keep that balance in their own database and use Stripe Checkout only to sell the packs. That's a perfectly normal setup; it just means your app owns the balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your app has to do
&lt;/h2&gt;

&lt;p&gt;Whichever provider you use, these are the parts your agent has to get right. Each comes with something you can ask it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One record of the balance.&lt;/strong&gt; Either your own database holds the balance, or the payment provider does. Never both by accident, or they'll drift apart. Ask your agent: "Where is each user's credit balance stored, and is that the only place we check before letting them use a credit?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spending in one safe step.&lt;/strong&gt; Checking the balance and taking the credit away must happen together, so two quick clicks or two open tabs can't both spend the last credit. Ask your agent: "If a user with 1 credit clicks twice at the same moment, what stops both clicks from going through?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add credits only when payment is confirmed.&lt;/strong&gt; The success page after checkout isn't proof of payment. Polar and Stripe send your app a payment message (a webhook) when a payment really goes through, and that's when credits should be added. Ask your agent: "Are credits added from the verified payment message, and not from the success page?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only once per payment.&lt;/strong&gt; Payment messages can arrive twice. Adding the same pack twice is free credits; missing it is a customer who paid for nothing. Ask your agent: "If the same payment message arrives twice, are credits still added only once?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed work gives credits back.&lt;/strong&gt; If the AI call fails after you took the credit, give it back, once. Ask your agent: "If the AI request fails, does the user get the credit back, and only one time?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A refund rule.&lt;/strong&gt; Decide what refunding a pack means when some credits are already used: refund only the unused part, or take the credits away and refund in full. Write it on your refund policy. My guide to &lt;a href="https://www.shiptopaid.com/blog/refunds-and-cancellations-ai-built-app" rel="noopener noreferrer"&gt;refunds and cancellations&lt;/a&gt; covers the access side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to test (in test mode, never with a real card)
&lt;/h2&gt;

&lt;p&gt;Use Polar's sandbox or Stripe's test mode with their &lt;a href="https://docs.stripe.com/testing" rel="noopener noreferrer"&gt;test cards&lt;/a&gt;. Go through this list as a customer would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buy a pack. Check the balance goes up by exactly the right amount.&lt;/li&gt;
&lt;li&gt;Use the app. Check the balance goes down by the right amount for each action.&lt;/li&gt;
&lt;li&gt;With 1 credit left, click the action button twice as fast as you can (and try it from two tabs). Only one should go through.&lt;/li&gt;
&lt;li&gt;Run the balance to zero. You should see your friendly "out of credits" screen, not an error.&lt;/li&gt;
&lt;li&gt;Top up and check you can carry on.&lt;/li&gt;
&lt;li&gt;Get the same payment message delivered twice. Your agent can show you how to resend one in test mode (Stripe's dashboard lets you resend events). The balance shouldn't change.&lt;/li&gt;
&lt;li&gt;Make an AI request fail on purpose and check the credit comes back once.&lt;/li&gt;
&lt;li&gt;Refund a pack and check your refund rule happens.&lt;/li&gt;
&lt;li&gt;If you have a monthly allowance, move time forward to the renewal and check the allowance resets or carries over the way you decided. On Stripe, &lt;a href="https://docs.stripe.com/billing/testing/test-clocks" rel="noopener noreferrer"&gt;test clocks&lt;/a&gt; do this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want customers to try the app before buying, a few free starter credits work like a small trial; my guide to &lt;a href="https://www.shiptopaid.com/blog/free-trial-vibe-coded-app" rel="noopener noreferrer"&gt;adding a free trial&lt;/a&gt; covers the trade-offs. And if someone ever says they paid but got no credits, my guide on &lt;a href="https://www.shiptopaid.com/blog/customer-paid-but-no-access" rel="noopener noreferrer"&gt;why customers get locked out after paying&lt;/a&gt; walks through the usual causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where ShipToPaid fits
&lt;/h2&gt;

&lt;p&gt;I made &lt;a href="https://www.shiptopaid.com" rel="noopener noreferrer"&gt;ShipToPaid&lt;/a&gt; so AI agents get payments right in apps like yours. The full pack has a usage credits skill and an add-credits command that guide your agent through a credit balance in your own database and/or Polar usage billing, with credits added only from verified payment messages and spending done in one safe step. To be clear about its limits: the starter app itself sells subscriptions, not credits, and the skill says its Polar usage-billing examples are checked against Polar's code library but haven't been tested with live usage billing. You don't need any of it to follow this guide.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I cap OpenAI spending per user in ASP.NET Core</title>
      <dc:creator>Martin Pisacreta</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:39:22 +0000</pubDate>
      <link>https://dev.to/martinpisacreta/how-i-cap-openai-spending-per-user-in-aspnet-core-27bb</link>
      <guid>https://dev.to/martinpisacreta/how-i-cap-openai-spending-per-user-in-aspnet-core-27bb</guid>
      <description>&lt;p&gt;A per-user spending limit needs to affect whether an application sends a request. Recording token usage afterward helps explain costs, but it cannot prevent several concurrent requests from starting against the same remaining balance.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/tree/f43b206" rel="noopener noreferrer"&gt;ASP.NET Core sample&lt;/a&gt; addresses that admission decision with reservations. It authenticates a user, reserves estimated exposure before contacting the provider, and records known or uncertain costs afterward. This walkthrough refers to commit &lt;code&gt;f43b206&lt;/code&gt;; it does not assume current model prices or claim live OpenAI validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take identity from authentication
&lt;/h2&gt;

&lt;p&gt;The caller submits a prompt. The server chooses the user account from a configured bearer token, rather than accepting a user ID in request JSON. That distinction keeps a caller from selecting someone else's budget.&lt;/p&gt;

&lt;p&gt;The request contract is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After authentication, the endpoint passes the resolved identity separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestAborted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/Program.cs" rel="noopener noreferrer"&gt;Source: Program.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The server maps configured token hashes to identities. The endpoint's JSON options reject unmapped members, so a request that adds a &lt;code&gt;user&lt;/code&gt; field gets a 400 instead of choosing a budget. Model selection, prices, credentials, and budgets also come from server configuration. The provider credential and the user's authentication token serve different purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate one attempt, then reserve every permitted attempt
&lt;/h2&gt;

&lt;p&gt;The policy calculates exposure from the maximum prompt size, an input overhead allowance, maximum output tokens, and explicitly configured prices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;MaxPromptUtf8Bytes&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;InputOverheadTokenAllowance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt;
        &lt;span class="n"&gt;InputUsdPerMillionTokens&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
     &lt;span class="n"&gt;MaxOutputTokens&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;OutputUsdPerMillionTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1000000m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatPolicy.cs#L40-L50" rel="noopener noreferrer"&gt;Source: ChatPolicy.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The input calculation allows one token per UTF-8 byte plus configured overhead. It is an admission estimate for this text-only request shape, rather than a tokenizer. The application must maintain suitable allowances and prices for its selected model.&lt;/p&gt;

&lt;p&gt;Retries also create exposure. Reserving enough for one attempt would leave later attempts outside the initial budget decision. The request estimate therefore includes every permitted attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="nf"&gt;MaximumEstimatedRequestUsd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;MaximumEstimatedAttemptUsd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MaxAttempts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatPolicy.cs#L52-L54" rel="noopener noreferrer"&gt;Source: ChatPolicy.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This approach can reject a request whose eventual successful response would have been cheaper. That is a consequence of reserving the allowed exposure before knowing how many attempts will happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve before the provider call
&lt;/h2&gt;

&lt;p&gt;The gateway makes its admission decision before entering the sending loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryReserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authenticatedUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaximumEstimatedRequestUsd&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BudgetExceededException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatGateway.cs#L77-L80" rel="noopener noreferrer"&gt;Source: ChatGateway.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The default ledger coordinates reservations with a lock. Available budget accounts for both recorded costs and held exposure, so concurrent callers cannot independently reserve the same balance. The HTTP endpoint translates insufficient budget into a 429 response.&lt;/p&gt;

&lt;p&gt;The lock protects this process's ledger. It does not coordinate separate application instances, and restarting loses the in-memory records. The &lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/SpendingLedger.cs" rel="noopener noreferrer"&gt;ledger implementation&lt;/a&gt; explicitly documents those limits. A deployment using multiple instances needs shared, durable, atomic reservations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track uncertainty when an attempt starts
&lt;/h2&gt;

&lt;p&gt;A failed response does not supply reliable token accounting. An interrupted connection can also leave the application without a usable answer about what happened remotely.&lt;/p&gt;

&lt;p&gt;Immediately before sending, the gateway adds one attempt's estimated exposure to its uncertain total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
&lt;span class="n"&gt;uncertain&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;attemptExposure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpCompletionOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseHeadersRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatGateway.cs#L106-L111" rel="noopener noreferrer"&gt;Source: ChatGateway.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The retry loop bounds the attempt count, retries 429 and server errors, and applies policy delays within an overall timeout. Earlier attempts retain their uncertain exposure even when a later attempt succeeds. A successful retry cannot establish the earlier attempts' billing outcome.&lt;/p&gt;

&lt;p&gt;This conservative treatment can hold more budget than the eventual provider charge. The sample preserves that uncertainty instead of assuming that every unsuccessful attempt was free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release unused exposure and account for returned usage
&lt;/h2&gt;

&lt;p&gt;When a response contains valid nonnegative token counts, the gateway calculates their cost using configured prices and removes that attempt's estimated uncertain exposure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;confirmed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EstimateUsageUsd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;uncertain&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;attemptExposure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;knownUsage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatGateway.cs#L142-L155" rel="noopener noreferrer"&gt;Source: ChatGateway.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Completion runs in a &lt;code&gt;finally&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uncertain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/ChatGateway.cs#L199-L204" rel="noopener noreferrer"&gt;Source: ChatGateway.cs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The ledger releases the original reservation while retaining the calculated confirmed cost and any uncertain exposure. Consequently, unattempted retries release their reserved share. Known usage replaces its attempt estimate. A successful answer without usage keeps that attempt's estimated exposure held.&lt;/p&gt;

&lt;p&gt;For example, suppose the configured estimate is $0.01 per attempt and three attempts are permitted. Admission reserves $0.03. If the first attempt succeeds with $0.002 of calculated usage, completion retains $0.002 and releases $0.028. These are illustrative figures, not OpenAI prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand what the cap measures
&lt;/h2&gt;

&lt;p&gt;Here, confirmed means calculated from returned usage and configured prices. It does not mean reconciled with a provider invoice. The ledger records reported usage even when its calculated cost exceeds the admission estimate.&lt;/p&gt;

&lt;p&gt;The mechanism controls admission against application accounting. Its effectiveness depends on configuration, supported request shapes, persistent accounting where required, and treatment of uncertain outcomes. It cannot promise an exact provider bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was verified
&lt;/h2&gt;

&lt;p&gt;For commit &lt;code&gt;f43b206&lt;/code&gt;, the sample builds on Windows with &lt;strong&gt;0 warnings&lt;/strong&gt;, the self-test passes &lt;strong&gt;9 of 9&lt;/strong&gt;, and a local HTTP check on 127.0.0.1 returned &lt;strong&gt;401 without a token&lt;/strong&gt;, &lt;strong&gt;400 for malformed JSON or a &lt;code&gt;user&lt;/code&gt; field&lt;/strong&gt;, and &lt;strong&gt;429 when the budget was exhausted, without calling the provider&lt;/strong&gt;. Those checks were run with Claude, the AI assistant I use for this kind of work.&lt;/p&gt;

&lt;p&gt;The sample was &lt;strong&gt;not tested against real OpenAI&lt;/strong&gt;. Its &lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/SelfTests.cs" rel="noopener noreferrer"&gt;self-tests&lt;/a&gt; use a scripted local provider. To run that path without an API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; LlmIntegration.csproj &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--self-test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/martinPisacreta/openai-spend-caps-dotnet/blob/f43b206/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; explains real-provider environment configuration and the memory ledger's limits. Live compatibility, current pricing, and actual billing remain outside the reported validation.&lt;/p&gt;

&lt;p&gt;If you want this in your ASP.NET Core app, I offer it as a fixed-price project on Upwork: &lt;a href="https://www.upwork.com/services/product/2114839106032222506" rel="noopener noreferrer"&gt;https://www.upwork.com/services/product/2114839106032222506&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>openai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One page, one question: how to structure headings and FAQ blocks so an answer engine can quote a passage</title>
      <dc:creator>AumCreate</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:38:43 +0000</pubDate>
      <link>https://dev.to/aumcreate/one-page-one-question-how-to-structure-headings-and-faq-blocks-so-an-answer-engine-can-quote-a-3bbd</link>
      <guid>https://dev.to/aumcreate/one-page-one-question-how-to-structure-headings-and-faq-blocks-so-an-answer-engine-can-quote-a-3bbd</guid>
      <description>&lt;p&gt;Someone asks an assistant a question. The assistant fetches a handful of pages, splits them into chunks, and keeps the chunk that looks like an answer. If your page buries the answer under four paragraphs of context, the chunk that survives is the context, and you don't get quoted.&lt;/p&gt;

&lt;p&gt;The unit that gets retrieved and quoted is the passage, not the page. Everything below follows from that: write pages whose passages survive being cut out and read alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a chunk looks like by the time it reaches the model
&lt;/h2&gt;

&lt;p&gt;Retrieval systems split HTML into pieces of a few hundred tokens, usually cutting near headings. The piece arrives without your navigation, without the paragraph before it, without the table of contents. A sentence that begins "as mentioned above" or "this approach" arrives with no referent.&lt;/p&gt;

&lt;p&gt;Test any paragraph by asking: if I paste only this block into an empty document, does it still say something true and complete? If not, it is a weak chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should a page answer one question or several?
&lt;/h2&gt;

&lt;p&gt;One. The heading, the opening line and the FAQ should all point at the same question, phrased the way a person would type it. A page that answers "how much does a 3-bedroom move cost in Singapore" will not also answer "how do I pack a kitchen" well enough to be quoted for either.&lt;/p&gt;

&lt;p&gt;Splitting is not a rule about word count. A short page answering two questions is worse than two short pages, because the retrieval system may pull half of the first answer and match it against the second question.&lt;/p&gt;

&lt;p&gt;When two of your pages answer the same question, merge them. Duplicate answers compete with each other and neither becomes the reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first sentence under a heading has to be the answer
&lt;/h2&gt;

&lt;p&gt;Readers who scan and extractors that chunk both want the answer in the first sentence, before the justification.&lt;/p&gt;

&lt;p&gt;Which version would you quote?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Why is my WordPress site slow?&lt;/span&gt;

Performance is a broad topic and there are many factors to consider. In this
article we will look at several areas that affect how quickly a page loads,
starting with the hosting environment and then moving on to caching...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Why is a WordPress site slow?&lt;/span&gt;

A WordPress page is slow when PHP rebuilds the page on every request and the
finished HTML is never stored. The usual causes are no page cache, an expensive
uncached query on each load, and images served at full resolution.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version answers inside one sentence and stays understandable out of context. The first version only announces that an answer is coming.&lt;/p&gt;

&lt;p&gt;Delete the announcement paragraph entirely rather than shortening it. Openings like "in this article we will cover" have no value as a chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write chunks that name their own subject
&lt;/h2&gt;

&lt;p&gt;A passage that gets pulled out of your page loses its title. Pronouns lose their reference and the chunk becomes useless.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of "it handles retries automatically", write "the queue handles retries automatically".&lt;/li&gt;
&lt;li&gt;Instead of "this approach scales", write "the queue-based import scales".&lt;/li&gt;
&lt;li&gt;Instead of "most people prefer the second option", name what the second option is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Numbers need units and a date. "5 working days" beats "fast turnaround". "Checked in October 2026" tells the reader the number has an owner and an expiry, and both are things a model can repeat without distorting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should FAQ blocks be arranged?
&lt;/h2&gt;

&lt;p&gt;An FAQ block is a set of small, self-contained answer chunks, and it works when you treat each item as its own page in miniature.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One question per item, phrased the way it gets searched. "Do you charge a yearly fee?" beats "Pricing notes".&lt;/li&gt;
&lt;li&gt;The first sentence answers. Detail, caveats, exceptions come after, and they can be dropped without breaking the answer.&lt;/li&gt;
&lt;li&gt;No nested sub-questions. If an item needs a follow-up question, it needs its own page.&lt;/li&gt;
&lt;li&gt;No filler openings. "Great question!" and "That's a common concern" are the first thing a chunk carries and the first thing a reader skips.&lt;/li&gt;
&lt;li&gt;Only questions someone actually asked. Six invented questions that pad a section produce six useless chunks and dilute the ones that matter.&lt;/li&gt;
&lt;li&gt;Keep the answers in the HTML source. Content injected by JavaScript after load is invisible to a fetcher that does not run scripts. A &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element is fine because the text is in the markup; a tab widget that loads its panel by request is not.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Frequently asked questions&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Does a heading have to be phrased as a question?&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;No, but a question heading matches how people search and gives the
  extractor a clean boundary. "Pricing" is a weaker heading than "How much
  does a delivery cost?"&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Can an extractor read text inside a collapsed element?&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Yes, when the text is in the HTML. A details element ships its content in
  the markup even while collapsed; a panel fetched by JavaScript after load
  does not.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured data for FAQ is optional. Markup can describe a page, but the text is what gets extracted and repeated. Adding schema to thin answers produces well-labelled thin answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A page skeleton you can copy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# How much does X cost?          &amp;lt;- the question, in the searcher's words&lt;/span&gt;

One or two sentences that answer it, with the number, the currency and
what is included. No preamble.

&lt;span class="gu"&gt;## What is included in the price?&lt;/span&gt;

The first sentence states the inclusion. Then the detail.

&lt;span class="gu"&gt;## What changes the price?&lt;/span&gt;

Named factors, each with a short explanation.

&lt;span class="gu"&gt;## How do I start?&lt;/span&gt;

The concrete next action, one step, not a menu of five.

&lt;span class="gu"&gt;## Frequently asked questions&lt;/span&gt;

Four to six items, each answer-first and self-contained.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the heading is not "Pricing". A heading that repeats the question gives the extractor a clean boundary and gives the reader a reason to stop scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that break otherwise good passages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Comparisons that say "the first option" without naming it, in a table where the column headers were cut off with the chunk.&lt;/li&gt;
&lt;li&gt;Answers that begin with "it depends" and never reach the actual answer. Put the dependency in the second sentence.&lt;/li&gt;
&lt;li&gt;Numbers with no date and no scope. A figure that was true last year, or true only for one project size, gets repeated as if it were general.&lt;/li&gt;
&lt;li&gt;A single page that is really six questions stapled together, each half-answered.&lt;/li&gt;
&lt;li&gt;Repeated boilerplate under every heading, so that three different chunks all start with the same sentence about the company.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before you publish, read the first sentence of every section on its own
&lt;/h2&gt;

&lt;p&gt;Scroll through the page and read only the first sentence under each heading, with the heading attached. If those sentences form a coherent answer to the page's question, the page will survive chunking. If any of them needs the preceding paragraph to make sense, rewrite it.&lt;/p&gt;

&lt;p&gt;That check takes two minutes and catches most of what makes a page unquotable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>seo</category>
      <category>writing</category>
    </item>
  </channel>
</rss>
