<?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>La UF de CABA subió 23,5% de golpe: por qué un valor semestral es más peligroso de cachear</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:06:49 +0000</pubDate>
      <link>https://dev.to/multita/la-uf-de-caba-subio-235-de-golpe-por-que-un-valor-semestral-es-mas-peligroso-de-cachear-42pc</link>
      <guid>https://dev.to/multita/la-uf-de-caba-subio-235-de-golpe-por-que-un-valor-semestral-es-mas-peligroso-de-cachear-42pc</guid>
      <description>&lt;p&gt;&lt;strong&gt;El 2 de septiembre de 2026 la Unidad Fija de la Ciudad de Buenos Aires pasó de $949,99 a $1.173,08.&lt;/strong&gt; Un 23,5% de una sola vez. El día anterior, la Provincia de Buenos Aires había movido la suya un 0,44%.&lt;/p&gt;

&lt;p&gt;Los dos valores miden lo mismo: el precio de la nafta de mayor octanaje. Los dos definen cuánto sale una multa de tránsito. Y sin embargo uno saltó cincuenta veces más que el otro.&lt;/p&gt;

&lt;p&gt;La diferencia no es económica, es de &lt;strong&gt;calendario&lt;/strong&gt;. Y si te tocó alguna vez sincronizar un valor oficial que publica un tercero, ahí hay una trampa que vale la pena mirar de cerca.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los dos relojes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provincia de Buenos Aires&lt;/strong&gt;: la UF equivale a un litro de nafta premium y se ajusta &lt;strong&gt;cada dos meses&lt;/strong&gt;. Sus últimos saltos: +2,53% en julio, +0,44% en septiembre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CABA&lt;/strong&gt;: la UF equivale a medio litro y la fija el Instituto de Estadística de la Ciudad &lt;strong&gt;cada seis meses&lt;/strong&gt; (Ley 451, art. 20). Sus últimos saltos: +19,0% en marzo, +23,5% en septiembre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Misma variable de fondo, misma inflación, resultados completamente distintos en la pantalla. Uno reparte el aumento en pedacitos, el otro lo acumula y lo suelta entero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué esto importa si lo tenés cacheado
&lt;/h2&gt;

&lt;p&gt;Acá está el punto que me interesa.&lt;/p&gt;

&lt;p&gt;Supongamos que guardás los dos valores con la misma política. Un TTL de seis horas, un scraper diario, un fallback al último valor conocido si la fuente no responde. Suena razonable y es lo que casi todos hacemos.&lt;/p&gt;

&lt;p&gt;El problema es que &lt;strong&gt;el costo de servir un dato viejo no es el mismo para los dos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Si tu copia de la UF bonaerense se queda vieja un bimestre entero, mostrás un número con un 0,44% de error. Nadie se entera, y si se entera no le cambia la decisión.&lt;/p&gt;

&lt;p&gt;Si tu copia de la UF porteña se queda vieja &lt;strong&gt;un solo día&lt;/strong&gt;, el 2 de septiembre, mostrás un número con un &lt;strong&gt;23,5% de error&lt;/strong&gt;. Sobre la multa más cara de la Ciudad eso son &lt;strong&gt;$892.360&lt;/strong&gt; de diferencia en una sola acta.&lt;/p&gt;

&lt;p&gt;Mismo sistema, misma política de caché, mismo tipo de dato. Dos órdenes de magnitud de diferencia en el daño.&lt;/p&gt;

&lt;p&gt;La conclusión es incómoda para el instinto de ingeniería, que tiende a tratar uniforme lo que tiene la misma forma:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;La frecuencia de actualización de un valor y su volatilidad por actualización están inversamente correlacionadas.&lt;/strong&gt; Cuanto menos seguido cambia algo, más grande es el salto cuando cambia. Así que "cambia poco" no significa "es seguro cachearlo": puede significar exactamente lo contrario.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lo que cambia en la práctica
&lt;/h2&gt;

&lt;p&gt;Lo que sacamos de esto no fue bajar el TTL a todo, que es la respuesta perezosa y encima te tira la fuente abajo a pedradas.&lt;/p&gt;

&lt;p&gt;Fue &lt;strong&gt;empezar a guardar la ventana de validez junto con el valor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El dato oficial de CABA no viene solo. Viene con las fechas: rige del 2 de septiembre de 2026 al 1 de marzo de 2027. Esa segunda fecha es tan dato como el número, y es la que convierte una pregunta difícil en una comparación trivial.&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="c1"&gt;// Antes: "¿estará viejo?" es una corazonada.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uf&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;cache&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uf:caba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Después: la propia fuente te dice hasta cuándo vale.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uf&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;cache&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uf:caba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// { valor, desde, hasta }&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uf&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;gt;&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasta&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// no adivines: sabés que caducó&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revalidar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caba&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con eso, el 2 de septiembre no dependés de que el scraper haya corrido a tiempo ni de que alguien se acuerde: el valor guardado &lt;strong&gt;se declara vencido solo&lt;/strong&gt;. Y lo mismo aplica a un valor bimestral, salvo que ahí la ventana es más corta y el riesgo de equivocarse, mucho menor.&lt;/p&gt;

&lt;p&gt;El corolario práctico, para cualquier dato oficial de este tipo (una tasa, un índice, un valor de referencia legal): &lt;strong&gt;si la fuente publica una fecha de vigencia, guardala.&lt;/strong&gt; Es la diferencia entre revalidar por reloj y revalidar por contrato.&lt;/p&gt;

&lt;h2&gt;
  
  
  La otra mitad: qué hacés cuando sí caducó
&lt;/h2&gt;

