<?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-com-ar).</description>
    <link>https://dev.to/multita-com-ar</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%2Forganization%2Fprofile_image%2F13700%2F961c0568-96b7-4d9b-af53-64b004629b16.png</url>
      <title>DEV Community: Multita</title>
      <link>https://dev.to/multita-com-ar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/multita-com-ar"/>
    <language>en</language>
    <item>
      <title>Cómo normalizar datos de 33 fuentes que no se parecen en nada</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 06 Jul 2026 03:00:00 +0000</pubDate>
      <link>https://dev.to/multita-com-ar/como-normalizar-datos-de-33-fuentes-que-no-se-parecen-en-nada-4fln</link>
      <guid>https://dev.to/multita-com-ar/como-normalizar-datos-de-33-fuentes-que-no-se-parecen-en-nada-4fln</guid>
      <description>&lt;p&gt;Cuando juntás datos de muchos sistemas públicos, lo que cuesta no es pedirlos. Es que ninguno se parece al otro. Te llega un HTML viejo de uno, un JSON medio escondido de otro y un PDF del tercero, y los tres hablan de lo mismo con palabras distintas. La única forma de que tu producto no se vuelva un infierno de casos especiales es elegir un único formato de salida y traducir todo a eso.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema, con un ejemplo
&lt;/h2&gt;

&lt;p&gt;En &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; consultamos multas de tránsito en más de 33 sistemas oficiales argentinos. Un portal devuelve la fecha como "12/03/2026", otro como "2026-03-12", y un tercero la mete en una celda de una tabla sin etiqueta. Uno marca "PAGA", otro "Cancelada", otro "sin deuda". Tres maneras de decir lo mismo.&lt;/p&gt;

&lt;p&gt;Si dejás que esas diferencias lleguen a tu lógica de negocio, cada cálculo se llena de "si la jurisdicción es tal, entonces". Multiplicá eso por 33 y no mantenés nada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato de salida, y un traductor por fuente
&lt;/h2&gt;

&lt;p&gt;La regla que nos ordenó todo: un solo schema de salida, y un adaptador por fuente que traduce a ese schema. La lógica de negocio conoce el schema, nunca el portal.&lt;br&gt;
&lt;/p&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;"jurisdiccion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deuda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;209250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fecha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-12"&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;Los estados se reducen a un set chico y comparable (deuda, pago_voluntario, en_juzgado, sin_deuda). Los montos van siempre en pesos enteros, aunque la fuente los exprese en una unidad que se actualiza sola. Las fechas, siempre ISO. El adaptador hace el laburo sucio y el resto del sistema vive limpio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que ganás
&lt;/h2&gt;

&lt;p&gt;Cuando aparece la fuente 34, no tocás la lógica: escribís un adaptador y listo. Y cuando un portal cambia su HTML (pasa seguido), el incendio queda en un solo archivo. Esa separación es la que hace que la &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API de Multita&lt;/a&gt; devuelva una respuesta consistente sin importar de dónde salió el dato.&lt;/p&gt;

&lt;p&gt;Si vas a integrar varias fuentes que no controlás, definí el formato de salida antes de escribir el primer scraper. Es la decisión que más dolor te ahorra después.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>scraping</category>
      <category>json</category>
      <category>web</category>
      <category>datos</category>
    </item>
    <item>
      <title>Un solo event loop, captchas y reintentos: lo que aprendí scrapeando portales de gobierno</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:32:00 +0000</pubDate>
      <link>https://dev.to/multita-com-ar/un-solo-event-loop-captchas-y-reintentos-lo-que-aprendi-scrapeando-portales-de-gobierno-4de8</link>
      <guid>https://dev.to/multita-com-ar/un-solo-event-loop-captchas-y-reintentos-lo-que-aprendi-scrapeando-portales-de-gobierno-4de8</guid>
      <description>&lt;p&gt;Detrás de &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;la API de Multita&lt;/a&gt; hay un servicio que consulta portales de gobierno argentinos en vivo, en el momento en que se hace la consulta. Estas son las decisiones que más me costó aprender, contadas para que a vos te cuesten menos.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. No bloquees el event loop, nunca
&lt;/h2&gt;

&lt;p&gt;Parece obvio hasta que te pasa. Con un solo worker async, una operación bloqueante en una ruta (un hash de contraseña, una escritura pesada a la base) te frena todas las consultas en vuelo. La regla quedó grabada: lo bloqueante va a un &lt;strong&gt;thread aparte&lt;/strong&gt;, con un &lt;strong&gt;semáforo&lt;/strong&gt; que limita cuántos corren a la vez, y la ruta que dispara un trabajo largo responde al toque con un "recibido" y deja que el resultado se resuelva por atrás. El submit no espera al scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Los captchas no se "resuelven", se torean
&lt;/h2&gt;

&lt;p&gt;Los captcha modernos basados en score no se rompen: se conviven. Lo que más me ordenó fue entender que el token tiene que generarse en el &lt;strong&gt;contexto correcto&lt;/strong&gt; (la página del formulario, no el endpoint que recibe los datos), o el portal lo rechaza aunque sea válido. Y hay portales que directamente no se pueden automatizar de forma confiable: a esos los marco como no disponibles y sigo, en vez de pelearme con una pared.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Reintentar con cabeza, no en loop
&lt;/h2&gt;

&lt;p&gt;Un reintento ciego es la forma más rápida de que te bloqueen. Sirve distinguir tres cosas que parecen iguales y no lo son: "el auto no debe nada", "el portal se cayó" y "me cortaron por hacer demasiadas". La primera es una respuesta, las otras dos son reintentables con backoff. Confundir un portal caído con un "no debe nada" es un &lt;strong&gt;falso negativo carísimo&lt;/strong&gt;: alguien transfiere un auto creyendo que está limpio.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. El estado público a veces miente
&lt;/h2&gt;

