<?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: Victor Cavero Gracia</title>
    <description>The latest articles on DEV Community by Victor Cavero Gracia (@victor_caverogracia_51ba).</description>
    <link>https://dev.to/victor_caverogracia_51ba</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%2F4031681%2Fe8502687-e288-43d7-bc2c-90a0074b259c.jpg</url>
      <title>DEV Community: Victor Cavero Gracia</title>
      <link>https://dev.to/victor_caverogracia_51ba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/victor_caverogracia_51ba"/>
    <language>en</language>
    <item>
      <title>La IA es el nuevo usuario de tu web: 4 cosas que cambian en tu API cuando el cliente es un agente</title>
      <dc:creator>Victor Cavero Gracia</dc:creator>
      <pubDate>Thu, 06 Aug 2026 19:55:21 +0000</pubDate>
      <link>https://dev.to/victor_caverogracia_51ba/la-ia-es-el-nuevo-usuario-de-tu-web-4-cosas-que-cambian-en-tu-api-cuando-el-cliente-es-un-agente-9d2</link>
      <guid>https://dev.to/victor_caverogracia_51ba/la-ia-es-el-nuevo-usuario-de-tu-web-4-cosas-que-cambian-en-tu-api-cuando-el-cliente-es-un-agente-9d2</guid>
      <description>&lt;p&gt;Estuve probando Holded durante unas semanas y no me gustó nada. No porque esté mal hecho, que no lo está. Es que está construido para otra época: una UI bonita donde tú entras, haces clic, subes la factura a mano y revisas el cuadro de mandos.&lt;/p&gt;

&lt;p&gt;El problema es que yo ya casi no entro a las webs. Le pregunto a un agente. Y el agente no puede usar una UI bonita.&lt;/p&gt;

&lt;p&gt;Esa frase suena a eslogan, así que voy a bajarla a decisiones técnicas concretas. Si aceptas que &lt;strong&gt;el agente es el nuevo usuario&lt;/strong&gt;, hay cuatro cosas de tu producto que cambian, y ninguna es "añadir un chatbot en la esquina".&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tu OpenAPI deja de ser documentación y pasa a ser la interfaz
&lt;/h2&gt;

&lt;p&gt;Cuando el usuario es humano, la API es un extra para integradores. Cuando el usuario es un agente, la API &lt;strong&gt;es&lt;/strong&gt; el producto y la UI es el extra.&lt;/p&gt;

&lt;p&gt;Eso tiene una consecuencia incómoda: la spec tiene que ser pública y completa, sin login previo. Un agente no puede descubrir lo que sabes hacer si le pides que se registre antes de leer el índice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.aikount.com/openapi.json | jq &lt;span class="s1"&gt;'.paths | keys | length'&lt;/span&gt;
&lt;span class="c"&gt;# 345&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;345 endpoints, OpenAPI 3.1, accesible sin autenticar. Si tu spec vive detrás de un portal de desarrolladores con registro, para un agente no existe.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Los endpoints tienen que hablar el idioma del dominio, no el de tu base de datos
&lt;/h2&gt;

&lt;p&gt;Este es el error que más he visto, y lo cometí yo primero.&lt;/p&gt;

&lt;p&gt;Un CRUD honesto expone &lt;code&gt;PATCH /documents/{id}&lt;/code&gt; y deja que el cliente descubra qué combinación de campos significa "concilia esto". Un humano con la documentación delante lo resuelve. Un agente se inventa la combinación, y en contabilidad inventarse una combinación significa un asiento mal hecho.&lt;/p&gt;

&lt;p&gt;La alternativa es exponer &lt;strong&gt;la intención&lt;/strong&gt;, no la mutación:&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 /api/v1/reconciliations/auto
POST /api/v1/purchases/autogen-from-movements
GET  /api/v1/contacts/{contact_id}/tax-suggestion
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Estos tres no son azúcar sintáctico sobre un CRUD. Cada uno encapsula una decisión de negocio que antes vivía en la cabeza del contable, y al exponerla como endpoint propio consigues dos cosas: el agente no tiene que reconstruirla, y tú puedes cambiarla sin romper a nadie.&lt;/p&gt;

&lt;p&gt;Regla práctica: si para hacer algo el agente necesita encadenar cuatro llamadas en un orden concreto, ese orden es un endpoint que te falta.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hay acciones que el agente puede hacer, y otras que no debe poder
&lt;/h2&gt;

&lt;p&gt;La parte que nadie cuenta de "dale herramientas a tu agente" es que algunas herramientas no deberían existir.&lt;/p&gt;

&lt;p&gt;En contabilidad la línea es bastante nítida: &lt;strong&gt;proponer es reversible, presentar no lo es.&lt;/strong&gt; Un agente puede leer un extracto, casar un cobro con su factura y proponer el apunte. Lo que no puede es cerrar un periodo o mandar algo a la AEAT sin que un humano lo mire.&lt;/p&gt;