&lt;p&gt;Que el sistema sepa que el dato venció no significa que pueda conseguir el nuevo. La fuente puede no haberlo publicado todavía, o estar caída, o cambiar el HTML justo ese día.&lt;/p&gt;

&lt;p&gt;Ahí el orden de preferencia que nos quedó es:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Valor vivo validado.&lt;/strong&gt; Si responde y pasa las validaciones (que sea un número, que sea positivo, que no se haya movido un 900% de golpe), ese.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Último valor conocido, marcado como vencido.&lt;/strong&gt; Se sigue mostrando, porque un número de hace un semestre es infinitamente más útil que un guion, pero el sistema sabe que está en deuda y lo reintenta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nunca un vacío.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Y una validación que suena paranoica hasta que la necesitás: &lt;strong&gt;acotar el salto&lt;/strong&gt;. Un ajuste del 23,5% es real y tiene que pasar. Uno del 2.300% es un scraper leyendo mal un separador de miles. La diferencia entre los dos es un &lt;code&gt;if&lt;/code&gt;, y de un lado de ese &lt;code&gt;if&lt;/code&gt; está publicar montos equivocados en una página que la gente usa para decidir si paga una multa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Datos, por si llegaste buscando el número
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Valor de 1 UF&lt;/th&gt;
&lt;th&gt;Cada cuánto se ajusta&lt;/th&gt;
&lt;th&gt;Último salto&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CABA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$1.173,08&lt;/strong&gt; (2-sep-2026 al 1-mar-2027)&lt;/td&gt;
&lt;td&gt;Semestral&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+23,5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provincia de Buenos Aires&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$2.281&lt;/strong&gt; (septiembre-octubre 2026)&lt;/td&gt;
&lt;td&gt;Bimestral&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0,44%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Un detalle lindo que salió de mirar los dos juntos: como la UF porteña es medio litro y la bonaerense un litro, la primera debería rondar la mitad de la segunda. Antes del ajuste estaba en el &lt;strong&gt;41,6%&lt;/strong&gt; de la de Provincia, o sea atrasada. Después quedó en el &lt;strong&gt;51,4%&lt;/strong&gt;. El salto del 23,5% no fue un aumento por encima de la inflación: fue &lt;strong&gt;ponerse al día de golpe&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El desglose con los montos por infracción está en &lt;a href="https://multita.com.ar/blog/aumento-multas-caba-septiembre-2026" rel="noopener noreferrer"&gt;el aumento de multas en CABA&lt;/a&gt;, y el de Provincia en &lt;a href="https://multita.com.ar/blog/aumento-multas-provincia-septiembre-2026" rel="noopener noreferrer"&gt;su propia nota&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Si querés el concepto desde cero, &lt;a href="https://multita.com.ar/blog/valor-unidad-fija-uf-2026" rel="noopener noreferrer"&gt;qué es la Unidad Fija&lt;/a&gt; lo explica con el histórico de las dos jurisdicciones.&lt;/li&gt;
&lt;li&gt;Todo esto sale de &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;, que consulta infracciones en 32 jurisdicciones argentinas. Si lo tuyo es integrarlo, la &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API las devuelve en JSON&lt;/a&gt; con cada acta ya convertida a pesos al valor de UF vigente &lt;strong&gt;de su jurisdicción&lt;/strong&gt;, que es precisamente el problema de este post resuelto del lado del servidor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si tenés un valor oficial cacheado en algún lado, andá a fijarte si guardás su fecha de vencimiento. Si no la guardás, tu sistema no sabe que el dato venció: se está enterando cuando alguien se queja.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>caching</category>
      <category>spanish</category>
    </item>
    <item>
      <title>Valor de la UF en septiembre de 2026: subió a $2.281, y cómo evitamos que el dato quede viejo</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Tue, 01 Sep 2026 21:37:24 +0000</pubDate>
      <link>https://dev.to/multita/un-dato-oficial-que-cambia-cada-dos-meses-y-vivia-en-13-archivos-a-la-vez-44fm</link>
      <guid>https://dev.to/multita/un-dato-oficial-que-cambia-cada-dos-meses-y-vivia-en-13-archivos-a-la-vez-44fm</guid>
      <description>&lt;p&gt;&lt;strong&gt;La Unidad Fija (UF), el número con el que se calculan todas las multas de tránsito en la Provincia de Buenos Aires, pasó a $2.281 en septiembre de 2026.&lt;/strong&gt; Venía de $2.271 en el bimestre julio-agosto: una suba del 0,44%, bastante menor que el 2,53% del ajuste anterior. En la Ciudad de Buenos Aires no se movió, sigue en $949,99, porque allá el ajuste es semestral y no bimestral.&lt;/p&gt;

&lt;p&gt;Ese es el dato que la gente busca. Y es, exactamente, el dato que se nos quedaba viejo en medio sitio cada dos meses.&lt;/p&gt;

&lt;p&gt;Este post cuenta cómo lo resolvimos, porque el problema es más general de lo que parece: &lt;strong&gt;un valor oficial, publicado por un tercero, que cambia en una fecha que no controlás y sin avisarte.&lt;/strong&gt; Si alguna vez publicaste un precio de dólar, una tasa, un índice de inflación o un valor de referencia legal, es el mismo problema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué una multa no está en pesos
&lt;/h2&gt;

