x402 en Bitcoin Cash: pending por defecto, repetible por diseño
Un scheme de x402 propuesto para Bitcoin Cash hace que el cliente firme una transacción UTXO completa y que el facilitator la retransmita. Ejecutamos ese facilitator. Su default responde "pending" al primer settle, un reintento puede pagar dos veces y un solo pago sigue liquidándose como success.
El scheme vive en el PR #3632 del repo de x402. Es una de varias propuestas para chains que no son basadas en cuentas. Ya leímos las de BSV y Nano. BCH es la tercera, y es aquella cuyo código de facilitator está lo bastante completo como para ejecutarse.
Todo lo que sigue está fijado al head 249530b, el único commit del PR. El PR está abierto, sin revisión, y el bot marcó el commit como sin firmar. Una pregunta de alcance asociada, el issue #3633, no tiene comentarios. Es una propuesta en etapa de borrador y la leemos como tal. Lo que importa es qué debe aprender una plataforma de pagos para agentes antes de que un scheme así se publique.
Qué propone el PR
El pagador es una wallet UTXO. No hay saldo que debitar ni contrato de token. El cliente elige outputs sin gastar, construye el pago y el output de cambio, firma cada input y pone la transacción cruda completa, en base64, en el payload. El facilitator no cofirma, no completa ni modifica nada. Verifica y transmite.
La forma en el wire es pequeña:
{
"scheme": "exact",
"network": "bch:bitcoincash",
"asset": "BCH",
"amount": "1000", // satoshis, string decimal canónico
"payTo": "bitcoincash:q...",
"maxTimeoutSeconds": 300,
"extra": { "assetTransferMethod": "native", "paymentFlow": "upfront" }
}
Se definen dos redes, bch:bitcoincash y bch:bchtest (Chipnet). Solo existe el flujo upfront. No hay upto, ni batch settlement, ni patrocinio. El facilitator resuelve el valor y el script de cada input desde su propio provider e ignora los datos de origen que envía el cliente. Exige una sola clave de pagador en todos los inputs, cambio que vuelva al pagador, una comisión mínima de 1 sat por byte por defecto y un piso de dust de 546 sat.
Es un buen diseño para una chain UTXO. La spec también es franca sobre lo que no resuelve, y citaremos su propia lista de brechas donde importe. El comportamiento interesante está en el código.
La spec y el código no coinciden en alcance
El archivo de spec del PR dice que el scheme "transfiere una cantidad exacta de Bitcoin Cash nativo" a un output P2PKH, y que CashTokens, P2SH y CashScript quedan "fuera de este scheme". Su matriz de soporte marca los CashTokens como no soportados y dice que los prefijos de token "son rechazados por la política solo-P2PKH". También dice que define un contrato "compartido por los mecanismos BCH de TypeScript y Rust".
El cuerpo del PR dice otra cosa. Lista BCH nativo, CashTokens fungibles, capacidades y commitments de NFT, y destinos P2PKH, P2SH20 y P2SH32. El issue #3633 repite esa lista. El código coincide con el cuerpo: el paquete trae un fixture de CashToken con destino P2SH32, un método de transferencia cashtoken y chequeos de conservación de tokens. El PR no contiene Rust.
Así, el documento normativo, el que otros implementadores usarían, describe un scheme más pequeño que el que se publicaría. Es fácil de arreglar. Pero es el tipo de desvío que hace que dos implementaciones discrepen en el primer pago con tokens. No trataríamos el archivo de spec como autoridad hasta reconciliarlo con el código.
Experimento: ejecutar el facilitator
Copiamos los archivos del paquete en 249530b a un directorio temporal, instalamos @bitauth/libauth y @noble/hashes, y escribimos un provider simulado como el de los tests del propio PR. Un output P2PKH de 100,000 sat, un firmante de prueba BIP39, un requirement de 1,000 sat. Sin red, sin chain real. Tres escenarios.
1. El primer settle es pending
La política por defecto es confirmations(1). En settle() el facilitator verifica, transmite y luego llama una vez a getTransactionStatus. No hace polling. Una transacción recién transmitida está en el mempool, no en un bloque, así que la respuesta es settlement_pending:<txid> con success: false.
Nuestra ejecución imprimió exactamente eso, con un broadcast. La transacción pesó 225 bytes, un input, dos outputs (1,000 y 98,775 sat), comisión de 225 sat. Al cambiar el mock a "confirmed", el mismo payload se liquidó como success.
En el resource server de TypeScript de x402, un flujo upfront liquida antes de que corra el handler, y un settle fallido devuelve un error de pago en lugar del recurso. La petición pagada falla entonces con un 402 que lleva la razón pending. Es el comportamiento correcto para un pago sin confirmar. La pregunta es con qué frecuencia ocurre.
2. Cuánto dura "pending" en la chain real
Recorrimos 289 bloques BCH hacia atrás desde la punta con la API de bloques de Haskoin: alturas 974,269 a 974,557, del 2026-10-24 09:12 UTC al 2026-10-26 08:12 UTC. Los timestamps de bloque los reporta el minero, así que las cifras son cercanas, no exactas.
Intervalo entre bloques
Media 588 s. Mediana 390 s. Percentil 90: 1,382 s. El más largo, 2,998 s. El 35% de los intervalos superó los 600 s y 42 intervalos superaron los 1,200 s.
Un pago no llega al inicio de un intervalo. Cae en un momento al azar, así que tiende a caer dentro de los largos. Ponderar cada intervalo por su duración da la espera hasta el siguiente bloque para una transacción transmitida en un momento aleatorio:
Esperada: 603 s
Probabilidad de que la espera supere 30 s: 95%. Supere 60 s: 90%. Supere 300 s, el maxTimeoutSeconds de la propia petición: 61%. Supere 600 s: 38%.
Con la política por defecto, un agente que paga por petición vería un resultado pending casi siempre y uno confirmado tras unos diez minutos. No es un bug del código. Es lo que cuesta una confirmación en esta chain. Es un mal default para un scheme cuyo trabajo es pagar una llamada HTTP.
El README documenta dos alternativas: mempool y noDoubleSpendProof, que acepta una transacción sin confirmar mientras el provider no reporte una prueba de doble gasto. Ambas son finalidad más débil y ambas son opt-in explícito. Creemos que la descripción honesta es que el default es seguro pero inutilizable para llamadas interactivas, y que las configuraciones usables cargan un riesgo que el comerciante debe poner precio.
3. Reintentar tras pending puede pagar dos veces
Supongamos que un cliente trata settlement_pending como cualquier pago fallido y corre el flujo de nuevo. El selector de UTXO del cliente pide al provider los outputs sin gastar y toma primero el más grande. Un provider con semántica listunspent estilo Electrum refleja los gastos del mempool: el input original ya no está y aparece el nuevo output de cambio.
Lo modelamos en el mock. El primer intento devolvió settlement_pending. El segundo construyó una transacción nueva desde el output de cambio, y el facilitator la transmitió. Se transmitieron dos transacciones distintas, y ambas quedaron pending. El output del comerciante se paga dos veces, una por intento, y ninguna se ha liquidado.
El facilitator tiene la herramienta correcta para un reintento seguro: un settlement store indexado por txid y un binding de la petición, de modo que el mismo payload reintentado se reconcilia por txid sin volver a transmitir. Pero el cliente no puede usarla si no reenvía los mismos bytes. Nada en el scheme del cliente persiste el payload pending ni lo indica. La spec menciona que "una respuesta de broadcast perdida es indeterminada y requiere reconciliar por TXID". Deja el lado cliente de esa regla a la wallet.
No afirmamos que toda pareja wallet-provider lo haga. Afirmamos que el cliente de referencia con un provider tipo Electrum lo hace, y que el protocolo no le da al agente ninguna señal para actuar distinto.
4. Un payload liquidado se repite como success
El settlement store asigna a cada txid un binding, que es el conjunto de requirements aceptados más la info del recurso. Tras un settle exitoso marca el registro como aceptado y lo conserva. Una llamada posterior con el mismo payload encuentra el mismo binding, toma la rama "same", ve la transacción confirmada, omite el chequeo de unspent y devuelve success: true otra vez.
Llamamos a settle() cinco veces con un payload confirmado. Cinco successes, un broadcast. El test del propio PR afirma lo mismo para un reintento: "retried ... success: true", broadcast llamado una vez. Es idempotencia intencional para una respuesta perdida. Pero el store no distingue un reintento por respuesta perdida de una repetición deliberada, y el binding no tiene ningún elemento por petición.
Que importe depende del resource server. Si trata cada settle exitoso como una compra, un pago de 1,000 sat compra ese recurso a ese precio tantas veces como el cliente reenvíe el header, hasta que se limpie el store en memoria. Otras dos pruebas se comportaron como dice el diseño: la misma transacción contra otro recurso devolvió transaction_already_claimed_for_another_request, y un payload reconstruido desde el mismo conjunto de UTXO del mock fue idéntico byte a byte al primero, así que cayó en el mismo registro.
En EVM exact, una autorización EIP-3009 repetida falla on-chain porque el nonce ya se gastó. Aquí la chain no ayuda, porque el facilitator nunca vuelve a transmitir. La protección tiene que ser estado de aplicación. Es la misma familia de problema que las brechas de free-riding de nuestra auditoría two-phase y los casos de replay de la auditoría de la extensión recovery: una idempotencia correcta para el facilitator y explotable para el vendedor.
Lo que el scheme hace bien
Resuelve los source outputs por sí mismo en lugar de confiar en el cliente. Verifica que el cambio vuelva al pagador, de modo que un pago no se convierta en silencio en una transferencia a varios destinatarios. Trata un timeout de broadcast como indeterminado y consulta el txid antes de liberar un claim. Se niega a reutilizar una transacción para dos recursos. Declara que maxTimeoutSeconds no es una expiración de transacción, porque una transacción BCH no tiene una expiración firmada que verificar, y admite que la ventana de tiempo por tanto no se aplica.
La spec también dice claramente que el settlement store en memoria es de un solo proceso, y que producción necesita un store compartido y durable con compare-and-set atómico y recuperación tras caídas. Es la admisión correcta. Significa que el resultado de replay de arriba es el comportamiento del store de referencia, no un veredicto sobre uno endurecido.
Hallazgos menores
provider.tsimporta@noble/hashes/sha256, pero elpackage.jsondel paquete lista solo@bitauth/libauthy@x402/corecomo dependencias de runtime. En una instalación pnpm estricta el import puede no resolverse salvo que algo más del workspace lo eleve.- El PR es grande para lo que agrega. De los 97 archivos cambiados, la mayoría son ediciones pequeñas de configuración eslint o postcss en ejemplos y paquetes. El paquete BCH en sí son unas dos docenas de archivos.
- La unidad de precio es el satoshi. Con la tasa de CoinGecko que consultamos hoy, $278.57 por BCH, 1,000 sat son unos $0.0028 y la comisión de 225 sat unos $0.0006, cerca de 22% sobre ese pago. Un activo nativo volátil convierte el precio por llamada en un objetivo móvil.
Qué significa para LLM4Agents
Nuestros rails liquidan en USDC, y nada de esto lo cambia. Un activo nativo y volátil con finalidad de diez minutos no encaja con un gateway que cotiza un precio por llamada y devuelve la respuesta en la misma petición. No agregaremos un rail BCH a partir de esta propuesta.
Lo útil es la lista de verificación que el PR obliga a hacer. Todo rail que evaluemos plantea tres preguntas: cuánto falta para que success sea final, qué pasa con un cliente que reintenta tras una respuesta no final, y qué impide que un pago liquidado se presente otra vez. EVM exact responde la tercera on-chain. Los schemes UTXO y de ledger la responden con estado del facilitator, lo que la vuelve una propiedad de despliegue que hay que probar, no leer en una spec. Nuestra auditoría de finality miró la primera pregunta en Base y Solana. Esta suma una chain donde la respuesta honesta es diez minutos.
Cómo mantenerse en la frontera
- Agregar la prueba de replay a nuestros chequeos de facilitator: liquidar un pago, presentar luego el mismo payload y exigir un fallo salvo un reintento documentado por respuesta perdida. Correrla contra cada facilitator al que apunte el gateway.
- Agregar la prueba de reintento: hacer que el facilitator responda "pending" y verificar que nuestro camino de cliente no reconstruya ni vuelva a pagar. Un pago pending debe reenviarse byte a byte o abandonarse, nunca recrearse.
- Registrar, por rail, el tiempo medido hasta el nivel de finalidad que aceptamos y la probabilidad de que supere el timeout de la petición. Las cifras de 603 segundos y 61% son la plantilla.
- Mantener explícito el contrato del settlement store en cualquier scheme que adoptemos: compartido, atómico, a prueba de caídas, con un elemento por petición en el binding.
- Vigilar el PR #3632 y el issue #3633. Repetir el harness si la spec se reconcilia con el código, o si cambian la política por defecto o el binding.
- Mantener los schemes UTXO y de ledger en la lista de vigilancia como clase. Es donde x402 deja de heredar garantías de un contrato de modelo de cuentas y donde el comportamiento del facilitator importa más.
Paga por llamada en USDC
Un endpoint compatible con OpenAI, liquidación en stablecoin por petición.
Registrar agente