<?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: Sokol</title>
    <description>The latest articles on DEV Community by Sokol (@alexsokol31337).</description>
    <link>https://dev.to/alexsokol31337</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%2F3948434%2F36b29388-9af1-4e13-b17d-92dfaf0e43d5.jpg</url>
      <title>DEV Community: Sokol</title>
      <link>https://dev.to/alexsokol31337</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexsokol31337"/>
    <language>en</language>
    <item>
      <title>Robotex: a real browser for your bots, behind a plain HTTP API</title>
      <dc:creator>Sokol</dc:creator>
      <pubDate>Wed, 14 Oct 2026 08:57:54 +0000</pubDate>
      <link>https://dev.to/alexsokol31337/robotex-a-real-browser-for-your-bots-behind-a-plain-http-api-527o</link>
      <guid>https://dev.to/alexsokol31337/robotex-a-real-browser-for-your-bots-behind-a-plain-http-api-527o</guid>
      <description>&lt;p&gt;Every bot that works with a website sooner or later hits one of three walls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a captcha;&lt;/li&gt;
&lt;li&gt;anti-bot protection (Cloudflare's "Just a moment…" page, Turnstile, reCAPTCHA);&lt;/li&gt;
&lt;li&gt;heavy JavaScript that has to run before you can even submit a form.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spider built on raw HTTP requests is helpless here. You need a real browser. But a browser is heavy: it eats half a gigabyte of RAM per window, it has to be hidden from detection, and it has to live next to your bot. Most of the time, though, the bot doesn't need the browser at all. It only needs it to get &lt;em&gt;through the door&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the idea behind &lt;strong&gt;&lt;a href="https://robotex.sokolab.xyz/" rel="noopener noreferrer"&gt;Robotex&lt;/a&gt;&lt;/strong&gt;: a browser-as-a-service that you call only where there's no way around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: the browser opens the door, the bot walks in
&lt;/h2&gt;

&lt;p&gt;Robotex opens the page in real Chrome, passes the anti-bot checks, runs a declarative scenario (fill, click, wait, grab HTML or text) and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;cookies&lt;/strong&gt;, including &lt;code&gt;cf_clearance&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the browser's own &lt;strong&gt;User-Agent&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;optionally, the rendered HTML and a screenshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there your bot keeps working with a plain HTTP client, which is fast and cheap. When the site shows a challenge again, the bot calls Robotex once more in the same session.&lt;/p&gt;

&lt;p&gt;It's a service, not a library, so the bot can be written in anything that speaks HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first request
&lt;/h2&gt;

&lt;p&gt;The synchronous endpoint is the simplest way in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://app.robotex.sokolab.xyz/v1/solve
X-API-Key: &amp;lt;your key&amp;gt;
Content-Type: application/json

{
  "url": "https://example.com/login",
  "proxy": "http://user:pass@host:port",
  "captcha_type": "cloudflare",
  "actions": [
    {"type": "fill", "selector": "#email", "value": "user@example.com"},
    {"type": "fill", "selector": "#password", "value": "..."},
    {"type": "click", "selector": "#submit"},
    {"type": "wait_for", "selector": ".dashboard", "timeout": 30}
  ]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (X11; Linux x86_64) ... Chrome/155.0.0.0 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cookies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cf_clearance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".example.com"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wait_for"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1830&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9120&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response also sets a &lt;code&gt;session_id&lt;/code&gt; cookie. That's a persistent Chrome profile on the server: cookies, local storage and a browser fingerprint. Send it back next time, and to the site you're still the same visitor, so the anti-bot check usually doesn't come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the result in Python
&lt;/h2&gt;

&lt;p&gt;The important part is what you do &lt;em&gt;after&lt;/em&gt; the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;ROBOTEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://app.robotex.sokolab.xyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROXY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@host:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;robotex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROBOTEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rbx_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;robotex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/solve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROXY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captcha_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudflare&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# session_id stays in robotex's cookie jar for the next calls
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;


&lt;span class="n"&gt;solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROXY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                     &lt;span class="c1"&gt;# the same proxy
&lt;/span&gt;    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;  &lt;span class="c1"&gt;# the same UA
&lt;/span&gt;    &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# same session, fresh cf_clearance
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make this work: &lt;strong&gt;the same User-Agent&lt;/strong&gt; and &lt;strong&gt;the same proxy&lt;/strong&gt;. Cloudflare ties &lt;code&gt;cf_clearance&lt;/code&gt; to the UA and usually to the IP. Change either and the cookie is worthless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned the hard way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't spoof the User-Agent
&lt;/h3&gt;

&lt;p&gt;Early versions let clients pass their own &lt;code&gt;user_agent&lt;/code&gt;. It seemed harmless: it's just a string. But you can only change the string. &lt;code&gt;navigator.userAgentData&lt;/code&gt;, client hints and the engine's behaviour still belong to the real Chrome, and Cloudflare sees the mismatch. I measured it on Chrome 155 / Linux against a site behind Cloudflare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User-Agent in the browser&lt;/th&gt;
&lt;th&gt;Cloudflare&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;native (Linux Chrome 155)&lt;/td&gt;
&lt;td&gt;passed 3 of 3, 7–9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux Chrome 147&lt;/td&gt;
&lt;td&gt;failed 3 of 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows Chrome 155&lt;/td&gt;
&lt;td&gt;failed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the parameter is gone (passing it returns &lt;code&gt;422&lt;/code&gt;), and the rule is: &lt;strong&gt;the service defines the browser's identity, the client adapts to the response.&lt;/strong&gt; The same went for the window size. Each profile instead gets a plausible fingerprint (screen size and &lt;code&gt;devicePixelRatio&lt;/code&gt; drawn from a real-world distribution) generated once and applied natively by Chrome, with no JS patches, since those broke the Cloudflare check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use real Chrome, not "Chromium with flags"
&lt;/h3&gt;

&lt;p&gt;Robotex drives real Google Chrome through &lt;a href="https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python" rel="noopener noreferrer"&gt;Patchright&lt;/a&gt;, an undetected Playwright fork without the CDP leaks anti-bots look for. Chrome runs headful under Xvfb, because headless mode is one more signal to hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the proxy's geo and the browser agree
&lt;/h3&gt;

&lt;p&gt;Locale and timezone are derived from the proxy's exit IP. A German IP with a browser in &lt;code&gt;America/New_York&lt;/code&gt; is a red flag. And no fingerprint will save you on a dirty IP: proxy quality matters more than any trick on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture v2: a queue between the API and the browsers
&lt;/h2&gt;

&lt;p&gt;The first version ran Chrome inside the API process. When all browser slots were busy, a request simply got a &lt;code&gt;503&lt;/code&gt;. The current version splits it in two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → api (FastAPI) ──task──▶ RabbitMQ ──▶ worker (dramatiq, Chrome + Xvfb)
            ▲                                         │
            └── GET /v2/tasks/{id} ◀── Redis ◀────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;api&lt;/strong&gt; has no browser at all: it queues tasks and serves results;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ&lt;/strong&gt; holds tasks until a worker has a free slot, so they wait instead of failing;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;workers&lt;/strong&gt; run Chrome, several windows each;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; keeps task statuses and results for a day. Proxy credentials and captcha keys never go there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/v1/solve&lt;/code&gt; still exists and runs on top of the same queue, so old clients didn't change a line. New clients can go async:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /v2/tasks
X-API-Key: &amp;lt;your key&amp;gt;

{"url": "https://example.com/login", "actions": [...], "browser_idle_timeout": 10}

→ 202 {"task_id": "0b9c…", "status": "queued", "url": "/v2/tasks/0b9c…"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then poll &lt;code&gt;GET /v2/tasks/{id}&lt;/code&gt; (with the same &lt;code&gt;session_id&lt;/code&gt; cookie) until the status is &lt;code&gt;done&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping the browser window open
&lt;/h3&gt;

&lt;p&gt;The feature I like most in v2 is &lt;code&gt;browser_idle_timeout&lt;/code&gt;. With it, the session's Chrome window &lt;strong&gt;stays open&lt;/strong&gt; for up to 120 idle minutes after the task. The next task with the same &lt;code&gt;session_id&lt;/code&gt; is routed to the personal queue of the worker holding that window and runs right there: no Chrome start, no second anti-bot check. Leave out the &lt;code&gt;url&lt;/code&gt;, and the actions run on the page that's already open.&lt;/p&gt;

&lt;p&gt;That makes multi-step scenarios natural: log in, let the bot do its HTTP work, come back and continue filling the same form. When you're done, &lt;code&gt;DELETE /v2/sessions/current&lt;/code&gt; closes the window and frees the slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Captchas inside pages:&lt;/strong&gt; the &lt;code&gt;captcha&lt;/code&gt; step solves Turnstile, reCAPTCHA and hCaptcha through your own 2captcha key before the form is submitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-bot pages:&lt;/strong&gt; a detect → solve → verify loop for Cloudflare, before and during the scenario. Turnstile passes reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets stay out of logs:&lt;/strong&gt; proxy credentials, captcha keys, &lt;code&gt;fill&lt;/code&gt; values and cookie values are masked.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Robotex is free. You only pay for your own proxies and, if you need it, 2captcha.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing:&lt;/strong&gt; &lt;a href="https://robotex.sokolab.xyz/" rel="noopener noreferrer"&gt;robotex.sokolab.xyz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API and Swagger:&lt;/strong&gt; &lt;a href="https://app.robotex.sokolab.xyz/docs" rel="noopener noreferrer"&gt;app.robotex.sokolab.xyz/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API key:&lt;/strong&gt; a test key for the demo instance is in the &lt;a href="https://redmine.sokolab.xyz/projects/robotex/boards" rel="noopener noreferrer"&gt;project boards&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://git.sokolab.xyz/s0k0l/robotex" rel="noopener noreferrer"&gt;git.sokolab.xyz/s0k0l/robotex&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions and bugs:&lt;/strong&gt; &lt;a href="https://redmine.sokolab.xyz/projects/robotex" rel="noopener noreferrer"&gt;Redmine forum and issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next on the roadmap: workers on several hosts, a second engine (Firefox via Camoufox) for sites where a Chrome fingerprint doesn't fit, and a bot that hands out personal API keys.&lt;/p&gt;

&lt;p&gt;If you've fought Cloudflare from a scraper, I'd love to hear what worked for you, and what broke. And please use Robotex only on sites where you have the right to do so.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>KeePass can't protect you from a stealer. So I built something that can.</title>
      <dc:creator>Sokol</dc:creator>
      <pubDate>Wed, 27 May 2026 15:04:13 +0000</pubDate>
      <link>https://dev.to/alexsokol31337/keepass-cant-protect-you-from-a-stealer-so-i-built-something-that-can-2n3l</link>
      <guid>https://dev.to/alexsokol31337/keepass-cant-protect-you-from-a-stealer-so-i-built-something-that-can-2n3l</guid>
      <description>&lt;h2&gt;
  
  
  I built a hardware password manager because KeePass doesn't actually protect you
&lt;/h2&gt;

&lt;p&gt;I'm a hobbyist developer. I work with sensitive data every day — accounts, crypto wallets, corporate systems, personal communications.&lt;/p&gt;

&lt;p&gt;One day I had an uncomfortable realization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;I was using KeePassXC. Strong master password, encrypted database, local-only. I thought I was doing everything right.&lt;/p&gt;

&lt;p&gt;Then I thought about what actually happens when I unlock my vault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The encrypted database sits on disk — readable by any process with file access&lt;/li&gt;
&lt;li&gt;The decrypted copy lives in RAM — readable by any process with memory access&lt;/li&gt;
&lt;li&gt;The master password gets typed on a keyboard — interceptable by any keylogger&lt;/li&gt;
&lt;li&gt;All of this happens inside an OS shared with dozens of other running programs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;KeePass protects you from someone stealing your hard drive. It does not protect you from malware already running on your machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A stealer with a keylogger gets your master password when you type it, then grabs the database file. Game over. And you'll never know it happened.&lt;/p&gt;

&lt;p&gt;The same is true for Bitwarden, 1Password, and every other software password manager. They're all prisoners of the OS they run on.&lt;/p&gt;

&lt;p&gt;Cloud managers add another layer: you're now trusting a third-party server and its breach history.&lt;/p&gt;

&lt;p&gt;Hardware keys like YubiKey solve part of the problem (2FA), but they don't replace a full offline password manager. They still require the target system to support their protocol.&lt;/p&gt;

&lt;p&gt;I wanted something different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;A device that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stores passwords &lt;strong&gt;completely offline&lt;/strong&gt;, on hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never transfers the database to the computer&lt;/strong&gt; — not even in encrypted form&lt;/li&gt;
&lt;li&gt;Simply &lt;strong&gt;types&lt;/strong&gt; the password like a keyboard (USB HID)&lt;/li&gt;
&lt;li&gt;Works on &lt;strong&gt;any OS&lt;/strong&gt; without installing software&lt;/li&gt;
&lt;li&gt;Has no moving parts, no battery, no cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing Protokey
&lt;/h2&gt;

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

&lt;p&gt;Protokey is a compact USB device built on a microcontroller that acts as a "smart keyboard with memory."&lt;/p&gt;

&lt;p&gt;You browse your vault on a 3.5" touchscreen → select an account → tap autofill → Protokey types your login, password, and Enter into whatever fi m,eld is focused. The computer sees only keystrokes. The database never leaves the device.&lt;/p&gt;




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

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

&lt;p&gt;&lt;strong&gt;Hardware:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Panlee SC01 — 3.5" capacitive touchscreen (320×480), SD card slot, ESP32 microcontroller&lt;/li&gt;
&lt;li&gt;Arduino with ATmega32u4 — handles USB HID pairing with the host computer&lt;/li&gt;
&lt;li&gt;No physical buttons, no battery, no external power required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;![Inside the device]d(&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d87jmgqxiikbntoyw92s.jpg" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d87jmgqxiikbntoyw92s.jpg&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firmware &amp;amp; encryption:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firmware, flash memory, bootloader, and user data are all encrypted&lt;/li&gt;
&lt;li&gt;Master password stays in your head; derived key unlocks your data on the flash&lt;/li&gt;
&lt;li&gt;Encryption keys for the firmware ship &lt;strong&gt;in the box with your device&lt;/strong&gt; — they belong to you from day one&lt;/li&gt;
&lt;li&gt;You can take full custody of your keys. If you want us to hold a copy for recovery, that's opt-in. If you want us to delete our copy, we will.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Unlike most hardware security products, we can't update your device without your explicit involvement. That's a feature, not a limitation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The vault
&lt;/h2&gt;

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

&lt;p&gt;Each record supports up to 16 fields. Field types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text / word&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;login / email / phone&lt;/td&gt;
&lt;td&gt;aggregation (fast search)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;secret&lt;/td&gt;
&lt;td&gt;hidden field with generator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOTP&lt;/td&gt;
&lt;td&gt;2FA time-based codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tags&lt;/td&gt;
&lt;td&gt;comma-separated, filterable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autofill template&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{field_name}&lt;/code&gt;, &lt;code&gt;{tab}&lt;/code&gt;, &lt;code&gt;{enter}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;The autofill template is the key feature. You define exactly what gets typed and in what order. Works for single-field logins, multi-step forms, desktop apps, terminal prompts — anything that accepts keyboard input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfoh5ty49oyiuymvkzup.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfoh5ty49oyiuymvkzup.jpg" alt="Filter by record type. WiFi for exmple." width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Search and filtering happen in real time across visible fields, tags, and aggregation fields. Deep search (across all hidden fields) takes up to 3 minutes on 10,000 records — worth it when you need it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxyxadb1gntnmhkqjohd2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxyxadb1gntnmhkqjohd2.jpg" alt="Aggregation by name" width="800" height="1067"&gt;&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1ytn8ttutnomhl30pni.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1ytn8ttutnomhl30pni.jpg" alt="Aggregation by name listing" width="800" height="1067"&gt;&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnoy3ajn5rzg1fqtlstl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnoy3ajn5rzg1fqtlstl.jpg" alt="Aggregation" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Web interface
&lt;/h2&gt;

&lt;p&gt;WiFi is used exclusively for device administration — never for password transmission.&lt;/p&gt;

&lt;p&gt;Connect Protokey to your local network → access the web panel (HTTPS) to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create and edit records&lt;/li&gt;
&lt;li&gt;Import/export backups&lt;/li&gt;
&lt;li&gt;Update firmware&lt;/li&gt;
&lt;li&gt;Manage TOTP entries&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The WiFi credential setup and web panel access are done from the device's settings menu.&lt;/p&gt;




&lt;h2&gt;
  
  
  Physical threat protection
&lt;/h2&gt;

&lt;p&gt;This one I'm quietly proud of.&lt;/p&gt;

&lt;p&gt;Protokey has a duress system built into the PIN settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Second PIN&lt;/strong&gt; — differs from your real PIN by one character. When entered, it switches the device into "panic mode": only shows records tagged with a specific decoy tag, sets the second PIN as the new primary, and wipes all duress settings — so there's no evidence a second PIN ever existed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blank password entry&lt;/strong&gt; — by default, the device accepts any input at the unlock screen. You can require a specific master password, or leave it open and rely on PIN only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wipe after N attempts&lt;/strong&gt; — standard brute-force protection.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: if someone demands access to your device, you hand them a plausible decoy vault. Your real records are in a hidden layer. The device shows no signs of having one.&lt;/p&gt;

&lt;p&gt;Restoring from that state requires a backup. Keep one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Working MVP assembled from components ordered locally (Ozon)&lt;/li&gt;
&lt;li&gt;✅ 3D-printed enclosure — 2cm thick, 3.5" wide, very light&lt;/li&gt;
&lt;li&gt;✅ Full password manager on 10,000 records&lt;/li&gt;
&lt;li&gt;✅ Autofill, TOTP, search, filters, web interface, duress protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I understand the open-source expectation in this space. Here's where I stand: Protokey is a solo project with no funding. Opening the firmware immediately means someone with a factory in Shenzhen ships it for $40 while I'm still figuring out how to make the second batch. I'm choosing a middle path: the cryptography is documented and auditable, the firmware is available for review by security researchers, and I commit to open-sourcing if the project reaches sustainable production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm posting this
&lt;/h2&gt;

&lt;p&gt;I built Protokey because I wanted it for myself. Now I want to know if others feel the same way.&lt;/p&gt;

&lt;p&gt;A few questions for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How do you store passwords today, and what's your biggest concern about it?&lt;/li&gt;
&lt;li&gt;What's missing from current password managers?&lt;/li&gt;
&lt;li&gt;Would you use a device like this?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're interested in following development — I'm setting up a Telegram channel.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Protokey — your passwords under your physical control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Telegram: &lt;a href="https://t.me/+-PpaF-4qGHNkZmY6" rel="noopener noreferrer"&gt;https://t.me/+-PpaF-4qGHNkZmY6&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>devjournal</category>
      <category>arduino</category>
    </item>
  </channel>
</rss>
