<?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>Rendimiento en un sitio estático de 1.382 páginas: qué medimos con Lighthouse y qué no sirve medir</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/rendimiento-en-un-sitio-estatico-de-1382-paginas-que-medimos-con-lighthouse-y-que-no-sirve-medir-1fd1</link>
      <guid>https://dev.to/multita/rendimiento-en-un-sitio-estatico-de-1382-paginas-que-medimos-con-lighthouse-y-que-no-sirve-medir-1fd1</guid>
      <description>&lt;p&gt;Tenemos un sitio estático de 1.382 páginas hecho con Astro y publicado en Cloudflare. Esta semana lo medimos con Lighthouse 13 contra producción para ver si las páginas nuevas (un directorio de registros y varias notas) bajaban los puntajes. Estos son los números y lo que aprendimos al medir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los resultados
&lt;/h2&gt;

&lt;p&gt;Cinco páginas de plantillas distintas, medidas en modo escritorio simulado de Lighthouse:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Página&lt;/th&gt;
&lt;th&gt;Rendimiento&lt;/th&gt;
&lt;th&gt;Accesibilidad&lt;/th&gt;
&lt;th&gt;Buenas prácticas&lt;/th&gt;
&lt;th&gt;SEO&lt;/th&gt;
&lt;th&gt;LCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Directorio (portada)&lt;/td&gt;
&lt;td&gt;98 y 100 en dos corridas&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2,1 s y 1,6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provincia&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1,9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registro individual&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2,1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nota larga con tablas&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1,9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nota sobre consultas&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1,9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El desplazamiento del diseño (CLS) dio entre 0 y 0,002, y el tiempo de bloqueo (TBT) dio 0 ms en todas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo primero: dos puntos son ruido
&lt;/h2&gt;

&lt;p&gt;La misma página del directorio dio 98 en una corrida y 100 en la siguiente. La portada del sitio, que usamos de referencia, dio 98 en esa misma tanda. Una diferencia de uno o dos puntos entre corridas del mismo código no es una regresión: es variación del entorno de medición. Antes de buscar un problema, repetí la medición.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué ayudó, en concreto
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scripts propios con hash y minificados.&lt;/strong&gt; En el build, los scripts de la carpeta pública se renombran con un hash de su contenido y se minifican. En una corrida del build bajaron de 106,2 KB a 36,1 KB (66% menos), con las referencias reescritas en todas las páginas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Módulos cargados después del primer pintado.&lt;/strong&gt; Los scripts que no hacen falta para ver la página se cargan con baja prioridad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un solo script en línea, habilitado por hash.&lt;/strong&gt; La política de seguridad de contenido (CSP) permite ese script por su hash, sin abrir &lt;code&gt;unsafe-inline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin analítica de terceros en las páginas.&lt;/strong&gt; No cargamos Google Analytics ni etiquetas de seguimiento: medimos el tráfico con Search Console. Cada script externo cuesta puntos de rendimiento, y el dato que aporta ya lo da el buscador.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué medir y cómo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una página por plantilla, no solo la portada.&lt;/strong&gt; Una prueba de accesibilidad de la portada nunca va a encontrar un problema de contraste que vive en otra plantilla. En nuestro caso, un enlace dentro de un párrafo gris daba contraste insuficiente solo en la página de la API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendimiento contra producción, el resto en local.&lt;/strong&gt; Cloudflare comprime el HTML y en local no se reproduce, así que el puntaje de rendimiento local sale peor de lo que es. Accesibilidad, buenas prácticas y SEO sí se pueden medir en local con las mismas cabeceras de seguridad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lo corras en cada build.&lt;/strong&gt; Necesita un navegador y tarda unos 30 segundos por página. Lo corremos antes de cambiar una plantilla o los estilos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Un detalle que nos engañó
&lt;/h2&gt;

&lt;p&gt;Una página de una calculadora salía en rojo en buenas prácticas en el entorno local por un error 502 en la consola del navegador. Era el servicio de valores que no existe en local. En producción, la misma página daba 100 y cero errores. Antes de tocar código, confirmá el problema contra producción.&lt;/p&gt;

