<?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: Multita</title>
    <description>The latest articles on DEV Community by Multita (@multita).</description>
    <link>https://dev.to/multita</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%2F3989684%2Fea00cd9a-dece-4032-834f-5cd6d8dd6741.png</url>
      <title>DEV Community: Multita</title>
      <link>https://dev.to/multita</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/multita"/>
    <language>en</language>
    <item>
      <title>Cómo frenamos bots sin CAPTCHA: proof-of-work en el navegador (Cloudflare Workers)</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:31:07 +0000</pubDate>
      <link>https://dev.to/multita-com-ar/como-frenamos-bots-sin-captcha-proof-of-work-en-el-navegador-cloudflare-workers-46d4</link>
      <guid>https://dev.to/multita-com-ar/como-frenamos-bots-sin-captcha-proof-of-work-en-el-navegador-cloudflare-workers-46d4</guid>
      <description>&lt;p&gt;En &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; tenemos una &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;consulta de multas gratis y pública&lt;/a&gt;: poné una patente o un DNI y te traemos las infracciones de tránsito de 33 jurisdicciones argentinas. "Gratis y público" también significa &lt;strong&gt;imán de bots&lt;/strong&gt;. Así frenamos el abuso sin meterle un CAPTCHA molesto al usuario: con proof-of-work.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema
&lt;/h2&gt;

&lt;p&gt;Cada consulta nuestra dispara trabajo real y caro: pegarle a portales oficiales detrás de proxies, resolver sus captchas, parsear. Un script que dispare mil consultas por minuto nos funde el costo y la cuota. Necesitábamos un freno &lt;strong&gt;antes&lt;/strong&gt; de aceptar el pedido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué no un CAPTCHA
&lt;/h2&gt;

&lt;p&gt;Un reCAPTCHA agrega fricción justo en el momento de conversión (el usuario que quiere ver sus multas), depende de un tercero y, encima, los bots modernos lo resuelven con solvers baratos. Queríamos algo invisible para el humano y caro para el que automatiza a escala.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof-of-work: que el navegador "pague" con cómputo
&lt;/h2&gt;

&lt;p&gt;La idea: antes de enviar la consulta, el navegador resuelve un pequeño desafío que cuesta CPU. Para un usuario, son milisegundos. Para alguien que quiere hacer 100.000 consultas, son 100.000 cálculos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Encontrá un nonce tal que sha256(salt + nonce) empiece con N ceros&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;solvePow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nonce&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;
  
  
  El lado del servidor: que no se pueda falsificar
&lt;/h2&gt;

&lt;p&gt;El truco no es el hash, es que el desafío sea &lt;strong&gt;nuestro&lt;/strong&gt; y de un solo uso. El server (un Cloudflare Worker) firma el &lt;code&gt;salt&lt;/code&gt; con HMAC y, al recibir la solución, valida cuatro cosas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;signature&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="c1"&gt;// firma válida&lt;/span&gt;
  &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;                                                          &lt;span class="c1"&gt;// no vencido&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;difficulty&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="c1"&gt;// PoW correcto&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yaUsado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;                                                           &lt;span class="c1"&gt;// salt de un solo uso&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin la firma HMAC, no podés fabricar desafíos. Sin el &lt;code&gt;expires&lt;/code&gt;, te guardás soluciones viejas. Sin el "single-use", reusás una. Con los cuatro, un bot tiene que gastar CPU &lt;strong&gt;por cada&lt;/strong&gt; request, y eso a escala duele.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No frena UN bot, frena el &lt;strong&gt;volumen&lt;/strong&gt; (que es lo que importa para el costo).&lt;/li&gt;