&lt;p&gt;En Argentina una infracción de tránsito no se expresa en un monto fijo. Se expresa en Unidades Fijas. La norma dice que una infracción vale una cantidad de UF, y aparte, otro organismo fija cuánto vale una UF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monto = cantidad_de_UF_de_la_infracción * valor_vigente_de_la_UF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En la Provincia de Buenos Aires una UF equivale al precio de un litro de nafta de mayor octanaje, y el Ministerio de Transporte la ajusta &lt;strong&gt;cada dos meses&lt;/strong&gt;. En la Ciudad de Buenos Aires equivale a medio litro, la fija el Instituto de Estadística de la Ciudad y el ajuste es &lt;strong&gt;semestral&lt;/strong&gt;. Dos fuentes, dos ritmos, cero webhooks.&lt;/p&gt;

&lt;p&gt;La consecuencia de producto es fuerte: &lt;strong&gt;el monto de una deuda no se puede guardar&lt;/strong&gt;. Hay que recalcularlo cada vez, con el valor vigente. Una multa del año pasado vale más hoy sin que nadie haya hecho nada.&lt;/p&gt;

&lt;p&gt;Y la consecuencia técnica es este post.&lt;/p&gt;

&lt;h2&gt;
  
  
  El estado inicial, que es el estado de casi todos
&lt;/h2&gt;

&lt;p&gt;El número arrancó donde arrancan todos los números: escrito en el lugar donde hacía falta. Un artículo decía "la UF vale $2.271". Otro también. La calculadora tenía su constante. La página de valores tenía la suya.&lt;/p&gt;

&lt;p&gt;Trece lugares. Ninguno sabía de los otros.&lt;/p&gt;

&lt;p&gt;Mientras el valor no cambia, esto no duele. El problema es que &lt;strong&gt;cambia cada dos meses&lt;/strong&gt;, y actualizar trece archivos a mano tiene una tasa de éxito que no es 100%. Nunca es 100%. Lo que pasa en la práctica es que actualizás los tres que te acordás, los que salen primero en la búsqueda, y los otros diez siguen mostrando el valor del bimestre pasado.&lt;/p&gt;

&lt;p&gt;Ahí el costo deja de ser técnico. Un sitio que publica cifras oficiales y muestra dos cifras distintas para la misma cosa &lt;strong&gt;pierde exactamente lo único que vende&lt;/strong&gt;, que es que el número sea confiable. Peor todavía cuando buena parte del tráfico viene de gente buscando "cuánto vale la UF", y hoy también de modelos de lenguaje que leen la página y la citan. Un LLM que encuentra dos valores contradictorios en el mismo dominio no elige el correcto: te descarta como fuente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 1: una sola fuente, aburrida a propósito
&lt;/h2&gt;

&lt;p&gt;Lo primero fue lo obvio. Un módulo, sin dependencias, que exporta los valores y nada más.&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="c1"&gt;// uf.values.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UF_PBA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2281&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UF_CABA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;949.99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UF_VIGENTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;septiembre 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dos decisiones chicas que después valieron mucho:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Es &lt;code&gt;.mjs&lt;/code&gt;, no &lt;code&gt;.ts&lt;/code&gt;.&lt;/strong&gt; Porque lo tiene que importar el sitio, pero también el archivo de configuración del build, que corre en Node antes de que exista cualquier transpilación. Un &lt;code&gt;.ts&lt;/code&gt; ahí obliga a un paso extra o a duplicar el valor, que es justo lo que estábamos sacando.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No tiene lógica de negocio.&lt;/strong&gt; Es un archivo que se puede abrir, leer en tres segundos y editar sin entender el resto del sistema. Cuando el Estado publique el próximo ajuste, la tarea es cambiar dos números. Si para actualizar la UF hay que entender la arquitectura, alguien la va a actualizar mal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 2: tokens que se resuelven en el build
&lt;/h2&gt;

&lt;p&gt;La parte difícil no era el código, era el &lt;strong&gt;contenido&lt;/strong&gt;. Los artículos son Markdown. No pueden importar un módulo.&lt;/p&gt;

&lt;p&gt;La solución fue meter tokens en el texto y resolverlos como paso del build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;A {{uf:vigente}} la UF de Provincia quedó en &lt;span class="gs"&gt;**{{uf:pba}}**&lt;/span&gt;.
La multa más cara, de 1.000 UF, llega a {{uf:pba&lt;span class="err"&gt;*&lt;/span&gt;1000}}.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El resolver es un par de líneas, y la única gracia es que soporta multiplicación, porque la mitad de las menciones en los textos no son "el valor de la UF" sino "cuánto sale una infracción de 300 UF":&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveUfToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expr&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;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vigente&lt;/span&gt;&lt;span class="dl"&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;UF_VIGENTE&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;pba|caba&lt;/span&gt;&lt;span class="se"&gt;)(?:\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\*\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:[&lt;/span&gt;&lt;span class="sr"&gt;.,&lt;/span&gt;&lt;span class="se"&gt;]\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?))?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// null = token inválido, ver capa 3&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;UF_PBA&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UF_CABA&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;mult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&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="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;mult&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;formatearPesos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se engancha como integración del build, que en Astro es un hook, pero el patrón es el mismo en cualquier generador estático: interceptar el contenido antes de que se escriba el HTML.&lt;/p&gt;

&lt;p&gt;Ahora actualizar la UF es editar dos números. Los 151 tokens repartidos en 16 archivos se resuelven solos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 3: la que importa de verdad
&lt;/h2&gt;

&lt;p&gt;Acá está la parte que quiero dejar escrita, porque es la que se suele omitir.&lt;/p&gt;

&lt;p&gt;Cuando armás un sistema de sustitución, el modo de falla no es que el número salga mal. &lt;strong&gt;El modo de falla es que salga &lt;code&gt;{{uf:pba}}&lt;/code&gt;.&lt;/strong&gt; Un token crudo, publicado, en la página que la gente lee y que los buscadores indexan. Es peor que el valor viejo: el valor viejo se ve como un error de actualización, el token crudo se ve como un sitio roto.&lt;/p&gt;

