El día que el facilitador de mainnet dejó de liquidar

Un error genérico, nueve días de bloqueo y una cuenta muda. Cómo lo diagnosticamos, qué publicamos y por qué la conclusión no fue esperar.

El síntoma

El 29 de agosto de 2026, cada intento de cobrar en la red principal de Stellar moría en el mismo punto. El facilitador hospedado que usábamos respondía 200 con un cuerpo que decía {"invalidReason":"unexpected_verify_error","isValid":false}. El checkout devolvía entonces un segundo 402 y no se cobraba nada.

Conviene decir lo que no pasó: no se perdió dinero de nadie. El flujo se detiene antes de liquidar, que es exactamente para lo que existe el paso de verificación. El costo fue de calendario, no de fondos.

La prueba A/B que ahorra días

Lo primero fue separar nuestro código del servicio ajeno. Mismo código, mismo activo USDC, mismo flujo, dos redes:

  • En testnet: {"isValid":true,"payer":"GAGP5AWB..."}.
  • En mainnet: {"invalidReason":"unexpected_verify_error","isValid":false}.

Con eso, la hipótesis "nuestra implementación está mal" queda muy debilitada, y el trabajo pasa a ser descartar todo lo demás.

Lo que descartamos, uno por uno

  • La llave de API de mainnet. Válida: GET /x402/supported respondía 200 y anunciaba soporte para stellar:pubnet.
  • La transacción. Decodificada a mano: invocaba el contrato USDC correcto, función transfer, origen el comprador, destino el vendedor, monto en unidades mínimas, con la autorización firmada y una expiración por delante del ledger actual.
  • La simulación. Nuestra propia simulación contra el RPC de mainnet pasaba sin error.
  • El RPC. Tres proveedores distintos, los tres en sincronía con Horizon, el mismo error en los tres.
  • Nuestras cuentas. Comprador y vendedor con saldo y con la línea de confianza de USDC abierta, verificado en la red.
  • Un transitorio. Tres intentos consecutivos, mismo resultado.

Queda una sola explicación razonable: unexpected_verify_error es un error genérico, es decir una excepción no controlada dentro del servicio ajeno.

La causa probable, y el dato que faltaba

Buscando fuera de nuestro código apareció un issue abierto desde el 19 de agosto con dos integradores independientes reportando el mismo límite. Al revisar la red, la cuenta patrocinadora que el servicio usaba en mainnet no enviaba una sola transacción desde el 10 de agosto. Otra cuenta aparecía activa a diario desde el 13 y parecía haberla reemplazado, aunque nadie fuera del equipo mantenedor podía confirmarlo.

Nuestro aporte al hilo fue un dato que no estaba: el propio endpoint de descubrimiento, /supported, seguía anunciando la cuenta retirada como firmante de mainnet. No era un problema de documentación desactualizada: el servicio le decía a cualquier integrador nuevo que usara algo que llevaba semanas en silencio. Además, nuestro fallo ocurría en la verificación, antes de la liquidación, que es donde fallaban los otros dos reportes.

Publicamos toda la evidencia en el issue el mismo día, como tercer integrador independiente. El problema upstream quedó resuelto el 11 de septiembre.

Por qué no esperamos

Cuando el bloqueo lleva nueve días y no depende de ti, la pregunta deja de ser "cómo lo arreglo" y pasa a ser "por qué esto podía detenerme". La respuesta incómoda es que la capacidad de cobrar de una tienda dependía de un servicio hospedado por otros, sin alternativa y sin compromiso de servicio.

Evaluamos tres caminos. Seguir esperando era insostenible. Auto hospedar el plugin existente lo descartamos por licencia: es AGPL-3.0, y nuestro proyecto no lleva dependencias AGPL en ninguna ruta. El tercero era escribir un facilitador mínimo propio, y fue el que tomamos.

Mínimo es literal: la verificación y la liquidación son las del SDK oficial de x402 para Stellar, Apache-2.0, sin reimplementar nada de criptografía. Lo que escribimos fue la superficie HTTP, la autenticación y las guardas alrededor.

Las reglas que quedaron escritas

Un facilitador patrocina las comisiones de red, así que paga quien lo corre. De ahí salieron dos condiciones que no son opcionales en nuestro código: la cuenta que paga comisiones es una cuenta aparte, y la verificación y la liquidación rechazan un pago cuyo destino sea esa misma cuenta. El servicio nunca tiene fondos de un comercio ni de un comprador: el pago va del comprador al vendedor dentro de la transacción.

Y una regla de convivencia: el facilitador propio es nuestro predeterminado, no un requisito del protocolo. Una tienda puede apuntar a cualquier facilitador, incluido el hospedado, con una variable de entorno. Eso es lo que nos separa de ser un intermediario.

El 2 de septiembre, con el facilitador propio en producción, liquidamos la primera compra real en mainnet. En el camino apareció un segundo problema que nadie había reportado: la comisión de inclusión fija que usaba el cliente quedaba por debajo del mínimo de la red. También quedó corregido.