<?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>Forem: Dawid Makowski</title>
    <description>The latest articles on Forem by Dawid Makowski (@makowskid).</description>
    <link>https://forem.com/makowskid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1136220%2Fb1d5a79a-620a-4979-a704-60a3cddf744b.jpg</url>
      <title>Forem: Dawid Makowski</title>
      <link>https://forem.com/makowskid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/makowskid"/>
    <language>en</language>
    <item>
      <title>Laravel Response Cache Serving Wrong Language: Fixing spatie/laravel-responsecache with mcamara/laravel-localization</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Sun, 19 Apr 2026 05:44:34 +0000</pubDate>
      <link>https://forem.com/makowskid/laravel-response-cache-serving-wrong-language-fixing-spatielaravel-responsecache-with-175l</link>
      <guid>https://forem.com/makowskid/laravel-response-cache-serving-wrong-language-fixing-spatielaravel-responsecache-with-175l</guid>
      <description>&lt;p&gt;If you're running a multilingual Laravel site with &lt;code&gt;mcamara/laravel-localization&lt;/code&gt; and &lt;code&gt;spatie/laravel-responsecache&lt;/code&gt;, your response cache might be serving the wrong locale to visitors. Here's how we diagnosed and fixed a bug where our English site kept getting cached in Chinese.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Laravel Multilingual Site Cache Stuck on Wrong Language
&lt;/h2&gt;