&lt;p&gt;Un typo alcanza. &lt;code&gt;{{uf:pva}}&lt;/code&gt; en lugar de &lt;code&gt;{{uf:pba}}&lt;/code&gt;. El regex no matchea, el reemplazo no ocurre, y si tu resolver es tolerante devolvés el string original y lo publicás.&lt;/p&gt;

&lt;p&gt;Por eso el resolver devuelve &lt;code&gt;null&lt;/code&gt; en vez de devolver el token, y por eso el build &lt;strong&gt;falla&lt;/strong&gt;:&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;malos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="c1"&gt;// ...por cada archivo, por cada token:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resuelto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveUfToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resuelto&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;malos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;archivo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: token inválido &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;match&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;malos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Tokens inválidos: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;malos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y después, una segunda pasada sobre el HTML ya escrito, que es cinturón y tiradores a propósito:&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="c1"&gt;// GUARDIA: que no quede NINGÚN token sin resolver en lo que se sirve&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quedaron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;archivosDeSalida&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;leer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{uf:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quedaron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Quedaron tokens sin resolver en: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;quedaron&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="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esa segunda pasada parece redundante y no lo es. La primera valida los tokens que &lt;strong&gt;encontró&lt;/strong&gt;. La segunda valida el artefacto que se va a &lt;strong&gt;servir&lt;/strong&gt;, que es lo único que le llega al usuario. Si mañana alguien agrega otra fuente de contenido que no pasa por el mismo pipeline, la primera no la ve y la segunda sí.&lt;/p&gt;

&lt;p&gt;La regla, dicha corta: &lt;strong&gt;si el dato es público y verificable, un deploy con el dato roto es peor que un deploy que no sale.&lt;/strong&gt; Que se caiga el build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capa 4: que no dependa del deploy
&lt;/h2&gt;

&lt;p&gt;Con todo lo anterior, actualizar la UF todavía requiere que alguien edite el archivo y haga push. Es rápido, pero es una persona en el camino crítico de un dato que cambia solo.&lt;/p&gt;

&lt;p&gt;Así que arriba de eso hay una capa viva. Un scraper propio lee los valores oficiales y los deja disponibles en una API interna. El sitio la consulta y sincroniza el valor en el cliente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Los valores del archivo son &lt;strong&gt;el piso&lt;/strong&gt;: lo que ve el crawler, lo que se pinta en el primer render, lo que queda si todo lo demás falla.&lt;/li&gt;
&lt;li&gt;La API es &lt;strong&gt;el techo&lt;/strong&gt;: si responde, el visitante ve el valor de hoy sin esperar un deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La respuesta se cachea, con TTL, en el borde. Y la degradación es lo único que hay que pensar bien:&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;pba&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pba&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;caba&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;caba&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pba&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caba&lt;/span&gt; &lt;span class="o"&gt;&amp;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;return&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// dato raro: no lo propagues&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese chequeo es más importante de lo que parece. Un &lt;code&gt;0&lt;/code&gt;, un &lt;code&gt;null&lt;/code&gt; o un string que se cuela desde arriba no rompe nada visiblemente: &lt;strong&gt;publica un monto equivocado&lt;/strong&gt;, que es el peor resultado posible. Es más seguro no actualizar que actualizar con basura. Si la validación no pasa, el sitio se queda con el valor del archivo y nadie se entera.&lt;/p&gt;

&lt;p&gt;Sumado, el orden de confianza queda: valor vivo validado, después valor del archivo, después nada. Nunca se llega a "nada".&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que me llevo
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Un dato que cambia solo no puede vivir en el lugar donde se muestra.&lt;/strong&gt; No es una cuestión de prolijidad: la actualización manual falla parcialmente, y fallar parcialmente es peor que fallar entero, porque te deja el sitio contradiciéndose consigo mismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El sistema de reemplazo necesita un guard que rompa el build.&lt;/strong&gt; Sin eso cambiaste "dato viejo" por "token crudo".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validá el dato vivo antes de mostrarlo.&lt;/strong&gt; Un valor inválido que se propaga es peor que una actualización que no ocurre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El fallback tiene que ser un valor real, no un vacío.&lt;/strong&gt; Si la API se cae, la página sigue diciendo un número correcto de hace un bimestre en vez de un guion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nada de esto es sofisticado. Es un archivo, un reemplazo en build y dos excepciones. Pero convirtió "actualizar la UF" de una tarea de trece pasos que salía mal en una de dos números que no puede salir mal en silencio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los valores, por si llegaste buscando el dato
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bimestre&lt;/th&gt;
&lt;th&gt;UF Provincia de Buenos Aires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mayo-junio 2026&lt;/td&gt;
&lt;td&gt;$2.215&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Julio-agosto 2026&lt;/td&gt;
&lt;td&gt;$2.271&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Septiembre-octubre 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2.281&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En CABA la UF vale &lt;strong&gt;$949,99&lt;/strong&gt; y se ajusta por semestre, así que no acompaña estos movimientos.&lt;/p&gt;