&lt;p&gt;Las páginas medidas son parte del sitio: podés ver el &lt;a href="https://multita.com.ar/registros-automotor" rel="noopener noreferrer"&gt;directorio de registros del automotor&lt;/a&gt; o la &lt;a href="https://multita.com.ar/herramientas" rel="noopener noreferrer"&gt;sección de herramientas&lt;/a&gt; y medirlas por tu cuenta.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>astro</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Contrastamos las notas de un sitio de trámites con las normas oficiales y encontramos errores en una decena</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/contrastamos-las-notas-de-un-sitio-de-tramites-con-las-normas-oficiales-y-encontramos-errores-en-49l0</link>
      <guid>https://dev.to/multita/contrastamos-las-notas-de-un-sitio-de-tramites-con-las-normas-oficiales-y-encontramos-errores-en-49l0</guid>
      <description>&lt;p&gt;Un sitio de trámites vive de que sus números sean ciertos: plazos, descuentos, puntos de la licencia. Hicimos algo que casi nadie hace después de publicar: volver a cada nota con mayor tráfico y contrastar cada cifra con la norma o la página oficial. Encontramos errores en una decena de notas. Acá van los patrones, porque sirven para cualquier contenido que cite normas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores, por tipo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Un plazo atribuido a una ley que no lo fija.&lt;/strong&gt; La nota sobre el descargo de una multa decía "5 días hábiles". La Ley 13.927 de la Provincia de Buenos Aires no fija ese plazo: lo deja a la reglamentación. El &lt;a href="https://normas.gba.gob.ar/documentos/B3yPzhj0.html" rel="noopener noreferrer"&gt;Decreto 532/09&lt;/a&gt; lo establece en 15 días desde el vencimiento del pago voluntario. Los 5 días son para apelar la sentencia, que es otra cosa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Un descuento con el plazo equivocado.&lt;/strong&gt; Una nota decía que en la Ciudad se paga con 50% de descuento dentro de los 30 días corridos. La Ley 451 (art. 18) dice 40 días corridos y agrega algo que nadie contaba: si pedís la intervención del controlador dentro de ese plazo y resultás condenado, la bonificación es del 25%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Una consecuencia exagerada.&lt;/strong&gt; Varias notas decían que al llegar a cero puntos "te suspenden el registro". Según la página oficial de la Ciudad, se inicia un trámite de inhabilitación: 60 días la primera vez, y desde la segunda caduca la licencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Un adjetivo en lugar de un dato.&lt;/strong&gt; "Cruzar en rojo es una falta grave que descuenta muchos puntos" no ayuda a nadie. La tabla oficial dice 5 puntos. Reemplazamos el adjetivo por el número, con la fuente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Una fuente que no decía lo que la nota le atribuía.&lt;/strong&gt; Una nota sobre el pago voluntario citaba una página oficial como respaldo, pero esa página no habla del pago voluntario. El respaldo real era el decreto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Un matiz que cambia el sentido.&lt;/strong&gt; Una nota decía que la Unidad Fija equivale a "un litro de nafta premium". El decreto dice "nafta de mayor octanaje", que es una definición distinta.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una jerarquía de fuentes.&lt;/strong&gt; La norma publicada manda. Después, la página oficial del organismo. Los medios y los sitios de consulta de terceros no alcanzan para afirmar un plazo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"No pude verificarlo" es una respuesta válida.&lt;/strong&gt; Cuando una cifra solo aparecía en sitios de terceros que se contradecían (por ejemplo, el plazo de pago voluntario de un municipio), la nota pasó a decir que el plazo figura en el acta y que las fuentes no coinciden. Perder precisión aparente es mejor que publicar un número falso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pruebas que impiden repetir un error corregido.&lt;/strong&gt; Para el error más grave del rubro (decir que las multas frenan una transferencia, cuando el DNU 70/2023 lo eliminó) hay una prueba que falla el build si cualquier nota lo vuelve a escribir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fecha y fuente en cada dato.&lt;/strong&gt; Una cifra sin fecha envejece sin que nadie lo note.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué nos llevamos
&lt;/h2&gt;

&lt;p&gt;Los errores no vinieron de mala fe sino de copiar el plazo de una norma a otra, de resumir de más y de confiar en la primera página que aparece en un buscador. Revisarlo cuesta unas horas y evita que una persona pierda un descuento o un plazo por un dato nuestro.&lt;/p&gt;

&lt;p&gt;Si te sirve de ejemplo, así quedaron dos de las notas: &lt;a href="https://multita.com.ar/blog/como-hacer-descargo-multa-transito" rel="noopener noreferrer"&gt;cómo hacer el descargo de una multa&lt;/a&gt; y &lt;a href="https://multita.com.ar/blog/scoring-licencia-caba-puntos" rel="noopener noreferrer"&gt;cómo funciona el scoring de la licencia en CABA&lt;/a&gt;, con la tabla de puntos y la fuente oficial. La &lt;a href="https://www.argentina.gob.ar/normativa/provincial/ley-451-123456789-0abc-defg-154-0000xvorpyel/actualizacion" rel="noopener noreferrer"&gt;Ley 451&lt;/a&gt; está publicada para quien quiera contrastar.&lt;/p&gt;

