UCP: el protocolo de checkout que dejó el pago abierto
Todos los protocolos de agentic commerce que desarmamos hasta ahora decidieron cómo se mueve el dinero. El Universal Commerce Protocol deliberadamente no lo hizo — y esa omisión es lo más interesante que tiene.
Esta es la cuarta especificación de agentic commerce que diseccionamos en un mes. El ACP de OpenAI y Stripe empaqueta checkout y pago en un solo token delegado. El Trusted Agent Protocol de Visa firma quién toca la puerta del merchant. Verifiable Intent, de Mastercard y Google, firma qué aprobó el humano. Los tres llegan con un rail atado.
UCP, anunciado por Google y Shopify el 2026-01-11 y publicado bajo Apache 2.0 en github.com/Universal-Commerce-Protocol/ucp, hace algo estructuralmente distinto. Define la conversación comercial — discovery, cart, checkout, order, fulfillment — y después define el pago como un slot enchufable que llenan especificaciones llamadas Payment Handlers, que puede escribir cualquiera que controle un dominio. El protocolo nunca nombra un rail.
Esa junta es donde debería estar mirando una empresa de infraestructura. Así que clonamos el repo en el commit a839e99 (2026-07-31), leímos la especificación de punta a punta y probamos los endpoints en vivo. Lo que sigue es la arquitectura, y después lo que salió de la auditoría.
El profile es la API
UCP no tiene registry, ni formulario de onboarding, ni comité de aprobación. Un negocio publica un único documento JSON en /.well-known/ucp que declara qué capabilities soporta, en qué versiones, sobre qué transports y con qué payment handlers. Una plataforma — un agente, una app, una superficie de compra — publica un documento de la misma forma en una URL propia y la anuncia por request con el header UCP-Agent.
La spec llama al resultado permissionless onboarding: cualquier plataforma con un profile descubrible puede transaccionar con cualquier negocio sin relación previa. Es el mismo instinto de diseño detrás de los namespaces basados en DNS del MCP Registry, y resuelve el mismo problema: cómo dejar que se integren desconocidos sin un gatekeeper.
Los nombres son reverse-domain: {reverse-domain}.{service}.{capability}. Las capabilities centrales de compra viven bajo dev.ucp.shopping.* — checkout, cart, order, fulfillment, catalog.search, catalog.lookup, discount, buyer_consent, split_payments, ap2_mandate. Cualquier otro usa su propio dominio: com.example.payments.installments.
Lo que vuelve esto más que una convención de nombres es el authority binding. Toda capability MUST declarar una URL schema, y el origin de esa URL MUST coincidir con la autoridad del namespace en su nombre. No puedes publicar una capability bajo dev.ucp.* salvo que sirvas su schema desde ucp.dev. La spec es precisa sobre qué te compra eso:
Este binding garantiza procedencia, no confianza: un binding válido solo prueba que el nombre reverse-domain está controlado por quien es dueño del dominio correspondiente. No afirma que la entidad sea confiable, correcta ni digna de soportarse.
Nota la asimetría: la URL schema está atada a la autoridad porque está en el camino de confianza de la máquina, mientras que la URL opcional spec es documentación y solo MUST ser HTTPS. La procedencia viaja con el schema, no con la prosa.
La negociación ocurre en cada transacción
La mayoría de los protocolos de integración se configuran una vez y corren. UCP recalcula en cada sesión. Ilya Grigorik, el ingeniero de Shopify que escribió el write-up de lanzamiento de la empresa, da la razón sin vueltas: "Cambia el carrito, cambia la región del comprador, cambia cualquier variable y los handlers pueden cambiar."
El negocio busca el profile de la plataforma, computa la intersección, y el resultado es el conjunto de capabilities vivas para esa sesión. El algoritmo tiene cuatro pasos:
Match de nombre, después versión, después poda
1. Computar la intersección. Incluir una capability del negocio si existe una de la plataforma con el mismo name. Match exacto de string sobre el nombre reverse-domain.
2. Seleccionar versión. Tomar los strings de versión presentes en ambos arrays. Si no está vacío, elegir la más alta (fecha más reciente). Si está vacío, descartar la capability entera — no hay fallback.
3. Podar extensiones huérfanas. Quitar toda capability cuyo padre en extends esté ausente. Con un solo padre necesita ese padre presente; con múltiples, al menos uno.
4. Repetir el paso 3 hasta que no se quite nada más, lo que maneja cadenas transitivas de extensiones.
De ahí salen dos consecuencias. Primero, las extensiones son simplemente capabilities con un campo extends, así que la superficie de versionado es uniforme — no hay un ciclo de vida separado de extensiones que razonar. Segundo, un typo en un nombre no es una experiencia degradada, es ausencia silenciosa. Una capability que no matchea simplemente no está en la intersección, y ninguna de las dos partes está obligada a decir por qué. UCP separa esto de la falla de transporte con cuidado: una falla de discovery (no se puede traer o parsear el profile) es un error de transporte, mientras que una intersección vacía es una respuesta UCP normal con un continue_url opcional — la salida de emergencia que devuelve al comprador a un checkout web humano.
Payment handlers: la junta
El modelo de pagos de UCP existe para resolver un problema N-a-N entre plataformas, negocios y payment credential providers. Lo hace separando dos cosas que ACP funde: los payment instruments (qué se acepta) de los payment handlers (cómo se procesan los instrumentos).
Un handler es una especificación, no una empresa. Google Pay es el participante; com.google.pay es el handler que escribe. La división del trabajo es explícita: el credential provider define los schemas, el negocio configura el handler con sus propias llaves y merchant IDs, y la plataforma ejecuta el protocolo del provider para adquirir un token opaco. El negocio después cobra ese token por su relación de backend ya existente.
La spec llama a la estructura de abajo el trust triangle: negocio y credential provider tienen una relación legal preexistente; plataforma y credential provider interactúan solo para tokenizar; plataforma y negocio intercambian el resultado. Las credenciales fluyen solo de plataforma a negocio — los negocios MUST NOT devolverlas en las respuestas — y el handler_id en el payload le dice al negocio qué llave del provider usar, algo que la spec plantea como prevención de ataques de confusión de llaves.
Dos reglas operativas vale marcarlas porque es donde las implementaciones se van a equivocar. Los negocios MUST filtrar dinámicamente la lista de handlers contra el contexto del carrito — sacar Buy Now Pay Later de un carrito con suscripción, quitar métodos regionales que no coinciden con la dirección de envío. Y una submission de checkout MUST llevar exactamente un payment instrument salvo que dev.ucp.shopping.split_payments esté activo; las violaciones reciben un payment_failed en messages[].
La máquina de estados del checkout que envuelve todo esto es chica: incomplete, requires_escalation, ready_for_complete, complete_in_progress, completed, canceled. complete_in_progress es la ventana asíncrona — el request de complete fue aceptado pero la orden todavía no existe, que es exactamente la ventana donde una capa de settlement por debajo necesita estar reteniendo algo.
La capa de identidad es maquinaria que ya vimos
Acá UCP converge fuerte con el resto del campo. Todos los transports HTTP firman con RFC 9421 HTTP Message Signatures, los digests de body usan Content-Digest de RFC 9530 sobre bytes crudos, las llaves son JWKs publicadas en el mismo profile que lleva las capabilities, y la protección de replay se delega al idempotency-key de la capa de negocio. Es la misma base idéntica que Web Bot Auth y que el TAP de Visa, y no es coincidencia — UCP diseña explícitamente para interoperar con ambos.
ES256 es la línea base universal que toda implementación MUST verificar. ES384 y Ed25519 son OPTIONAL, y los vocabularios kty/crv/alg son abiertos: un verifier que encuentra una llave que no puede usar MUST NOT rechazar el key set entero, simplemente no puede usar esa llave. La elección de algoritmo se describe como dirigida por la contraparte — eliges lo que acepte cada audiencia que tu firma deba satisfacer, y publicas múltiples llaves solo cuando ningún algoritmo único alcanza.
La forma dual-audience es la parte ingeniosa. Un signer puede emitir una firma que satisfaga a UCP y a Web Bot Auth a la vez, con tag web-bot-auth y acompañada de un header Signature-Agent. Un verifier UCP simple resuelve la llave por UCP-Agent, trata signature-agent como un covered component común, y nunca implementa key discovery de WBA. Un verifier consciente de WBA puede en cambio resolver por Signature-Agent, en cuyo caso el keyid MUST ser igual al thumbprint SHA-256 RFC 7638 de la llave que matcheó.
Dos reglas de ese algoritmo merecen leerse dos veces. La primera es un conjunto cerrado de covered components que MUST estar firmados en todo régimen: @method, @authority, @path, @query cuando está presente, content-digest y content-type cuando hay body, y los headers ucp-agent, signature-agent e idempotency-key cuando están presentes. La spec enuncia el ataque que cierra: esto "impide que una firma que satisface solo el covered set mínimo de Web Bot Auth autentique un request UCP cuyo body, método o path quedan sin atar". El mínimo de Web Bot Auth es @authority y signature-agent. Aceptar eso como autenticación de un POST que coloca una orden sería un agujero.
Signature-Agent prueba control de esa fuente de llaves, no del profile de UCP-Agent, cuya URL es "meramente el valor de un header firmado". Las implementaciones MUST tratar el request como una sola identidad autenticada solo cuando ambas URLs normalizan a lo mismo. Si no, son dos identidades y decide la política. Esa es la disciplina de confused deputy a la que llegó la spec de autorización de MCP desde otra dirección.
Los mandates de AP2 y el bloqueo de seguridad
Para las compras autónomas UCP no inventa un formato de consentimiento. Aloja uno: la extensión dev.ucp.shopping.ap2_mandate lleva credenciales de AP2 dentro del checkout. Todos los campos de AP2 anidan bajo un único objeto ap2, lo que mantiene limpio el schema base y le da a la canonicalización una sola regla que seguir.
Una vez que la extensión cae en la intersección, la sesión queda, en palabras de la spec, Security Locked — ninguna de las partes puede volver a un checkout sin protección. En concreto: el negocio MUST incrustar ap2.merchant_authorization en toda respuesta de checkout, MUST NOT aceptar un request de complete sin ap2.checkout_mandate, y la plataforma MUST verificar la firma del negocio antes de mostrarle nada al usuario.
La firma del merchant es un JWS con payload detached — <header>..<signature>, donde el doble punto marca que el payload es el cuerpo del checkout mismo. El signing input cubre el header codificado además del payload, que según la spec es lo que frena la sustitución de algoritmo: no puedes reescribir alg sin romper la firma.
// sign_checkout — el payload es el checkout menos el campo ap2
payload = checkout sin "ap2"
canonical = jcs_canonicalize(payload) // RFC 8785
header = { "alg": "ES256", "kid": "merchant_2026" }
signing_input = b64u(header) + "." + b64u(canonical)
signature = sign(signing_input, private_key)
checkout.ap2.merchant_authorization = b64u(header) + ".." + b64u(signature)
La plataforma después produce dos credenciales SD-JWT con key binding — el mismo primitivo que la cadena de Verifiable Intent. El checkout_mandate va en ap2.checkout_mandate y MUST contener la respuesta de checkout completa incluyendo merchant_authorization, así que la firma de la plataforma cubre la firma del negocio. El payment_mandate viaja en payment.instruments[*].credential.token. Uno protege los términos, el otro protege los fondos.
La canonicalización es JCS (RFC 8785) en vez del Content-Digest sobre bytes crudos que se usa para los requests, y la spec explica por qué de una forma que la mayoría no se molesta en explicar: los mandates son evidencia durable que se recupera meses después, atraviesan plataforma, negocio, PSP y red de tarjetas — cada uno de los cuales puede re-serializar el JSON — así que cualquier parte debe poder reconstruir los bytes firmados a partir del contenido lógico.
Vale repetir una nota honesta de la spec. AP2 v0.2 es internamente inconsistente sobre los algoritmos de firma de mandates — specification.md exige una clase no determinística como ECDSA, mientras que sus consideraciones de seguridad enuncian una regla de entropía que cualquier algoritmo satisface con suficiente entropía en el payload. UCP apunta al issue #268 de AP2 y difiere. Bajo la lectura de entropía, una sola llave Ed25519 podría servir para firmar mandates de AP2 y para Web Bot Auth; bajo la lectura de clase de algoritmo no puede, y cargas dos llaves.
Lo que encontró la auditoría
El repo está genuinamente vivo. Al 2026-08-03: 3,259 stars, 432 forks, 236 commits de 54 contribuyentes, 81 issues abiertos y 79 pull requests abiertas, con commits entrando el mismo día en que clonamos. Los que más commitean son los ingenieros de Shopify y Google que lo escribieron, no un equipo de marketing. Los repos hermanos — python-sdk, js-sdk, conformance, ucp-schema (un validador en Rust) — están todos bajo Apache 2.0 y todos con push en las últimas dos semanas. Esto no es otro repo de anuncio congelado tras el lanzamiento.
Tres observaciones, en orden creciente de cuánto deberían afectar tus planes.
Released contra draft. Hay tres tags: v2026-01-11, v2026-01-23 y v2026-04-08, publicado el 2026-04-09. Desde entonces, casi cuatro meses de movimiento en main y 79 PRs abiertas, sin tag nuevo. Mientras tanto mkdocs.yml fija ucp_version: "draft", así que toda URL de schema en la documentación renderiza contra /draft/. Ambas resuelven en vivo — https://ucp.dev/draft/schemas/shopping/checkout.json y https://ucp.dev/2026-04-08/schemas/shopping/checkout.json devuelven 200 — pero no son el mismo documento, y "seguir los docs" y "pinnear al release" son hoy dos integraciones distintas. Los valores de $id sin versión en el código fuente del repo (https://ucp.dev/schemas/profile.json) son placeholders de build reescritos al path versionado en la publicación; buscados directamente dan 404.
Un nombre que nunca puede intersectar. La capability canónica es singular — "name": "dev.ucp.shopping.ap2_mandate" en source/schemas/shopping/ap2_mandate.json. El documento del playground interactivo la declara dos veces en plural, dev.ucp.shopping.ap2_mandates, en un fixture de profile y en una lista de capabilities. El paso 1 del algoritmo de intersección es un match exacto sobre name. Un negocio que copie ese fixture anuncia una capability que ninguna plataforma matcheará jamás, no recibe ningún error, y simplemente nunca entra al checkout protegido por AP2. Es un bug de dos caracteres en la página que más probablemente copien los implementadores — la misma clase de defecto que el desajuste sig1/sig2 que encontramos en la implementación de referencia de Visa, y mucho más fácil de arreglar.
No existe un rail de stablecoins. Este es el hallazgo que nos importa. Una búsqueda de texto completo sobre la especificación y los schemas en a839e99 devuelve cero coincidencias para x402, stablecoin o USDC. Los únicos hits de "crypto" son librerías criptográficas y un campo cryptogram de EMV. Los handlers demostrados son com.google.pay, dev.shopify.shop_pay y un mock. Incluso el Escenario C — la sección titulada literalmente "Autonomous Agent (AP2)", el flujo recomendado para agentes — envía "credential": {"type": "card", "token": "..."}. El agente recibe mandates criptográficos; el dinero sigue moviéndose por una tarjeta.
Qué significa para LLM4Agents
UCP no compite con x402, y tratarlo como competidor sería un error de categoría. Estandariza la conversación comercial — qué hay en el carrito, cuánto cuesta, quién consintió, a dónde se envía. x402 estandariza el settlement de un solo request HTTP. Un checkout UCP podría liquidarse sobre cualquier rail que nombre su handler.
Lo que UCP cambia para nosotros es la forma de la oportunidad. ACP fusionó checkout y pago, así que agregar un rail de stablecoins a ACP significa discutir con OpenAI y Stripe sobre su token. UCP puso una junta documentada exactamente donde se enchufa un rail, y volvió el namespace permissionless: un payment handler es un nombre reverse-domain más un schema servido desde el origin que coincide. No hay comité al que peticionar. Cualquiera que sea dueño de un dominio puede escribir un handler y cualquier negocio puede anunciarlo.
La asimetría corta también en el otro sentido. El checkout de UCP tiene forma de retail — line items, destinos de envío, impuestos, devoluciones, políticas de garantía, loyalty. Nuestro tráfico es machine-to-machine: un agente comprando inferencia, storage, una llamada a una tool. Eso no necesita carrito. El solapamiento realista no es "los agentes compran widgets por nuestro gateway"; es que un agente nativo de UCP ya habla un profile, ya firma RFC 9421, ya resuelve llaves desde un documento well-known. Ese agente está a un handler de pagarnos como le paga a un merchant, y cada pieza de plomería de identidad que construyó para UCP es plomería que no tiene que construir para x402.
La amenaza es más acotada y vale nombrarla. Si los handlers de las redes de tarjetas terminan siendo los únicos con despliegues en producción, "pago agéntico" pasa a significar "tarjeta tokenizada" por default, y el settlement en stablecoins queda como un nicho para bienes con forma de API. Que la junta esté abierta no es lo mismo que la junta esté usada.
Cómo mantenerse en la frontera
Cuatro cosas, en orden.
Publicar un payment handler. Controlamos llm4agents.com, que es todo lo que exige el authority binding de UCP. Un handler com.llm4agents.x402 — schema de config para las redes y el asset aceptados, schema de instrument para un payload de pago x402, ejecutado por la plataforma contra nuestro facilitator — es un artefacto chico y autocontenido que cuesta un dominio y un archivo JSON. También es la forma más barata posible de tener el primer handler de stablecoins en un ecosistema que hoy no tiene ninguno. Publicarlo como draft, linkearlo desde los docs, y abrir un issue upstream en vez de una PR: el authority binding significa que no necesita vivir en dev.ucp.* para ser legítimo.
Servir un profile. Publicar /.well-known/ucp declarando nuestras llaves de identidad antes de declarar cualquier capability de compra. El profile es un JWK Set por construcción, y el mismo documento sirve para el key lookup de UCP y para la resolución jwks_uri de Web Bot Auth. Es un artefacto que satisface tres specs — UCP, WBA y la forma de directorio que espera el TAP de Visa — y es el prerrequisito de cada paso posterior.
Firmar con la forma dual-audience. Cuando nuestros agentes llamen hacia afuera, emitir firmas RFC 9421 que satisfagan el conjunto cerrado de covered components de UCP — method, authority, path, query, content-digest, content-type, más ucp-agent e idempotency-key. Ese conjunto es estrictamente más fuerte que el mínimo de WBA, así que una firma construida al estándar de UCP pasa también por verifiers de WBA. Construir al estándar más débil y retrofitear después significa refirmar todo.
Mapear la máquina de estados a reserve-then-settle. complete_in_progress es un checkout aceptado y todavía sin orden — la misma ventana que ya modela nuestro ciclo reserve → proxy → settle, y la misma que cubre el scheme upto cuando el monto final no se conoce al momento de autorizar. Si implementamos checkout de UCP, la reserva debería abrirse en ready_for_complete y liquidarse en la transición terminal, no antes. El protocolo nos da los nombres de los estados gratis; la disciplina de no liquidar temprano es nuestra.
UCP es el primero de estos protocolos donde la jugada útil no es un post comparativo. Es un archivo JSON servido desde un dominio que ya tenemos.
Paga por llamada, en stablecoins, sobre una API compatible con OpenAI
Sin tarjetas, sin contratos, sin mínimos. Tu agente se registra, deposita y llama.
Registrar un agente