Copperx es una plataforma financiera basada en stablecoins que combina enlaces de pago, facturas, API, cuentas bancarias virtuales, pagos y tarjetas corporativas para empresas que operan internacionalmente. Este artículo explica cómo integrar un flujo de pago al estilo Copperx en tu propio backend, cómo conciliar los eventos del lado fiat con la liquidación on-chain en USDC y cómo verificar las transferencias de forma independiente utilizando un endpoint RPC. Separa el comportamiento documentado de la plataforma de los pasos de verificación que debes ejecutar tú mismo, y muestra dónde encaja una capa RPC fiable en la arquitectura. El objetivo es un procedimiento de conciliación reproducible en lugar de una visión general de marketing.
Alcance y el problema de conciliación
Copperx se describe en materiales públicos como un neobanco y plataforma de pagos basado en stablecoins: ofrece una pasarela de pago para aceptar stablecoins mediante enlaces, facturas y API, cuentas financieras denominadas en USD, EUR y AED, pagos globales y tarjetas corporativas. La ficha de Circle Alliance lo describe como autocustodial, con USDC, Programmable Wallets, Paymaster y CCTP entre los productos utilizados, y enumera Arbitrum, Base, Ethereum, OP Mainnet y Polygon PoS entre las blockchains compatibles. Esas son afirmaciones documentadas sobre la plataforma; no son garantías sobre la configuración específica de tu cuenta, la disponibilidad de corredores o las direcciones de contrato.
El problema de ingeniería que aborda este artículo es más limitado que la superficie del producto. Cuando un cliente paga una factura en stablecoins, tu backend recibe un webhook o un evento de API de la plataforma de pago y, por separado, una transferencia se liquida en una blockchain. Esos dos registros deben coincidir. Si no coinciden, tienes un cliente que pagó y no recibió crédito, o un crédito sin respaldo on-chain. Una capa de conciliación que verifica de forma independiente la pata on-chain es la diferencia entre confiar en un webhook y saber que los fondos se movieron.
Esta guía asume que ya tienes una cuenta de Copperx o una pasarela de stablecoins comparable, un backend que puede recibir webhooks y acceso a un endpoint RPC para cada cadena que aceptes. No asume ningún precio, corredor o soporte de cadena en particular; confírmalos en el panel de tu cuenta y en la documentación de la plataforma antes de construir.
- Comportamiento documentado de la plataforma: enlaces de pago, facturas, API, cuentas bancarias virtuales, pagos, tarjetas, posicionamiento autocustodial.
- Lo que debes verificar tú mismo: que una transferencia específica existe, llegó a la dirección esperada y coincide con el importe y la cadena esperados.
- Lo que este artículo no afirma: ninguna latencia, comisión, tasa de éxito o soporte de cadena medidos más allá de lo que publica la plataforma.
Modelo mental: tres libros contables que deben coincidir
Una integración de pagos con stablecoins es un problema de tres libros contables. El primer libro es la base de datos de tu aplicación: pedidos, facturas, identidades de clientes y la máquina de estados que decide cuándo se cumple un pedido. El segundo es el libro de la plataforma de pago: la intención de pago, su estado y los movimientos de saldo fiat o stablecoin que registra. El tercero es la blockchain: la transferencia ERC-20 real de USDC desde el pagador a la dirección receptora.
La mayoría de los errores de integración provienen de colapsar estos tres en uno. Un webhook es una afirmación del libro de la plataforma, no una prueba del libro de la blockchain. Una transferencia en blockchain es prueba de liquidación, pero no te dice a qué cliente o pedido pertenece. Tu trabajo es unirlos con una clave duradera. La clave más limpia es el hash de la transacción más la dirección receptora más el ID de cadena, almacenados en la intención de pago cuando la plataforma los reporta. Si la plataforma no expone un hash de transacción al crear la intención, captúralo en el primer evento de estado que lo incluya y trata la intención como no verificada hasta entonces.
Otra distinción útil es entre el riel de pago y el riel de liquidación. Un cliente puede pagar con tarjeta, transferencia bancaria o stablecoin; la plataforma puede liquidarte en USDC en una de varias cadenas. La cadena que verificas es la cadena de liquidación, no necesariamente la que usó el cliente. Lee la documentación de la plataforma para saber en qué cadena liquida una cuenta determinada y verifica esa cadena específicamente.
Requisitos previos y supuestos del entorno
Antes de escribir código de conciliación, confirma cuatro cosas. Primero, la URL base exacta de la API y el esquema de autenticación para tu cuenta de Copperx, tomados de la documentación para desarrolladores de la plataforma y no de memoria. Segundo, el secreto de firma del webhook y el nombre de la cabecera utilizada para las firmas. Tercero, la cadena de liquidación y la dirección del contrato de USDC en esa cadena, que debes copiar de una fuente canónica como la documentación de USDC de Circle y no de un resultado de búsqueda en un explorador de bloques. Cuarto, un endpoint RPC para esa cadena que controles o que provenga de un proveedor con un SLA documentado.
Para la capa RPC, no codifiques un endpoint público. Usa una variable de entorno como RPC_URL y apúntala a un endpoint gestionado. Si estás eligiendo un proveedor, el Asistente RPC tiene recomendaciones concretas; para la metodología de este artículo, cualquier endpoint que soporte eth_getTransactionReceipt y eth_call es suficiente. El servicio RPC gestionado de OnFinality y las opciones de nodo dedicado son puntos de partida razonables si necesitas un rendimiento predecible en varias cadenas.
- URL base de la API y esquema de autenticación de la documentación para desarrolladores de Copperx.
- Secreto de firma del webhook y nombre de la cabecera de firma.
- ID de cadena de liquidación y dirección canónica del contrato de USDC.
- RPC_URL para esa cadena, almacenada en la configuración del entorno, nunca en el código fuente.
Creación de una intención de pago y almacenamiento de la clave de unión
El primer paso de integración es crear una intención de pago desde tu backend y persistir el identificador de la plataforma junto a tu propio ID de pedido. La ruta exacta del endpoint y los campos del payload dependen de la versión de la API de Copperx que estés usando; la forma siguiente es ilustrativa y debe adaptarse al esquema documentado. La decisión de diseño importante es que la fila de tu base de datos se crea antes de que la llamada a la API retorne, de modo que un webhook que llegue rápidamente aún encuentre una fila que actualizar.
Almacena como mínimo: tu ID de pedido, el ID de intención de pago de la plataforma, el importe y la moneda esperados, el ID de cadena de liquidación, la dirección receptora esperada y un campo de estado con un estado no verificado explícito. No marques un pedido como pagado al crear la intención. La creación de la intención significa que se le ha pedido al cliente que pague, no que ya lo haya hecho.
curl -X POST "$COPPERX_API_BASE/payment-intents" \
-H "Authorization: Bearer $COPPERX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "149.00",
"currency": "USDC",
"reference": "order_1042",
"metadata": { "customer_id": "cus_88" }
}'Manejo de webhooks sin duplicar créditos
Los webhooks se entregan al menos una vez en la mayoría de los sistemas de pago, y las plataformas al estilo Copperx no son una excepción. Tu manejador debe ser idempotente. El patrón estándar es verificar la firma, analizar el ID del evento e insertar el ID del evento en una tabla con restricción de unicidad antes de hacer cualquier trabajo. Si la inserción entra en conflicto, devuelve 200 inmediatamente y no hagas nada. Esto hace que la reentrega sea inofensiva.
Después de la deduplicación, actualiza la fila de la intención de pago solo si el estado entrante es una transición hacia adelante. Un evento 'pendiente' que llega tarde no debe sobrescribir un estado 'pagado'. Mantén una máquina de estados pequeña y rechaza las transiciones hacia atrás. Por último, cuando el evento incluya un hash de transacción, escríbelo en la fila y encola un trabajo de verificación. No realices la llamada RPC dentro del manejador del webhook; los manejadores de webhooks deben ser rápidos y no deben fallar porque un endpoint RPC esté lento.
import hashlib, hmac, json
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = b"WEBHOOK_SIGNING_SECRET"
@app.post("/webhooks/copperx")
def copperx_webhook():
raw = request.get_data()
sig = request.headers.get("X-Copperx-Signature", "")
expected = hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = json.loads(raw)
event_id = event["id"]
if not db.insert_event_once(event_id):
return {"ok": True}, 200
intent = event["data"]
db.apply_forward_transition(
intent_id=intent["id"],
new_status=intent["status"],
tx_hash=intent.get("transaction_hash"),
)
if intent.get("transaction_hash"):
queue.enqueue("verify_settlement", intent["id"])
return {"ok": True}, 200Verificación de la liquidación on-chain con una llamada RPC
El trabajo de verificación toma el hash de transacción almacenado, obtiene el recibo y comprueba tres cosas: que la transacción se completó con éxito, que el recibo contiene un log Transfer del contrato de USDC esperado y que el destinatario y el importe del log coinciden con la intención. Este es el paso que convierte una afirmación de la plataforma en evidencia.
Usa eth_getTransactionReceipt para el hash. Comprueba que status sea igual a 0x1. Luego busca en los logs el tema del evento Transfer de ERC-20, que es el hash keccak256 de Transfer(address,address,uint256). Decodifica las direcciones from y to indexadas y el valor no indexado. Compara la dirección to con tu dirección receptora esperada, sin distinguir mayúsculas y minúsculas tras la normalización del checksum, y compara el valor con el importe esperado en la unidad más pequeña del token. USDC usa seis decimales, por lo que 149,00 USDC son 149000000.
Si el recibo es nulo, la transacción aún no se ha minado o el hash es incorrecto. Trata el nulo como pendiente con un reintento acotado, no como fallo. Si el recibo existe pero el log no coincide, trátalo como una discrepancia y envíalo a revisión manual en lugar de acreditar automáticamente.
const RPC_URL = process.env.RPC_URL;
const USDC = process.env.USDC_CONTRACT;
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
async function verifySettlement(txHash, expectedTo, expectedAmount) {
const res = await fetch(RPC_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 1, method: "eth_getTransactionReceipt",
params: [txHash],
}),
});
const { result: receipt } = await res.json();
if (!receipt) return { state: "pending" };
if (receipt.status !== "0x1") return { state: "failed" };
const match = receipt.logs.find((log) =>
log.address.toLowerCase() === USDC.toLowerCase() &&
log.topics[0] === TRANSFER_TOPIC &&
"0x" + log.topics[2].slice(26) === expectedTo.toLowerCase()
);
if (!match) return { state: "mismatch" };
const value = BigInt(match.data);
if (value !== BigInt(expectedAmount)) return { state: "amount_mismatch" };
return { state: "verified", blockNumber: receipt.blockNumber };
}Una tabla de resultados para tu propia ejecución de conciliación
No aceptes cifras de rendimiento publicadas para tu integración; mide las tuyas. Ejecuta un pequeño lote de pagos de prueba en tu entorno de staging y registra la siguiente tabla. Las columnas son el mínimo necesario para diagnosticar dónde se pierde tiempo y dónde ocurren las discrepancias. Rellena tus propios valores; la tabla siguiente es una plantilla, no un resultado.
| Caso de prueba | Intención creada (UTC) | Webhook recibido (UTC) | Recibo disponible (UTC) | Estado de verificación | Notas |
|---|---|---|---|---|---|
| Camino feliz, cadena única | |||||
| Webhook reenviado dos veces | |||||
| Recibo nulo en el primer sondeo | |||||
| Discrepancia de importe inyectada | |||||
| Dirección receptora incorrecta |
Las filas interesantes son las de fallo. Una integración correcta debería mostrar 'verified' para el camino feliz, ningún crédito duplicado para la reentrega, 'pending' y luego 'verified' para el caso de recibo nulo, y 'amount_mismatch' o 'mismatch' para las fallas inyectadas. Si alguna fila de fallo muestra 'verified', tu lógica de coincidencia es demasiado laxa.
Solución de problemas comunes de conciliación
El fallo más común es una discrepancia de cadena. La plataforma liquida en una cadena, tu RPC_URL apunta a otra y el recibo siempre es nulo. Confirma la cadena de liquidación a partir de la documentación de la plataforma y del payload de la intención, e incluye el ID de cadena en tu intención almacenada para que el trabajo de verificación no pueda usar el endpoint equivocado.
El segundo fallo más común es la confusión de decimales. USDC tiene seis decimales, pero algunos tokens usan dieciocho. Si comparas un importe legible por humanos con un valor de log sin procesar, todos los pagos discreparán. Convierte siempre a la unidad más pequeña del token usando los decimales del contrato y almacena los importes como enteros.
El tercero es la normalización de direcciones. Los temas de los logs son palabras de 32 bytes con la dirección rellenada a la izquierda; comparar el tema sin procesar con una dirección de 20 bytes nunca coincidirá. Corta los últimos 20 bytes y luego compara sin distinguir mayúsculas y minúsculas. El cuarto es un recibo que existe pero cuyo log proviene de un contrato diferente que también emite eventos Transfer; filtra siempre por la dirección del contrato de USDC. El quinto es una reorganización: un recibo puede volverse inválido si el bloque se reorganiza fuera de la cadena. Para pagos de alto valor, espera una profundidad de confirmación adecuada a la cadena antes de la liquidación final.
- Discrepancia de cadena: almacena el ID de cadena con la intención y selecciona RPC_URL por cadena.
- Discrepancia de decimales: convierte con los decimales del token, almacena enteros.
- Relleno de direcciones: corta los temas a 20 bytes antes de comparar.
- Contrato incorrecto: filtra los logs por la dirección canónica de USDC.
- Riesgo de reorganización: exige confirmaciones antes del crédito final en pedidos de alto valor.
Limitaciones, compensaciones y lo que la plataforma no resuelve
La verificación independiente añade latencia y superficie operativa. Ahora dependes de un endpoint RPC, debes manejar sus fallos y mantener actualizadas las direcciones de contrato y los ID de cadena. La compensación vale la pena para pagos por encima de un umbral que definas; por debajo de ese umbral, confiar en el webhook puede ser aceptable. Un patrón común es acreditar automáticamente importes pequeños en el webhook y exigir verificación on-chain por encima de un límite configurable.
También hay cosas que la plataforma no puede verificar por ti. No puede decirte si tu cliente está sancionado, si tu propio tratamiento contable de los recibos en stablecoins es correcto en tu jurisdicción o si el corredor que estás usando estará disponible el próximo trimestre. Esas son cuestiones de cumplimiento y tesorería, no de integración. Del mismo modo, el posicionamiento autocustodial de la plataforma significa que la gestión de claves es tu responsabilidad cuando corresponda; no asumas que la plataforma custodia tus claves.
Por último, la fiabilidad RPC no es uniforme entre cadenas. Un endpoint público puede limitarte la tasa durante una conciliación por lotes, y un único proveedor puede tener un rendimiento degradado en una cadena. Si liquidas en varias cadenas, considera una configuración RPC multicadena y monitorea las tasas de error por cadena en lugar de un único agregado.
Próximos pasos: endurecimiento y escalado de la integración
Una vez que el camino feliz y las filas de fallo de tu tabla de resultados se comporten correctamente, endurece el sistema en tres direcciones. Primero, añade un barrido periódico que vuelva a verificar cualquier intención atascada en 'pending' más allá de un tiempo de espera, para que un webhook perdido no deje un pedido sin cumplir. Segundo, añade alertas sobre las transiciones de estado de verificación a 'mismatch' y sobre las tasas de error RPC, porque esas son las señales que preceden a las quejas de los clientes. Tercero, documenta la cadena de liquidación y la dirección del contrato por corredor en un archivo de configuración bajo control de versiones, para que una migración de cadena sea un cambio revisado en lugar de un incidente.
Para la capa RPC, pasa de un único endpoint a un servicio gestionado con límites documentados y mantén un endpoint de respaldo configurado. Si necesitas rendimiento dedicado o redes privadas, un nodo dedicado es el paso adecuado desde el RPC compartido. Para la elección de endpoints específicos por cadena, consulta el Asistente RPC en lugar de adivinar; para la arquitectura más amplia, la guía de endpoints RPC multicadena y la guía de alojamiento de nodos blockchain cubren los detalles operativos que este artículo omite deliberadamente.
- Añade un barrido de intenciones pendientes con tiempo de espera y reverificación.
- Alerta sobre estados de discrepancia y tasas de error RPC.
- Controla versiones de la configuración de cadena y contrato por corredor.
- Pasa a un servicio RPC gestionado con respaldo antes de escalar el volumen.