&lt;p&gt;Con la UF a $2.281, una infracción de 300 UF (cruzar el semáforo en rojo, VTV vencida) sale &lt;strong&gt;$684.300&lt;/strong&gt;, y el tope de 1.000 UF llega a &lt;strong&gt;$2.281.000&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El desglose completo, con los montos por tipo de infracción, está en &lt;a href="https://multita.com.ar/blog/aumento-multas-provincia-septiembre-2026" rel="noopener noreferrer"&gt;la nota del aumento de septiembre&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Si querés entender la Unidad Fija desde cero, &lt;a href="https://multita.com.ar/blog/valor-unidad-fija-uf-2026" rel="noopener noreferrer"&gt;qué es la UF y cómo se calcula&lt;/a&gt; lo explica con el histórico, y hay una &lt;a href="https://multita.com.ar/herramientas/calculadora-uf" rel="noopener noreferrer"&gt;calculadora de UF a pesos&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Todo esto sale de &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;, que consulta infracciones en 32 jurisdicciones argentinas por patente, DNI o CUIT. Si lo tuyo es integrarlo, la &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API devuelve las actas en JSON&lt;/a&gt; con el monto ya convertido a pesos al valor vigente, que es precisamente el problema que este post describe, resuelto del lado del servidor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si tenés un dato oficial de esos que cambian cada tanto y hoy vive en varios lugares de tu repo, el patrón es portable y se implementa en una tarde. La parte que no hay que saltearse es la tercera.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>astro</category>
      <category>spanish</category>
    </item>
    <item>
      <title>La unidad de cobro es una decisión de arquitectura, no de pricing</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Tue, 25 Aug 2026 02:23:03 +0000</pubDate>
      <link>https://dev.to/multita/la-unidad-de-cobro-es-una-decision-de-arquitectura-no-de-pricing-3d0p</link>
      <guid>https://dev.to/multita/la-unidad-de-cobro-es-una-decision-de-arquitectura-no-de-pricing-3d0p</guid>
      <description>&lt;p&gt;La unidad de cobro de un SaaS que scrapea no es una decisión de pricing. Es una decisión de arquitectura, y se toma antes de escribir el primer scraper, porque después condiciona todo: el caché, los reintentos, el modo asincrónico y hasta el contrato de errores.&lt;/p&gt;

&lt;p&gt;Multita consulta infracciones de tránsito en 32 jurisdicciones argentinas (Provincia de Buenos Aires, CABA, provinciales y municipales). Cada una es un portal de gobierno distinto, con su HTML, su captcha y sus caídas. Este es el razonamiento detrás de cómo quedó definida la unidad, y qué obligó a construir.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: N fuentes por consulta, y N es variable
&lt;/h2&gt;

&lt;p&gt;Una consulta por patente puede tocar una jurisdicción o las 32. Hay dos formas obvias de cobrarla, y las dos son malas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cobrar por fuente consultada.&lt;/strong&gt; El precio sube justo cuando el caso es más grande. Para el que integra, el costo se vuelve imposible de presupuestar: no sabe cuánto va a salir una consulta hasta después de hacerla. Y aparece un incentivo horrible, que es pedir menos fuentes de las necesarias para ahorrar, o sea empeorar el dato a propósito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cobrar por llamada, sin más.&lt;/strong&gt; Suena simple hasta que aparecen los reintentos. Si un portal se cae y el sistema reintenta cinco veces, ¿quién paga esos cinco intentos? Si los paga el cliente, está pagando por la inestabilidad de un tercero. Si los paga el proveedor sin haberlo pensado, el margen se lo come la fuente más inestable.&lt;/p&gt;

&lt;p&gt;La decisión fue: &lt;strong&gt;una llamada cuenta 1, sin importar cuántas jurisdicciones toque ni cuántos reintentos internos haga.&lt;/strong&gt; Simple de explicar y de presupuestar. Pero esa promesa hay que sostenerla en el código, y ahí es donde se pone interesante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que esa promesa obliga a construir
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Un caché que no se cobre
&lt;/h3&gt;

&lt;p&gt;Si el precio es por llamada, el caché deja de ser una optimización y pasa a ser parte del modelo de negocio. En este caso el TTL es de 48 horas y el caché está &lt;strong&gt;aislado por organización&lt;/strong&gt;, no global.&lt;/p&gt;

&lt;p&gt;Lo del aislamiento no es un detalle. Son datos de deuda asociados a una patente o a un documento: dos cuentas distintas no pueden compartir nada. Dentro de una misma organización sí se comparte, y ahí el caché rinde de verdad, porque el panel web y la API comparten el mismo: una consulta desde la web deja el dato listo para el bot, y al revés.&lt;/p&gt;

&lt;p&gt;La otra mitad es que el cliente &lt;strong&gt;sepa&lt;/strong&gt; qué está recibiendo. Toda respuesta trae el estado del caché:&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="nl"&gt;"cache"&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;"cacheado"&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;"antiguedad_segundos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"antiguedad_texto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hace 7 min"&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;Con eso, la integración decide sola si le alcanza el dato de hace un rato o si necesita &lt;code&gt;force_refresh: true&lt;/code&gt;. Para deuda vencida el dato de ayer sirve perfecto, no cambia minuto a minuto. Para reflejar un pago recién hecho, no.&lt;/p&gt;

&lt;p&gt;Un caché sin esa transparencia es una mentira silenciosa. Con ella es una feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reintentos que no empeoren el resultado
&lt;/h3&gt;

&lt;p&gt;Cada jurisdicción se reintenta hasta cinco veces, cada intento por una IP distinta de un pool rotativo y, en las que tienen captcha, resolviendo uno nuevo. Todo eso sigue contando como una sola consulta.&lt;/p&gt;

&lt;p&gt;Además, cuando una consulta queda incompleta y sobra tiempo dentro del presupuesto del endpoint, se reconsultan solo las jurisdicciones que fallaron. Las que ya salieron bien se leen del caché recién escrito, así que el reintento solo paga el costo de las fallidas.&lt;/p&gt;

