<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Htl</title>
    <description>The latest articles on DEV Community by Htl (@htl-core).</description>
    <link>https://dev.to/htl-core</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4117424%2F65555c62-36d6-445c-88bf-277e51a64431.jpg</url>
      <title>DEV Community: Htl</title>
      <link>https://dev.to/htl-core</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/htl-core"/>
    <language>en</language>
    <item>
      <title>What Web Bot Auth deliberately leaves out</title>
      <dc:creator>Htl</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:38:27 +0000</pubDate>
      <link>https://dev.to/htl-core/what-web-bot-auth-deliberately-leaves-out-4gka</link>
      <guid>https://dev.to/htl-core/what-web-bot-auth-deliberately-leaves-out-4gka</guid>
      <description>&lt;p&gt;Your API doesn't need to know who's calling. It needs to know if a human is behind the call.&lt;/p&gt;

&lt;p&gt;The IETF Web Bot Auth working group just published its charter draft. I read it twice, because one line in the scope section wouldn't let me move past it: end-user authentication is explicitly out of scope.&lt;/p&gt;

&lt;p&gt;That's not an oversight. It's a design choice, and I think it's the right one. Web Bot Auth answers which agent is calling. Cloudflare and AWS already ship cryptographic proof that a request came from ChatGPT, Operator, or something like it. That's useful. It's necessary, even.&lt;/p&gt;

&lt;p&gt;But it leaves a gap. And the gap gets wider every month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;An agent can be legitimate and still not represent a consenting human behind it. A crawler can carry a valid signature and still burn through your compute budget. An authenticated ChatGPT request isn't the same thing as a person typing something on their phone at 9pm.&lt;/p&gt;

&lt;p&gt;Web Bot Auth solves identity of agent. Nobody's solved identity of user present, not in a way that's frictionless. The current answers are all some flavor of CAPTCHA or KYC.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Turnstile, hCaptcha, reCAPTCHA: a widget that interrupts the session, every time.&lt;/li&gt;
&lt;li&gt;KYC or ID verification, which is worse. It kills conversion, especially on a free tier.&lt;/li&gt;
&lt;li&gt;Rate limiting by IP, which falls apart on mobile, where a single carrier NAT sits behind thousands of users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three treat human verification like a checkpoint. None of them treat it like a signal that keeps updating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The header I'm proposing
&lt;/h2&gt;

&lt;p&gt;I built this as middleware first. It didn't survive contact with anyone who'd actually have to install it: nobody wants a new dependency sitting between them and their own request handler. A header does the same job and asks for nothing more.&lt;/p&gt;

&lt;p&gt;The idea: attach a signed behavioural score to every request, and let the receiving origin decide what to do with it. One header, X-Trust, versioned and HMAC-SHA256 signed:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Trust: v1.eyJzdWIiOiJ1c2VyLTEiLCJzY29yZSI6MC44OCwiaWF0IjoxNzc4NDU5MjU2LCJleHAiOjE3Nzg0NTkzNzZ9.&amp;lt;sig&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Decode the middle segment and you get sub, score, iat, exp, nonce. The score sits between 0 and 1, computed client side from signals the browser already exposes: keystroke timing variance, scroll gaps, and the kind of touch entropy a script never quite reproduces. It starts at 0.30 on page load and climbs as the session shows real interaction. It expires after 120 seconds and rides along as a normal header, nothing special about the transport.&lt;/p&gt;

&lt;p&gt;The receiving server can do whatever it wants with that number. My reference implementation annotates the request and passes it through untouched. No block, no widget, no redirect, no signup wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lines, twice
&lt;/h2&gt;

&lt;p&gt;Client:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { trustFetch } from '@htl-syterme/htl-core';
const myFetch = trustFetch(process.env.HTL_SECRET);
await myFetch('https://my-api.com/chat', { method: 'POST', body });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Server:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { requireTrust } from '@htl-syterme/htl-core';
const { trusted, score } = await requireTrust(req, process.env.HTL_SECRET);
if (!trusted) return res.status(403).end();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;This is not proof of humanity. A determined client can fake every one of these signals, and I've put that in the spec's threat model section myself, because pretending otherwise would make the rest of the document worthless. That's the honest part.&lt;/p&gt;

&lt;p&gt;What I'm still not sure about: whether keystroke and scroll signals hold up across users with motor impairments, or whether the score quietly penalizes anyone whose hands don't move the way the model assumes. I don't have that answer yet. Better to say so here than bury it in an appendix.&lt;/p&gt;

&lt;p&gt;What it is, is an economic signal. Combine it with a time bounded cap, 0.30 + 0.10 times floor(elapsed / 15s), and a bot has to hold a connection open for 90 seconds to reach 0.9. That's enough friction to make free tier farming a bad trade on most AI products.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -si https://pixmqidaoszxbdxffrxx.supabase.co/functions/v1/hyper-responder/demo | grep -i x-trust
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That returns a real signed header. No signup, no key, no install step.&lt;/p&gt;

&lt;p&gt;The spec, the reference verifier (zero dependencies, standard library only), and the Python package all live at github.com/htl-syterme/htl-core&lt;/p&gt;

&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;The Web Bot Auth draft is still in active discussion, and the mailing list is public. That timing means the human side of this gets designed alongside the agent side, instead of showing up two years later as somebody's commercial black box.&lt;/p&gt;

&lt;p&gt;If free tier abuse is already a line item on your cloud bill, this is worth ten minutes. If it isn't, ignore it, the whole thing is annotation only, so leaving it off costs you nothing.&lt;/p&gt;

&lt;p&gt;That's the part I keep coming back to.&lt;/p&gt;

&lt;p&gt;Amine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