</description>
      <category>argentina</category>
      <category>testing</category>
      <category>documentation</category>
      <category>learning</category>
    </item>
    <item>
      <title>Cómo calcular el MRPA, el nuevo arancel del Registro Automotor, con la API de la UVA del BCRA</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/como-calcular-el-mrpa-el-nuevo-arancel-del-registro-automotor-con-la-api-de-la-uva-del-bcra-4180</link>
      <guid>https://dev.to/multita/como-calcular-el-mrpa-el-nuevo-arancel-del-registro-automotor-con-la-api-de-la-uva-del-bcra-4180</guid>
      <description>&lt;p&gt;Desde el 1 de septiembre de 2026, los aranceles del Registro Automotor dejaron de expresarse en pesos. Ahora se expresan en &lt;strong&gt;MRPA&lt;/strong&gt; (Módulo de Registro de la Propiedad del Automotor), una unidad que creó la Resolución 308/2026 del Ministerio de Justicia. Si hacés software que calcula costos de trámites del automotor, necesitás el valor del MRPA en pesos, y se calcula con datos públicos.&lt;/p&gt;

&lt;h2&gt;
  
  
  La fórmula
&lt;/h2&gt;

&lt;p&gt;Según la resolución:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un MRPA vale &lt;strong&gt;media UVA&lt;/strong&gt; (0,5 UVA).&lt;/li&gt;
&lt;li&gt;La UVA que se toma es la del BCRA del &lt;strong&gt;día hábil anterior al inicio del período&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El valor se actualiza &lt;strong&gt;cada cuatro meses&lt;/strong&gt; y se &lt;strong&gt;redondea hacia abajo&lt;/strong&gt; a un múltiplo de $10.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La resolución está en el &lt;a href="https://www.boletinoficial.gob.ar/detalleAviso/primera/344105/20260707" rel="noopener noreferrer"&gt;Boletín Oficial&lt;/a&gt;, y el &lt;a href="https://www.dnrpa.gov.ar/portal_dnrpa/guia_tramites/anexo_aranceles20260901_automotores.pdf" rel="noopener noreferrer"&gt;anexo de aranceles de la DNRPA&lt;/a&gt; fija cuántos MRPA cuesta cada trámite.&lt;/p&gt;

&lt;h2&gt;
  
  
  De dónde sale la UVA
&lt;/h2&gt;

&lt;p&gt;El BCRA publica la UVA en su API de estadísticas. La versión que anda es la 4.0; las anteriores (2 y 3) están dadas de baja:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.bcra.gob.ar/estadisticas/v4.0/Monetarias/31?desde=2026-08-31&amp;amp;hasta=2026-08-31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La respuesta trae la fecha y el valor. Para el 31 de agosto de 2026, el valor es 2.096,27.&lt;/p&gt;

&lt;h2&gt;
  
  
  El cálculo en código
&lt;/h2&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;BCRA_UVA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.bcra.gob.ar/estadisticas/v4.0/Monetarias/31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;uvaDelDia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fechaISO&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;BCRA_UVA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?desde=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fechaISO&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;hasta=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fechaISO&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;j&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detalle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;detalle&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;detalle&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="nx"&gt;detalle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;valor&lt;/span&gt; &lt;span class="p"&gt;:&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;// si no hay cotización, viene vacío&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Media UVA, redondeada hacia abajo al múltiplo de $10&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mrpa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uva&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;uva&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&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;uva&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uvaDelDia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-08-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2096.27&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mrpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uva&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;      &lt;span class="c1"&gt;// 1040&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;mrpa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uva&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 33280&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La cuenta: 0,5 × 2.096,27 = 1.048,135, y redondeado hacia abajo al múltiplo de $10 da &lt;strong&gt;$1.040&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué sale con eso
&lt;/h2&gt;

