Estrategia de keepers
SubscriptionHub.charge(id) (EVM) y la instrucción charge de Anchor (Solana) son públicas y no requieren permisos. Cualquier EOA puede llamarlas. Un «keeper» es simplemente un planificador que lo hace.
Esta página explica cómo dispara OrcaRail los cobros, qué pasa cuando un keeper no está disponible y qué SLO publicamos.
Keeper principal: planificador de NestJS
El keeper principal está en api/src/scheduler/processors/subscription-auto-charge.processor.ts. Es un procesador de BullMQ que:
- Lee de la base de datos las suscripciones vencidas (un espejo del estado on-chain que mantiene el indexador).
- Las agrupa por chain id.
- Firma y envía una transacción
charge(ids[])por cadena con una EOA keeper por cadena. - Si tiene éxito, espera la confirmación y actualiza la base de datos mediante los eventos del indexador.
- Si hay revert, analiza el selector del revert y lo asigna a un estado de
SubscriptionAutoChargeEntity:ChargeSkipped(InsufficientAllowance)→past_due+ email de allowanceChargeSkipped(InsufficientBalance)→past_due+ email de saldoChargeSkipped(CapExceeded)→canceled(el pagador debe volver a aprobar)ChargeSkipped(NotDue)→ inofensivo (redondeo del indexador); se reintenta en 1 ciclo
Keeper de respaldo: Chainlink Automation (preferido)
A partir de la Fase 3, cada despliegue de SubscriptionHub registra un Upkeep en Chainlink Automation:
checkUpkeep(bytes)devuelveperformData = abi.encode(ids[])para los ID en los queisDue(id) && !chargedThisBlock(id).performUpkeep(bytes)llama acharge(ids[])por lotes.- Se financia con LINK de la tesorería de la plataforma.
- Configurado en modo «conditional» con un límite de gas adecuado para el tamaño del lote.
Keeper de respaldo: Gelato Web3 Functions (alternativa)
Si Chainlink no está disponible en una cadena (o el precio no encaja), una Gelato Web3 Function consulta la vista isDue() del hub y envía charge(ids[]) cuando toca. Se paga a posteriori desde el 1Balance de Gelato.
Keeper de Solana
La rama de Solana en NestJS usa el mismo patrón de planificador con un keeper que firma en SOL. Hoy Solana no tiene un equivalente de Chainlink Automation con una cobertura comparable; opciones de respaldo:
- Clockwork (si su estado de mantenimiento es aceptable en el momento del despliegue): cron nativo de Solana.
- Una segunda instancia de NestJS en otra región, como redundancia más sencilla.
Las comisiones de Solana son lo bastante bajas como para que la monitorización del saldo de gas sea menos crítica que en EVM.
Gestión del gas
EVM
- Cadenas con política de paymaster de Alchemy: el keeper llama a
charge()como una UserOperation patrocinada mediante la lógica existente en api/src/withdrawals/services/gas-strategy. - Cadenas sin paymaster: el keeper paga con su propio saldo de gas caliente, que la wallet de la plataforma recarga a diario.
- Techo del precio del gas: los cobros se aplazan un bloque si el precio del gas supera un umbral por cadena.
- Gas subvencionado por el comercio (opcional): los comercios pueden definir una
gas_refund_address; el reembolso se extrae mediante la lógica de reparto de comisiones.
Solana
- El keeper funciona con un saldo estable de SOL por cadena.
- La comisión de prioridad se fija dinámicamente según los bloques recientes; cada reintento sube la prioridad un paso.
SLO
| Métrica | Objetivo | Se aplica a |
|---|---|---|
charge() en los 10 minutos siguientes a nextChargeAt | 99,5 % | Todas las cadenas compatibles |
charge() en la hora siguiente a nextChargeAt | 99,9 % | Todas las cadenas compatibles |
| Saldo de gas caliente de la EOA keeper por encima del umbral de alerta | 99,99 % | Todas las cadenas compatibles |
Eventos Paid / Charged indexados en menos de 2 minutos | 99,5 % | Todas las cadenas compatibles |
Tasa de revert de charge() (solo reverts no inofensivos) | < 0,1 % | Todas las cadenas compatibles |
Los reverts no inofensivos excluyen ChargeSkipped(InsufficientAllowance), ChargeSkipped(InsufficientBalance) y ChargeSkipped(CapExceeded): son condiciones esperadas del lado del pagador.
Monitorización y alertas
Con el pipeline existente de Sentry + Datadog:
- Tasa de revert de
charge()por hora > umbral → PagerDuty - Saldo nativo de la EOA keeper por debajo de
gasPrice × gasLimit × 100→ PagerDuty - Saldo del upkeep de Chainlink por debajo del umbral → aviso
1Balancede Gelato por debajo del umbral → aviso- Retraso de eventos del indexador > 5 minutos → aviso
Paneles: uno por cadena y un resumen global. Los runbooks para caídas están enlazados desde las guardias.
Protección frente a reorgs
SubscriptionHub.charge(id)aplicablock.timestamp > lastChargedAtde forma monótona. Un reorg que repetiría un cobro se revierte conAlreadyChargedThisPeriod.- El programa de Solana usa
last_charged_atdel mismo modo; en Solana los reorgs son mucho más raros, pero se mantiene el mismo invariante.
Intervención manual
En emergencias (por ejemplo, NestJS y Chainlink caídos a la vez), los comercios pueden disparar charge(id) manualmente con una acción del panel. La transacción se firma desde la propia wallet del comercio; no es custodial.
Lo que el keeper no puede hacer
Lista explícita, para que el alcance quede claro:
- Crear suscripciones sin la firma del pagador.
- Subir
cap,amountointervalde una suscripción existente. - Mover fondos a una dirección que no esté en el
Splitde la suscripción. - Saltarse
nextChargeAtolastChargedAt. - Desactivar los flujos de pausa, cancelación o reembolso.
El keeper tiene gas y nada más.