Fase 2: suscripciones por allowance
20 de abril de 2026: la Fase 2 lleva la facturación recurrente a un SubscriptionHub on-chain. El pagador firma un único approve en su wallet y cualquiera (incluido nuestro keeper) puede llamar a charge(id) cuando vence un periodo. Los fondos van directamente del pagador a los repartos del comercio; OrcaRail nunca los retiene.
En resumen
SubscriptionHub.createSubscription(...)registra el calendario on-chain.- El pagador firma
approve(USDC, hub, cap)una vez en su wallet de siempre. Es el mismo paso que ya documentamos en Cobro automático. - Cualquier EOA puede llamar a
charge(id). Nuestro programador en NestJS lo hace por defecto; Chainlink/Gelato lo respaldarán a partir de la Fase 3. - Protección frente a reorganizaciones mediante un
lastChargedAtmonótono. Sin cobros dobles. - Los suscriptores existentes migran de forma voluntaria, en la renovación.
Qué cambia en esta fase
Hoy, api/src/scheduler/processors/subscription-auto-charge.processor.ts usa una smart account controlada por OrcaRail para cobrar al pagador mediante el approve existente. Funciona, pero la smart account es nuestra: la confianza pasa por nosotros.
Tras la Fase 2:
El allowance sigue yendo del pagador a un contrato, pero el contrato es auditable e inmutable, y cualquiera, no solo nosotros, puede ser el keeper.
Contratos introducidos
SubscriptionHub
Una instancia por cadena. Superficie mínima:
function createSubscription(
bytes32 id,
address payer,
IERC20 token,
uint256 amount,
uint64 interval,
uint256 cap,
Split calldata split
) external;
function charge(bytes32 id) external; // any caller
function cancel(bytes32 id) external; // payer only
function subscription(bytes32 id) view returns (Subscription memory);
event SubscriptionCreated(bytes32 indexed id, address indexed payer, ...);
event Charged(bytes32 indexed id, uint256 amount, uint64 nextChargeAt);
event ChargeSkipped(bytes32 indexed id, bytes32 reason);
event Canceled(bytes32 indexed id);
Invariantes que impone charge()
block.timestamp >= nextChargeAt: no se puede cobrar antes de tiempo.block.timestamp > lastChargedAt: monótono, así las reorganizaciones no pueden cobrar dos veces.amountCharged + amount <= cap: impone el límite de la aprobación además del allowance de ERC-20.token.allowance(payer, hub) >= amount: revert explícito conInsufficientAllowancepara un texto de webhook claro.token.balanceOf(payer) >= amount: igual, conInsufficientBalance.
El Split se reutiliza de la Fase 1: el mismo FeeSplitter, la misma semántica de puntos básicos.
Cambios en la API
El gran cambio es que api/src/scheduler/processors/subscription-auto-charge.processor.ts deja de cobrar desde nuestra smart account y pasa a llamar a SubscriptionHub.charge(id) con una EOA de keeper.
En concreto:
- La EOA del keeper es una hot wallet por cadena que solo tiene gas. No tiene ninguna autoridad sobre los fondos de los comercios.
- Si la cadena tiene una política de paymaster de Alchemy (consulta api/src/withdrawals/services/gas-strategy), el keeper usa el camino patrocinado; si no, paga con su propio saldo de gas.
- Si hay un revert, el procesador interpreta el selector del error y actualiza los mismos estados de SubscriptionAutoChargeEntity que usamos hoy (
past_due,insufficient_balance, etc.). - Los webhooks y los emails de
api/src/mail/mail-templates/subscription-*.hbsse envían sin cambios.
La experiencia del pagador en pay recibe dos ajustes, ambos menores:
- El paso
approve()ahora apunta el spender a la dirección deSubscriptionHuben lugar de a nuestra hot wallet. Es un cambio de texto y un valor de configuración; el flujo que ve el pagador es idéntico. - Un botón de «revocar» que llama a
SubscriptionHub.cancel(id)y luego pide unapprove(hub, 0)on-chain para una limpieza completa. La documentación actual de revocación del cobro automático sigue siendo correcta.
Obsolescencias
- El camino de cobro custodial en
subscription-auto-charge.processor.ts: con el flag, solo se ejecuta la ramaSubscriptionHub.charge. - La smart account de OrcaRail como
approval_spender_address: las suscripciones nuevas guardan en su lugar la dirección del hub. - El campo
approval_spender_addresssigue en la respuesta de la API; simplemente apunta a un sitio nuevo y auditable.
Plan de migración
Los suscriptores existentes nunca se migran a la fuerza. Ofrecemos dos caminos:
- Al renovar: en el siguiente ciclo, un banner del panel pide al pagador que vuelva a firmar
approve(hub, cap)en el nuevoSubscriptionHub. Un clic. El allowance antiguo a la hot wallet heredada se puede dejar o revocar; ya no importa. - Por iniciativa del usuario: un botón «Pasar a no custodial» en la página de detalle de la suscripción hace lo mismo de inmediato.
Las suscripciones que nunca migren se quedan en el camino custodial hasta su cancelación. Ese camino se desactiva en la Fase 7, con mucha antelación.
Riesgos y mitigaciones
- Confusión del pagador en la migración. Mitigación: la plantilla de email reproduce el lenguaje del
subscription-auto-charge-approved.hbsexistente; banner en el panel; entrada de preguntas frecuentes en /docs/non-custodial/migration-guide. - Una caída del keeper se salta un cobro. Mitigación:
charge()no requiere permisos; cualquier instancia de Chainlink Automation o Gelato puede ser el respaldo, y también es posible uncharge()manual iniciado por el comercio. Plan completo en la Fase 3. - Cobro doble por reorganización. Mitigación: invariante monótono de
lastChargedAt; test unitario con reorganizaciones en fork. - El pagador revoca entre ciclos. Mitigación:
charge()emiteChargeSkipped(AllowanceRevoked); se envía la plantilla de email existentesubscription-insufficient-allowance.hbs; la suscripción pasa apast_due. - Desviación de la lista de tokens permitidos. Mitigación: el hub solo acepta tokens con
SubscriptionHub.isAllowed(token); los cambios requieren la multisig.
Lista de estado
-
SubscriptionHubimplementado con todos los invariantes - Tests de fuzzing para casos de límite, allowance, reorganización y omisión
- Auditoría incremental de
SubscriptionHubcompletada - EOA de keeper aprovisionada por cadena, con gas monitorizado mediante Sentry + Datadog
- La app de pago apunta el
approveaSubscriptionHub - Endpoint
approve-auto-chargeactualizado para aceptar allowances con el hub como spender - Banner del panel para las suscripciones existentes
- Grupo canario migrado y estable durante 14 días
Qué sigue
Despliegue en varias cadenas y un segundo keeper: Fase 3.
Documentación de referencia: Estado de la Fase 2 · Guía de migración · Referencia de contratos · Por qué el cobro por allowance