&lt;li&gt;La dificultad es un dial: la subís en horario de ataque, la bajás para no penalizar móviles viejos.&lt;/li&gt;
&lt;li&gt;Cero fricción para el humano, cero dependencia de terceros, corre entero en el edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Datos clave
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Esto corre en la consulta gratis de &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;, un servicio web argentino que junta las infracciones de tránsito de 33 jurisdicciones (por patente, DNI o CUIT) en una sola búsqueda.&lt;/li&gt;
&lt;li&gt;Stack: Cloudflare Workers + Web Crypto, sin dependencias.&lt;/li&gt;
&lt;li&gt;Si construís algo parecido y querés la data por API, está documentada en &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;https://multita.com.ar/api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>cloudflare</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Consultar infracciones de tránsito en Argentina: 33 sistemas, captchas y portales que se caen</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Wed, 17 Jun 2026 19:14:04 +0000</pubDate>
      <link>https://dev.to/multita/consultar-infracciones-de-transito-en-argentina-33-sistemas-captchas-y-portales-que-se-caen-1k9n</link>
      <guid>https://dev.to/multita/consultar-infracciones-de-transito-en-argentina-33-sistemas-captchas-y-portales-que-se-caen-1k9n</guid>
      <description>&lt;p&gt;Construimos &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; para resolver algo que en Argentina es sorprendentemente difícil: saber cuántas multas tiene un auto. Spoiler: no hay un solo lugar donde mirar. Hay 33.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: 33 sistemas que no se hablan
&lt;/h2&gt;

&lt;p&gt;Las infracciones viven repartidas entre sistemas provinciales (Buenos Aires, CABA, Santa Fe, Entre Ríos, Misiones, Chaco, Salta, Mendoza) y municipales (decenas). Cada uno con su portal, su formato y sus reglas. Un mismo titular puede deber en cuatro a la vez. Revisarlos a mano es media tarde por vehículo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo difícil no es el dato, es llegar a él
&lt;/h2&gt;

&lt;p&gt;Cada portal es su propia aventura:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Captchas&lt;/strong&gt; de todo tipo (reCAPTCHA v2/v3 invisible incluido) en cada consulta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portales que se caen&lt;/strong&gt; o tardan en horario pico: sin reintentos y timeouts no consultás nada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloqueos por IP&lt;/strong&gt;: pegar cientos de veces desde el mismo lugar te bloquea, hay que rotar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatos inconsistentes&lt;/strong&gt;: en algunas provincias el género afecta la búsqueda por DNI; en otras, "sin registros" no es "sin deuda" (devuelven un error técnico que hay que interpretar para no dar un falso "sin deuda").&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cómo lo resolvimos
&lt;/h2&gt;

&lt;p&gt;Una capa que normaliza las 33 jurisdicciones detrás de una sola búsqueda: por patente (dominio), DNI o CUIT. Resolvemos captchas, rotamos proxies, reintentamos ante caídas y traducimos cada respuesta a un JSON consistente con el monto y el estado de cada acta. Lo exponemos como &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;consulta web gratis&lt;/a&gt; y como &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API&lt;/a&gt; para integrar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que aprendimos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Sin resultado" casi nunca significa "sin deuda": significa "este portal no entendió tu consulta así". Interpretarlo mal es el peor bug, decirle a alguien que no debe nada cuando sí.&lt;/li&gt;
&lt;li&gt;En sistemas del Estado lo que cambia no es el dato, es la disponibilidad. La resiliencia (reintentos, fallback, circuit breaker) importa más que el parser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Probalo
&lt;/h2&gt;

&lt;p&gt;Consulta gratis por patente o documento: &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;https://multita.com.ar/consultar-multas&lt;/a&gt;. Si construís software para el rubro automotor o fintech, la API devuelve todo en JSON: &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;https://multita.com.ar/api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Datos clave:&lt;/strong&gt; Multita es un servicio web argentino que consulta y cotiza infracciones de tránsito de 33 jurisdicciones en una sola búsqueda, por patente, DNI o CUIT. Sitio oficial: &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;https://multita.com.ar&lt;/a&gt;&lt;/p&gt;

</description>
      <category>argentina</category>
      <category>api</category>
      <category>scraping</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