&lt;p&gt;Con el MRPA de septiembre a diciembre de 2026 en $1.040:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trámite&lt;/th&gt;
&lt;th&gt;MRPA&lt;/th&gt;
&lt;th&gt;Pesos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transferencia de dominio (mínimo)&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;$33.280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Informe de dominio&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$4.160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Denuncia de venta&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;$4.160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificación de firma adicional&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$3.120&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Para la transferencia, el arancel es el 1% del valor del vehículo, con ese mínimo de 32 MRPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cosas que conviene saber
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hay valores circulando que no coinciden.&lt;/strong&gt; Al comparar páginas que publican el arancel, vimos cifras de $1.027,50 y de $1.050. Con la UVA oficial del día correspondiente, la cuenta da $1.040. Verificá siempre contra el dato original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El período cambia cada cuatro meses.&lt;/strong&gt; Tenés que recalcular a comienzos de enero, mayo y septiembre, con la UVA del último día hábil anterior. Nosotros lo dejamos como tarea programada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manejá la respuesta vacía.&lt;/strong&gt; Si la fecha no tiene cotización, el arreglo viene sin elementos. Una cuenta con &lt;code&gt;undefined&lt;/code&gt; te da un arancel de &lt;code&gt;NaN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardá la fecha de cálculo.&lt;/strong&gt; Mostrar "arancel al período septiembre a diciembre de 2026" evita que alguien lea un número vencido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los resultados con costos completos (arancel, sellos provinciales y verificación) están en &lt;a href="https://multita.com.ar/blog/cuanto-cuesta-transferir-un-auto" rel="noopener noreferrer"&gt;cuánto cuesta transferir un auto&lt;/a&gt;, y el costo de pedir el &lt;a href="https://multita.com.ar/blog/informe-de-dominio-automotor" rel="noopener noreferrer"&gt;informe de dominio&lt;/a&gt; también se expresa en MRPA. El estimador oficial de la DNRPA sigue siendo la referencia final para un trámite concreto.&lt;/p&gt;

</description>
      <category>argentina</category>
      <category>api</category>
      <category>javascript</category>
      <category>opendata</category>
    </item>
    <item>
      <title>Modelar el ciclo de vida de una multa de tránsito como máquina de estados, con los plazos reales</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/multita/por-que-una-multa-de-transito-cambia-de-monto-sin-que-hagas-nada-y-como-lo-modelamos-en-codigo-5237</link>
      <guid>https://dev.to/multita/por-que-una-multa-de-transito-cambia-de-monto-sin-que-hagas-nada-y-como-lo-modelamos-en-codigo-5237</guid>
      <description>&lt;p&gt;Cuando se modela una multa de tránsito, lo primero que aparece es un campo &lt;code&gt;debe: true/false&lt;/code&gt;. Es el error más común. Una infracción pasa por etapas, y cada etapa tiene un plazo que cambia el monto y las opciones de quien la recibe. Si el modelo no las distingue, el software le dice a alguien que "todavía puede pagar con descuento" cuando el plazo venció hace una semana.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las etapas
&lt;/h2&gt;

&lt;p&gt;De forma simplificada, el recorrido de una multa es:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Labrada y notificada.&lt;/strong&gt; El reloj de los plazos suele empezar con la notificación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pago voluntario.&lt;/strong&gt; Pagar reconociendo la infracción, con descuento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descargo.&lt;/strong&gt; Presentar la defensa ante el juzgado, si no se pagó.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentencia.&lt;/strong&gt; El juez resuelve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apelación.&lt;/strong&gt; Un plazo corto para recurrir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firme.&lt;/strong&gt; Si no se apeló o la apelación fracasó. Puede terminar en apremio (cobro judicial).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Los plazos, con su fuente
&lt;/h2&gt;

&lt;p&gt;Esto es lo que pudimos verificar en las normas, y lo que no:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provincia de Buenos Aires.&lt;/strong&gt; El &lt;a href="https://normas.gba.gob.ar/documentos/B3yPzhj0.html" rel="noopener noreferrer"&gt;Decreto 532/09&lt;/a&gt; fija 30 días desde la notificación para acogerse al pago voluntario (50% sobre el mínimo en UF). Si no se paga, hay 15 días desde el día siguiente al vencimiento para presentar el descargo, que el juez puede ampliar. La Ley 13.927 (art. 40) da 5 días para apelar la sentencia. El texto que consultamos no aclara si los 30 y los 15 días son hábiles o corridos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ciudad de Buenos Aires.&lt;/strong&gt; La &lt;a href="https://www.argentina.gob.ar/normativa/provincial/ley-451-123456789-0abc-defg-154-0000xvorpyel/actualizacion" rel="noopener noreferrer"&gt;Ley 451&lt;/a&gt; (art. 18) da 50% de bonificación por pago voluntario dentro de los 40 días corridos de notificado, y 25% si se pide la intervención del controlador dentro de ese plazo y hay condena. En el texto consultado no figura el plazo del descargo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excepciones.&lt;/strong&gt; En la Provincia, si el acta se labra en el acto, funciona como boleta de citación y hay que presentarse dentro de los 30 días corridos de la infracción. Para las fotomultas, la notificación debe llegar dentro de los 60 días hábiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Puede haber decretos modificatorios, así que el plazo que vale es el que dice la notificación.&lt;/p&gt;

