auth-capture: el scheme de reembolsos de x402, medido contra la cadena
Tres schemes de x402 mueven dinero hacia adelante. El cuarto es el único que puede moverlo de vuelta. Lo auditamos contra el repo y contra la cadena, y los dos cuentan historias distintas.
x402 tiene hoy cuatro payment schemes. exact transfiere un monto fijo. upto autoriza un techo y liquida el uso real. batch-settlement almacena un commitment y lo redime después. Los tres son push payments: una vez que el settlement aterriza, el dinero se fue y el único recurso es que el vendedor mande voluntariamente una transferencia nueva.
auth-capture es el cuarto. Es el único scheme del protocolo donde el pagador puede recuperar fondos sin la cooperación del vendedor. Eso lo vuelve el scheme más interesante para agentes autónomos — un agente que paga por un trabajo que no puede verificar de antemano necesita una salida — y el menos terminado del repositorio.
Clonamos x402-foundation/x402 el 2026-08-12 en HEAD c8247c4c, leímos los dos documentos de la spec y cada línea de la implementación publicada, bajamos el source del contrato de escrow desde upstream, y después barrimos 24 horas de logs de Base mainnet para ver qué está haciendo realmente la base del scheme. Esto continúa la serie de auditorías que cubrió la capa de extensiones, la API del facilitator y el settlement plane self-hosted.
Qué es el scheme, realmente
auth-capture no es criptografía nueva. Es un binding HTTP sobre un stack de contratos existente y auditado: base/commerce-payments, licencia MIT, creado el 2025-03-06, hoy con 134 stars y un único release etiquetado — v1.0.0, publicado el 2025-05-07 — desplegado en Base mainnet y Base Sepolia.
El stack es un escrow singleton, AuthCaptureEscrow en 0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff, más un conjunto de token collectors que saben extraer fondos bajo distintos primitivos de autorización. Upstream despliega seis collectors. El scheme de x402 cablea dos: el collector ERC-3009 en 0x0E3dF9510de65469C4518D7843919c0b8C7A7757 y el de Permit2 en 0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26. Los otros cuatro — incluido el Spend Permission collector, que conectaría esto con el mundo de las session keys, y el de pre-approval — quedan sin usar.
El escrow fue revisado cinco veces por Coinbase Protocol Security y Spearbit entre marzo y abril de 2025, más un sexto reporte de Spearbit fechado 2026-07-22 que cubre un cambio posterior. Es un contrato plano — ReentrancyGuardTransient, implementación de token store inmutable, sin proxy, sin ruta de upgrade. En Base mainnet hoy tiene 11,053 bytes de código.
Dos caminos, cinco verbos, tres deadlines
El scheme elige entre dos caminos de settlement con un solo booleano: extra.autoCapture.
Con autoCapture: false — el default — el facilitator llama authorize(). Los fondos salen del pagador y quedan en escrow. El servidor entrega el recurso. Más tarde, una entidad llamada captureAuthorizer llama capture() para finalizar los fondos hacia el receptor, o void() para devolverlos. Si el captureAuthorizer no hace nada antes del capture deadline, el pagador llama reclaim() y recupera el dinero unilateralmente. Después de un capture, refund() sigue disponible hasta un segundo deadline.
Con autoCapture: true, el facilitator llama charge(). Los fondos van directo al receptor. Sin escrow, sin void, sin reclaim — solo refund() dentro de la ventana de reembolso.
Tres timestamps absolutos gobiernan el ciclo de vida, y el contrato exige su orden:
// AuthCaptureEscrow, en cada authorize() / charge()
if (preApprovalExp > authorizationExp || authorizationExp > refundExp) {
revert InvalidExpiries(preApprovalExp, authorizationExp, refundExp);
}
preApprovalExpiry se deriva del lado del cliente como now + maxTimeoutSeconds y bloquea el settlement una vez pasado. authorizationExpiry es el campo de wire captureDeadline: bloquea el capture y habilita el reclaim. refundExpiry es el campo de wire refundDeadline. La consecuencia práctica conviene decirla sin rodeos: para el receptor, la finalidad no es el capture, es el refund deadline.
msg.sender para authorize, capture, void, refund y charge. La spec dice que puede ser el EOA del facilitator o cualquier smart contract que termine llamando al escrow. Lo define el servidor, en extra, y el pagador lo firma.
Qué firma el cliente
El pagador produce exactamente una firma. Todo lo demás lo reconstruye el facilitator.
La parte ingeniosa es el nonce. En vez de llevar los parámetros del pago en un witness struct — el patrón que usa upto con permitWitnessTransferFrom de Permit2 — auth-capture deriva el nonce del pago mismo:
paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer))
nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash))
El campo del pagador va en cero para que el facilitator pueda recomputar el hash antes de saber quién paga. Todos los demás campos del struct on-chain — receptor, token, monto máximo, los tres expiries, ambos límites de fee, el fee receiver, el captureAuthorizer — están dentro de ese hash. Tocar cualquiera cambia el nonce, y el nonce es lo que se firmó. La frescura viene de un salt de 32 bytes generado por el cliente, que también está en el struct.
Eso le da al scheme una propiedad genuinamente elegante: un solo chequeo sobre el nonce del wire impone, de forma transitiva, igualdad en doce campos on-chain. La lista de verificación de la spec lo dice explícitamente en el paso 12, y anota con razón que los chequeos campo por campo se vuelven innecesarios.
La firma en sí es un ReceiveWithAuthorization de ERC-3009 — el mismo primitivo gasless que está bajo exact en EVM, con el dominio EIP-712 atado al contrato del token — o un PermitTransferFrom de Permit2 sin witness alguno. Hay soporte de EIP-6492 para smart wallets todavía no desplegadas.
Hallazgo uno: el scheme publica un cliente y nada más
La especificación describe un procedimiento de verificación de 13 pasos y uno de settlement de 7 pasos para el facilitator, con una tabla que mapea diecisiete reverts tipados del contrato a códigos estables de invalidReason. Nada de eso existe en código.
En @x402/evm — versión 2.22.0, publicada el 2026-08-11, 487,567 descargas en el mes hasta el 2026-08-09 — los otros tres schemes publican directorios client/, facilitator/ y server/. auth-capture publica client/ y nada más. El package exporta exactamente un subpath para él, ./auth-capture/client. No hay middleware de servidor, no hay facilitator, y no hay ejemplo de servidor en el repositorio; el README del ejemplo de cliente dice que apuntes RESOURCE_SERVER_URL "a cualquier endpoint auth-capture", lo cual presupone que existe alguno.
En los SDK de los otros lenguajes la cuenta es cero. Python, Go y Java no contienen ninguna ocurrencia del string auth-capture en ninguna forma. Nuestra auditoría previa de x402-rs encontró siete schemes implementados; auth-capture no estaba entre ellos.
El README del package es honesto al respecto: "This package currently ships the client only: detecting auth-capture payment requirements and signing the payment payload. Server and facilitator support follow in a later change." Eso se escribió para el PR #2486, mergeado el 2026-05-29. Es el último commit que tocó auth-capture en cualquier parte del repositorio. La spec se mergeó el 2026-05-12 y se renombró el 2026-05-20. En los dos meses y medio siguientes el repo avanzó y el scheme no.
Dos PRs siguen abiertos de ese período: el #2308, la propuesta original del SDK de TypeScript del 2026-05-14 con once comentarios, y el #2359, una actualización de la spec del 2026-05-18 con seis comentarios que reemplazaría el booleano autoCapture por un payload.type explícito que cubre authorize, charge, capture, void y refund como operaciones pedidas por el servidor. Ninguno aterrizó.
Un síntoma menor del mismo abandono: el README del package y el del ejemplo de cliente enlazan ambos a scheme_auth-capture_evm.md. El rename del 2026-05-20 dejó el nombre real en scheme_auth_capture_evm.md. La URL con guiones devuelve 404 en GitHub hoy; la de guiones bajos devuelve 200. Todos los punteros del código hacia la spec están rotos.
Hallazgo dos: la semántica de fees ya divergió
Este importa más, porque es una divergencia viva entre la spec y el contrato que ella misma nombra como fuente de verdad.
La spec de x402 documenta el sistema de fees en basis points, aplicados on-chain:
Fee distribution: feeAmount = amount * feeBps / 10000, remainder goes to receiver.
Y su tabla de reverts tipados mapea el error del contrato FeeBpsOutOfRange a la razón fee_bps_out_of_range.
Upstream, el PR #90 de commerce-payments — abierto el 2026-06-22, mergeado el 2026-07-16, titulado "Rounding and billing fix" — reemplazó eso por completo. En main, capture() y charge() reciben un uint256 feeAmount absoluto en vez de un uint16 feeBps. Los minFeeBps y maxFeeBps firmados por el pagador sobreviven, pero ahora derivan límites dentro de los cuales debe caer el monto absoluto que provee el operador:
minFee = amount * minFeeBps / 10_000
maxFee = amount * maxFeeBps / 10_000
require(minFee <= feeAmount <= maxFee)
El error FeeBpsOutOfRange pasó a llamarse FeeAmountOutOfRange, y los eventos PaymentCharged y PaymentCaptured cambiaron tipos de campo. El PR se describe a sí mismo, con sus propias palabras, como un "Breaking public ABI change" y le dice a los integradores que migren antes de actualizar. Spearbit lo auditó el 2026-07-22.
Así que la spec de x402 documenta hoy una aritmética de fees que el contrato upstream ya no ejecuta, e instruye a los facilitators a decodificar un error que ya no existe. La salvación es que el contrato desplegado no se movió: confirmamos desde los topic hashes de los eventos en nuestro barrido on-chain que Base mainnet sigue emitiendo las firmas v1.0.0 con uint16 feeBps. No hay release v1.1.0 ni dirección de despliegue nueva.
Ese respiro es temporal, y el redeploy será más filoso de lo que parece. AuthCaptureEscrow no es upgradeable, así que publicar el ABI nuevo implica una dirección nueva — y la dirección del escrow entra en la derivación del nonce. Un redeploy invalida silenciosamente toda forma de firma del scheme, no solo la llamada de fee.
Hallazgo tres: el cliente chequea presencia, no política
Leyendo AuthCaptureEvmScheme.createPaymentPayload línea por línea, la validación es enteramente estructural. Lanza excepción si falta o tiene mal tipo name, version, captureAuthorizer, feeRecipient, captureDeadline, refundDeadline, minFeeBps, maxFeeBps o maxTimeoutSeconds. Después firma.
Nunca los compara entre sí, y nunca los compara contra nada que al pagador le importe.
Computa preApprovalExpiry = now + maxTimeoutSeconds y jamás verifica que el resultado sea menor o igual a captureDeadline. Un servidor que anuncia un maxTimeoutSeconds generoso y un captureDeadline cercano obtiene una firma válida garantizada a revertir con InvalidExpiries — round-trip desperdiciado, y un modo de falla invisible hasta el settlement.
Más grave: no hay techo sobre el propio captureDeadline. El reclaim solo está disponible después de ese timestamp. Un servidor que lo fija a un año deja los fondos de un agente encerrados en escrow durante un año, con capture o void enteramente a su discreción durante toda la ventana, y el SDK del cliente no levanta nada. Tampoco hay allowlist sobre captureAuthorizer, la única dirección que controla el dinero una vez en escrow.
Y feeRecipient tiene una trampa que el issue #3004, abierto el 2026-07-31 y todavía sin un solo comentario, describe con precisión: ponerlo en address(0) no significa "sin fee recipient". Significa que el captureAuthorizer puede nombrar cualquier dirección no-cero al momento del capture. Es una autorización de payout con comodín, acotada solo por maxFeeBps, y una UI de wallet que lo renderiza como 0x0 le muestra al pagador algo que no es lo que firmó.
Lo cual lleva al hueco que está debajo de todos estos. specs/CONTRIBUTING.md pide a los autores de schemes documentar prevención de replay, alcance de la autorización y atomicidad del settlement bajo una sección de seguridad. Existen siete de esas secciones repartidas en los documentos de red de exact, upto y batch-settlement. En los dos documentos de auth-capture, la palabra "security" no aparece ni una vez — ni como encabezado ni en prosa. El PR #2902, que agregaría una cubriendo exactamente estas carreras operativas y el trust model del captureAuthorizer, está abierto desde el 2026-07-18.
Qué dice la cadena
Las specs describen intención. Nosotros queríamos uso, así que corrimos dos barridos el 2026-08-12.
Primero, el lado de la demanda. Muestreamos 1,500 de los 15,299 recursos indexados en el x402 Bazaar y contamos schemes en cada entrada de accepts: 3,152 exact, 110 upto, 31 batch-settlement, 2 onchain, 1 agent-pay. Cero auth-capture. Nadie anuncia un endpoint x402 reembolsable, lo cual se sigue directamente de que no existe código de servidor ni de facilitator para servirlo.
Segundo, el contrato de abajo. Bajamos todos los logs de AuthCaptureEscrow en Base mainnet a lo largo de 43,000 bloques — del 2026-08-11T09:12:49Z al 2026-08-12T09:06:09Z, una ventana limpia de 24 horas — y los decodificamos.
2,137 authorize · 2,131 capture · 18 charge · 4 void · 2 refund · 0 reclaim
El contrato está ocupado: aproximadamente un pago cada 40 segundos, $158,515.61 de volumen total autorizado, con montos que van de $0.01 a $10,500 y una mediana de $10.50. Cada pago fue en USDC, y cada uno usó el collector de ERC-3009. El collector de Permit2 no vio tráfico alguno.
El camino de dos fases domina por completo — 18 de 2,155 pagos, menos del 1%, tomaron el atajo de charge(). Y la maquinaria de recurso, la razón entera por la que este scheme existe, se disparó seis veces: cuatro voids y dos refunds, el 0.28% de los pagos. El reclaim se disparó cero veces, que es la lectura más sana del mismo número: ningún captureAuthorizer dejó a un pagador varado más allá del deadline en esta ventana.
La concentración es el otro hallazgo. Aparecen cuatro captureAuthorizers distintos en la ventana, y uno solo concentra 2,103 de 2,155 autorizaciones — 97.6%. Los dos principales no son EOAs; ambos son proxies ERC-1967 con bytecode idéntico byte a byte, apuntando a dos direcciones de implementación distintas. La dirección que un pagador firma dentro de PaymentInfo.operator, y que después sostiene derechos unilaterales de capture y void sobre sus fondos en escrow, es un contrato cuya lógica puede reemplazarse después de hecha la firma.
Junta los dos barridos y el cuadro queda claro. El escrow es real, vivo, auditado y mueve seis cifras al día — impulsado por los productos de comercio de Coinbase, no por x402. El binding de x402 encima es una spec, un cliente y ninguna contraparte.
Qué significa para LLM4Agents
El gateway ya corre esta máquina de estados. Nuestro ciclo reserve → proxy → settle es autorizar-y-después-capturar con la retención mantenida off-chain en nuestro ledger: reservamos contra un balance antes de la llamada de inferencia, proxeamos el request, y liquidamos el costo real. auth-capture es la misma forma con la retención movida on-chain y el derecho de liberación entregado a un tercero nombrado.
Para inferencia por llamada, ese intercambio es malo y los datos lo dicen. El escrow agrega una retención on-chain, una segunda transacción para capturar y una dependencia de liveness sobre un captureAuthorizer — a cambio de una ruta de recurso que se dispara en el 0.28% de los pagos. Para una llamada de modelo de $0.004, el overhead de settlement excede el valor en disputa por órdenes de magnitud. exact sigue siendo lo correcto para el billing central del gateway, y upto sigue siendo el primitivo adecuado cuando el costo real no se conoce al momento del request.
Donde deja de ser malo es en el techo de nuestro rango de precios. El gateway no solo vende tokens. Vende trabajos largos contra el Workspace — renders de video, corridas de deep research, trabajo documental por lotes — donde un solo request puede costar dólares, tardar minutos y producir output que el comprador no puede verificar antes de pagar. Esa es exactamente la forma para la que se diseñó auth-capture, y exactamente donde un agente que nos compra tiene una razón legítima para querer una salida.
La amenaza es más sutil que un scheme competidor. Cada framework de pagos de agentes que auditamos este trimestre converge en el mismo primitivo bajo distintos nombres — el conjunto de constraints de Verifiable Intent, el allowance del Agentic Commerce Protocol, la spend permission de ERC-7715, el techo de upto. auth-capture agrega la pieza que a todos esos les falta: una reversión. Si un protocolo de rail de tarjeta publica resolución de disputas creíble del lado del agente antes de que los rails de stablecoin publiquen un scheme de escrow funcional, "settlement final" deja de leerse como feature y empieza a leerse como carencia.
La oportunidad es el inverso del hallazgo uno. Un scheme especificado, auditado, respaldado por contratos, con un procedimiento de verificación completo y ningún facilitator que lo implemente, es un carril libre. Quien publique el primer facilitator auth-capture funcional define cómo funciona x402 reembolsable en la práctica.
Cómo mantenerse en la frontera
Seis pasos, en orden de apalancamiento.
Envolver el cliente en un policy guard antes de pagar jamás bajo este scheme. El SDK valida tipos; nosotros necesitamos validar valores. Rechazar captureDeadline más allá de un horizonte configurado. Exigir preApprovalExpiry <= captureDeadline localmente para nunca firmar un payload garantizado a revertir. Rechazar feeRecipient == address(0) salvo allowlist explícita para ese host, y rechazar maxFeeBps por encima de un techo. Allowlist de captureAuthorizers conocidos. Es un wrapper chico sobre AuthCaptureEvmScheme y va upstream como PR, no en nuestro fork.
Construir el facilitator faltante contra Base Sepolia. La spec entrega un diseño completo — trece pasos de verificación, siete de settlement, diecisiete reverts tipados. Pinearlo explícitamente a commerce-payments v1.0.0 y poner el ABI de fees detrás de una constante de versión para que el redeploy del PR #90 sea un cambio de config y no una reescritura. Correrlo en staging bajo el mismo gate de conformance que usamos para el resto del settlement plane.
Exponer auth-capture solo por encima de un umbral de costo. Mapear authorize → deliver → capture sobre nuestro reserve → proxy → settle existente y ofrecerlo en trabajos donde la reserva supere una cifra en dólares que valga la pena disputar. Mantener exact en todo lo demás. Publicar el umbral es parte del producto.
Subir upstream los tres hallazgos. Los links rotos de la spec son un fix de dos líneas. La divergencia de semántica de fees es un problema de corrección real que morderá al primer facilitator que se publique. La sección de seguridad faltante ya tiene un PR abierto — el #2902 — que merece que se le adjunten las carreras operativas que encontramos, incluido el hueco de ordenamiento del lado del cliente y el comodín de feeRecipient del #3004.
Seguir de cerca el issue #3065. La propuesta de release con verificación — criterios de aceptación co-firmados que deciden capture contra void mecánicamente en vez de por el juicio del captureAuthorizer — es hacia donde va de verdad el pago de trabajos agente-a-agente. Tiene dieciséis comentarios en menos de una semana, más movimiento del que ha visto el scheme entero desde mayo. Acoplado con la extensión offer-receipt produciría la prueba de pago anclada que le falta a la capa de reputación de ERC-8004.
Alertar sobre el redeploy. Como la dirección del escrow es un input del hash del nonce, un despliegue nuevo cambia cada firma del scheme, no solo la llamada de fee. Pinear las direcciones canónicas en config, probarlas en el boot y avisar ante drift.
El scheme está inconcluso, y ese es el punto. Tres meses de spec sin contraparte no es un callejón sin salida — es una posición sin reclamar en la única parte de la superficie de x402 donde el dinero todavía puede moverse hacia atrás.
Paga por llamada. Liquida en stablecoins.
Un gateway compatible con OpenAI, con settlement x402, sin crédito prepago y sin lock-in.
Registra tu agente