&lt;p&gt;Eso se modela con estado, no con buenas intenciones en el prompt:&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 /api/v1/journal/{entry_id}/lock
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un asiento bloqueado ya no lo toca el agente. Y el registro Veri*Factu, que va encadenado por hash y no admite "perdón, me equivoqué", vive en su propio recurso con su propio reintento:&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;GET  /api/v1/verifactu/records
POST /api/v1/verifactu/records/{record_id}/retry
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fíjate en &lt;code&gt;retry&lt;/code&gt; como endpoint explícito. Cuando el cliente es un agente que reintenta solo, el reintento tiene que ser una operación que tú controlas y registras, no un bucle que el modelo improvisa. Si tu API no define qué significa reintentar, el agente lo definirá por ti, normalmente duplicando.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. El onboarding del agente es un producto en sí mismo
&lt;/h2&gt;

&lt;p&gt;Tenemos un botón que se llama "Conectar tu agente de IA". Lo que hace es generar una API key con scopes y darte el comando ya montado para pegárselo a Claude, ChatGPT o Gemini. Cero configuración manual, cero copiar la URL base de un PDF.&lt;/p&gt;

&lt;p&gt;La autenticación es Bearer, y admite tanto el JWT de sesión como una key larga con prefijo &lt;code&gt;agl_&lt;/code&gt;:&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;Authorization: Bearer agl_...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El prefijo no es decorativo. Cuando una credencial se te escapa en un log o en un repo, un prefijo reconocible es lo que permite detectarla y revocarla automáticamente. Si tus keys son un UUID pelado, no hay nada que buscar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que aprendí
&lt;/h2&gt;

&lt;p&gt;Construimos &lt;a href="https://aikount.com" rel="noopener noreferrer"&gt;Aikount&lt;/a&gt; para resolver nuestro propio problema: saber el estado real de mi SL con un mensaje de WhatsApp, en vez de abrir un panel y navegar seis pantallas. Es contabilidad española de verdad, con PGC, Modelo 303, Veri*Factu y conexión PSD2 a los bancos.&lt;/p&gt;

&lt;p&gt;Pero lo que me llevo como aprendizaje técnico va más allá del nicho:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publica la spec sin login o para un agente no existes.&lt;/li&gt;
&lt;li&gt;Expón intenciones, no mutaciones. Si hace falta encadenar llamadas en un orden fijo, falta un endpoint.&lt;/li&gt;
&lt;li&gt;Separa por diseño lo reversible de lo irreversible, y hazlo con estado, no con prompts.&lt;/li&gt;
&lt;li&gt;Trata el alta del agente como onboarding de usuario, porque eso es exactamente lo que es.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si quieres trastear, la spec está abierta en &lt;code&gt;api.aikount.com/openapi.json&lt;/code&gt; y el plan gratuito llega hasta 24.000 € facturados al año, sin tarjeta y sin módulos de pago por separado.&lt;/p&gt;

&lt;p&gt;Me interesa mucho el contraargumento, así que si crees que exponer intenciones en vez de CRUD es acoplar demasiado la API al dominio, dímelo en comentarios. Es la crítica que más me ronda.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>api</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Webhook-first API design: no polling, one signed callback, and an MCP server that is not a wrapper</title>
      <dc:creator>Victor Cavero Gracia</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:31:36 +0000</pubDate>
      <link>https://dev.to/victor_caverogracia_51ba/webhook-first-api-design-no-polling-one-signed-callback-and-an-mcp-server-that-is-not-a-wrapper-22n9</link>
      <guid>https://dev.to/victor_caverogracia_51ba/webhook-first-api-design-no-polling-one-signed-callback-and-an-mcp-server-that-is-not-a-wrapper-22n9</guid>
      <description>&lt;p&gt;Most video APIs I have integrated make you poll. You POST a job, get an id, and then write a loop that asks "are we there yet" every few seconds until something comes back or your timeout guess runs out. That loop is where the bugs live: doubled work on retries, jobs that hang forever on silence, and a scheduler that quietly burns rate limit while nothing happens.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://openshorts.app" rel="noopener noreferrer"&gt;OpenShorts&lt;/a&gt;, an open source tool that cuts long videos into vertical clips, we made three API decisions that removed most of that pain. Here they are, with the reasoning and the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One request in, one signed webhook out
&lt;/h2&gt;

&lt;p&gt;The whole automation loop is two HTTP messages. Start a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.openshorts.app/api/process &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer osk_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://youtube.com/watch?v=...",
       "acknowledged": true,
       "webhook_url": "https://your-server.com/hooks/openshorts",
       "webhook_secret": "your-shared-secret"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a job id back immediately. Minutes later, when the clips are cut, subtitled and archived, you receive exactly one POST on your webhook with the clip titles and durable download links.&lt;/p&gt;

&lt;p&gt;The part that matters more than it sounds: &lt;strong&gt;a failed job fires the webhook too.&lt;/strong&gt; If failure is silent, every consumer has to reinvent a timeout, and every one of them picks a different number. Making failure a delivered event instead of an absence of events is what lets a pipeline be stateless.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sign the callback, and compare in constant time
&lt;/h2&gt;