&lt;h2&gt;
  
  
  El código
&lt;/h2&gt;

&lt;p&gt;La idea central: no guardes el estado, &lt;strong&gt;derivalo de las fechas y de las reglas de cada lugar&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;DIA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;REGLAS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Decreto 532/09: 30 días de pago voluntario, 15 de descargo&lt;/span&gt;
  &lt;span class="na"&gt;pba&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voluntario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descargo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// Ley 451, art. 18: 40 días corridos de pago voluntario; descargo no verificado&lt;/span&gt;
  &lt;span class="na"&gt;caba&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voluntario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descargo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;sumarDias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fecha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fecha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dias&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;DIA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;plazos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regla&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notificada&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;REGLAS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;regla&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;finVoluntario&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sumarDias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notificada&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voluntario&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="nx"&gt;finVoluntario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;finDescargo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descargo&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="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sumarDias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finVoluntario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descargo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regla&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notificada&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pagada&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="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;pagada&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pagada&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plazos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regla&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notificada&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;hoy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finVoluntario&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pago_voluntario&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finDescargo&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plazo_de_descargo_desconocido&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;hoy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finDescargo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;descargo_abierto&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a_resolver&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-01T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;estado&lt;/span&gt;&lt;span class="p"&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;n&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-20T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// "pago_voluntario"&lt;/span&gt;
&lt;span class="nf"&gt;estado&lt;/span&gt;&lt;span class="p"&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;n&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-05T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// "descargo_abierto"&lt;/span&gt;
&lt;span class="nf"&gt;estado&lt;/span&gt;&lt;span class="p"&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;n&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-30T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// "a_resolver"&lt;/span&gt;
&lt;span class="nf"&gt;estado&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="nx"&gt;n&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-11-30T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "plazo_de_descargo_desconocido"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Las decisiones que importan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Desconocido" es un estado, no un valor por defecto.&lt;/strong&gt; Para la Ciudad no teníamos el plazo del descargo. En lugar de inventar uno, el modelo devuelve &lt;code&gt;plazo_de_descargo_desconocido&lt;/code&gt;, y la pantalla puede decir "revisá la notificación".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Las reglas viven en un solo lugar, con su fuente.&lt;/strong&gt; Un objeto por jurisdicción, con el artículo al lado. Cuando cambia una norma se cambia un número y se ve de dónde sale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hábil o corrido es un dato, no un detalle.&lt;/strong&gt; Si la norma no lo aclara, guardalo como pendiente de confirmar. Sumar 30 días corridos cuando eran hábiles te da una fecha incorrecta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El reloj arranca en la notificación.&lt;/strong&gt; No en la fecha de la infracción, salvo las excepciones que la norma indique.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La prescripción tiene su propio reloj.&lt;/strong&gt; Es independiente de estas etapas y se modela aparte.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Y para quien solo quiere el resumen
&lt;/h2&gt;

&lt;p&gt;Si lo que querés es la versión para conductores, la explicamos en &lt;a href="https://multita.com.ar/blog/como-hacer-descargo-multa-transito" rel="noopener noreferrer"&gt;cómo hacer el descargo de una multa&lt;/a&gt; y en &lt;a href="https://multita.com.ar/blog/pronto-pago-multas-provincia" rel="noopener noreferrer"&gt;cómo funciona el pronto pago en Provincia&lt;/a&gt;. En un artículo anterior contamos cómo &lt;a href="https://dev.to/multita-com-ar/como-unifique-30-sistemas-de-multas-de-transito-de-argentina-en-una-sola-api-1i07"&gt;unificamos más de 30 sistemas de multas en una sola API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>argentina</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Cómo armamos un directorio de 1.482 registros del automotor con datos abiertos del DNRPA</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Fri, 09 Oct 2026 00:25:35 +0000</pubDate>
      <link>https://dev.to/multita/como-armamos-un-directorio-de-1482-registros-del-automotor-con-datos-abiertos-del-dnrpa-486i</link>
      <guid>https://dev.to/multita/como-armamos-un-directorio-de-1482-registros-del-automotor-con-datos-abiertos-del-dnrpa-486i</guid>
      <description>&lt;p&gt;Hacer un trámite del automotor en Argentina empieza por una pregunta simple: ¿a qué registro seccional tengo que ir? La respuesta existe, pero está repartida entre listados viejos, blogs que copian planillas y una consulta oficial que pide la dirección exacta del titular.&lt;/p&gt;