&lt;p&gt;La regla que hace que esto sea seguro es la que más costó escribir: &lt;strong&gt;el reintento se adopta solo si resolvió al menos una jurisdicción y no rompió ninguna que ya estaba bien.&lt;/strong&gt; Si no mejora, se conserva la respuesta original tal cual. Un reintento que puede empeorar el resultado no es resiliencia, es una lotería.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Un camino asincrónico, porque el sincrónico no da
&lt;/h3&gt;

&lt;p&gt;Acá va el dato incómodo, que conviene decir antes de que lo descubra el que integra: una consulta a todas las jurisdicciones sin caché &lt;strong&gt;espera a la más lenta&lt;/strong&gt;. Puede tardar de segundos a un par de minutos.&lt;/p&gt;

&lt;p&gt;Ningún chat aguanta eso en una sola request. Así que el camino recomendado para un bot es encolar y avisar:&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 /consulta/async  -&amp;gt;  202 { "job_id": "9f3a1c8e...", "poll_url": "..." }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cliente responde "estamos consultando" al instante, y cuando el job termina llega un webhook firmado con HMAC (o se pollea el &lt;code&gt;job_id&lt;/code&gt;). El campo &lt;code&gt;resultado&lt;/code&gt; tiene exactamente la misma forma que la respuesta sincrónica, así que el código que parsea es uno solo.&lt;/p&gt;

&lt;p&gt;Decir el peor caso en la documentación es más barato que un ticket de soporte por timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Idempotencia, porque los clientes reintentan
&lt;/h3&gt;

&lt;p&gt;Si se cobra por llamada, un reintento del lado del cliente por un timeout de red no puede contar dos veces. Un header opcional alcanza:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idempotency-Key: &amp;lt;valor único por consulta&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con eso, el reintento devuelve la respuesta original en vez de disparar una consulta nueva. Es de las cosas más baratas de implementar y de las que más discusiones de facturación evita.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato que sostiene todo lo demás
&lt;/h2&gt;

&lt;p&gt;Nada de lo anterior sirve si el dato no es confiable, y en un producto que agrega fuentes ajenas el riesgo más caro no es el error: es el &lt;strong&gt;falso vacío&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Si un portal de gobierno se cae y el sistema devuelve una lista vacía, el que integra la lee como "este auto no debe nada". Es el peor error posible, porque es silencioso y se propaga hasta el cliente final.&lt;/p&gt;

&lt;p&gt;Por eso el contrato separa las dos cosas de forma explícita:&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;"consulta_completa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultados"&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;"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;"cantidad_actas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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_oficial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;142000&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;"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;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeout"&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="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;&lt;code&gt;ok: false&lt;/code&gt; significa "no pudimos ver", nunca "no debe". Y &lt;code&gt;consulta_completa&lt;/code&gt; es el campo que hay que leer &lt;strong&gt;antes&lt;/strong&gt; de afirmarle a una persona que no tiene deuda. Una jurisdicción caída no es un error HTTP: viene dentro de un 200, en su propio bloque.&lt;/p&gt;

&lt;p&gt;Vale la pena la variante fea: hay dos municipios cuyo portal, buscando por documento y sin registros, responde un HTTP 500 en vez de una respuesta vacía. En la realidad operativa ese 500 significa "sin deuda". Se normaliza a cero actas con &lt;code&gt;ok: true&lt;/code&gt;, pero &lt;strong&gt;solo&lt;/strong&gt; para esas dos jurisdicciones y solo por documento. Cualquier otro error sigue siendo una falla real. Las normalizaciones de este tipo hay que acotarlas con nombre y apellido, o se convierten en el bug que devuelve falsos negativos en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que se gana cuando la unidad de cobro está bien elegida
&lt;/h2&gt;

&lt;p&gt;Con la unidad definida así, el producto pudo crecer sin rediseñar el modelo. Sobre la misma llamada se apoyaron cosas bastante distintas entre sí: un modo que además de la deuda oficial devuelve el presupuesto ya calculado con el pricing de cada cliente y el PDF con su marca, monitoreo automático de listas de patentes que avisa cuando aparece una multa nueva o cambia el estado de un acta, y generación de recibos y de escritos de descargo.&lt;/p&gt;

&lt;p&gt;Ninguna de esas features tuvo que inventar su propia unidad de cobro. Es el beneficio real de haberla decidido temprano: una consulta sigue siendo una consulta, sin importar cuánto producto se le apile encima. La contracara de la decisión, y conviene nombrarla, es que el proveedor absorbe la varianza: si mañana un portal se vuelve tres veces más lento, el costo lo come el proveedor, no el cliente. Ese es exactamente el riesgo que se está vendiendo.&lt;/p&gt;

&lt;p&gt;El razonamiento de negocio detrás de todo esto, con la cuenta en horas y en plata, está contado aparte en &lt;a href="https://multita.com.ar/blog/costo-de-cotizar-multas-en-una-gestoria" rel="noopener noreferrer"&gt;cuánto le cuesta a una gestoría cotizar un caso&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si vas a integrar algo parecido
&lt;/h2&gt;