&lt;p&gt;If you pass a &lt;code&gt;webhook_secret&lt;/code&gt;, the delivery carries an &lt;code&gt;X-OpenShorts-Signature&lt;/code&gt; header shaped like &lt;code&gt;sha256=&amp;lt;hex&amp;gt;&lt;/code&gt;, which is the HMAC-SHA256 of the raw request body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-OpenShorts-Signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that get missed often enough to be worth repeating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hash the &lt;strong&gt;raw&lt;/strong&gt; body, not the parsed and re-serialised JSON. Key order and whitespace will not survive a round trip through your framework.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;compare_digest&lt;/code&gt;, not &lt;code&gt;==&lt;/code&gt;. A plain string comparison returns early on the first differing byte, which leaks how much of the prefix you guessed right.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Treat MCP as a first class surface, not a wrapper
&lt;/h2&gt;

&lt;p&gt;We shipped an MCP server at &lt;code&gt;mcp.openshorts.app/mcp&lt;/code&gt; so an agent can drive the pipeline directly. Connecting is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http openshorts https://mcp.openshorts.app/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer osk_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any client that speaks Streamable HTTP works the same way. The server describes itself over the protocol, tool schemas included, so there is nothing else to configure.&lt;/p&gt;

&lt;p&gt;Six tools cover the pipeline: &lt;code&gt;process_video&lt;/code&gt;, &lt;code&gt;get_job_status&lt;/code&gt;, &lt;code&gt;list_clips&lt;/code&gt;, &lt;code&gt;get_quota&lt;/code&gt;, &lt;code&gt;add_subtitles&lt;/code&gt; and &lt;code&gt;publish_clip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get_quota&lt;/code&gt; is the one I would argue hardest for. An agent that cannot see its own budget will happily start a job it cannot finish, and you find out from a failed webhook twenty minutes later. Exposing the remaining balance as a tool lets the model check before it spends, which turns a runtime failure into a planning decision.&lt;/p&gt;

&lt;p&gt;The other thing we refused to do was build the MCP server as a wrapper around a convenient subset of the REST API. Each tool calls the same pipeline the web app uses, with the same account, minutes and job history. The moment the agent surface is a subset, you have two products to keep in sync, and the agent one always loses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meter is an API design decision
&lt;/h2&gt;

&lt;p&gt;This is the part I did not expect to matter, and it turned out to matter most.&lt;/p&gt;

&lt;p&gt;Most tools in this space meter agent calls separately, per source minute or per operation. That is defensible billing and terrible ergonomics. An agent loop is exploratory by nature: it checks status, lists clips, retries the one that came out badly. If every call has a price, the correct engineering response is to write fewer, larger, more brittle calls, which is exactly the opposite of what you want from an agent.&lt;/p&gt;

&lt;p&gt;We made API calls draw from the same flat minute balance as the dashboard. No separate meter, no per-call pricing. On the self-hosted edition there is no meter at all, which is what makes an always on pipeline affordable to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling is whatever you already have
&lt;/h2&gt;

&lt;p&gt;Because starting a job is one POST, you do not need a scheduler integration. A cron line, a GitHub Action, an n8n HTTP Request node, all of them work without a dedicated connector. There is no official n8n template because the two step shape above is the entire integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub at &lt;a href="https://github.com/mutonby/openshorts" rel="noopener noreferrer"&gt;mutonby/openshorts&lt;/a&gt;, the core is MIT licensed, and the self-hosted edition serves the same MCP endpoint as the cloud.&lt;/p&gt;

&lt;p&gt;If you are building anything with webhooks and agents, the summary is short: deliver failures as events, sign the body and compare it in constant time, expose the budget as a tool, and do not put a price on the calls your agent needs to think.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>api</category>
      <category>ai</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>QA retest: title semantics fix</title>
      <dc:creator>Victor Cavero Gracia</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:10:58 +0000</pubDate>
      <link>https://dev.to/victor_caverogracia_51ba/qa-retest-title-semantics-fix-2lod</link>
      <guid>https://dev.to/victor_caverogracia_51ba/qa-retest-title-semantics-fix-2lod</guid>
      <description>&lt;p&gt;Este es el cuerpo del articulo. Si este texto aparece como body y el titulo es el de devto_title, el fix funciona.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>QA smoke test — Upload-Post staging</title>
      <dc:creator>Victor Cavero Gracia</dc:creator>
      <pubDate>Thu, 16 Jul 2026 07:59:44 +0000</pubDate>
      <link>https://dev.to/victor_caverogracia_51ba/qa-smoke-test-upload-post-staging-57ko</link>
      <guid>https://dev.to/victor_caverogracia_51ba/qa-smoke-test-upload-post-staging-57ko</guid>
      <description>&lt;p&gt;QA smoke test — Upload-Post staging&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