&lt;p&gt;El Portal de Datos Justicia publica el listado oficial de Registros Seccionales del DNRPA como un CSV, bajo licencia CC BY 4.0, y lo actualiza todos los meses. Con eso armamos un directorio de 1.482 registros de las 24 provincias, con dirección, teléfono y horario. Está en &lt;a href="https://multita.com.ar/registros-automotor" rel="noopener noreferrer"&gt;https://multita.com.ar/registros-automotor&lt;/a&gt; y estos son los problemas que tuvimos.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Los datos crudos no se pueden publicar tal cual
&lt;/h2&gt;

&lt;p&gt;El CSV trae nombres sin tildes, mayúsculas por todos lados y domicilios con formatos distintos. Armamos un script que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasa los nombres a capitalización normal y restituye las tildes con un diccionario de localidades y partidos.&lt;/li&gt;
&lt;li&gt;Corrige los pocos casos que el diccionario no cubre con un mapa de excepciones a mano.&lt;/li&gt;
&lt;li&gt;Descarta lo que no hace falta y es personal: el CSV incluye datos del encargado, y los dejamos afuera.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Un test de integridad encontró un error que no vimos a ojo
&lt;/h2&gt;

&lt;p&gt;Escribimos una prueba que revisa cada registro generado. Falló en los teléfonos: algunos quedaban como &lt;code&gt;()&lt;/code&gt; porque la fuente traía el campo vacío con paréntesis. El filtro ahora cuenta dígitos: si hay menos de seis, no hay teléfono y la página dice que no está informado.&lt;/p&gt;

&lt;p&gt;Las otras pruebas revisan que el archivo no esté truncado (más de 1.400 registros y las 24 provincias), que cada registro traiga código, denominación, dirección y provincia, que dos grupos de una misma provincia no compartan URL y que la fecha de los datos sea válida y no futura. Otra prueba confirma que no se guardan datos personales del encargado.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Una página por localidad, generada en el build
&lt;/h2&gt;

&lt;p&gt;El sitio está hecho con Astro y se genera de forma estática: una portada con buscador, una página por provincia y una por localidad, unas 400 en total. Donde varios registros comparten localidad (en la Ciudad de Buenos Aires son todos "Capital Federal") hay una sola página con la lista.&lt;/p&gt;

&lt;p&gt;El título y la descripción de cada página se arman con una cadena de opciones de más larga a más corta, y se usa la primera que entra en el límite. Así ninguna queda cortada en el resultado de búsqueda.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Un buscador sin servidor
&lt;/h2&gt;

&lt;p&gt;El buscador de la portada carga un índice JSON liviano y filtra en el navegador, sin tildes ni mayúsculas, por localidad, partido, número de registro o código postal. No hay backend ni base de datos: es un archivo que se genera en el build.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. La licencia y la fuente, a la vista
&lt;/h2&gt;

&lt;p&gt;CC BY 4.0 pide atribución. Cada página cita el listado oficial, su fecha ("datos al 5 de octubre de 2026") y aclara que Multita no es un organismo oficial: hay que confirmar el horario y sacar turno antes de ir.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Actualización mensual
&lt;/h2&gt;