&lt;p&gt;La misma deuda aparece distinta según el portal, y un aviso genérico de la cuenta del usuario puede ensuciar el resultado si lo tomás literal. Por eso la capa de normalización no es opcional: es la que convierte 33 formatos en algo en lo que se puede confiar para cotizar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;Todo esto termina en algo simple de usar: una &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;consulta de multas gratis&lt;/a&gt; y una &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API en JSON&lt;/a&gt;. Lo difícil queda del lado de adentro, que es como tiene que ser.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Multita consulta infracciones de tránsito de 33 jurisdicciones argentinas en vivo, por patente, DNI o CUIT.&lt;/li&gt;
&lt;li&gt;Patrón: async sin bloquear el loop, trabajo pesado en threads con semáforo, reintentos con backoff, normalización de estados.&lt;/li&gt;
&lt;li&gt;Consulta web gratis + API para integrar. Datos de portales oficiales; no es un sitio del gobierno.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;https://multita.com.ar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>api</category>
      <category>argentina</category>
    </item>
    <item>
      <title>Cómo unifiqué +30 sistemas de multas de tránsito de Argentina en una sola API</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:48:00 +0000</pubDate>
      <link>https://dev.to/multita-com-ar/como-unifique-30-sistemas-de-multas-de-transito-de-argentina-en-una-sola-api-1i07</link>
      <guid>https://dev.to/multita-com-ar/como-unifique-30-sistemas-de-multas-de-transito-de-argentina-en-una-sola-api-1i07</guid>
      <description>&lt;p&gt;En Argentina, consultar las multas de un auto no es una consulta: son treinta y tres. Cada provincia tiene su sistema, muchos municipios el suyo, y ninguno se habla con el otro. Armé &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; para resolverlo con una sola API que devuelve todo en JSON, y dejo acá el detrás de escena por si alguna vez te toca integrar algo del rubro automotor o fintech en el país.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: 33 portales que no se parecen en nada
&lt;/h2&gt;

&lt;p&gt;No es "scrapear 33 veces la misma página". Cada portal tiene su mundo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unos buscan por &lt;strong&gt;patente (dominio)&lt;/strong&gt;, otros por &lt;strong&gt;DNI&lt;/strong&gt; y otros por &lt;strong&gt;CUIT&lt;/strong&gt;. Algunos aceptan los tres, algunos uno solo.&lt;/li&gt;
&lt;li&gt;Las respuestas van de HTML viejo a JSON medio escondido, pasando por un PDF que hay que leer.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;estado&lt;/strong&gt; de un acta significa cosas distintas según la jurisdicción: "pago voluntario", "en juzgado de faltas", "resolver en mandatarios", "labrada a mano". Lo que en un lado es deuda firme, en otro todavía se está cocinando.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  La idea: un contrato de salida único
&lt;/h2&gt;

&lt;p&gt;La premisa fue simple de enunciar y molesta de cumplir: &lt;strong&gt;una request, un esquema de respuesta&lt;/strong&gt;, pase lo que pase del otro lado.&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;-X&lt;/span&gt; POST https://multita.com.ar/api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Api-Key: TU_KEY"&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tipo":"dominio","valor":"AB123CD"}'&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;"consulta"&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;"tipo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dominio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AB123CD"&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;"actas"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiccion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deuda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nl"&gt;"monto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;209250&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiccion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pago_voluntario"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"monto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104600&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiccion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"caba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en_juzgado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nl"&gt;"monto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95000&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resumen"&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;"cantidad"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;408850&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lo difícil no fue pedir, fue normalizar
&lt;/h2&gt;

&lt;p&gt;Hacer la request es lo de menos. El laburo real estuvo en mapear cada portal a un mismo modelo: un set acotado de &lt;strong&gt;estados&lt;/strong&gt; comparables entre jurisdicciones, los &lt;strong&gt;montos&lt;/strong&gt; en pesos (algunas trabajan en Unidades Fijas que se actualizan, así que hay que cotizar), y la &lt;strong&gt;deduplicación&lt;/strong&gt; de la misma acta que aparece dos veces. Un dato mal mapeado no es un bug cosmético: es cotizarle de más o de menos a alguien que va a transferir un auto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué habilita
&lt;/h2&gt;

&lt;p&gt;Pasar de cuatro ventanas y quince minutos por auto a una sola llamada. Hay una &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;consulta web gratis&lt;/a&gt; para probar sin integrar nada, y la &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API documentada&lt;/a&gt; para meterla en un CRM o un chatbot. Si te interesa el contexto del rubro, escribí una &lt;a href="https://multita.com.ar/blog/que-es-el-dnrpa" rel="noopener noreferrer"&gt;guía de qué es el registro del automotor (DNRPA)&lt;/a&gt; y por qué las multas frenan los trámites.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Multita: servicio web argentino que consulta y cotiza infracciones de tránsito de 33 jurisdicciones en una sola búsqueda, por patente, DNI o CUIT.&lt;/li&gt;
&lt;li&gt;Salida JSON normalizada (estados comparables, montos en pesos), consulta web gratis y API para integrar.&lt;/li&gt;
&lt;li&gt;Toda la data sale de los portales oficiales; Multita la ordena, no es un sitio del gobierno.&lt;/li&gt;
&lt;li&gt;Sitio y docs: &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;https://multita.com.ar&lt;/a&gt; y &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>api</category>
      <category>argentina</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <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>
  </channel>
</rss>