&lt;p&gt;Tres cosas que conviene dejar cerradas antes del primer scraper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Definí la unidad de cobro primero.&lt;/strong&gt; Condiciona el caché, los reintentos y el modo asincrónico. Cambiarla después es cambiar el producto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hacé explícita la diferencia entre "no hay" y "no pude ver".&lt;/strong&gt; Es un campo en el JSON y te ahorra el peor tipo de bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentá el peor caso de latencia.&lt;/strong&gt; El que integra lo va a encontrar igual; mejor que lo encuentre leyendo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El contrato completo está en la &lt;a href="https://multita.com.ar/openapi.json" rel="noopener noreferrer"&gt;especificación OpenAPI&lt;/a&gt;, y hay una versión en texto plano pensada para pasarle a un asistente en &lt;a href="https://multita.com.ar/llms-full.txt" rel="noopener noreferrer"&gt;llms-full.txt&lt;/a&gt;. La documentación de la &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;API de consulta y cotización&lt;/a&gt; tiene los ejemplos completos.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>saas</category>
    </item>
    <item>
      <title>Entity SEO: how sameAs, Wikidata and Crunchbase tell Google your brand is real</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/entity-seo-how-sameas-wikidata-and-crunchbase-tell-google-your-brand-is-real-3h4e</link>
      <guid>https://dev.to/multita/entity-seo-how-sameas-wikidata-and-crunchbase-tell-google-your-brand-is-real-3h4e</guid>
      <description>&lt;p&gt;Most SEO advice is about links and keywords. There is a quieter layer that matters more for a young brand: entity SEO, the job of convincing Google (and the AI models reading the same graph) that your brand is one real, identifiable thing. Here is what actually moved the needle for &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  sameAs is not a backlink
&lt;/h2&gt;

&lt;p&gt;The common confusion: people think linking out to their own profiles drains authority. &lt;code&gt;sameAs&lt;/code&gt; is not that. It lives in your structured data and it is an identity statement, not a vote. It says "these profiles and this domain are the same entity". It passes no PageRank and it costs you nothing.&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="nl"&gt;"sameAs"&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="s2"&gt;"https://www.linkedin.com/company/multita/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"https://www.crunchbase.com/organization/multita"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"https://www.wikidata.org/wiki/Q..."&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;
  
  
  Wikidata and Crunchbase: the graph the models read
&lt;/h2&gt;

&lt;p&gt;A Wikidata item and a Crunchbase profile are not big traffic sources. Their value is being structured, trusted nodes about your entity, the kind that get pulled into the Knowledge Graph that Google and AI assistants consult. For a new company with no Wikipedia page yet, they are the most credible "this exists" signal you can plant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters for AI answers
&lt;/h2&gt;

&lt;p&gt;When someone asks an assistant about your category, the model names you correctly only if your entity is well defined across these sources. Vague entity, vague or wrong citation. A consistent name, URL and description repeated across your structured data, your Wikidata item and your profiles is what makes the answer land on you.&lt;/p&gt;

&lt;p&gt;Entity SEO will not rank a page by itself. It makes everything else you do attach to a brand the machines actually recognize, and for a new site that recognition is worth more than another handful of links.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>marketing</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Getting a brand-new site read by Google and AI: IndexNow, llms.txt and structured data</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/getting-a-brand-new-site-read-by-google-and-ai-indexnow-llmstxt-and-structured-data-1dp0</link>
      <guid>https://dev.to/multita/getting-a-brand-new-site-read-by-google-and-ai-indexnow-llmstxt-and-structured-data-1dp0</guid>
      <description>&lt;p&gt;A new domain has no authority and no crawl history, so the default is to wait weeks for Google to notice you. You can skip a lot of that wait. When we launched &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;, three cheap and boring pieces did most of the work of getting indexed and getting quoted by AI assistants.&lt;/p&gt;

&lt;h2&gt;
  
  
  IndexNow: stop waiting for the crawler
&lt;/h2&gt;

&lt;p&gt;IndexNow lets you ping search engines the moment a page changes, instead of waiting to be crawled. You drop a key file at your root and POST the changed URLs. On Cloudflare it is a few lines in a Worker. New pages got picked up in hours, not days.&lt;/p&gt;

&lt;h2&gt;
  
  
  llms.txt: a map for the models
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a plain text file at your root that points AI crawlers at your most useful pages, in clean markdown. It is young as conventions go, but it costs nothing and it tells a model "this is what the site is about" without making it fight your nav and footer to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data: say what you are in a language machines trust
&lt;/h2&gt;

&lt;p&gt;JSON-LD is the part both Google and the models lean on. An &lt;code&gt;Organization&lt;/code&gt; and a &lt;code&gt;WebSite&lt;/code&gt; block state your name, your URL and what you do, as data instead of guesswork.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Organization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Multita&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://multita.com.ar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is clearer entity recognition, a real shot at a brand panel, and a much higher chance that an AI answer cites you correctly instead of inventing something.&lt;/p&gt;

&lt;p&gt;None of this buys authority. It buys legibility, which for a new site is the thing standing between you and being found. Ship all three on launch day.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>ai</category>
    </item>
    <item>
      <title>API design: raw data, or totals with the business logic baked in?</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/api-design-raw-data-or-totals-with-the-business-logic-baked-in-1och</link>
      <guid>https://dev.to/multita/api-design-raw-data-or-totals-with-the-business-logic-baked-in-1och</guid>
      <description>&lt;p&gt;When you expose a data API you hit this fork fast: do you return raw, normalized records and let the client do the math, or do you return totals with your business logic already applied? We faced it building the API for &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt;, which returns the traffic fines on a vehicle, and the answer that held up was "both, on purpose".&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for raw
&lt;/h2&gt;

&lt;p&gt;Raw, normalized records are honest. Each fine comes back with its amount, status and jurisdiction, and the integrator decides what to do with them. A developer building a dashboard wants exactly this. They have their own rules, and a pre-chewed total just gets in the way.&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;"items"&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;"jurisdiction"&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;"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;"debt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&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;"jurisdiction"&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;"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;"voluntary_payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&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="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;
  
  
  The case for baked-in totals
&lt;/h2&gt;