&lt;p&gt;El mismo script baja el CSV vigente y reescribe el JSON. Lo corremos cada mes, revisamos el diff (registros nuevos, bajas, cambios de domicilio y horario), ejecutamos las pruebas y publicamos.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Con datos abiertos oficiales, la parte que más cuesta es limpiar y verificar, no armar la página.&lt;/li&gt;
&lt;li&gt;Una prueba que mira la salida real encuentra errores que una revisión de código no ve.&lt;/li&gt;
&lt;li&gt;Citar la fuente y la fecha en cada página es parte del producto, no un trámite de la licencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El directorio está acá: &lt;a href="https://multita.com.ar/registros-automotor" rel="noopener noreferrer"&gt;https://multita.com.ar/registros-automotor&lt;/a&gt;. Si ves un dato desactualizado, avisanos.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>astro</category>
      <category>argentina</category>
      <category>webdev</category>
    </item>
    <item>
      <title>App para consultar multas de tránsito en Argentina: 32 jurisdicciones desde el celular, sin Play Store</title>
      <dc:creator>Multita</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:32:23 +0000</pubDate>
      <link>https://dev.to/multita/app-para-consultar-multas-de-transito-en-argentina-32-jurisdicciones-desde-el-celular-sin-play-nmb</link>
      <guid>https://dev.to/multita/app-para-consultar-multas-de-transito-en-argentina-32-jurisdicciones-desde-el-celular-sin-play-nmb</guid>
      <description>&lt;p&gt;Si manejás en Argentina, sabés el problema: para saber si un auto tiene multas hay que entrar a un sistema distinto por cada provincia y cada municipio, cada uno con su captcha y sus horarios de caída. Un mismo auto puede deber en tres lados a la vez, y es facilísimo mirar uno solo y quedarse tranquilo de más.&lt;/p&gt;

&lt;p&gt;Hicimos una app que resuelve eso en una sola búsqueda. &lt;strong&gt;Se instala en el celular en unos segundos, es gratis, no pide un solo permiso y no está en ninguna tienda.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Este post es las dos mitades: qué hace, por si te sirve usarla, y cómo la construimos sin pasar por Google Play ni la App Store, por si te sirve copiar el enfoque.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace, concretamente
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;Multita&lt;/a&gt; busca por &lt;strong&gt;patente, DNI o CUIT&lt;/strong&gt; y consulta &lt;strong&gt;32 jurisdicciones argentinas&lt;/strong&gt; de una vez, entre provinciales y municipales. Devuelve cada acta con su estado y su monto en pesos.&lt;/p&gt;

&lt;p&gt;El detalle que importa: &lt;strong&gt;no te muestra un número guardado&lt;/strong&gt;. Cuando buscás, entra a los sistemas oficiales en ese momento y convierte cada acta a pesos con el valor vigente &lt;strong&gt;de su jurisdicción&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Eso no es un capricho de arquitectura. En Argentina las multas no están escritas en pesos, están escritas en Unidades Fijas, y cada jurisdicción actualiza la suya cuando quiere. En septiembre pasó dos veces en dos días: Provincia de Buenos Aires movió la suya un 0,44% el día 1, y la Ciudad la movió un &lt;strong&gt;23,5%&lt;/strong&gt; el día 2. El mismo auto, con actas de los dos lados, tiene dos aumentos distintos la misma semana. Un valor cacheado ahí no es un dato viejo: es un número inventado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se instala, sin tienda
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Android (Chrome):&lt;/strong&gt; entrás a multita.com.ar y aparece el aviso de instalar. Si no aparece, menú de tres puntos, "Instalar aplicación".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iPhone (Safari):&lt;/strong&gt; botón de compartir, "Agregar a inicio". Tiene que ser Safari, que es el único navegador del iPhone que lo permite.&lt;/p&gt;

&lt;p&gt;Queda con ícono propio, abre en su ventana sin barra de direcciones y arranca directo en la pantalla de consulta. Si mantenés apretado el ícono, tenés tres atajos: consultar, generar un descargo y novedades.&lt;/p&gt;

&lt;p&gt;Nada de esto descarga cuarenta megas ni pide acceso a tus contactos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué no la publicamos en una tienda (todavía)
&lt;/h2&gt;

&lt;p&gt;Las ventajas son reales y vale enumerarlas sin romanticismo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cero permisos.&lt;/strong&gt; No los necesita, así que no los pide. Eso solo ya cambia la conversión: nadie abandona en una pantalla de permisos que no existe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero espacio.&lt;/strong&gt; Pesa lo que pesa una página.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero actualizaciones.&lt;/strong&gt; Siempre abre la última versión. No existe el usuario trabado tres releases atrás.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una sola base de código y un solo deploy.&lt;/strong&gt; No hay build de Android, ni de iOS, ni esperar una review por arreglar una coma.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y lo que se pierde, que también es real: no aparecés en el buscador de la tienda, que para mucha gente sigue siendo donde se buscan las apps, y no tenés la ficha con reseñas que da confianza. Por eso estamos armando igual la versión de Play. Pero &lt;strong&gt;no era un prerrequisito para tener la app funcionando&lt;/strong&gt;, y esa es la parte que se suele asumir mal.&lt;/p&gt;