&lt;p&gt;About once a day, our production Laravel 12 site started serving Chinese content to all visitors. The homepage, blog posts, service pages - everything rendered in Simplified Chinese instead of English. The only fix was clearing the response cache manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan responsecache:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Content would go back to English... until the next day when it flipped to Chinese again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Multilingual Laravel Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 12&lt;/strong&gt; with &lt;code&gt;mcamara/laravel-localization&lt;/code&gt; for URL-prefixed i18n (&lt;code&gt;/en/blog&lt;/code&gt;, &lt;code&gt;/zh/blog&lt;/code&gt;, &lt;code&gt;/de/blog&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;spatie/laravel-responsecache&lt;/code&gt;&lt;/strong&gt; for full-page HTML caching (7-day TTL) using &lt;code&gt;DefaultHasher&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Six supported locales including &lt;code&gt;zh&lt;/code&gt; (Chinese Simplified)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hideDefaultLocaleInURL&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt; -- all URLs have an explicit locale prefix&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Root Cause: Why spatie/laravel-responsecache Ignores the Locale
&lt;/h2&gt;

&lt;p&gt;The response cache stores rendered HTML keyed by a hash of the request URL. Since our URLs include locale prefixes (&lt;code&gt;/en/blog&lt;/code&gt; vs &lt;code&gt;/zh/blog&lt;/code&gt;), each locale should get its own cache entry. So how was Chinese content leaking into English pages?&lt;/p&gt;

&lt;p&gt;Three things were conspiring against us.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;useAcceptLanguageHeader&lt;/code&gt; Overrides URL Locale During Route Resolution
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;config/laravellocalization.php&lt;/code&gt;, there's this setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'useAcceptLanguageHeader'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&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;This tells &lt;code&gt;mcamara/laravel-localization&lt;/code&gt;: "If you can't determine the locale from the URL, check the browser's &lt;code&gt;Accept-Language&lt;/code&gt; header."&lt;/p&gt;

&lt;p&gt;Sounds reasonable. But &lt;code&gt;LaravelLocalization::setLocale()&lt;/code&gt; runs during &lt;strong&gt;route resolution&lt;/strong&gt; on every request. When the URL segment doesn't contain a recognized locale (e.g., a request to &lt;code&gt;/&lt;/code&gt;), it calls &lt;code&gt;getCurrentLocale()&lt;/code&gt; as a fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getCurrentLocale&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currentLocale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currentLocale&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;useAcceptLanguageHeader&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="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runningInConsole&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$negotiator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LanguageNegotiator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$negotiator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;negotiateLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;configRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.locale'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a Chinese bot (Baidu, Sogou, etc.) hits the root URL &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;Accept-Language: zh&lt;/code&gt;, the negotiator returns &lt;code&gt;zh&lt;/code&gt;, and &lt;code&gt;app()-&amp;gt;setLocale('zh')&lt;/code&gt; is called as a &lt;strong&gt;side effect&lt;/strong&gt; during route resolution. The page renders in Chinese, gets cached, and every subsequent visitor to that URL sees Chinese content until the cache expires or is cleared.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Spatie's &lt;code&gt;DefaultHasher&lt;/code&gt; Doesn't Include Locale in the Cache Key
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;DefaultHasher&lt;/code&gt; in &lt;code&gt;spatie/laravel-responsecache&lt;/code&gt; generates cache keys like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'responsecache-'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'xxh128'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHost&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPathInfo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMethod&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$suffix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The active locale is only present &lt;strong&gt;implicitly&lt;/strong&gt; through the URL path. If any edge case causes &lt;code&gt;app()-&amp;gt;getLocale()&lt;/code&gt; to return the wrong locale during rendering, the wrong HTML gets cached under the "correct" URL's key. There's no defense-in-depth -- the &lt;code&gt;DefaultHasher&lt;/code&gt; trusts that the URL path and the active locale always agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Synchronous &lt;code&gt;Artisan::call()&lt;/code&gt; in Model Observers Mutates Locale State
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;ClearResponseCacheObserver&lt;/code&gt; did this on every model update :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;refreshSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ResponseCache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// Nuke the entire cache&lt;/span&gt;
    &lt;span class="nc"&gt;Artisan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'generate-sitemap'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Runs synchronously!&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;submitToIndexNow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Notify search engines&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sitemap command calls &lt;code&gt;LaravelLocalization::setLocale('en')&lt;/code&gt; internally, mutating the application locale &lt;strong&gt;during the active web request&lt;/strong&gt;. Then &lt;code&gt;submitToIndexNow&lt;/code&gt; notifies search engines (including Chinese ones) about all locale URLs via IndexNow, and their bots come crawling within seconds - while the response cache is still empty after the clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Three Changes to Make Laravel Response Cache Locale-Aware
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: Custom Locale-Aware Hasher for &lt;code&gt;spatie/laravel-responsecache&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We created a custom hasher that extends &lt;code&gt;DefaultHasher&lt;/code&gt; and explicitly includes &lt;code&gt;app()-&amp;gt;getLocale()&lt;/code&gt; in every cache key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\CacheProfiles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Spatie\ResponseCache\Hasher\DefaultHasher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocaleAwareHasher&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DefaultHasher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getHashFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheNameSuffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCacheNameSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&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;getLocale&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'responsecache-'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'xxh128'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHost&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNormalizedRequestUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMethod&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$locale&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$cacheNameSuffix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it in &lt;code&gt;config/responsecache.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'hasher'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Http\CacheProfiles\LocaleAwareHasher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now even if the URL says &lt;code&gt;/en/blog&lt;/code&gt; but &lt;code&gt;app()-&amp;gt;getLocale()&lt;/code&gt; somehow returns &lt;code&gt;zh&lt;/code&gt;, they get separate cache entries instead of one overwriting the other. This makes locale cache contamination structurally impossible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: Disable &lt;code&gt;useAcceptLanguageHeader&lt;/code&gt; in &lt;code&gt;mcamara/laravel-localization&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/laravellocalization.php&lt;/span&gt;
&lt;span class="s1"&gt;'useAcceptLanguageHeader'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since all our URLs have explicit locale prefixes (&lt;code&gt;/en/&lt;/code&gt;, &lt;code&gt;/zh/&lt;/code&gt;, etc.), there's no need for &lt;code&gt;Accept-Language&lt;/code&gt; header detection. With this disabled, requests without a locale prefix fall back to the default app locale (&lt;code&gt;en&lt;/code&gt;) instead of negotiating from whatever language a bot's headers specify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This was the core trigger.&lt;/strong&gt; One config line that let Chinese bots poison the response cache through the &lt;code&gt;Accept-Language&lt;/code&gt; header.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: Move Sitemap Generation to a Queued Job
&lt;/h3&gt;

&lt;p&gt;We replaced the synchronous &lt;code&gt;Artisan::call('generate-sitemap')&lt;/code&gt; in the model observer with a queued job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Jobs/GenerateSitemapJob.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateSitemapJob&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Artisan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'generate-sitemap'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In the observer:&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;refreshSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ResponseCache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;GenerateSitemapJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Runs in isolated queue worker&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;submitToIndexNow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sitemap command's &lt;code&gt;LaravelLocalization::setLocale('en')&lt;/code&gt; call now runs in an isolated queue worker process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Check If Your Laravel App Is Affected
&lt;/h2&gt;

&lt;p&gt;You're likely vulnerable to this response cache locale bug if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;useAcceptLanguageHeader&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;config/laravellocalization.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You're using &lt;code&gt;DefaultHasher&lt;/code&gt; (the default) in &lt;code&gt;config/responsecache.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You have non-Latin locales enabled (Chinese, Arabic, etc.) that make the bug immediately visible&lt;/li&gt;
&lt;li&gt;You're using &lt;code&gt;spatie/laravel-responsecache&lt;/code&gt; with &lt;code&gt;mcamara/laravel-localization&lt;/code&gt; together&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't Forget: Clear Response Cache After Deploy
&lt;/h2&gt;

&lt;p&gt;Since the cache key format changes with the new &lt;code&gt;LocaleAwareHasher&lt;/code&gt;, you must clear the existing response cache after deploying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan responsecache:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Vibe Code Rescue: Turn Your AI-Built Prototype Into a Product That Can Actually Scale</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Thu, 09 Apr 2026 15:09:54 +0000</pubDate>
      <link>https://forem.com/makowskid/vibe-code-rescue-turn-your-ai-built-prototype-into-a-product-that-can-actually-scale-1md9</link>
      <guid>https://forem.com/makowskid/vibe-code-rescue-turn-your-ai-built-prototype-into-a-product-that-can-actually-scale-1md9</guid>
      <description>&lt;p&gt;At a certain point you need professional engineering discipline, not more prompting.&lt;/p&gt;

&lt;p&gt;That is exactly where &lt;strong&gt;A2Z WEB's Vibe Code Rescue&lt;/strong&gt; comes in. In a focused, time-boxed engagement, our senior engineers and CTOs audit your code, stress-test your infrastructure, surface vulnerabilities, and hand you a prioritized roadmap to get scale-ready. You walk away with total clarity on the real health of your product, and a credible plan to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You should keep reading if any of this sounds familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You (or a non-technical co-founder) shipped your MVP using Cursor, Claude, Lovable, Bolt, v0, Replit, GitHub Copilot, or a similar AI-first workflow.&lt;/li&gt;
&lt;li&gt;Real users are now in the product, and every new feature seems to break two old ones.&lt;/li&gt;
&lt;li&gt;You suspect there are security holes, but nobody on the team can confidently say where they are.&lt;/li&gt;
&lt;li&gt;Your cloud bill is climbing faster than your revenue.&lt;/li&gt;
&lt;li&gt;Investors, enterprise customers, or a partner just asked about security, uptime, or architecture, and you froze.&lt;/li&gt;
&lt;li&gt;You are about to hire your first engineers and you want them to inherit a codebase they will not immediately want to throw away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you nodded at two or more of those, your product has hit the limit of what vibe coding alone can deliver. That is not a failure. It is a predictable inflection point, and it has a predictable fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "vibe coded" really leaves behind
&lt;/h2&gt;

&lt;p&gt;Speed has a price, and AI-generated codebases tend to pay it in the same places every time. In a typical Vibe Code Rescue we find:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Security holes nobody designed in.&lt;/strong&gt; SQL injection, broken authentication and session handling, exposed API keys, missing authorization checks, public storage buckets, leaky logs, and personal data sitting in places it should never be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture held together by duct tape.&lt;/strong&gt; Business logic copy-pasted across screens, state managed in five different ways, no clear boundary between frontend and backend, and database schemas that quietly assume nothing will ever change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragile, unrepeatable deployments.&lt;/strong&gt; No environments, no migrations, no rollbacks, no CI, no real version control discipline. "It works on prod" because prod is the only place it has ever worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero test coverage.&lt;/strong&gt; Every shipment is a coin flip. Regressions are discovered by users, not by the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud spend on autopilot.&lt;/strong&gt; Oversized instances, forgotten dev environments, chatty AI calls with no caching, storage that nobody is cleaning up. Your infrastructure invoice is a tax on guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A codebase no human can confidently change.&lt;/strong&gt; New features take longer every week because nobody fully understands what is already there, including the AI that wrote it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this means the AI did a bad job. It means the AI did exactly what it was asked to do: make it work, fast. Making it safe, scalable, maintainable, and affordable is a different job, and it needs different people.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vibe Code Rescue, step by step
&lt;/h2&gt;

&lt;p&gt;We run the engagement as a structured, two-week sprint led by a senior CTO and a small team of senior engineers. Every line of analysis is done by a real human, supported (not replaced) by industry-standard tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: See the truth
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Discovery and goal setting.&lt;/strong&gt; A working session with you and your team to align on what the product is supposed to do, who is using it, what is on fire, and what success looks like in 6 and 12 months. We write it down so we are auditing against your reality, not a generic checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Codebase analysis.&lt;/strong&gt; We run your repository through industry-standard static analysis, dependency scanning, and code quality tooling, then a senior engineer walks the code by hand. You get hard data on technical debt, code quality, dead code, dependency risk, license risk, and structural weaknesses, with concrete file and line references.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Architecture review.&lt;/strong&gt; We map how your system is actually built today: services, data flows, integrations, third-party dependencies, AI calls, and failure points. We compare that map against where you want to be in 12 months and flag the gaps that will hurt you first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Security testing.&lt;/strong&gt; A focused application security review covering the OWASP Top 10 and the issues we see most often in AI-generated codebases: SQL and NoSQL injection, authentication and session weaknesses, broken access control, data exposure, secrets in source, insecure file handling, vulnerable dependencies, CORS and CSRF issues, and personal data handling. Where it is safe to do so, we demonstrate how easy each issue is to exploit, so the risk is undeniable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Load and resilience testing.&lt;/strong&gt; We push your system until it breaks, in a controlled environment, so you know exactly how much traffic your product can handle, where it falls over first, and how it behaves under failure. No more guessing whether you can survive a launch, a Product Hunt spike, or a single big customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 2: Get a plan you can actually execute
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;6. Cloud and cost audit.&lt;/strong&gt; We review your cloud setup (AWS, GCP, Azure, Vercel, Supabase, Render, Fly, and friends), look at how much you are spending and where, and identify quick wins and structural changes that bring the bill back under control without sacrificing performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. AI usage review.&lt;/strong&gt; If your product calls LLMs or other AI APIs, we review prompts, model choices, caching, retries, guardrails, evaluation, and cost per request. AI features should be reliable line items, not surprises at the end of the month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Prioritized remediation roadmap.&lt;/strong&gt; Everything we found is consolidated into a single, ranked action plan. Each item has a clear severity, an estimate of effort, an owner profile (who should do it), and a recommended sequence. Critical security and stability issues come first; long-term refactors come later, with a clear story of why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Executive readout.&lt;/strong&gt; A live walkthrough with you and, if you want, your investors, board, or key customers. Plain language, no jargon, no defensiveness. You finish the call knowing exactly where you stand and what to do on Monday morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you walk away with
&lt;/h2&gt;

&lt;p&gt;Every Vibe Code Rescue concludes with a concrete, written package, not a verbal "you should probably refactor things":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A full &lt;strong&gt;Technical Health Report&lt;/strong&gt; covering code quality, architecture, security, performance, infrastructure, and cost.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Security Findings Report&lt;/strong&gt; with severity, evidence, exploitability, and remediation guidance for each issue.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Load and Resilience Report&lt;/strong&gt; with measured breaking points, bottlenecks, and recommendations.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Cloud Cost Review&lt;/strong&gt; with itemized savings opportunities and projected monthly impact.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Prioritized Remediation Roadmap&lt;/strong&gt; for the next 30, 60, and 90 days, plus a longer-term architecture direction.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Executive Summary &amp;amp; Live Readout&lt;/strong&gt; — a plain-language brief you can share with non-technical stakeholders, investors, and enterprise buyers, delivered in a working call with the senior team that did the work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is yours to keep, share, and act on, with or without us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why A2Z WEB
&lt;/h2&gt;

&lt;p&gt;We are not a generic agency that "also does audits." Vibe Code Rescue is delivered by the same senior CTOs and engineers who run our &lt;a href="https://a2zweb.co/en/services/chief-technology-officer-as-a-service-ctoaas" rel="noopener noreferrer"&gt;CTO as a Service&lt;/a&gt;, &lt;a href="https://a2zweb.co/en/services/tech-auditing-strategy-consulting" rel="noopener noreferrer"&gt;Tech Auditing and Strategy Consulting&lt;/a&gt;, &lt;a href="https://a2zweb.co/en/services/custom-software-development" rel="noopener noreferrer"&gt;Custom Software Development&lt;/a&gt;, &lt;a href="https://a2zweb.co/en/services/ai-automation" rel="noopener noreferrer"&gt;AI Automation&lt;/a&gt;, and &lt;a href="https://a2zweb.co/en/services/cloud-cost-optimization-audit" rel="noopener noreferrer"&gt;Cloud Cost Optimization Audit&lt;/a&gt; practices for funded startups and established companies.&lt;/p&gt;

&lt;p&gt;A few things that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Senior people only.&lt;/strong&gt; No juniors quietly billed at senior rates. Every audit is led by a CTO-level engineer who has shipped, scaled, and rescued real products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical, fact-based recommendations.&lt;/strong&gt; We translate between business priorities and technical reality, so your decisions rest on data, not vibes (pun intended).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-fluent, not AI-naive.&lt;/strong&gt; We use the same AI tools your team uses. We know exactly what they are good at, where they cut corners, and how to clean up after them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOC 2 aligned process.&lt;/strong&gt; Your code, data, and findings are handled with the same security discipline we expect from your product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real path forward.&lt;/strong&gt; When the audit is done, we can hand the roadmap back to your team, work alongside them, or take ownership of the remediation as a fractional engineering team. Your call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The "no surprises" guarantee
&lt;/h2&gt;

&lt;p&gt;If, at the end of the engagement, you do not feel you have a clearer, more honest picture of your product than you did when you started, we will refund the engagement fee. We can promise that because we have never had to.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How long does it take?&lt;/strong&gt;&lt;br&gt;
Two weeks from kickoff to executive readout. We can move faster for urgent situations (pre-launch, due diligence, security incident); ask us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do we need to pause feature work?&lt;/strong&gt;&lt;br&gt;
No. The audit runs in parallel with your normal development. We will need a few hours of your team's time across the two weeks, mostly for kickoff, questions, and the readout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will you need access to production?&lt;/strong&gt;&lt;br&gt;
We work in a read-only mode by default, against a staging environment or a snapshot, and we agree on every access scope in writing before we touch anything. Nothing destructive happens without your explicit approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stack do you cover?&lt;/strong&gt;&lt;br&gt;
TypeScript and JavaScript (Node, Next.js, React, Vue, Svelte), Python (Django, FastAPI, Flask), Ruby on Rails, PHP (Laravel), Go, mobile (React Native, Flutter, Swift, Kotlin), and the usual cloud and database suspects (AWS, GCP, Azure, Vercel, Supabase, Postgres, MySQL, MongoDB, Redis). If you are on something more exotic, ask us; we have probably seen it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the audit finds something really bad?&lt;/strong&gt;&lt;br&gt;
Then you will be glad you ran it now instead of after the breach, the outage, or the failed enterprise security review. We will help you triage and, if you want, fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you also build the fixes?&lt;/strong&gt;&lt;br&gt;
Yes. After the audit you can engage A2Z WEB as a fractional engineering team, a CTO as a Service partner, or a full custom development team, depending on what you actually need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does it cost?&lt;/strong&gt;&lt;br&gt;
A fixed fee for the two-week engagement, agreed upfront, with no hidden extras. We will quote it on the intro call once we understand the size and shape of your product. It is meaningfully less than the cost of the first serious incident it will prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to find out what your product is really made of?
&lt;/h2&gt;

&lt;p&gt;Book a 30-minute, no-pressure intro call. We will ask a handful of questions about your product, your stack, and what is keeping you up at night, and tell you honestly whether a Vibe Code Rescue is the right next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://a2zweb.co/en/contact" rel="noopener noreferrer"&gt;&lt;strong&gt;Book your intro call&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your AI got you to v0.1. Let us help you get the rest of the way.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>aicodeaudit</category>
      <category>technicaldebt</category>
      <category>owasp</category>
    </item>
    <item>
      <title>Smarter Scoring, Sharper Hiring: A Major Update to the CV Match Score API</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Thu, 09 Apr 2026 11:42:04 +0000</pubDate>
      <link>https://forem.com/makowskid/smarter-scoring-sharper-hiring-a-major-update-to-the-cv-match-score-api-27eg</link>
      <guid>https://forem.com/makowskid/smarter-scoring-sharper-hiring-a-major-update-to-the-cv-match-score-api-27eg</guid>
      <description>&lt;p&gt;We've just shipped one of the most requested improvements to the &lt;a href="https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score" rel="noopener noreferrer"&gt;&lt;strong&gt;Resume/CV &amp;amp; Job Description Compatibility Scoring&lt;/strong&gt;&lt;/a&gt; endpoint. If you're building ATS integrations, candidate-screening tools, or HR analytics dashboards on top of SharpAPI, this update unlocks a whole new level of control over how matches are scored.&lt;/p&gt;

&lt;p&gt;Here's everything that's new, why it matters, and how to start using it today.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;context&lt;/code&gt; parameter is now a &lt;strong&gt;formal directive contract&lt;/strong&gt;, not a free-form note.&lt;/li&gt;
&lt;li&gt;You get three simple directive shapes: &lt;strong&gt;&lt;code&gt;EMPHASIZE&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;DEEMPHASIZE&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;CREDIT&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Directives now influence the &lt;strong&gt;&lt;code&gt;overall_match&lt;/code&gt;&lt;/strong&gt; score — not just the individual metrics.&lt;/li&gt;
&lt;li&gt;Role-family standard credentials (think &lt;em&gt;Excel / SQL / Power BI&lt;/em&gt; for finance roles) are &lt;strong&gt;credited automatically&lt;/strong&gt; even when the job description forgets to mention them.&lt;/li&gt;
&lt;li&gt;Maximum &lt;code&gt;context&lt;/code&gt; length is formally set to &lt;strong&gt;5000 characters&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;Jump straight to the product page:&lt;/strong&gt; &lt;a href="https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score" rel="noopener noreferrer"&gt;sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem we were solving
&lt;/h2&gt;

&lt;p&gt;When we first launched the CV Match Score endpoint, it scored every resume against every job description using a &lt;strong&gt;fixed weighting table&lt;/strong&gt;: skills, experience, and technical stack at the top; education, certifications, and soft skills as supporting signals. That works beautifully for the &lt;em&gt;average&lt;/em&gt; hiring scenario — but hiring is never average.&lt;/p&gt;

&lt;p&gt;A startup hiring a fresher values &lt;strong&gt;education and credentials&lt;/strong&gt; far more than ten years of experience. A contract-hiring desk cares about &lt;strong&gt;skills&lt;/strong&gt;, not tenure. A remote-first company doesn't want &lt;strong&gt;location&lt;/strong&gt; pulling the score down at all. And sometimes, the job description simply &lt;em&gt;forgets&lt;/em&gt; to list the obvious credentials — like Excel for a finance analyst — leaving the scoring engine to assume they don't matter.&lt;/p&gt;

&lt;p&gt;Customers kept asking the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"How do we tell the engine that **for this role&lt;/em&gt;&lt;em&gt;, education matters more than experience?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you can.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ What's new
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A formal directive contract for the &lt;code&gt;context&lt;/code&gt; parameter
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; field already existed, but its behaviour was fuzzy — long prose instructions often had unpredictable effects. We've replaced that with a &lt;strong&gt;clean, predictable contract&lt;/strong&gt; built around three directive shapes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🔼 &lt;code&gt;EMPHASIZE: &amp;lt;topic&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Increases the weight of the matching metric(s) by one step&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EMPHASIZE: skills&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔽 &lt;code&gt;DEEMPHASIZE: &amp;lt;topic&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Decreases the weight of the matching metric(s) by one step (never zero)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DEEMPHASIZE: experience&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;➕ `CREDIT: &amp;lt;skill \&lt;/td&gt;
&lt;td&gt;tool \&lt;/td&gt;
&lt;td&gt;cert&amp;gt;`&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Directives can be mixed freely in a single &lt;code&gt;context&lt;/code&gt; string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMPHASIZE: skills. EMPHASIZE: education. DEEMPHASIZE: experience. CREDIT: Excel and Analytics certificates.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to memorise the full schema of 20 metrics, either. Topics accept &lt;strong&gt;plain-English categories&lt;/strong&gt; like &lt;code&gt;skills&lt;/code&gt;, &lt;code&gt;experience&lt;/code&gt;, &lt;code&gt;education&lt;/code&gt;, &lt;code&gt;certificates&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;management&lt;/code&gt;, or &lt;code&gt;tenure&lt;/code&gt;, and the engine maps them onto the related metrics internally.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. &lt;code&gt;context&lt;/code&gt; now moves the &lt;code&gt;overall_match&lt;/code&gt; score
&lt;/h3&gt;

&lt;p&gt;Before this update, the &lt;code&gt;overall_match&lt;/code&gt; was computed from a hardcoded weighted average of the 20 individual metrics — meaning even if &lt;code&gt;context&lt;/code&gt; shifted individual scores, the final overall number often stayed stubbornly still.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not anymore.&lt;/strong&gt; Directives now adjust the internal weighting table &lt;em&gt;before&lt;/em&gt; the weighted average is computed. A single &lt;code&gt;EMPHASIZE: skills&lt;/code&gt; now propagates all the way through to the final score.&lt;/p&gt;

&lt;p&gt;Here's what that looks like conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Baseline weights:       skills=3, experience=3, education=1, certifications=1 ...
After EMPHASIZE: skills + DEEMPHASIZE: experience + EMPHASIZE: education:
Adjusted weights:       skills=4, experience=2, education=2, certifications=1 ...
overall_match = Σ(score × adjusted_weight) / Σ(adjusted_weights)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same resume. Same job description. Different scoring lens. That's the magic.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Role-family standard credentials are now credited
&lt;/h3&gt;

&lt;p&gt;Here's a real example from customer feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A finance JD said &lt;em&gt;"Required: Excel, Python, analytics skills"&lt;/em&gt; but didn't list any specific certifications. A candidate with Advanced Excel, SQL, and Power BI certifications was getting &lt;code&gt;certifications_match: 0&lt;/code&gt; — because the JD was silent about certs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's obviously wrong. Those certifications are &lt;strong&gt;industry-standard&lt;/strong&gt; for finance roles, and the engine should have credited them.&lt;/p&gt;

&lt;p&gt;With this update, when the job description doesn't enumerate required certifications but the resume lists credentials that are commonly expected for the role family, the engine now credits them proportionally in the &lt;strong&gt;40–70&lt;/strong&gt; range, with the reasoning noted in the &lt;code&gt;explanations&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;This applies across role families:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;strong&gt;Finance / analyst roles&lt;/strong&gt; → Advanced Excel, SQL, Power BI, Tableau&lt;/li&gt;
&lt;li&gt;👨‍💻 &lt;strong&gt;Software engineering&lt;/strong&gt; → Git, CI/CD, cloud platform certs&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Project management&lt;/strong&gt; → PMP, PRINCE2, Scrum, Agile&lt;/li&gt;
&lt;li&gt;🏥 &lt;strong&gt;Healthcare&lt;/strong&gt; → CPR, BLS, specialty certifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more zero-scores on obvious credentials just because the JD was lazy.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Formalised 5000-character limit on &lt;code&gt;context&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We've set a clean, generous upper bound of &lt;strong&gt;5000 characters&lt;/strong&gt; for the &lt;code&gt;context&lt;/code&gt; field. That's comfortably above any realistic directive payload — enough room for dozens of directives plus explanatory notes — while keeping the full prompt within safe token budgets.&lt;/p&gt;

&lt;p&gt;Requests exceeding the limit return an HTTP &lt;code&gt;422&lt;/code&gt; validation error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended patterns
&lt;/h2&gt;

&lt;p&gt;These are the &lt;code&gt;context&lt;/code&gt; recipes that performed best in our internal validation. Drop them in as starting points and tune from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1 — Entry-level / fresher hiring
&lt;/h3&gt;

&lt;p&gt;Prioritise credentials and potential over years of experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMPHASIZE: skills. EMPHASIZE: education. DEEMPHASIZE: experience. CREDIT: Excel and Analytics certificates.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2 — Senior technical hiring
&lt;/h3&gt;

&lt;p&gt;Stack match is critical; formal education is secondary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMPHASIZE: skills. EMPHASIZE: technical_stack_match. DEEMPHASIZE: education.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 3 — Leadership / management hiring
&lt;/h3&gt;

&lt;p&gt;Management experience outweighs hands-on technical skills.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMPHASIZE: management. EMPHASIZE: experience. DEEMPHASIZE: technical_stack_match.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 4 — Remote-first hiring
&lt;/h3&gt;

&lt;p&gt;Location should not penalise candidates at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEEMPHASIZE: location. EMPHASIZE: skills.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Be specific.&lt;/strong&gt; &lt;code&gt;CREDIT: Excel, SQL, Power BI&lt;/code&gt; moves the needle. Abstract framing like &lt;em&gt;"practical competency outweighs formal credentials"&lt;/em&gt; does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use contrast pairs.&lt;/strong&gt; Combine &lt;code&gt;EMPHASIZE&lt;/code&gt; with &lt;code&gt;DEEMPHASIZE&lt;/code&gt; in the same request — contrast is a strong signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name tools and credentials explicitly.&lt;/strong&gt; Avoid vague qualities like &lt;em&gt;"talent"&lt;/em&gt; or &lt;em&gt;"potential"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip percentage instructions.&lt;/strong&gt; Directives like &lt;em&gt;"reduce weight by 50%"&lt;/em&gt; are interpreted conservatively as a single &lt;code&gt;DEEMPHASIZE&lt;/code&gt; step. Use discrete directives — they're more reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;context&lt;/code&gt; focused.&lt;/strong&gt; A handful of targeted directives outperforms long prose paragraphs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ What &lt;code&gt;context&lt;/code&gt; can and can't do
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ &lt;code&gt;context&lt;/code&gt; &lt;strong&gt;can&lt;/strong&gt; change&lt;/th&gt;
&lt;th&gt;❌ &lt;code&gt;context&lt;/code&gt; &lt;strong&gt;cannot&lt;/strong&gt; change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weights of any of the 20 metrics in &lt;code&gt;overall_match&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Arithmetic metrics like &lt;code&gt;stability_score&lt;/code&gt; or &lt;code&gt;location_preference_match&lt;/code&gt; (scores are computed from dates/geography)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit for role-family-standard skills and certifications&lt;/td&gt;
&lt;td&gt;The JSON schema or the number of metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scoring emphasis across individual dimensions&lt;/td&gt;
&lt;td&gt;Personal-data protection rules (resume anonymisation is non-negotiable)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on arithmetic metrics:&lt;/strong&gt; &lt;code&gt;context&lt;/code&gt; still adjusts their &lt;em&gt;weight&lt;/em&gt; in &lt;code&gt;overall_match&lt;/code&gt; — it just doesn't rewrite the individual score itself. If a candidate has a 1-year average tenure, &lt;code&gt;stability_score&lt;/code&gt; reflects that fact regardless of directives, but you can make it count for more or less in the overall outcome.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A full request example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/hr/resume_job_match_score
Content-Type: multipart/form-data
Authorization: Bearer &amp;lt;YOUR_API_KEY&amp;gt;

file:     &amp;lt;resume.pdf&amp;gt;
content:  "Junior Finance Associate — entry-level role.
           Required: Excel, Python, financial modelling, analytics."
language: English
context:  "EMPHASIZE: skills. EMPHASIZE: education.
           DEEMPHASIZE: experience. CREDIT: Excel and Analytics certificates."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response structure is unchanged — you still get the 20 scored metrics plus &lt;code&gt;explanations&lt;/code&gt; for the most important ones — but the numbers will now reflect the directive-adjusted weights.&lt;/p&gt;




&lt;h2&gt;
  
  
  A word on non-determinism
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; parameter is our primary lever for steering the engine, and we tune it continuously based on the real-world cases our customers bring us. If you hit a scenario where the output doesn't match your expectations, send us the exact request/response — that's the single most valuable feedback we can get.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;Ready to try it? Here's your shortlist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the product details:&lt;/strong&gt; &lt;a href="https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score" rel="noopener noreferrer"&gt;Resume/CV &amp;amp; Job Description Compatibility Scoring&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;See it live:&lt;/strong&gt; &lt;a href="https://cvmatchscore.com" rel="noopener noreferrer"&gt;CVMatchScore.com&lt;/a&gt; — a fully working product built on this exact endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grab an API key&lt;/strong&gt; from your &lt;a href="https://sharpapi.com" rel="noopener noreferrer"&gt;SharpAPI dashboard&lt;/a&gt; and send your first request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell us what's missing&lt;/strong&gt; — every directive pattern we've shipped started as customer feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Happy matching. 🎯&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👉 Try the Resume/CV &amp;amp; Job Description Compatibility Scoring API today:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score" rel="noopener noreferrer"&gt;sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.vecteezy.com/free-photos/resume" rel="noopener noreferrer"&gt;Resume Stock photos by Vecteezy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hrtech</category>
      <category>resumeparsing</category>
      <category>cvparsing</category>
      <category>hrapi</category>
    </item>
    <item>
      <title>Your AI API, your rules: Introducing Custom AI Job instructions</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Thu, 02 Apr 2026 13:26:10 +0000</pubDate>
      <link>https://forem.com/makowskid/your-ai-api-your-rules-introducing-custom-ai-job-instructions-nbf</link>
      <guid>https://forem.com/makowskid/your-ai-api-your-rules-introducing-custom-ai-job-instructions-nbf</guid>
      <description>&lt;h2&gt;
  
  
  The problem with one-size-fits-all AI
&lt;/h2&gt;

&lt;p&gt;SharpAPI's endpoints are built to work out of the box. You send a resume, you get a score. You send a job title, you get a description. That's the promise, and it holds for most use cases.&lt;/p&gt;

&lt;p&gt;The workaround until now was wrapping our API in your own middleware, manually injecting context before every call. That works, but it's boilerplate you shouldn't have to write.&lt;/p&gt;

&lt;p&gt;We shipped a native per-account, per-endpoint instruction system. You write the context once. We inject it into every relevant AI request automatically, at the right layer, without touching your API calls.&lt;/p&gt;

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

&lt;p&gt;Every SharpAPI account now has a &lt;strong&gt;Customize AI Jobs&lt;/strong&gt; section in the dashboard. It lists all available AI job types grouped by category, from HR and recruitment endpoints to e-commerce and content tools.&lt;/p&gt;

&lt;p&gt;For each job type, you can write a free-form instruction. Think of it as a persistent system note to the underlying model: "for every resume scoring request I make, apply these priorities." Once saved and toggled active, that instruction is automatically injected before the AI processes your request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Open the dashboard and go to Customize AI Jobs&lt;/strong&gt; Find it in the sidebar, right below Custom Workflows. All job types are listed and grouped by category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Expand the endpoint you want to customize&lt;/strong&gt; Each card has an inline editor. Write your instruction in plain language, no special syntax required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Save and activate&lt;/strong&gt; You'll see a confirmation prompt before saving, since changing instructions affects your output. Toggle it on, and you're done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Your API calls stay exactly the same&lt;/strong&gt; No changes to request format, headers, or parameters. The customization happens server-side, invisibly, for every matching request.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Say you're using the Resume Scoring endpoint to screen candidates for a Cloud Architect role. Without a custom prompt, the model scores resumes against a generic template. With one, you can shift those priorities permanently for your account:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Weight cloud certifications (AWS, GCP, Azure) significantly higher than general programming experience. Penalize resumes with no evidence of distributed systems work. Flag any candidate with fewer than 3 years of hands-on infrastructure experience regardless of their total years in software.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your API call stays unchanged. But behind the scenes, your custom instruction is already baked in. Every candidate's resume is scored against your actual hiring criteria, not a generic baseline.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you can customize
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tone and style&lt;/strong&gt; Lock content generation to your brand voice. Formal, conversational, regional, industry-specific -- it's up to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scoring weights&lt;/strong&gt; Boost or suppress specific signals in scoring endpoints. Relevant for resume scoring, content quality checks, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output structure&lt;/strong&gt; Tell the model to always include or exclude certain fields, use specific phrasing, or follow your internal format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain context&lt;/strong&gt; Give the model background it otherwise wouldn't have: your industry, your audience, your product category.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on Custom Workflows:&lt;/strong&gt; Custom Workflows already support user-defined prompts as a first-class feature. Custom Job Prompts do not apply to Workflow requests -- they're scoped to predefined endpoints only.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Managing your prompts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Save a prompt&lt;/td&gt;
&lt;td&gt;Stores the instruction and activates it immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toggle off&lt;/td&gt;
&lt;td&gt;Disables the prompt without deleting it. Default AI behavior restored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toggle on&lt;/td&gt;
&lt;td&gt;Re-activates a previously saved prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;Removes the prompt entirely. Endpoint returns to default behavior.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every save triggers a confirmation dialog. This is intentional: custom prompts can meaningfully change your outputs, and we want the decision to feel deliberate rather than accidental.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Custom prompts are powerful, but they can also produce unexpected results if they conflict with the underlying endpoint logic. If your results start looking off, toggling the prompt off is the fastest way to confirm whether it's the source of the issue.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What this means for teams and integrators
&lt;/h2&gt;

&lt;p&gt;If you're building a product on top of SharpAPI and serving multiple clients, custom prompts let you pre-configure the AI layer for each account without maintaining separate middleware or prompt injection logic on your end. Each account operates independently: one customer's scoring weights don't bleed into another's.&lt;/p&gt;

&lt;p&gt;For solo developers, this is simply a way to stop re-writing the same context in every API wrapper you build. Write it once, use it forever, change it whenever your needs shift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;Custom Job Prompts are available to all accounts with API access. No plan upgrade required. Log into your dashboard, find &lt;strong&gt;Customize AI Jobs&lt;/strong&gt; in the sidebar, and start tuning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://sharpapi.com/en/dashboard/custom-prompts" rel="noopener noreferrer"&gt;Open Dashboard&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>api</category>
      <category>custom</category>
    </item>
    <item>
      <title>The Cloud Is Just Someone Else's Computer. Sometimes That Computer Gets Hit by a Drone.</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Tue, 31 Mar 2026 08:58:36 +0000</pubDate>
      <link>https://forem.com/makowskid/the-cloud-is-just-someone-elses-computer-sometimes-that-computer-gets-hit-by-a-drone-2od1</link>
      <guid>https://forem.com/makowskid/the-cloud-is-just-someone-elses-computer-sometimes-that-computer-gets-hit-by-a-drone-2od1</guid>
      <description>&lt;h3&gt;
  
  
  When "Multi-Region Strategy" Means "Outrunning a Military Conflict"
&lt;/h3&gt;

&lt;p&gt;So last week a military drone blew up the AWS data center where my customer's platform runs. The platform serves millions of users across seven countries. I had to spend about a week moving everything from Bahrain to Europe. By hand. Because every single automated migration tool was also broken. Because, you know, the drones.&lt;/p&gt;

&lt;p&gt;I run a software consultancy. I've been in tech long enough to have planned for almost every disaster imaginable. Floods, earthquakes, ransomware, that one guy who drops the production database on a Friday afternoon. "Military drone strike on your cloud provider" was never on the list.&lt;/p&gt;

&lt;p&gt;And Bahrain is not an isolated case. Right now, data centers in more than ten countries are being targeted or threatened by either Iranian or russian drones. This isn't a regional incident. It's a global pattern.&lt;/p&gt;

&lt;p&gt;And yet, here we are. Welcome to DevOps in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disaster Recovery Used to Mean Hurricanes. Now It Means Drones.
&lt;/h2&gt;

&lt;p&gt;If you've worked in infrastructure long enough, you've imagined the disaster scenarios. An earthquake takes out a data center in Tokyo. Hurricane floods a facility in Virginia. Maybe a biblical-scale power outage somewhere in Texas (actually, that one happens pretty regularly). You build for resilience, you plan your failovers, you sleep slightly less terribly at night.&lt;/p&gt;

&lt;p&gt;And I don't say "earthquake" lightly. Exactly a year ago, my wife and I were on the top floor of our skyscraper condo in Bangkok when a 7.7 magnitude earthquake hit. One second I was pushing a commit. The next second I was crawling on the floor. The building was swaying two meters each side, and water from the rooftop pool came crashing through into our living room. I still get flashbacks from that. So yes, I understand natural disasters on a very personal, visceral level. I expected those to be the thing that would eventually force me to move servers under pressure.&lt;/p&gt;

&lt;p&gt;What I never rehearsed was: "Your entire AWS region is down because a military drone hit all availability zones in Bahrain."&lt;/p&gt;

&lt;p&gt;Yet here we are.&lt;/p&gt;

&lt;p&gt;In early March, Iranian drones struck multiple AWS facilities across the UAE and Bahrain. This wasn't some theoretical threat model from a security conference whiteboard. This was the first confirmed military attack on a major hyperscale cloud provider's infrastructure. Banking apps went down. Payment systems collapsed. Delivery platforms across the Gulf went dark. And somewhere in Thailand, my phone started buzzing with messages from a very worried customer in Saudi Arabia.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's No Terraform Module for Surviving a War Zone
&lt;/h2&gt;

&lt;p&gt;Here's what you need to understand about the week that followed: every single automated migration tool AWS provides was broken. CloudWatch, the thing that tells you if your servers are even alive? Gone. RDS Snapshots, the thing you use to back up databases before you touch anything? Unavailable. Cross-region transfer? Dead. AMI copies? Nope.&lt;/p&gt;

&lt;p&gt;It was like showing up to a house fire and discovering that not only is your fire truck empty, but someone also stole the hydrant.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable engineer would do. I had to rebuilt multiple production environments from scratch, on bare Linux images, in Europe. By hand. For a platform serving millions of users across seven countries. I wrote custom scripts to export, compress, and transfer everything over the public internet (because AWS's own internal backbone between regions was also down). I wrote manual rescue scripts for files that kept failing for days with &lt;code&gt;InternalError&lt;/code&gt;. I worked nights because often it was the only window where platform traffic was low enough to safely verify everything.&lt;/p&gt;

&lt;p&gt;One week of controlled chaos. And by the end of it, the entire platform was running smoothly from Europe, as if nothing had happened.&lt;/p&gt;

&lt;p&gt;But everything had happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  We're a Software Company. Why Do We Keep Running From Wars?
&lt;/h2&gt;

&lt;p&gt;I could tell this story as a purely technical narrative. Here's the architecture, here's the migration plan, here's the clever script that saved the day. But that would miss the point entirely.&lt;/p&gt;

&lt;p&gt;Because here's what my day-to-day actually looks like:&lt;/p&gt;

&lt;p&gt;I run a small tech consultancy. We build custom software. We manage cloud infrastructure. We automate businesses with AI workflows. Very normal stuff. And yet somehow, every single person on my team has been touched by war. Not metaphorically. Literally.&lt;/p&gt;

&lt;p&gt;I live in Thailand, which recently had skirmishes with Cambodia along the border. My Iranian engineer had to flee Iran with his entire family. One of my coworkers lives in Ukraine, literally in a war zone, delivering code between power cuts because the grid keeps getting hit by Iranian-designed drones. A couple of months ago he went to an immigration office across the border and couldn't come back for days because russians bombed the only bridge on his route home. Another colleague had to evacuate Ukraine with his whole family.&lt;/p&gt;

&lt;p&gt;We write code and configure servers. We're not defense contractors. We're not geopolitical analysts. We're developers who just want to ship clean code and go home.&lt;/p&gt;

&lt;p&gt;And yet, every week, somewhere on this planet, a conflict reaches through the internet cables and grabs us by the collar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strangest Plot Twist of 2026
&lt;/h2&gt;

&lt;p&gt;And now, in what might be the most unexpected geopolitical crossover episode of the decade: Ukraine is protecting Saudi skies.&lt;/p&gt;

&lt;p&gt;Let that sink in for a second. The country that has been fighting for its own survival since 2022, that has become the world's foremost expert on shooting down drones because it had no choice, has just signed defense cooperation agreements with Saudi Arabia, Qatar, and the UAE. Over 200 Ukrainian drone-countering specialists are now deployed across the Gulf, helping defend the very region where my customer's servers used to live.&lt;/p&gt;

&lt;p&gt;The same drones that forced me to migrate infrastructure out of Bahrain? Ukraine knows those drones intimately. They've been dealing with their Iranian-made cousins, the Shaheds, for years.&lt;/p&gt;

&lt;p&gt;So now the country of my colleague who codes between blackouts is also the country protecting the airspace above my customer's business. If you wrote this as fiction, your editor would tell you it's too on the nose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Else Is Living This?
&lt;/h2&gt;

&lt;p&gt;I can't be the only one. There must be thousands of engineers, sysadmins, CTOs, and DevOps folks out there who have spent the last few years making decisions that no technical manual covers. Moving workloads because of missiles. Rerouting traffic because of sanctions. Keeping systems alive through infrastructure that's being actively targeted.&lt;/p&gt;

&lt;p&gt;If you've had to migrate production systems because of armed conflict, I'd love to hear your story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Normal (Which Is Not Normal At All)
&lt;/h2&gt;

&lt;p&gt;Twenty years ago, your biggest infrastructure worry was a hard drive failing or router dropping packets. Ten years ago, it was maybe a ransomware attack. Today, it's a state-sponsored drone strike on your cloud provider's physical data center.&lt;/p&gt;

&lt;p&gt;We've entered an era where "disaster recovery" needs to account for actual disasters of the military kind. Where your multi-region strategy isn't just about latency and compliance, it's about geopolitical risk assessment.&lt;/p&gt;

&lt;p&gt;The conflicts we see on the news aren't happening "over there" anymore. They're happening inside our dashboards, our uptime monitors, our incident channels. Every single one of us in tech is connected to these events whether we like it or not.&lt;/p&gt;

&lt;p&gt;The world got very small, and very complicated, very fast.&lt;/p&gt;




&lt;p&gt;Check more at &lt;a href="https://dawidmakowski.com/en/2026/03/the-cloud-is-just-someone-elses-computer-sometimes-that-computer-gets-hit-by-a-drone/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
      <category>war</category>
    </item>
    <item>
      <title>Introducing Custom Workflows: Turn Any AI Prompt Into a Production API</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Mon, 16 Mar 2026 16:28:07 +0000</pubDate>
      <link>https://forem.com/makowskid/introducing-custom-workflows-turn-any-ai-prompt-into-a-production-api-3h05</link>
      <guid>https://forem.com/makowskid/introducing-custom-workflows-turn-any-ai-prompt-into-a-production-api-3h05</guid>
      <description>&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@campaign_creators?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Campaign Creators&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/man-writing-on-white-board---kQ4tBklJI?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building AI-powered features into your product usually means weeks of backend development, prompt engineering, infrastructure setup, and deployment headaches. What if you could skip all of that and go from an idea to a live REST API endpoint in under 60 seconds?&lt;/p&gt;

&lt;p&gt;That is exactly what &lt;a href="https://sharpapi.com/custom-workflows" rel="noopener noreferrer"&gt;&lt;strong&gt;Custom Workflows&lt;/strong&gt;&lt;/a&gt; delivers. A visual builder in the SharpAPI Dashboard lets you define typed input parameters, write your processing logic as a plain-English prompt, and generate a production-ready API endpoint on the spot. The result is a fully authenticated REST API that accepts your inputs, runs them through the AI model of your choice, and returns structured JSON matching the output schema you defined. No backend code. No deployment pipeline. No infrastructure to manage. Just your idea, instantly available as an endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: AI Integration Is Still Too Hard
&lt;/h2&gt;

&lt;p&gt;Every development team has a backlog of AI features they want to build. Extract data from invoices. Classify support tickets. Generate product descriptions. Score leads. Moderate content.&lt;/p&gt;

&lt;p&gt;Each of these features follows the same pattern: take some input, send it to an AI model, parse the response, and return structured data. And yet, building each one from scratch means writing boilerplate code, handling API communication with model providers, parsing unpredictable outputs into reliable schemas, setting up async processing for long-running tasks, and deploying the whole thing somewhere.&lt;/p&gt;

&lt;p&gt;Custom Workflows eliminates every one of those steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: A 5-Step Visual Builder
&lt;/h2&gt;

&lt;p&gt;The Custom Workflows builder lives right in the SharpAPI Dashboard. It walks you through five straightforward steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Basic Info.&lt;/strong&gt; Give your workflow a name and description, then choose your input mode (JSON for structured data or Form-Data for file uploads).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Parameters.&lt;/strong&gt; Define your input parameters with types, labels, default values, and required/optional flags. The system supports the full range of types you would expect: strings, numbers, booleans, arrays, and objects for JSON mode, plus text and file inputs for Form-Data mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: AI Prompt.&lt;/strong&gt; Write what you want the AI to do, in plain English. No prompt engineering expertise needed. If you are not sure where to start, pick one of the pre-made templates (more on those below) and customize it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Output Schema.&lt;/strong&gt; Define the JSON structure you want back. Or just click one button and let the AI analyze your workflow context to suggest an optimal schema automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Review &amp;amp; Save.&lt;/strong&gt; Preview everything, activate, and grab your endpoint URL. You are live.&lt;/p&gt;

&lt;p&gt;That is the whole process. Five steps, and you have a production API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get: A Real API, Not a Toy
&lt;/h2&gt;

&lt;p&gt;Every saved workflow immediately becomes a live REST endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/v1/custom/{your-workflow-slug}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the same API key authentication (&lt;code&gt;X-API-KEY&lt;/code&gt; header) as every other SharpAPI endpoint. No additional setup or configuration.&lt;/p&gt;

&lt;p&gt;Processing follows the standard SharpAPI async pattern for reliability at scale. You send a POST request and get back a &lt;code&gt;202 Accepted&lt;/code&gt; with a &lt;code&gt;status_url&lt;/code&gt; and &lt;code&gt;job_id&lt;/code&gt;. Poll the status URL until the job completes, then retrieve your structured JSON result matching the exact output schema you defined.&lt;/p&gt;

&lt;p&gt;Every workflow also exposes a self-describing GET endpoint that returns its complete specification: parameter definitions, types, labels, required flags, defaults, output schema, and endpoint URL. This is perfect for auto-generating client code or feeding into other AI systems that need to understand what your API does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type Safety Without the Boilerplate
&lt;/h2&gt;

&lt;p&gt;The parameter system is strict by design. Every input parameter is validated against its declared type before any processing begins. Required parameters must be present. Default values fill in for optional ones. And in JSON mode, unknown parameters are automatically rejected, preventing typos and unexpected inputs from sneaking through.&lt;/p&gt;

&lt;p&gt;This gives you the kind of type safety you would normally need backend code to enforce, except you get it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;p&gt;Here is where it gets practical. Custom Workflows can power a wide range of features across industries:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document Processing.&lt;/strong&gt; Upload invoices, contracts, receipts, or forms and extract structured data at scale. Pair Form-Data input mode with a file parameter and you have a document processing API in minutes. Think insurance claim forms, medical records intake, shipping manifests, purchase orders, or tax documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Analysis.&lt;/strong&gt; Sentiment analysis, content moderation, tone detection, urgency classification. Feed text in, get structured scores and labels out. Great for analyzing customer reviews, social media mentions, support tickets, survey responses, app store feedback, and forum posts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Generation.&lt;/strong&gt; Automated email drafts, product descriptions, social media posts, marketing copy. Consistent, on-brand content delivered via API. Use it for personalized outreach emails, SEO meta descriptions, product listing copy for e-commerce catalogs, newsletter sections, ad variations, or localized marketing content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Enrichment.&lt;/strong&gt; Add AI-powered insights, classifications, and predictions to your existing data pipelines. Plug a Custom Workflow into your ETL process and enrich records as they flow through. Classify CRM leads by intent, tag support conversations by topic, extract skills from resumes, or categorize transactions by spend type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk Assessment.&lt;/strong&gt; Compliance checking, fraud detection signals, policy violation screening. Get structured risk scores through a simple API call. Screen user-generated content for policy violations, flag suspicious transaction patterns, evaluate vendor contracts for non-standard clauses, or check marketing copy against regulatory guidelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer Support Automation.&lt;/strong&gt; Route incoming tickets to the right team, suggest reply templates, extract key details (order numbers, product names, issue categories) from customer messages, or generate first-draft responses for agents to review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E-commerce &amp;amp; Product.&lt;/strong&gt; Automate product categorization and tagging, generate comparison tables, extract specs from manufacturer datasheets, translate listings for international storefronts, or normalize product attributes across multiple suppliers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HR &amp;amp; Recruitment.&lt;/strong&gt; Parse resumes and extract structured candidate profiles, screen cover letters, generate interview question sets tailored to job descriptions, summarize employee feedback surveys, or classify internal knowledge base articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legal &amp;amp; Finance.&lt;/strong&gt; Summarize contract clauses, extract key terms and obligations, flag non-standard language in agreements, categorize expense reports, or generate structured abstracts from regulatory filings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Education &amp;amp; Research.&lt;/strong&gt; Summarize academic papers, extract citations and key findings, generate quiz questions from study materials, classify research topics, or transform lecture notes into structured outlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDKs for Every Stack
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Official SDKs are available for &lt;strong&gt;PHP&lt;/strong&gt;, &lt;strong&gt;Laravel&lt;/strong&gt;, &lt;strong&gt;Python&lt;/strong&gt;, and &lt;strong&gt;JavaScript/TypeScript&lt;/strong&gt;. Browse all Custom Workflow SDK packages on &lt;a href="https://github.com/orgs/sharpapi/repositories?q=custom" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pricing: No Surprises
&lt;/h2&gt;

&lt;p&gt;Custom Workflows use the same credit-based billing system as every other SharpAPI endpoint. There is no extra charge, no separate pricing tier, no hidden fees. If you have credits, you can run workflows.&lt;/p&gt;

&lt;p&gt;The feature is available on all plans, from the free tier up to enterprise. Scale and Enterprise plans include additional support for custom endpoint configuration, dedicated account management, and custom SLAs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30+ pre-built AI endpoints&lt;/strong&gt; already available alongside Custom Workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited custom workflows&lt;/strong&gt; on every plan&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;99.9% API uptime&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SOC 2 Type II certified&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Custom Workflows is live in the SharpAPI Dashboard right now. Log in, click "Create Workflow," and have your first custom AI endpoint running in under a minute.&lt;/p&gt;

&lt;p&gt;Whether you are building a document processing pipeline, adding AI-powered content analysis to your SaaS product, or just prototyping a new feature idea, Custom Workflows gives you the fastest path from concept to production API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to turn your AI prompts into production endpoints?&lt;/strong&gt; Head to &lt;a href="https://sharpapi.com/custom-workflows" rel="noopener noreferrer"&gt;https://sharpapi.com/custom-workflows&lt;/a&gt; and start building.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>workflow</category>
      <category>custom</category>
    </item>
    <item>
      <title>You Just Gave OpenClaw the Keys to Your Entire Digital Life - on a VPS Server You Don't Know How to Secure</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Tue, 24 Feb 2026 15:08:37 +0000</pubDate>
      <link>https://forem.com/makowskid/you-just-gave-openclaw-the-keys-to-your-entire-digital-life-on-a-vps-server-you-dont-know-how-to-36ab</link>
      <guid>https://forem.com/makowskid/you-just-gave-openclaw-the-keys-to-your-entire-digital-life-on-a-vps-server-you-dont-know-how-to-36ab</guid>
      <description>&lt;p&gt;You Put OpenClaw on a VPS. It Has Access to Everything. You Secured None of It. Let's Fix That in 30 Minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This guide was written for the brave souls who saw the OpenClaw hype train, jumped aboard, spun up their first VPS, and then had the terrifying realization that they now need to learn Linux security. You're going to be fine. Probably.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Look, I get it. You saw the hype. You saw the tweets. You saw some guy on Reddit say OpenClaw changed his life, and now you're sitting there at 2 AM with your very first VPS, a fresh install of &lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;, and the sudden realization that you just handed an AI assistant the keys to your email, your calendar, your Google Drive, your private documents, and basically your entire digital soul - all running on a server you've never secured before because, well, this is your first server.&lt;/p&gt;

&lt;p&gt;Now, let's be fair: a fresh Ubuntu installation isn't actually a house with no doors. Ubuntu ships with sensible defaults - no unnecessary open ports, no sketchy services running, SSH with reasonable settings. Credit where credit is due. &lt;strong&gt;But here's the problem:&lt;/strong&gt; you're not running a fresh Ubuntu installation anymore. You're running OpenClaw on top of it. With plugins. And skills. And extensions. And API keys to everything you own. &lt;/p&gt;

&lt;p&gt;Here's a fun fact to help you sleep tonight: every VPS that connects to the internet gets fully port-scanned by automated bots within &lt;strong&gt;10 to 20 minutes&lt;/strong&gt;. Not hours. Not days. &lt;em&gt;Minutes.&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;But with OpenClaw and its ecosystem of plugins exposing additional services and APIs? Now you're &lt;em&gt;interesting.&lt;/em&gt; And on the internet, you do not want to be interesting.&lt;/p&gt;

&lt;p&gt;And look - I'm not here to trash OpenClaw. It's genuinely cool. The AI-assistant-on-your-own-server dream is alive and well. But let's be honest with ourselves for a moment: OpenClaw, plus all of its plugins, skills, and extensions, is not exactly what security researchers would call "airtight." It's more what they'd call "a fun afternoon." The platform is young, moving fast, and the attack surface grows with every skill you install. The real risk isn't Ubuntu's defaults - it's everything you're bolting on top of them.&lt;/p&gt;

&lt;p&gt;So since we can't fix OpenClaw's security overnight, let's make damn sure that the server underneath it is locked down tight, so that even if someone finds a vulnerability in a plugin, they hit a brick wall instead of a buffet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good news?&lt;/strong&gt; I recommend Ubuntu for your VPS, and I'm going to walk you through this whole thing step by step. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Ubuntu?&lt;/strong&gt; Because it has a massive community, a mountain of security tools, and - this is the important part - any non-technical noob can harden it in about 30 minutes with proper guidance. &lt;/p&gt;

&lt;p&gt;Let's go. And please, for the love of everything, &lt;strong&gt;don't close your terminal until I tell you to.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 0 - Non-root user
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before we do anything else&lt;/strong&gt; - if you're still logged in as &lt;code&gt;root&lt;/code&gt; like some kind of digital cowboy, we need to fix that immediately. Root is the god account. It can do anything, break anything, and delete anything, including itself. &lt;/p&gt;

&lt;p&gt;So let's create a proper user and give it sudo powers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser ubuntu
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, because typing your password every time you run &lt;code&gt;sudo&lt;/code&gt; gets old approximately 4 seconds after the first time, let's set up passwordless sudo. Run &lt;code&gt;visudo&lt;/code&gt; and add this line at the very bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ubuntu ALL=(ALL) NOPASSWD:ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point on, you do everything as &lt;code&gt;ubuntu&lt;/code&gt; and use &lt;code&gt;sudo&lt;/code&gt; when you need elevated privileges. Think of &lt;code&gt;root&lt;/code&gt; as the emergency fire axe behind glass - it's there if you need it, but you shouldn't be casually swinging it around on a Tuesday afternoon. &lt;/p&gt;

&lt;p&gt;Now copy your SSH key to the new user (&lt;code&gt;ssh-copy-id&lt;/code&gt; or manually paste it into &lt;code&gt;/home/ubuntu/.ssh/authorized_keys&lt;/code&gt;), log in as &lt;code&gt;ubuntu&lt;/code&gt; in a new terminal to make sure it works, and &lt;strong&gt;never log in as root again.&lt;/strong&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Your Timezone
&lt;/h2&gt;

&lt;p&gt;Before we do anything dramatic, let's make sure your server knows what time it is. This sounds trivial, but accurate timestamps in your logs are the difference between "I can see exactly when someone broke in" and "something happened... at some point...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg-reconfigure tzdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick your timezone from the menu. It's interactive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Update Everything
&lt;/h2&gt;

&lt;p&gt;Your server shipped with software that was already outdated by the time you clicked "Deploy." Let's fix that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This updates all your packages to their latest versions, patching known vulnerabilities. Think of it as putting on pants before leaving the house. Bare minimum.&lt;/p&gt;

&lt;p&gt;Now, because we both know you're going to forget to do this regularly (I know you, and I love you, but I know you), let's set up &lt;strong&gt;automatic security updates&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unattended-upgrades &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;--priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;low unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configures your server to automatically install critical security patches without you having to remember. It's like hiring a tiny robot butler whose only job is to lock the doors you keep leaving open.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Set Up Ubuntu Pro
&lt;/h2&gt;

&lt;p&gt;Ubuntu Pro gives you expanded security maintenance, kernel live patching, and compliance tools. And here's the kicker - &lt;strong&gt;it's free for up to 5 machines.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://ubuntu.com/pro" rel="noopener noreferrer"&gt;ubuntu.com/pro&lt;/a&gt;, grab your token, and attach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pro attach YOUR_TOKEN_HERE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extends your security coverage and covers thousands of additional packages. It's like getting the extended warranty, except it actually does something.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Lock Down SSH
&lt;/h2&gt;

&lt;p&gt;SSH is how you talk to your server. It's also how &lt;em&gt;everyone else&lt;/em&gt; tries to talk to your server. By default, it's running on port 22, which is the first port every bot on the internet checks. That's like hiding your house key under the doormat - the one place literally everyone looks first.&lt;/p&gt;

&lt;p&gt;Edit your SSH config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what your config should look like. I'll explain each line, because I respect you and your journey:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Port 55222                            &lt;span class="c"&gt;# Move SSH off the default port. Not foolproof, but stops 90% of really lazy scanners.&lt;/span&gt;
LoginGraceTime 2m                     &lt;span class="c"&gt;# You get 2 minutes to authenticate. After that, goodbye.&lt;/span&gt;
PermitRootLogin no                    &lt;span class="c"&gt;# NOBODY logs in as root. Ever. Not even you. Especially you.&lt;/span&gt;
MaxAuthTries 5                        &lt;span class="c"&gt;# 5 wrong passwords and we hang up on you. Rude? Maybe. Secure? Yes.&lt;/span&gt;
PasswordAuthentication no             &lt;span class="c"&gt;# No passwords. Period. Keys only. Passwords are the cargo shorts of security.&lt;/span&gt;
PermitEmptyPasswords no               &lt;span class="c"&gt;# Just... no. Come on.&lt;/span&gt;
AllowUsers ubuntu                     &lt;span class="c"&gt;# Only the 'ubuntu' user can log in. Everyone else can go home.&lt;/span&gt;
X11Forwarding no                      &lt;span class="c"&gt;# No graphical forwarding. This is a server, not a gaming PC.&lt;/span&gt;
PermitUserEnvironment no              &lt;span class="c"&gt;# Don't let users set environment variables through SSH. Trust issues? You bet.&lt;/span&gt;
AllowAgentForwarding no               &lt;span class="c"&gt;# No SSH agent forwarding. Reduces the risk of key theft.&lt;/span&gt;
AllowTcpForwarding no                 &lt;span class="c"&gt;# No TCP tunneling through your server. It's not a VPN.&lt;/span&gt;
PermitTunnel no                       &lt;span class="c"&gt;# Same energy as above. No tunnels.&lt;/span&gt;
KbdInteractiveAuthentication &lt;span class="nb"&gt;yes&lt;/span&gt;      &lt;span class="c"&gt;# Needed for 2FA (we'll get there, be patient).&lt;/span&gt;
ChallengeResponseAuthentication &lt;span class="nb"&gt;yes&lt;/span&gt;   &lt;span class="c"&gt;# Also needed for 2FA. The dynamic duo.&lt;/span&gt;
AuthenticationMethods publickey,keyboard-interactive  &lt;span class="c"&gt;# Key first, then 2FA code. Belt AND suspenders.&lt;/span&gt;
UsePAM &lt;span class="nb"&gt;yes&lt;/span&gt;                            &lt;span class="c"&gt;# Use PAM for authentication. Required for Google Authenticator.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key takeaways:&lt;/strong&gt; We moved the SSH port (so bots can't find it easily), disabled root login (so even if someone gets in, they're not god), killed password authentication (keys only, like a VIP club), and set up the groundwork for two-factor authentication.&lt;/p&gt;

&lt;p&gt;Now reload SSH so it actually pays attention to what we just told it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl reload ssh.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sshd -t&lt;/code&gt; part tests your config first. If there's a typo, it'll tell you before you lock yourself out. Because locking yourself out of your own server is a very special kind of pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ CRITICAL: Do NOT close your current terminal session yet. Open a NEW terminal and test that you can still connect with your new settings before closing anything.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; /path/to/your-key &lt;span class="nt"&gt;-p&lt;/span&gt; 55222 ubuntu@your-server-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Install Fail2Ban
&lt;/h2&gt;

&lt;p&gt;Fail2Ban watches your authentication logs and automatically bans IP addresses that fail to log in too many times. It's basically a nightclub bouncer for your server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out of the box, Fail2Ban will monitor SSH and ban anyone who fails authentication repeatedly. You can customize the jail settings later, but the defaults are already pretty solid for keeping the riff-raff out.&lt;/p&gt;

&lt;p&gt;Think of it this way: Step 4 made it harder to get in. Step 5 makes sure that anyone who keeps trying gets permanently shown the door.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Set Up Two-Factor Authentication
&lt;/h2&gt;

&lt;p&gt;This is the big one. This is where we go from "pretty secure" to "okay, now I can actually sleep at night."&lt;/p&gt;

&lt;p&gt;Two-factor authentication means that even if someone somehow gets your SSH key (it happens - laptops get stolen, backups get leaked, your cat walks across your keyboard and emails it to someone), they STILL can't get in without the 6-digit code from your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install Google Authenticator:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libpam-google-authenticator &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Switch to your ubuntu user and run the setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;su - ubuntu
google-authenticator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll ask you some questions. Here are the correct answers (you're welcome):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time-based tokens?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Time-based is more secure than counter-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update .google_authenticator file?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This saves your config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disallow multiple uses?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each code works exactly once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Increase time window?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;n&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keep it tight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;y&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slows down brute-force attempts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It'll show you a QR code. Scan it with Google Authenticator, Authy, or whatever TOTP app you prefer. &lt;strong&gt;And for the love of all that is holy, SAVE THE EMERGENCY SCRATCH CODES.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Put them in a password manager (important!). They're your "break glass in case of emergency" codes if you lose your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure PAM&lt;/strong&gt; (this tells SSH to actually &lt;em&gt;use&lt;/em&gt; the authenticator):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/pam.d/sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this line &lt;strong&gt;at the very top:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth required pam_google_authenticator.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;comment out&lt;/strong&gt; this line (to prevent a double password prompt, which is annoying and unnecessary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# @include common-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make sure your SSH config has these lines set correctly&lt;/strong&gt; (most of them should already be right from Step 4):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;KbdInteractiveAuthentication &lt;span class="nb"&gt;yes
&lt;/span&gt;ChallengeResponseAuthentication &lt;span class="nb"&gt;yes
&lt;/span&gt;AuthenticationMethods publickey,keyboard-interactive
UsePAM &lt;span class="nb"&gt;yes
&lt;/span&gt;PasswordAuthentication no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test the config and restart SSH:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now test in a NEW terminal&lt;/strong&gt; (seriously, keep your current session open - are you sensing a pattern here?):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; /path/to/your-key &lt;span class="nt"&gt;-p&lt;/span&gt; 55222 ubuntu@your-server-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your login flow should now be: &lt;strong&gt;SSH key → TOTP verification code → you're in.&lt;/strong&gt; No password involved. Just your key and your phone. It's like a secret handshake, but actually secure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Monitor Your Logs
&lt;/h2&gt;

&lt;p&gt;Congratulations, your server is now significantly more secure than it was 20 minutes ago. But you need to actually check on things occasionally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your SSH authentication logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /var/log/auth.log | &lt;span class="nb"&gt;grep &lt;/span&gt;sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For live monitoring&lt;/strong&gt; (great for watching scans/attacks happen in real-time, which is weirdly entertaining):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/auth.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeated failed login attempts&lt;/strong&gt; - Fail2Ban should catch these, but check anyway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login attempts from unfamiliar IP addresses&lt;/strong&gt; - If you see IPs you don't recognize, investigate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown usernames&lt;/strong&gt; - If someone's trying to log in as "admin" or "test," that's a bot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Successful logins at weird hours&lt;/strong&gt; - If you logged in at 3 AM and you were asleep at 3 AM, we have a problem&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bonus Round: For the Ambitious
&lt;/h2&gt;

&lt;p&gt;If you've made it this far and you're feeling confident (possibly &lt;em&gt;too&lt;/em&gt; confident, but I respect the energy), here are two next-level options:&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailscale
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; creates a private mesh VPN between your devices. Once set up, you can access your server through a private network that isn't exposed to the public internet at all. It's like having a secret tunnel to your server that only you know about. The setup is shockingly simple for something this powerful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Tunnel
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt; lets you expose your OpenClaw instance to the internet without opening ANY inbound ports on your server. Zero. None. The server reaches out to Cloudflare, and Cloudflare handles all incoming traffic. It's like having a P.O. Box for your server - people can send you mail, but they don't know where you live.&lt;/p&gt;

&lt;p&gt;Both of these are excellent options if you want to take your security from "solid" to "paranoid, but like, in a healthy way."&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're running OpenClaw on a VPS, you've put your most private digital life on a server connected to the open internet. Your emails. Your calendar. Your documents. Your credentials. All of it sitting there, protected by whatever security you bothered to set up.&lt;/p&gt;

&lt;p&gt;Is your server now impenetrable? No. Nothing is impenetrable. But you've gone from being a soft, delicious target to being the server that's maybe not worth the effort today when there are millions of easier ones to hit. &lt;/p&gt;

&lt;p&gt;You've got this. Probably. I believe in you. Mostly.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>How We Automated Invoice Processing for Our Clients</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Mon, 23 Feb 2026 15:11:05 +0000</pubDate>
      <link>https://forem.com/makowskid/how-we-automated-invoice-processing-for-our-clients-1jae</link>
      <guid>https://forem.com/makowskid/how-we-automated-invoice-processing-for-our-clients-1jae</guid>
      <description>&lt;p&gt;If you've ever watched a finance team manually key in invoice data from a stack of PDFs, phone photos, and scanned documents, you know the look. It's somewhere between existential dread and quiet resignation. We've seen it across multiple client engagements - fintech, retail, logistics - and the story is always the same. Smart people doing dumb work because the tools they have can't handle the chaos of real invoices.&lt;/p&gt;

&lt;p&gt;So we fixed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem We Kept Running Into
&lt;/h3&gt;

&lt;p&gt;Across our client projects, invoice processing kept showing up as a bottleneck. Not because the concept is hard, but because the reality is messy. Invoices arrive as pristine PDFs sometimes, sure. But more often they show up as phone photos taken at weird angles with bad lighting, scanned TIFFs from office equipment that should have been retired a decade ago, or flattened PDFs where the text is baked into images and not selectable.&lt;/p&gt;

&lt;p&gt;Every off-the-shelf OCR solution we tried would work great on the clean files and fall apart on everything else. And "everything else" is most of what shows up in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Built
&lt;/h3&gt;

&lt;p&gt;We designed a multi-step AI pipeline that approaches invoice parsing the way a human would -- if that human could process thousands of documents per hour without making mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: ML-powered OCR&lt;/strong&gt; extracts the raw content from the file, regardless of format or quality. This isn't your basic Tesseract setup. It's trained to handle the messy stuff -- crumpled paper, shadows, skewed scans, multi-page documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: AI processing rounds&lt;/strong&gt; take that raw extraction and run it through multiple validation and structuring passes. This is where the magic happens. The AI identifies what's a line item vs. a header vs. a tax calculation, maps seller and buyer information correctly, and resolves ambiguities that would trip up simpler systems.&lt;/p&gt;

&lt;p&gt;The result is a clean, structured JSON object with 100+ data fields covering everything you'd ever need from an invoice: document metadata, seller/buyer details with full addresses, financial breakdowns with tax calculations, individual line items, payment terms, logistics info, e-invoice metadata, and reference numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Handles
&lt;/h3&gt;

&lt;p&gt;We built this for the real world, not demo day. That means it works with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 file formats&lt;/strong&gt;: PDF, DOC, DOCX, JPG, JPEG, PNG, TIFF, TIF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messy phone photos&lt;/strong&gt;: crumpled paper, bad lighting, weird angles -- the kind of stuff your field teams actually send in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanned invoices and flattened PDFs&lt;/strong&gt;: where the content is images, not selectable text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-page invoices&lt;/strong&gt;: processes the full document, not just the first page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-currency invoices&lt;/strong&gt;: extracts currency info, exchange rates, VAT/GST/SST IDs, and country-specific tax details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80+ languages&lt;/strong&gt;: from English to Japanese to Arabic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Our Clients Use It
&lt;/h3&gt;

&lt;p&gt;Once we had this working, the use cases multiplied fast:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounts payable automation&lt;/strong&gt; - the obvious one. Invoices come in, structured data comes out, AP workflow picks it up. Processing time goes from minutes per invoice to seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ERP and accounting integration&lt;/strong&gt; - clean, consistent data piped straight into QuickBooks, Xero, SAP, NetSuite, or whatever the client is running. No more "which field maps to what" conversations with the finance team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spend analytics&lt;/strong&gt; - when every invoice is structured data, building dashboards and running analyses across your entire vendor base becomes trivial. Clients use this to spot trends, negotiate better terms, and flag cost-saving opportunities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fraud detection&lt;/strong&gt; - cross-referencing parsed invoice data against purchase orders and contracts to automatically flag discrepancies before payments go out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expense management and compliance&lt;/strong&gt; - automating expense report validation against company policies and maintaining audit trails without human intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  We Made It Available to Everyone
&lt;/h3&gt;

&lt;p&gt;Rather than keep this locked inside client projects, we productized the entire pipeline as part of &lt;a href="https://sharpapi.com/" rel="noopener noreferrer"&gt;SharpAPI&lt;/a&gt; -- our AI workflow automation API. The Invoice Parsing endpoint is live and available on all plans.&lt;/p&gt;

&lt;p&gt;Integration follows the same simple async pattern as all SharpAPI endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s1"&gt;'https://sharpapi.com/api/v1/invoice/parse'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--form&lt;/span&gt; &lt;span class="s1"&gt;'file=@"invoice.pdf"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;POST your file, get a job ID, poll for results. That's it.&lt;/p&gt;

&lt;p&gt;And because we know developers hate writing boilerplate HTTP code, there are ready-to-go SDK packages for &lt;strong&gt;PHP, Laravel, Node.js, Python, and .NET&lt;/strong&gt; on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/orgs/sharpapi/repositories?q=invoice" rel="noopener noreferrer"&gt;Browse Invoice Parsing SDKs on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters for Your Business
&lt;/h3&gt;

&lt;p&gt;Manual invoice processing costs businesses an average of $15-$40 per invoice when you factor in labor, errors, and delays. At scale, that adds up fast. If your team processes a few hundred invoices a month, you're looking at significant savings just by automating the extraction step -- not to mention the reduction in errors that cause payment disputes, compliance issues, and vendor relationship headaches.&lt;/p&gt;

&lt;p&gt;We built this because we kept seeing the same problem across different industries and different clients. If your business deals with invoices at any meaningful volume, this is the kind of automation that pays for itself in the first week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Started
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product page&lt;/strong&gt;: &lt;a href="https://sharpapi.com/en/catalog/ai/accounting-finance/invoice-parser" rel="noopener noreferrer"&gt;SharpAPI Invoice Parser&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detailed blog post with full response examples&lt;/strong&gt;: &lt;a href="https://sharpapi.com/en/blog/post/invoice-parsing-api-extract-structured-data-from-any-invoice" rel="noopener noreferrer"&gt;Invoice Parsing API - Extract Structured Data from Any Invoice&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs on SOC 2 Type II certified infrastructure, so your invoice data is handled with the same security standards we maintain across all our client work.&lt;/p&gt;

&lt;p&gt;Got a specific invoice processing challenge? &lt;a href="https://a2zweb.co/contact" rel="noopener noreferrer"&gt;Talk to us&lt;/a&gt; - whether you need the API integrated into your existing systems or a full custom workflow built around it, that's literally what we do.&lt;/p&gt;

</description>
      <category>finance</category>
      <category>api</category>
      <category>parsing</category>
    </item>
    <item>
      <title>Invoice Parsing API - Extract Structured Data from Any Invoice</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Sun, 22 Feb 2026 08:41:16 +0000</pubDate>
      <link>https://forem.com/makowskid/invoice-parsing-api-extract-structured-data-from-any-invoice-3ph7</link>
      <guid>https://forem.com/makowskid/invoice-parsing-api-extract-structured-data-from-any-invoice-3ph7</guid>
      <description>&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@karepesinde?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Cht Gsml&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/desk-with-papers-glasses-calculator-and-office-supplies-sW02MHv37yk?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Upload an invoice in any of 8 supported formats (DOC, DOCX, PDF, JPG, JPEG, PNG, TIFF, TIF), and the API extracts and structures the entire document into a comprehensive data object. We're talking seller and buyer details, full financial breakdowns with tax calculations, individual line items, payment terms, logistics info, e-invoice metadata, and reference numbers — all neatly organized and ready for your systems.&lt;/p&gt;

&lt;p&gt;And yes, it handles scanned invoices and flattened PDFs where the content is images rather than selectable text. Because in the real world, invoices aren't always pixel-perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Automated Accounts Payable
&lt;/h3&gt;

&lt;p&gt;Feed invoices directly into your AP workflow. The API extracts vendor details, amounts, due dates, and line items — eliminating manual data entry and reducing processing time from minutes per invoice to seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  ERP &amp;amp; Accounting System Integration
&lt;/h3&gt;

&lt;p&gt;Pipe structured invoice data straight into your ERP (SAP, Oracle, NetSuite, QuickBooks, Xero — you name it). No more reconciliation headaches, no more "which field maps to what" conversations. The data comes out clean and consistent every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spend Analytics &amp;amp; Vendor Management
&lt;/h3&gt;

&lt;p&gt;Aggregate invoice data across your entire vendor base to spot spending trends, negotiate better terms, and identify cost-saving opportunities. When every invoice is structured data, building dashboards and running analyses becomes trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expense Management &amp;amp; Compliance
&lt;/h3&gt;

&lt;p&gt;Automate expense report validation by parsing receipts and invoices. Cross-reference extracted data against company policies, flag anomalies, and maintain a clean audit trail — all without human intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Currency &amp;amp; Cross-Border Operations
&lt;/h3&gt;

&lt;p&gt;The API extracts currency information, exchange rates, VAT/GST/SST IDs, and country-specific tax details. Perfect for businesses operating across borders that need to handle invoices in multiple currencies and comply with regional tax requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invoice Verification &amp;amp; Fraud Detection
&lt;/h3&gt;

&lt;p&gt;Compare extracted invoice data against purchase orders, delivery receipts, and contracts. Automatically flag discrepancies in quantities, pricing, or vendor information before payments go out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Like all SharpAPI endpoints, Invoice Parsing follows a simple two-step async pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Submit your invoice file via a POST request to &lt;code&gt;https://sharpapi.com/api/v1/finance/parse_invoice&lt;/code&gt;. You'll receive a job ID and status URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Poll the status URL until the job completes. The result contains the full structured invoice data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s1"&gt;'https://sharpapi.com/api/v1/finance/parse_invoice'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--form&lt;/span&gt; &lt;span class="s1"&gt;'file=@"invoice.pdf"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes over 100 individual data fields organized into logical sections: document metadata, invoice details, references, e-invoice data, seller/buyer information, financials with tax breakdowns, line items, payment terms, and logistics data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Manual invoice processing costs businesses an average of $15–$40 per invoice when you factor in labor, errors, and delays. At scale, that adds up fast. By automating the extraction step, you're not just saving time — you're eliminating the single biggest source of AP errors and freeing your team to focus on work that actually requires human judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;The Invoice Parsing endpoint is available now on all SharpAPI plans. Check out the details of &lt;a href="https://sharpapi.com/en/catalog/ai/accounting-finance/invoice-parser" rel="noopener noreferrer"&gt;Invoice Parsing&lt;/a&gt; for detailed request/response schemas, supported languages, and integration guides.&lt;/p&gt;

&lt;p&gt;Your invoices aren't going to parse themselves. Well, actually — now they will.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Integrate faster with SharpAPI SDKs.&lt;/strong&gt;&lt;br&gt;
Ready-to-use client packages for PHP, Laravel, Node.js, Python, Flutter, and .NET are available on GitHub.&lt;br&gt;
&lt;a href="https://github.com/orgs/sharpapi/repositories?q=invoice" rel="noopener noreferrer"&gt;Browse Invoice Parsing SDKs →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>invoice</category>
      <category>parser</category>
      <category>aiparsing</category>
      <category>invoiceparsing</category>
    </item>
    <item>
      <title>Your Penetration Test Report Just Landed. Read This Before You Panic.</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Tue, 17 Feb 2026 10:29:04 +0000</pubDate>
      <link>https://forem.com/makowskid/your-penetration-test-report-just-landed-read-this-before-you-panic-1a6m</link>
      <guid>https://forem.com/makowskid/your-penetration-test-report-just-landed-read-this-before-you-panic-1a6m</guid>
      <description>&lt;p&gt;I've watched the same movie play out too many times: a management team receives a penetration testing report, sees a wall of findings with scary-sounding names, and immediately assumes their platform is on fire.&lt;/p&gt;

&lt;p&gt;It's not on fire. It's almost never on fire.&lt;/p&gt;

&lt;p&gt;But the report sure makes it &lt;em&gt;look&lt;/em&gt; like it is. And that's the problem I keep running into - not with the platforms, but with how people read these reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Translation Problem
&lt;/h2&gt;

&lt;p&gt;When you're a CTO or an external CTO-as-a-Service advisor, part of the job is translating between the world of security tooling and the world of business decision-making. These two worlds speak very different languages. Security tools speak in volumes of automated findings. Business leaders speak in risk, cost, and "should I be worried right now?"&lt;/p&gt;

&lt;p&gt;That gap is where the panic starts.&lt;/p&gt;

&lt;p&gt;Over the years I've learned to get ahead of it. Before every VAPT (Vulnerability Assessment and Penetration Testing) cycle, I walk my clients through what to expect from the results - what the findings actually mean, what's noise, and what deserves real attention. It's part education, part expectation management, and part gentle reminder that a 200-page PDF full of findings does not mean the sky is falling. Sometimes it just means the scanner was very thorough and a bit too enthusiastic.&lt;/p&gt;

&lt;p&gt;The goal is simple: give non-technical stakeholders the mental framework to read a VAPT report without losing sleep. Because the report is only half of the story. The other half - the part that actually matters - is interpreting those findings in the context of &lt;em&gt;your&lt;/em&gt; platform, &lt;em&gt;your&lt;/em&gt; architecture, and &lt;em&gt;your&lt;/em&gt; specific business requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a VAPT Report (For Humans)
&lt;/h2&gt;

&lt;p&gt;Here's what most people don't realize about penetration testing: the raw output of any engagement is never the final verdict on your security. It's a starting point for analysis.&lt;/p&gt;

&lt;p&gt;VAPT teams rely on automated scanning tools to generate their initial findings. These tools are designed to cast an absurdly wide net. They flag anything that &lt;em&gt;could&lt;/em&gt; theoretically be a concern. And I mean &lt;em&gt;anything&lt;/em&gt;. Your OAuth integration with Google? Flagged. Your CDN serving static assets from a different domain? Flagged. A cookie that JavaScript can access because your entire framework was literally designed that way? You better believe that's flagged. Any open port on the server, even port 80 or 443? Yup, also flagged.&lt;/p&gt;

&lt;p&gt;This isn't a flaw in the process. It's how the process works. The tools are doing their job. The question is what happens next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quality Gap Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;And here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;Not all VAPT teams are created equal. In fact, there's a pretty dramatic quality spectrum, and where your team falls on it determines whether you receive a useful, contextualised security assessment or a PDF-shaped anxiety attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget-oriented teams&lt;/strong&gt; tend to optimise for volume. They run the tools, collect the output, and forward everything to the client with minimal filtering. The result? A report with dozens - sometimes hundreds - of findings, many of which are informational noise or outright false positives. It looks impressive. It fills a lot of pages. But it creates exactly the kind of alarm that derails productive conversations about actual security.&lt;/p&gt;

&lt;p&gt;I've seen reports where the same exact finding was listed separately for every URL on the platform. Same issue, same root cause, same "vulnerability" - just presented 147 times to make the PDF thicker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More experienced teams&lt;/strong&gt; - and yes, they typically cost more - invest significant effort in triaging their tool output before presenting it. They separate signal from noise. They tell you what actually matters and why. They cross-reference previous engagement results instead of re-investigating known behaviours from scratch. Their reports are shorter, more accurate, and infinitely more useful. You're paying for judgment, not just scanning hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Severity Levels: A Quick Decoder Ring
&lt;/h2&gt;

&lt;p&gt;Every VAPT report categorises findings by severity. Here's the practical translation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical and High&lt;/strong&gt; - Stop what you're doing and fix these. These represent real, exploitable vulnerabilities. In a well-maintained platform with regular dependency updates, strong authentication, and proper encryption, these should be rare. If your report is full of them, you have a genuine problem. If it has zero, congratulations - that's the goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium and Low&lt;/strong&gt; - Read these with a calm mind. They often represent theoretical risks, hardening suggestions, or configuration preferences. Many are informational. Think of them as a security consultant saying "you &lt;em&gt;could&lt;/em&gt; also do this" rather than "your house is currently on fire."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Informational&lt;/strong&gt; - These are diagnostic notes. They describe how your platform behaves. They don't indicate risk. You can acknowledge them and move on.&lt;/p&gt;

&lt;p&gt;The number of findings in a report tells you almost nothing about how secure your platform is. A report with 150 findings and zero criticals is a dramatically better result than one with 5 findings and 2 criticals.&lt;/p&gt;

&lt;h2&gt;
  
  
  False Positives: The Uninvited Guests
&lt;/h2&gt;

&lt;p&gt;Every - and I mean &lt;em&gt;every&lt;/em&gt; - VAPT engagement produces false positives. These are findings that automated tools flag as potential issues but which, upon analysis, turn out to be expected framework behaviours, design decisions, or artefacts of the cloud infrastructure itself.&lt;/p&gt;

&lt;p&gt;In a recent engagement, we documented over 20 false positives across two reports. The cloud provider's own security infrastructure was triggering alerts during the scan - the scanning tools were essentially detecting the host's defence systems and reporting them as application vulnerabilities. That's like a home inspector flagging your alarm system as a security risk. Technically, something happened. Practically, it's the opposite of a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Is Everything
&lt;/h2&gt;

&lt;p&gt;If there's one thing I want people to take away from this, it's this: &lt;strong&gt;a VAPT report must always be read in the context of the specific platform it was conducted against.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security is not a one-size-fits-all discipline. A finding that represents a genuine vulnerability on one platform could be an intentional design decision on another. Session tokens in URLs? Alarming - unless they're part of a standard OAuth handshake with a provider like Google or Twitter, in which case they're temporary, scoped, and exactly where they're supposed to be. Cross-domain script includes? Suspicious - unless they're loading Google's reCAPTCHA or your SSO integration, in which case they're essential.&lt;/p&gt;

&lt;p&gt;The report is half of the truth. The contextual analysis is the other half. Without both, you're making decisions based on incomplete information - and in my experience, those decisions tend to lean toward unnecessary panic and wasted remediation effort.&lt;/p&gt;

&lt;p&gt;If you have a VAPT cycle coming up, prepare your stakeholders before the report lands. It'll save you a week of damage-control conversations that didn't need to happen.&lt;/p&gt;

</description>
      <category>pentest</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CVMatchScore.com: A Real-World Showcase of Our Resume Match API</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Fri, 09 Jan 2026 12:17:51 +0000</pubDate>
      <link>https://forem.com/makowskid/cvmatchscorecom-a-real-world-showcase-of-our-resume-match-api-1gio</link>
      <guid>https://forem.com/makowskid/cvmatchscorecom-a-real-world-showcase-of-our-resume-match-api-1gio</guid>
      <description>&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@helloimnik?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Nik&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/black-pencil-on-paper-3xNn1zGvBwY?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We just launched a small thing that accidentally became useful.&lt;/p&gt;

&lt;p&gt;It’s called &lt;strong&gt;CV Match Score&lt;/strong&gt;:&lt;br&gt;
&lt;a href="https://cvmatchscore.com" rel="noopener noreferrer"&gt;https://cvmatchscore.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The backstory is simple. I kept getting the same question from people building HR tools and workflows:&lt;/p&gt;

&lt;p&gt;“Cool endpoint. But what does it actually look like when a real human uses it?”&lt;/p&gt;

&lt;p&gt;Fair. JSON is charming, but it doesn’t exactly spark joy.&lt;/p&gt;

&lt;p&gt;So we built a tiny, fully functional product that showcases our SharpAPI endpoint in the wild, under real conditions, with real inputs. No screenshots. No “imagine it works like this.” It just works.&lt;/p&gt;

&lt;p&gt;Behind the scenes, CV Match Score uses this SharpAPI capability:&lt;br&gt;
&lt;a href="https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score" rel="noopener noreferrer"&gt;https://sharpapi.com/en/catalog/ai/hr-tech/resume-cv-job-match-score&lt;/a&gt;&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%2Fsharpapi.com%2Fstorage%2F3756%2Fconversions%2FChatGPT-Image-1-maj-2025%2C-13_51_39-full_size.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%2Fsharpapi.com%2Fstorage%2F3756%2Fconversions%2FChatGPT-Image-1-maj-2025%2C-13_51_39-full_size.jpg" alt="Resume Matching" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You upload a CV.&lt;/li&gt;
&lt;li&gt;You paste a job description.&lt;/li&gt;
&lt;li&gt;You get a structured match score with explanations that make sense to humans, not just to machines having a meeting about keywords.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why we built it (beyond our obvious addiction to shipping):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation tells you what an API returns.&lt;/li&gt;
&lt;li&gt;A working tool shows you what users &lt;em&gt;feel&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;And that’s the difference between “nice endpoint” and “this can be a feature inside my product next week.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HR tech products&lt;/li&gt;
&lt;li&gt;ATS features&lt;/li&gt;
&lt;li&gt;internal screening workflows&lt;/li&gt;
&lt;li&gt;recruitment automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tool is basically a fast, honest preview of what the endpoint enables when you put a UI in front of it.&lt;/p&gt;

&lt;p&gt;Try it, break it, tell us what’s missing!&lt;/p&gt;

</description>
      <category>hrtech</category>
      <category>cvparsing</category>
      <category>hrapi</category>
      <category>airesumeparser</category>
    </item>
    <item>
      <title>Fixing Spatie's Laravel ResponseCache to Respect Accept-Language</title>
      <dc:creator>Dawid Makowski</dc:creator>
      <pubDate>Thu, 21 Aug 2025 09:18:38 +0000</pubDate>
      <link>https://forem.com/makowskid/fixing-spaties-laravel-responsecache-to-respect-accept-language-17i6</link>
      <guid>https://forem.com/makowskid/fixing-spaties-laravel-responsecache-to-respect-accept-language-17i6</guid>
      <description>&lt;p&gt;I’ll be honest. Few things are more frustrating than solving a problem by reaching for a great package… only to realize the problem is still there. That was me last week, yelling at my API like it had just spoiled the finale of a Netflix show.&lt;/p&gt;

&lt;p&gt;I was using &lt;a href="https://github.com/spatie/laravel-responsecache" rel="noopener noreferrer"&gt;Spatie’s Laravel ResponseCache&lt;/a&gt; package. It’s rock solid, it works out of the box, and it’s built by people I trust. But here’s the kicker: I turned it on in production and suddenly my multilingual API was speaking one language only. The first request cached in English. Every Arabic request after that? Still English.&lt;/p&gt;

&lt;p&gt;So here’s what it took to fix it. I hope it will help some lost soul on the internet one day.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@douglasamarelo?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Douglas Lopes&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-laptop-computer-sitting-on-top-of-a-wooden-desk-ehyV_XOZ4iA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Spatie’s ResponseCache builds cache keys from the host, normalized URI, HTTP method, and a suffix (usually the user ID). That works fine most of the time, but it completely ignores request headers like &lt;code&gt;Accept-Language&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First request with &lt;code&gt;Accept-Language: en&lt;/code&gt; → response cached in English&lt;/li&gt;
&lt;li&gt;Second request with &lt;code&gt;Accept-Language: ar&lt;/code&gt; → still gets the English version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My API became linguistically challenged.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Middleware That Cares About Language
&lt;/h2&gt;

&lt;p&gt;Spatie lets you influence the cache key by setting a per-request attribute called &lt;code&gt;responsecache.cacheNameSuffix&lt;/code&gt;. All we have to do is populate it with something that changes when the language changes.&lt;/p&gt;

&lt;p&gt;Here’s the middleware I ended up shipping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetResponseCacheSuffixFromLanguage&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Take the first language from the Accept-Language header&lt;/span&gt;
        &lt;span class="nv"&gt;$accept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Accept-Language'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lower&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;value&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Add user id if you want to separate caches per user&lt;/span&gt;
        &lt;span class="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auth&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;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$accept&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="nv"&gt;$suffix&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'responsecache.cacheNameSuffix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$suffix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Optional: add a debug header so you can see what suffix is being used&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&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="nv"&gt;$suffix&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-ResponseCache-Suffix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$suffix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Registering the Middleware
&lt;/h2&gt;

&lt;p&gt;In Laravel 12, add it globally in &lt;code&gt;bootstrap/app.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Middleware\SetResponseCacheSuffixFromLanguage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Configuration\Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePath&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&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;withMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Middleware&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SetResponseCacheSuffixFromLanguage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the cache key varies by &lt;code&gt;Accept-Language&lt;/code&gt; and you no longer serve Arabic users the English version of your API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding the Vary Header
&lt;/h2&gt;

&lt;p&gt;If you’re running behind AWS ALB or a CDN, make sure your responses include &lt;code&gt;Vary: Accept-Language&lt;/code&gt;. Otherwise proxies and browsers might serve the wrong cached version.&lt;/p&gt;

&lt;p&gt;Just drop this into the middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Vary'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Vary'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;', Accept-Language'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Don’t Forget to Clear Old Cache
&lt;/h2&gt;

&lt;p&gt;The existing cache entries are language-agnostic, so clear them once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan responsecache:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How to Prove It Works
&lt;/h2&gt;

&lt;p&gt;Run these two curls and check the header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept-Language: en'&lt;/span&gt; https://api.example.com/v1/pages &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;X-ResponseCache-Suffix
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept-Language: ar'&lt;/span&gt; https://api.example.com/v1/pages &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;X-ResponseCache-Suffix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;en&lt;/code&gt; vs &lt;code&gt;ar&lt;/code&gt;. That’s your proof the cache is now language-aware.&lt;/p&gt;

&lt;p&gt;Will be chasing Spatie's team to add this to the package as well, fingers crossed!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://dawidmakowski.com/en/" rel="noopener noreferrer"&gt;Check my personal blog&lt;/a&gt; for more tech-related content.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>laravel</category>
      <category>spatie</category>
    </item>
  </channel>
</rss>