&lt;p&gt;Now picture a different integrator: a WhatsApp bot that quotes a client in a single message. It does not want to reimplement your discount rules, your "which fines actually count" logic, or your rounding. It wants a number it can trust. If every integrator re-derives that number, they will all get it a little wrong, each in their own way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we ship both
&lt;/h2&gt;

&lt;p&gt;So the &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;Multita API&lt;/a&gt; returns the normalized items plus an optional summary with the totals already computed. Raw for the people who want control, totals for the people who want an answer. The rule we follow is narrow: never force the client to reimplement business logic to get a correct number, and never hide the raw data either.&lt;/p&gt;

&lt;p&gt;If you are designing an API, start from who integrates it. A team with its own logic and a bot that just wants the result are different customers, and you can serve both without picking a side.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Construir software para un mercado que los devs ignoran</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/construir-software-para-un-mercado-que-los-devs-ignoran-1gmj</link>
      <guid>https://dev.to/multita/construir-software-para-un-mercado-que-los-devs-ignoran-1gmj</guid>
      <description>&lt;p&gt;Los mejores nichos suelen ser los aburridos. Mientras todos hacen otra app de productividad, hay industrias enteras moviéndose con planillas, PDFs y trámites a mano, esperando que alguien se tome el laburo de entenderlas. Nosotros caímos en una de esas: los trámites del automotor en Argentina.&lt;/p&gt;

&lt;h2&gt;
  
  
  El nicho que nadie quiere
&lt;/h2&gt;

&lt;p&gt;Acá hay una profesión entera, las gestorías, que vive de hacer transferencias, informes y libre deudas para sus clientes. Parte de su día es revisar las multas de cada auto, porque una multa impaga frena una transferencia y la deuda viaja con el vehículo, no con el dueño anterior. El detalle: esas multas están repartidas en más de 33 sistemas oficiales que no se hablan entre sí.&lt;/p&gt;

&lt;p&gt;Ningún dev mira eso. No es sexy, no sale en Hacker News. Y justo por eso la competencia es floja y la demanda es real y constante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un mercado ignorado juega a favor
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;La gente ya tiene el problema y ya paga por resolverlo, hoy con horas de trabajo manual.&lt;/li&gt;
&lt;li&gt;La competencia rara vez es técnica, así que un producto que de verdad funcione gana casi solo.&lt;/li&gt;
&lt;li&gt;Entender el dominio es la barrera. Una vez que sabés qué es una "denuncia de venta" o por qué un acta que arranca con "01-" no tiene descuento de gestoría, tenés algo que un extraño no copia en un fin de semana.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso último es lo importante. En &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; la parte difícil no fue el código, fue aprender las reglas raras de cada jurisdicción y meterlas en el producto. Ese conocimiento es el foso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo entrar
&lt;/h2&gt;

&lt;p&gt;Hablá con la gente que sufre el problema antes de escribir una línea. Las reglas que te cuenten, las que no están documentadas en ningún lado, son las que hacen que tu producto sea el que funciona y no el que casi funciona.&lt;/p&gt;

&lt;p&gt;Si estás buscando qué construir, mirá los rubros que todos evitan por aburridos. Ahí está la demanda sin atender.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>startup</category>
      <category>saas</category>
      <category>career</category>
    </item>
    <item>
      <title>Rate limiting de una herramienta pública con Cloudflare KV y D1</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Mon, 13 Jul 2026 11:59:00 +0000</pubDate>
      <link>https://dev.to/multita/rate-limiting-de-una-herramienta-publica-con-cloudflare-kv-y-d1-2b7p</link>
      <guid>https://dev.to/multita/rate-limiting-de-una-herramienta-publica-con-cloudflare-kv-y-d1-2b7p</guid>
      <description>&lt;p&gt;Si tu producto tiene una parte gratis y pública, es cuestión de horas hasta que alguien la quiera exprimir con un script. Nosotros tenemos una &lt;a href="https://multita.com.ar/consultar-multas" rel="noopener noreferrer"&gt;consulta de multas gratis&lt;/a&gt; que cualquiera usa sin registrarse, así que el rate limiting no era opcional. Lo resolvimos entero en el borde, con Cloudflare KV y D1, sin servidor propio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quién hace qué
&lt;/h2&gt;

&lt;p&gt;La división que nos funcionó es simple. KV guarda el contador, porque tiene TTL nativo y lecturas baratas en cada PoP. D1 (SQLite en el borde) guarda lo que sí querés conservar y cruzar después, como los leads del formulario.&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rl:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&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;hoy&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RATE&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="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usado&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;LIMITE_DIARIO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Probá mañana&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usado&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El TTL de 86400 hace que la clave se borre sola al otro día. No limpiás nada, y el set de claves nunca crece más allá de "gente que consultó hoy".&lt;/p&gt;

&lt;h2&gt;
  
  
  La trampa de la consistencia
&lt;/h2&gt;

&lt;p&gt;KV es eventualmente consistente, así que entre dos PoP hay una ventanita de un segundo donde alguien podría pasarse del límite. Para frenar abuso eso no me quita el sueño. Si necesitaras un límite estricto, con plata de por medio, ahí conviene un Durable Object, que te da consistencia fuerte a cambio de algo de latencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  El límite como embudo
&lt;/h2&gt;

&lt;p&gt;Un detalle de producto: el límite no es solo defensa, es conversión. Cuando alguien llega al tope, ese es el momento justo para ofrecerle el plan sin límite. El 429 deja de ser un error y pasa a ser una puerta.&lt;/p&gt;

&lt;p&gt;Si vas a abrir algo gratis al público, poné el rate limiting desde el día uno. Sale mucho más barato que sacarte un scraper de encima 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>cloudflare</category>
      <category>serverless</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <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>