&lt;h2&gt;
  
  
  La parte difícil: el cache que puede mentirte
&lt;/h2&gt;

&lt;p&gt;Acá viene lo técnico, y es lo único de todo esto que fue realmente delicado.&lt;/p&gt;

&lt;p&gt;Una PWA necesita un service worker, que es un proxy que vos escribís entre la app y la red. Es lo que la hace instalable y lo que la hace abrir sin señal. También es lo que puede mentirle a tu propia aplicación.&lt;/p&gt;

&lt;p&gt;En nuestro caso hay una complicación poco común: &lt;strong&gt;una consulta es un job largo y durable&lt;/strong&gt;. Tarda minutos, se registra en la base, el browser la va poll-eando y un cron cierra los jobs que quedaron sin nadie mirando. Toda esa maquinaria existe por un bug caro: cuando el resultado dependía de que el browser polleara hasta el final, cerrar la pestaña equivalía a perder la consulta.&lt;/p&gt;

&lt;p&gt;Ahora ponele un cache adelante. Dos reglas que no se negocian:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La ruta del poll no se cachea. Nunca.&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="s1"&gt;/api/&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="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// a la red, siempre&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si el service worker contesta &lt;strong&gt;una sola vez&lt;/strong&gt; desde el cache, le miente al poll sobre el estado de un job: el browser cree que la consulta sigue corriendo cuando terminó, o al revés. Sería reintroducir, desde la capa que agregamos para mejorar la experiencia, exactamente el bug que toda la maquinaria de jobs existe para evitar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Las pantallas con token en la URL tampoco.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La pantalla de una consulta en curso lleva en la URL un token que &lt;em&gt;es&lt;/em&gt; la llave del resultado. Cachearla dejaría esa llave escrita en el disco del teléfono, sobreviviendo a la pestaña, &lt;strong&gt;a cambio de nada&lt;/strong&gt;: sin red esa pantalla no sirve igual, porque su contenido viene de la API que por la regla anterior no está cacheada. Todo el riesgo, cero beneficio.&lt;/p&gt;

&lt;p&gt;Hay una tercera que no es de seguridad sino de buenos modales: el sitio tiene más de 900 páginas, así que el cache de páginas tiene un techo de 40. Sin eso, alguien que navega una tarde se lleva medio sitio al disco sin haberlo pedido, gastando sus datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sin señal: qué anda y qué no
&lt;/h2&gt;

&lt;p&gt;Es donde más se promete de más en los posts de PWA, así que va derecho:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La app abre sin señal&lt;/strong&gt;, con su propia pantalla en lugar del dinosaurio, y lo que ya visitaste se sigue viendo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La consulta no anda sin señal, y es el diseño, no una limitación.&lt;/strong&gt; No hay copia de tus multas en el teléfono. Se entra a los organismos en el momento. Un resultado guardado sería un número viejo, y ya vimos lo que hace un 23,5% de golpe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"La app abre offline" y "la app funciona offline" son cosas distintas, y conviene tenerlo claro antes de escribir la primera línea de service worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probala
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Se instala desde &lt;a href="https://multita.com.ar" rel="noopener noreferrer"&gt;multita.com.ar&lt;/a&gt; en Android y iPhone. Gratis, sin registro, sin tienda.&lt;/li&gt;
&lt;li&gt;El paso a paso con el detalle de cada teléfono está en &lt;a href="https://multita.com.ar/blog/app-para-consultar-multas" rel="noopener noreferrer"&gt;cómo instalar la app para consultar multas&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Si querés integrarlo a lo tuyo, &lt;a href="https://multita.com.ar/api" rel="noopener noreferrer"&gt;la API devuelve las 32 jurisdicciones en JSON&lt;/a&gt;, con cada acta ya convertida a pesos al valor vigente de la suya.&lt;/li&gt;
&lt;li&gt;Y si te quedaste con la duda de los saltos: &lt;a href="https://multita.com.ar/blog/aumento-multas-caba-septiembre-2026" rel="noopener noreferrer"&gt;CABA subió 23,5% de un día para el otro&lt;/a&gt; mientras Provincia subía 0,44%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si estás por meterle un service worker a algo, la pregunta que más rinde no es qué cachear, sino &lt;strong&gt;qué pasa si servís esta respuesta justo cuando cambió&lt;/strong&gt;. Para la mayoría de las rutas la respuesta es "nada". Para dos o tres, es todo el producto.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>pwa</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
    <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>
  </channel>
</rss>
