Saltar al contenido principal

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:

  1. Lee de la base de datos las suscripciones vencidas (un espejo del estado on-chain que mantiene el indexador).
  2. Las agrupa por chain id.
  3. Firma y envía una transacción charge(ids[]) por cadena con una EOA keeper por cadena.
  4. Si tiene éxito, espera la confirmación y actualiza la base de datos mediante los eventos del indexador.
  5. Si hay revert, analiza el selector del revert y lo asigna a un estado de SubscriptionAutoChargeEntity:
    • ChargeSkipped(InsufficientAllowance) → past_due + email de allowance
    • ChargeSkipped(InsufficientBalance) → past_due + email de saldo
    • ChargeSkipped(CapExceeded) → canceled (el pagador debe volver a aprobar)
    • ChargeSkipped(NotDue) → inofensivo (redondeo del indexador); se reintenta en 1 ciclo

A partir de la Fase 3, cada despliegue de SubscriptionHub registra un Upkeep en Chainlink Automation:

  • checkUpkeep(bytes) devuelve performData = abi.encode(ids[]) para los ID en los que isDue(id) && !chargedThisBlock(id).
  • performUpkeep(bytes) llama a charge(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étricaObjetivoSe aplica a
charge() en los 10 minutos siguientes a nextChargeAt99,5 %Todas las cadenas compatibles
charge() en la hora siguiente a nextChargeAt99,9 %Todas las cadenas compatibles
Saldo de gas caliente de la EOA keeper por encima del umbral de alerta99,99 %Todas las cadenas compatibles
Eventos Paid / Charged indexados en menos de 2 minutos99,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
  • 1Balance de 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) aplica block.timestamp > lastChargedAt de forma monótona. Un reorg que repetiría un cobro se revierte con AlreadyChargedThisPeriod.
  • El programa de Solana usa last_charged_at del 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, amount o interval de una suscripción existente.
  • Mover fondos a una dirección que no esté en el Split de la suscripción.
  • Saltarse nextChargeAt o lastChargedAt.
  • Desactivar los flujos de pausa, cancelación o reembolso.

El keeper tiene gas y nada más.

Consulta también​