habilitalo.com / Whitepaper v0.1
01

Resumen

Toda empresa que se conecta a un banco, procesa un CSV, recibe un webhook o integra datos de un sistema operativo o comercial enfrenta el mismo problema: transformar datos externos desordenados en registros internos limpios. La respuesta de la industria han sido plataformas intermediarias — Plaid, MX, Yodlee, Salt Edge — que abstraen la fuente pero reemplazan una dependencia por otra. Intercambias lock-in de proveedor por lock-in de agregador, pagas renta por conexión a perpetuidad, y pierdes control sobre el pipeline de datos entre tu fuente y tu sistema.

Construimos Habilitalo para resolver esto nosotros mismos. Comenzó como la capa de integración dentro de Balancealo, la plataforma de observabilidad financiera de Grupo Digitalo. Después de construir tres conectores, un libro de partida doble, un sistema de eventos y dos motores de análisis, nos dimos cuenta de que el patrón era lo suficientemente general como para ser un protocolo.

Este whitepaper describe ese protocolo tal como existe hoy: aproximadamente 5.500 líneas de TypeScript en 24 archivos, corriendo en producción. No es un documento de roadmap. Todo lo descrito aquí ha sido implementado, probado y desplegado. Donde mencionamos trabajo futuro, lo decimos explícitamente.

La integración de datos — financieros, operativos o comerciales — debería ser un protocolo, no una plataforma. Un conector debería ser una declaración: qué recibe, qué emite, cómo se autentica, acompañada de una función de transformación.

El resto (deduplicación, posteo al libro mayor, entrega de eventos, detección de anomalías) es infraestructura que cualquier conector puede usar sin construirla por sí mismo.

02

El Problema

La integración de datos — financieros, operativos y comerciales — está rota de tres maneras específicas.

1. Fragmentación

Cada institución financiera, sistema operativo o plataforma comercial expone datos de forma diferente. Los bancos chilenos a través de Fintoc usan post_date y sender_account.holder_name anidado. Un CSV de cartola bancaria puede tener Fecha, Monto y Glosa con formato numérico chileno (1.234.567,89). Otro CSV usa Date, Amount y Description con formato estadounidense (1,234,567.89). Una entrada manual desde un formulario tiene otra forma completamente distinta.

Cada fuente requiere su propia lógica de parsing, su propio manejo de fechas, su propia normalización de montos. Los equipos construyen esto desde cero cada vez. El código de parsing es sencillo; el problema es que se enreda con la lógica de negocio, haciendo imposible su reutilización.

2. Lock-In

Las plataformas de agregación que prometen resolver la fragmentación crean su propia dependencia. Tu pipeline de datos se convierte en: Banco → Agregador → Tu App. Cambiar de agregador significa reescribir tu capa de ingestión. El agregador controla el formato de datos, el mecanismo de entrega, el calendario de sincronización y el manejo de errores. No puedes inspeccionar ni modificar el pipeline entre el banco y tu base de datos.

3. Extracción de Renta

Los agregadores cobran por conexión, por cuenta, por mes. Esto tiene sentido económico para ellos pero crea incentivos perversos. Una fintech que atiende pequeñas empresas en América Latina no puede pagar USD 0,50/conexión/mes cuando su cliente promedio tiene 3 cuentas y paga USD 5/mes en total. El agregador se lleva el 30% de los ingresos por hacer lo que equivale a una llamada HTTP y una transformación de datos.

03

Qué es Habilitalo

Habilitalo es un protocolo abierto de Conectores que separa el qué de la integración (qué datos entran, qué eventos salen) del cómo (el pipeline que los procesa). Aunque nació en el contexto financiero, el protocolo es agnóstico al dominio: cualquier fuente de datos — financiera, operativa o comercial — puede expresarse como un conector.

Un Conector es una declaración con una función de transformación. La declaración le dice al sistema qué transporte usa el conector (webhook, carga de archivo, polling de API, entrada manual), qué eventos puede recibir de la fuente externa, qué eventos canónicos emite al sistema, y qué autenticación requiere.

La función de transformación toma datos crudos de la fuente y un objeto de contexto, y retorna un CanonicalEvent — un sobre uniforme que el resto del sistema puede procesar sin saber nada sobre la fuente.

Todo lo que está downstream del Conector — staging, deduplicación, posteo al libro mayor, entrega de eventos, detección de anomalías — es infraestructura compartida. Construir un nuevo conector significa escribir una función de transformación y un manifiesto JSON. El pipeline se encarga del resto.

Principios de Diseño

  1. Declaración sobre configuración. Un manifiesto de conector (habilitalo.connector.json) declara capacidades. Sin código de configuración imperativo.
  2. Aislamiento del transform. La función de transformación es el único código que escribe el autor del conector. Recibe datos crudos y retorna un evento canónico. Los efectos secundarios ocurren en el pipeline.
  3. Entrega al menos una vez. Los eventos se persisten antes de intentar la entrega. El sistema tolera entregas duplicadas; se espera que los consumidores sean idempotentes.
  4. Deduplicación basada en hash. Cada entrada recibe un hash SHA-256 determinístico. Los mismos datos ingestados dos veces producen el mismo hash y se descartan silenciosamente.
  5. Corrección de partida doble. Cada movimiento financiero produce un asiento contable balanceado. Débitos igualan créditos, siempre.
  6. Eventos fire-and-forget. La emisión de eventos nunca bloquea el pipeline. Los eventos son un mecanismo de notificación, no un mecanismo de flujo de control.
04

Conceptos Clave

Conector

La interfaz HabilitaloConnector es la abstracción central. Cada fuente de datos — una API bancaria, una carga de archivo, un formulario manual — se representa como un Conector.

TypeScript
interface HabilitaloConnector {
  /** Identificador único del conector (ej. "fintoc-v1") */
  id: string

  /** Versión semver de este conector */
  version: string

  /** Lo que este conector puede recibir */
  inbound: InboundDeclaration

  /** Qué eventos canónicos emite este conector */
  outbound: OutboundDeclaration

  /** Requisitos de autenticación */
  auth: AuthDeclaration

  /** Función transform: datos crudos -> CanonicalEvent */
  transform: HabilitaloTransform
}

La InboundDeclaration especifica el mecanismo de transporte y los tipos de evento que el conector maneja desde la fuente:

TypeScript
interface InboundDeclaration {
  /** Mecanismo de transporte: webhook, file, manual, api_poll */
  transport: 'webhook' | 'file' | 'manual' | 'api_poll'

  /** Tipos de evento que este conector puede recibir de la fuente */
  events: string[]
}

La OutboundDeclaration especifica qué tipos de eventos canónicos produce este conector:

TypeScript
interface OutboundDeclaration {
  /** Tipos de eventos canónicos que este conector emite */
  events: string[]
}

La AuthDeclaration le dice al sistema qué credenciales se requieren:

TypeScript
interface AuthDeclaration {
  /** Tipo de autenticación requerido por la fuente */
  type: 'api_key' | 'oauth2' | 'link_token' | 'none'

  /** Campos de credencial requeridos (ej. ["apiKey", "secretKey"]) */
  requiredFields?: string[]
}

La función de transformación recibe datos crudos y un objeto de contexto:

TypeScript
type HabilitaloTransform = (
  raw: unknown,
  context: ConnectorContext
) => CanonicalEvent

interface ConnectorContext {
  /** ID de organización/tenant */
  tenantId: string
  /** ID del data feed (si aplica) */
  feedId?: string
  /** Configuración del conector (específica por fuente) */
  config: Record<string, unknown>
  /** Trace ID para tracing distribuido */
  traceId: string
}

Evento Canónico

Cada conector emite eventos en una única forma canónica. Este es el sobre universal que fluye a través de todo el sistema — desde el conector al pipeline, al webhook, al motor de análisis.

TypeScript
interface CanonicalEvent {
  /** Identificador único del evento (UUIDv4) */
  id: string

  /** Timestamp ISO8601 de cuándo se emitió el evento */
  timestamp: string

  /** Sistema que originó el evento (ej. "habilitalo") */
  source: string

  /** Tipo de evento en notación de punto (ej. "bank.movement.created") */
  type: string

  /** Versión del esquema de este tipo de evento (semver) */
  version: string

  /** Payload específico del evento -- forma depende de `type` */
  payload: Record<string, unknown>

  /** Metadata de ruteo y tracing */
  meta: {
    connector: string
    tenant: string
    trace_id: string
  }
}

El helper createCanonicalEvent provee valores por defecto sensatos:

TypeScript
function createCanonicalEvent(
  params: Pick<CanonicalEvent, 'type' | 'payload' | 'meta'> & {
    id?: string
    version?: string
    source?: string
  }
): CanonicalEvent {
  return {
    id: params.id ?? crypto.randomUUID(),
    timestamp: new Date().toISOString(),
    source: params.source ?? 'habilitalo',
    type: params.type,
    version: params.version ?? '1.0.0',
    payload: params.payload,
    meta: params.meta,
  }
}

Registry

El ConnectorRegistry es el catálogo en tiempo de ejecución de todos los conectores disponibles. Sirve dos propósitos: es el mecanismo de búsqueda para el pipeline (dado un tipo de feed, encontrar el conector correcto) y es el puente de compatibilidad entre la nueva capa de protocolo y el sistema existente de SourceAdapter.

TypeScript
const ConnectorRegistry = {
  register(connector: HabilitaloConnector, adapter?: SourceAdapter): void
  get(id: string): HabilitaloConnector | undefined
  list(): HabilitaloConnector[]
  has(id: string): boolean
  getSourceAdapter: getSourceAdapter   // Puente legacy
  listSourceAdapters: listSourceAdapters // Puente legacy
}

La función bridgeAdapterToConnector envuelve un SourceAdapter existente en la interfaz HabilitaloConnector sin modificar el código del adapter:

TypeScript
function bridgeAdapterToConnector(
  adapter: SourceAdapter,
  manifest: {
    id: string
    version: string
    inbound: HabilitaloConnector['inbound']
    outbound: HabilitaloConnector['outbound']
    auth: HabilitaloConnector['auth']
  }
): HabilitaloConnector
05

El Pipeline de 3 Etapas

El pipeline es el núcleo de Habilitalo. Toma datos crudos de cualquier fuente y produce asientos contables balanceados, eventos emitidos y resultados de análisis. Las tres etapas son secuenciales y desacopladas — cada etapa lee y escribe a la base de datos, de modo que una falla en una etapa no corrompe las demás.

                     Etapa 1              Etapa 2              Etapa 3
                    (Ingestar)           (Procesar)           (Analizar)
                 +------------+      +--------------+     +------------+
Fuente Externa   | Adapter    | ---> | RawEntry     | --> | JournalEntry|
(banco, CSV,     | Transform  |      | (staging)    |     | (libro)     |
 formulario)     | Validar    |      | Deduplicar   |     | Balanceado  |
                 | Hash       |      | Partida doble|     | débito/créd.|
                 +------------+      +--------------+     +------------+
                                                                |
                                                          +-----+------+
                                                          |            |
                                                    [Eventos]   [Análisis]
                                                          |            |
                                                    Webhooks    Anomalías
                                                                Patrones

Etapa 1: Ingestión

La etapa de ingestión transforma datos crudos de la fuente en registros RawEntry en la tabla de staging. Aquí es donde ocurre el parsing específico de cada fuente: formatos de fecha, formatos de monto, mapeo de campos, detección de moneda.

Entrada: Datos crudos de la fuente (movimientos Fintoc, filas CSV, datos de formulario manual).

Proceso:

  1. El SourceAdapter apropiado transforma los datos de la fuente en RawEntryInput[].
  2. Cada entrada se valida: campos requeridos, límites de monto, sanidad de fecha, validez de moneda.
  3. Se calcula un hash SHA-256 desde campos normalizados: date|amount|accountId|description|reference.
  4. El hash se compara contra entradas existentes en la misma organización.
  5. Si es duplicado: se descarta silenciosamente (o se usa sufijo de secuencia para colisiones legítimas).
  6. Si es nuevo: se crea un RawEntry con estado pending.

Salida: IngestResult con conteos de entradas staged, duplicadas y con error.

TypeScript
interface IngestResult {
  staged: number
  duplicates: number
  errors: number
  entries: Array<{
    id?: string
    status: 'staged' | 'duplicate' | 'error'
    hash?: string
    error?: string
  }>
}

El RawEntryInput es el formato intermedio universal que produce cada adapter:

TypeScript
interface RawEntryInput {
  dataFeedId: string
  organizationId: string
  transactionDate: Date
  postDate?: Date | null
  amount: number
  currency?: Currency
  description: string
  counterparty?: string | null
  reference?: string | null
  sourceId?: string | null
  rawData: Record<string, unknown>
}

Decisiones clave de diseño en la etapa de ingestión:

  • Batch vs. individual. Para lotes de menos de 100 entradas, usamos createMany con hashes pre-calculados. Para lotes más grandes, hacemos procesamiento individual para evitar transacciones largas.
  • Preservación de datos crudos. Los datos originales se almacenan en rawData como columna JSON, permitiendo re-procesamiento o auditoría sin consultar la fuente nuevamente.
  • Estadísticas de feed. Después de una ingestión exitosa, el registro DataFeed se actualiza con incremento de totalEntries y timestamp lastSyncAt.

Etapa 2: Procesamiento

El procesador toma registros RawEntry pendientes y los convierte en registros JournalEntry de partida doble en el libro mayor.

Entrada: Registros RawEntry con estado pending.

Proceso:

  1. Obtener entradas pendientes ordenadas por fecha de transacción.
  2. Para cada entrada, marcar como processing (previene procesamiento concurrente).
  3. Verificación de race condition: confirmar que ninguna otra entrada con el mismo hash ya esté processed.
  4. Determinar dirección de la transacción por signo del monto (positivo = ingreso, negativo = gasto).
  5. Crear un JournalEntry con JournalLine[] débito/crédito balanceados.
  6. Marcar RawEntry como processed con referencia al JournalEntry.
  7. Fire-and-forget: emitir evento bank.movement.created.

Salida: ProcessorResult con conteos y detalles por entrada.

TypeScript
interface ProcessorResult {
  processed: number
  duplicates: number
  errors: number
  details: Array<{
    rawEntryId: string
    status: 'processed' | 'duplicate' | 'error'
    journalEntryId?: string
    error?: string
  }>
}

Manejo de errores: Si el procesamiento falla para una entrada, se marca como error con el mensaje de error almacenado. Las entradas fallidas pueden re-procesarse luego vía reprocessFailedEntries, que resetea su estado a pending y ejecuta el procesador de nuevo.

Etapa 3: Análisis

La etapa de análisis corre asincrónicamente después del posteo al libro mayor. Examina los asientos contables buscando dos clases de patrones:

  1. Anomalías: Transacciones que se desvían significativamente de las normas históricas.
  2. Recurrencias: Patrones repetitivos (suscripciones, salario, arriendo, etc.).

Ambos motores leen de las tablas JournalEntry y JournalLine y escriben sus resultados en tablas dedicadas (Anomaly, RecurringPattern, RecurringOccurrence).

06

Deduplicación

La deduplicación es crítica para un sistema que ingesta datos de múltiples fuentes en calendarios superpuestos. Fintoc puede entregar el mismo movimiento en dos sincronizaciones consecutivas. Un usuario puede subir el mismo CSV dos veces. El sistema debe absorber duplicados silenciosamente sin crear entradas dobles en el libro mayor.

Cálculo de Hash

Cada entrada recibe un hash SHA-256 calculado a partir de cinco campos normalizados:

SHA-256( date | amount | accountId | description | reference )

Reglas de normalización:

  • Fecha: Convertida a YYYY-MM-DD (componente de hora eliminado).
  • Monto: Valor absoluto con exactamente 2 decimales.
  • AccountId: Usado tal cual (UUID).
  • Descripción: En minúsculas, acentos removidos (normalización NFD), caracteres especiales eliminados, espacios múltiples colapsados.
  • Referencia: Misma normalización que descripción, incluida solo si está presente.
TypeScript
function calculateEntryHash(entry: HashableEntry): HashResult {
  const normalizedFields = {
    date: normalizeDate(entry.transactionDate),
    amount: normalizeAmount(entry.amount),
    accountId: entry.accountId,
    description: normalizeString(entry.description),
    reference: entry.reference ? normalizeString(entry.reference) : '',
  }

  const parts = [
    normalizedFields.date,
    normalizedFields.amount,
    normalizedFields.accountId,
    normalizedFields.description,
  ]

  if (normalizedFields.reference) {
    parts.push(normalizedFields.reference)
  }

  const hashInput = parts.join('|')
  const hash = createHash('sha256').update(hashInput).digest('hex')

  return { hash, normalizedFields }
}

Manejo de Colisiones

Las colisiones legítimas ocurren cuando una persona hace dos compras idénticas el mismo día. El sistema las distingue verificando sourceId — si dos entradas tienen el mismo hash pero identificadores de fuente diferentes, se tratan como transacciones distintas.

Para colisiones legítimas, se agrega un sufijo de secuencia:

TypeScript
function calculateEntryHashWithSequence(
  entry: HashableEntry,
  sequence: number
): string {
  const { hash: baseHash } = calculateEntryHash(entry)
  if (sequence === 0) return baseHash
  return createHash('sha256')
    .update(`${baseHash}|seq:${sequence}`)
    .digest('hex')
}

El sistema intenta hasta 10 números de secuencia antes de desistir y marcar la entrada como duplicada.

Restricción de Base de Datos

El hash de deduplicación se enforce a nivel de base de datos con una restricción unique sobre (organizationId, entryHash). Esto provee una red de seguridad contra race conditions. La ruta de ingestión en bulk usa createMany con skipDuplicates: true como guarda adicional.

07

Libro de Partida Doble

Cada movimiento financiero en Habilitalo se registra como un JournalEntry con registros JournalLine[] balanceados. Esto es contabilidad de partida doble propiamente tal: por cada débito, hay un crédito igual, por moneda.

Modelo de Datos

JournalEntry
  id: UUID
  organizationId: UUID
  entryNumber: auto-incremento por org
  description: string
  transactionDate: Date
  postDate: Date (por defecto la hora de creación)
  status: 'draft' | 'posted' | 'voided'
  sourceType: DataFeedType (fintoc, csv_import, manual, etc.)
  sourceRef: string (referencia al RawEntry u otra fuente)
  lines: JournalLine[]

JournalLine
  id: UUID
  journalEntryId: UUID
  accountId: UUID
  entryType: 'debit' | 'credit'
  amount: Decimal
  currency: Currency
  description: string
  counterparty: string
  categoryId: UUID (opcional)
  ownerId: UUID (opcional)

Validación de Balance

Antes de crear cualquier JournalEntry, el sistema valida que los débitos igualen los créditos para cada moneda:

TypeScript
function validateJournalLinesBalance(
  lines: Array<{ entryType: 'debit' | 'credit'; amount: number; currency: string }>
): ValidationResult {
  const byCurrency = new Map<string, { debits: number; credits: number }>()

  for (const line of lines) {
    const curr = byCurrency.get(line.currency) || { debits: 0, credits: 0 }
    if (line.entryType === 'debit') {
      curr.debits += line.amount
    } else {
      curr.credits += line.amount
    }
    byCurrency.set(line.currency, curr)
  }

  for (const [currency, { debits, credits }] of byCurrency) {
    const diff = Math.abs(debits - credits)
    if (diff > 0.01) {
      errors.push(
        `Líneas en ${currency} no están balanceadas: débitos=${debits}, créditos=${credits}`
      )
    }
  }

  return { valid: errors.length === 0, errors, warnings }
}

Naturaleza de Cuenta

Las cuentas tienen una naturaleza que determina cómo se calcula su saldo:

  • debit_normal (activos, gastos): Saldo = SUMA(débitos) - SUMA(créditos).
  • credit_normal (pasivos, patrimonio, ingresos): Saldo = SUMA(créditos) - SUMA(débitos).
TypeScript
async function calculateAccountBalance(
  accountId: string,
  asOfDate?: Date,
  includeVoided = false
): Promise<AccountBalance> {
  // ... obtener cuenta y agregar líneas ...

  const balance =
    account.accountNature === 'debit_normal'
      ? debits - credits
      : credits - debits

  return {
    accountId: account.id,
    accountName: account.name,
    accountNature: account.accountNature,
    totalDebits: debits,
    totalCredits: credits,
    balance,
    currency: account.currency,
  }
}

Tipos de Transacción

El libro mayor soporta tres tipos simples de transacción:

  • Ingreso: DÉBITO a la cuenta bancaria (aumenta activo), CRÉDITO a la contra de ingresos.
  • Gasto: DÉBITO a la contra de gastos, CRÉDITO a la cuenta bancaria (disminuye activo).
  • Transferencia: DÉBITO a la cuenta destino, CRÉDITO a la cuenta origen.
TypeScript
async function createSimpleEntry(
  input: SimpleTransactionInput & { organizationId: string }
): Promise<JournalEntryWithLines> {
  switch (input.type) {
    case 'income':
      return createIncomeEntry(input)
    case 'expense':
      return createExpenseEntry(input)
    case 'transfer':
      return createTransferEntry(input)
  }
}

Anulación/Reversión

Los asientos contables pueden ser anulados pero nunca eliminados. Una entrada anulada retiene sus líneas y metadata pero se excluye de los cálculos de saldo (a menos que includeVoided se establezca explícitamente). La operación de anulación registra una razón y timestamp para propósitos de auditoría.

08

Conectores Fundacionales

Habilitalo viene con tres conectores que cubren los escenarios de ingestión más comunes en fintech latinoamericana. El protocolo soporta conectores de cualquier dominio — financiero, operativo o comercial — estos tres fundacionales demuestran el patrón con datos financieros.

Fintoc (Open Banking Chile)

ID del Conector: fintoc-v1   Transporte: webhook   Auth: link_token   Emite: bank.movement.created

Fintoc provee acceso Open Banking a instituciones financieras chilenas. El conector transforma objetos FintocMovement en registros RawEntryInput.

Mapeo de campos

Campo FintocCampo RawEntryInputNotas
post_datepostDateString fecha ISO
transaction_datetransactionDateFallback a post_date
amountamountPositivo = ingreso, negativo = gasto
currencycurrencyMapeado: CLP, USD, EUR, UF
description + commentdescriptionConcatenados con separador
sender_account.holder_namecounterpartyPara ingresos (monto > 0)
recipient_account.holder_namecounterpartyPara gastos (monto < 0)
reference_idreferenceUsado en cálculo de hash
idsourceIdID de movimiento Fintoc para dedup

Estrategia de sincronización incremental

La integración con Fintoc usa un enfoque de sincronización por chunks:

  1. Ventana de 24 meses: Sincroniza hasta 2 años de datos históricos.
  2. 3 meses por chunk: Cada invocación procesa 3 meses de movimientos.
  3. Cuentas en paralelo: Todas las cuentas dentro de un link se sincronizan en paralelo.
  4. Integración con pipeline: Los movimientos fluyen por el pipeline completo.
  5. Reanudable: El progreso de sincronización se persiste como JSON en el registro FintocLink.

Manifiesto del conector

JSON
{
  "id": "fintoc-v1",
  "name": "Fintoc Open Banking",
  "version": "1.0.0",
  "category": "financial",
  "tags": ["banking", "chile", "latam"],
  "inbound": {
    "transport": "webhook",
    "events": ["payment.received"]
  },
  "outbound": {
    "events": ["bank.movement.created"]
  },
  "auth": {
    "type": "link_token",
    "requiredFields": ["linkToken"]
  },
  "habilitalo": "0.1"
}

CSV (Cartolas Bancarias)

ID del Conector: csv-v1   Transporte: file   Auth: none   Emite: bank.movement.created

El conector CSV parsea archivos de cartola bancaria con detección inteligente de formato.

Detección de formato de monto

FormatoMilesDecimalEjemplo
Chileno. (punto), (coma)1.234.567,89
US/Internacional, (coma). (punto)1,234,567.89

Auto-detección de columnas

CampoNombres de columna de fallback
Fechafecha, date, transaction_date, transactiondate
Montomonto, amount, valor, value, importe
Descripcióndescripcion, description, detalle, glosa, concepto
Contrapartecontraparte, counterparty, comercio, merchant
Referenciareferencia, reference, ref, numero
JSON
{
  "id": "csv-v1",
  "name": "CSV File Import",
  "version": "1.0.0",
  "category": "financial",
  "tags": ["csv", "file", "import", "bank-statement"],
  "inbound": { "transport": "file", "events": ["file.uploaded"] },
  "outbound": { "events": ["bank.movement.created"] },
  "auth": { "type": "none" },
  "habilitalo": "0.1"
}

Manual (Formularios Estructurados)

ID del Conector: manual-v1   Transporte: manual   Auth: none   Emite: bank.movement.created

El conector Manual es un adapter passthrough para datos estructurados enviados a través de formularios o llamadas API. Valida y normaliza los datos pero no realiza parsing. Este conector existe para asegurar que las transacciones ingresadas manualmente fluyan por el mismo pipeline que las fuentes automatizadas.

JSON
{
  "id": "manual-v1",
  "name": "Manual Entry",
  "version": "1.0.0",
  "category": "financial",
  "tags": ["manual", "entry", "form"],
  "inbound": { "transport": "manual", "events": ["entry.submitted"] },
  "outbound": { "events": ["bank.movement.created"] },
  "auth": { "type": "none" },
  "habilitalo": "0.1"
}
09

Sistema de Eventos

Habilitalo emite eventos vía webhooks cuando ocurren cosas significativas en el pipeline. Los eventos son el mecanismo por el cual los servicios downstream aprenden sobre la actividad financiera.

Arquitectura

El sistema de eventos se construye sobre tres tablas de base de datos:

  • WebhookEndpoint: Una URL registrada que recibe eventos. Tiene tipos de eventos suscritos, un secreto para firma HMAC, y seguimiento de fallas.
  • WebhookDelivery: Un registro de cada intento de entrega. Persistido antes del primer intento (garantía de al-menos-una-vez).
  • El emisor de eventos: La función emit() que orquesta la entrega.

Semántica de Entrega

Al-menos-una-vez: Un registro WebhookDelivery se crea en la base de datos antes de la primera solicitud HTTP.

Fire-and-forget: La función emit() nunca lanza una excepción. Todos los errores se capturan y loguean.

Reintento con backoff exponencial: Las entregas fallidas se reintentan hasta 3 veces:

Intento 1: inmediato
Intento 2: después de 1s    (1000ms × 4^0)
Intento 3: después de 4s    (1000ms × 4^1)
           (desistir después de 3 fallas)

Errores no reintentables: Respuestas HTTP 4xx (excepto 429 Too Many Requests) no se reintentan.

Firma HMAC-SHA256

Cada entrega de webhook se firma con el secreto del endpoint usando HMAC-SHA256:

TypeScript
const signature = createHmac('sha256', endpoint.secretHash)
  .update(payloadString)
  .digest('hex')

// Headers enviados:
// Content-Type: application/json
// X-Habilitalo-Event: bank.movement.created
// X-Habilitalo-Signature: <HMAC codificado en hex>

Filtrado de Eventos

Cada WebhookEndpoint tiene un array events que especifica qué tipos de eventos quiere recibir. El wildcard * suscribe a todos los eventos:

TypeScript
const subscribedEndpoints = endpoints.filter((ep) => {
  const events = ep.events as string[]
  return Array.isArray(events) && (
    events.includes(eventType) || events.includes('*')
  )
})

Tipos de Evento

Tipo de EventoDisparadorPayload
bank.movement.createdEl procesador crea un JournalEntry desde un RawEntryDetalles del movimiento, cuenta, monto, contraparte
analysis.anomaly.detectedEl detector de anomalías marca una transacciónTipo de anomalía, severidad, Z-score, esperado vs. detectado
analysis.recurrence.detectedEl detector de recurrencia identifica un patrónNombre del patrón, frecuencia, monto, confianza
ledger.entry.createdUn JournalEntry se postea al libro mayorAsiento contable completo con todas las líneas

API de Gestión de Webhooks

Los endpoints de webhook se gestionan a través de rutas REST API en /api/habilitalo/webhooks:

  • POST /api/habilitalo/webhooks — Crear un nuevo endpoint.
  • GET /api/habilitalo/webhooks?organizationId=... — Listar endpoints activos.
  • GET /api/habilitalo/webhooks/:id/deliveries — Ver historial de entregas.
10

Motores de Análisis

Habilitalo incluye dos motores de análisis que corren sobre datos del libro mayor, diseñados para detectar insights que serían difíciles de identificar manualmente.

Detección de Anomalías

El detector de anomalías usa análisis de Z-score sobre estadísticas de transacciones por categoría para marcar outliers. Detecta tres tipos de anomalías:

1. Monto Inusual (unusual_amount)

Para cada categoría de transacción, el sistema mantiene estadísticas corrientes calculadas desde líneas de journal en el período de lookback (por defecto: 90 días, mínimo 10 muestras).

Z = |monto - media_categoría| / desviación_estándar_categoría
SeveridadUmbral Z-score por Defecto
Baja2.0
Media2.5
Alta3.0
Crítica4.0

Niveles de sensibilidad ajustan todos los umbrales por un multiplicador:

SensibilidadMultiplicadorEfecto
Baja1.3xMenos alertas, solo outliers extremos
Media1.0xComportamiento por defecto
Alta0.7xMás alertas, captura desviaciones menores

2. Contraparte Nueva (new_counterparty)

Marca la primera transacción con una contraparte no vista previamente cuando el monto supera un umbral configurable (por defecto: CLP 100.000). La severidad escala con el monto:

  • baja: Monto ≥ umbral
  • media: Monto ≥ 2x umbral
  • alta: Monto ≥ 5x umbral

3. Sospecha de Duplicado (duplicate_suspect)

Encuentra transacciones con monto y descripción normalizada idénticos dentro de una ventana de 3 días.

Configuración

TypeScript
interface AnomalyConfig {
  lowThreshold: number            // Por defecto: 2.0
  mediumThreshold: number         // Por defecto: 2.5
  highThreshold: number           // Por defecto: 3.0
  criticalThreshold: number       // Por defecto: 4.0
  minSampleSize: number           // Por defecto: 10
  lookbackDays: number            // Por defecto: 90
  newCounterpartyAmountThreshold: number  // Por defecto: 100000 (CLP)
}

Detección de Recurrencia

El detector de recurrencia identifica patrones de transacciones repetitivas: suscripciones, depósitos de salario, pagos de arriendo, cuentas de servicios básicos.

Algoritmo

  1. Agrupar transacciones por descripción normalizada y bucket de monto (bucketing logarítmico, 15% de varianza).
  2. Filtrar grupos con menos del mínimo de ocurrencias (por defecto: 3).
  3. Calcular intervalos entre transacciones consecutivas en cada grupo.
  4. Verificar regularidad. Si la desviación estándar de intervalos excede la varianza máxima (por defecto: 5 días), el grupo no es suficientemente regular.
  5. Detectar frecuencia desde el intervalo promedio:
Intervalo PromedioFrecuencia
1–2 díasdiaria
5–9 díassemanal
12–18 díasquincenal
25–35 díasmensual
85–100 díastrimestral
350–380 díasanual
  1. Calcular confianza como score ponderado:
confianza = score_regularidad × 0.5
          + score_ocurrencias × 0.3
          + score_consistencia_monto × 0.2

Donde:
  score_regularidad       = 1 - (stddev_intervalos / promedio_intervalos)
  score_ocurrencias       = min(1, count_ocurrencias / 12)
  score_consistencia      = 1 - (stddev_monto / promedio_monto)

Los patrones con confianza bajo 0.7 se descartan.

  1. Predecir próxima ocurrencia sumando el intervalo promedio a la fecha de última ocurrencia.

Configuración

TypeScript
interface RecurrenceConfig {
  minOccurrences: number        // Por defecto: 3
  maxIntervalVariance: number   // Por defecto: 5 días
  amountVariancePercent: number // Por defecto: 15%
  lookbackDays: number          // Por defecto: 180
  minConfidence: number         // Por defecto: 0.7
}

Ciclo de vida de patrones

Los patrones detectados se almacenan como registros RecurringPattern con estado active. Cada transacción que coincide crea un registro RecurringOccurrence. El sistema también rastrea ocurrencias perdidas — habilitando alertas proactivas como "Tu suscripción de Netflix de $12.990 CLP se esperaba el 5 de marzo pero no ha llegado."

11

Posición en el Stack

Habilitalo es la capa de integración y observación del ecosistema Digitalo. Se ubica en la frontera entre sistemas externos y el mesh de servicios interno.

+------------------------------------------------------------------+
|                        MUNDO EXTERNO                              |
|  Bancos (Fintoc)  |  CSVs  |  Manual  |  Futuro: SAT, SII, etc. |
+--------+---------+--------+----------+---------------------------+
         |              |         |
         v              v         v
+------------------------------------------------------------------+
|                        HABILITALO                                 |
|  Conectores  ->  Pipeline  ->  Libro   ->  Eventos  ->  Análisis |
|  (protocolo)     (ingestar)    (partida   (webhooks)  (anomalía, |
|                  (procesar)     doble)                  recurrencia)
+--------+---------------------------------------------------------+
         |
         v
+------------------------------------------------------------------+
|                        SERVICIALO                                 |
|  Service mesh, orquestación, comunicación inter-servicios         |
+--------+---------------------------------------------------------+
         |
         v
+------------------------------------------------------------------+
|                        COMPENSALO                                 |
|  Pagos, desembolsos, cobranzas                                    |
+--------+---------------------------------------------------------+
         |
         v
+------------------------------------------------------------------+
|                        COORDINALO                                 |
|  Agendamiento, agenda, citas                                      |
+--------+---------------------------------------------------------+
         |
         v
+------------------------------------------------------------------+
|                        ACUMULALO                                  |
|  Wallet, fidelización, puntos                                     |
+------------------------------------------------------------------+

Los datos fluyen hacia adentro a través de Habilitalo: las fuentes externas empujan o son polleadas, los conectores transforman los datos, el pipeline los procesa, y los eventos notifican a los servicios downstream. Compensalo escucha bank.movement.created para conciliar pagos. Coordinalo escucha analysis.recurrence.detected para programar recordatorios.

Habilitalo es extraíble. Corre como un módulo dentro de Balancealo hoy, pero el grafo de dependencias está diseñado para su futura extracción como servicio independiente.

12

Implementación Técnica

Stack Tecnológico

  • Runtime: Next.js 15+ sobre Node.js
  • Lenguaje: TypeScript 5 (modo estricto)
  • Base de datos: PostgreSQL vía Supabase
  • ORM: Prisma con migraciones
  • Criptografía: Módulo crypto nativo de Node.js (SHA-256, HMAC-SHA256)
  • Multi-tenancy: Basado en organizaciones con aislamiento de datos a nivel de fila

Estructura de Archivos

src/lib/habilitalo/                           (~5.500 LOC)
|-- index.ts                                  Exports centrales
|-- README.md                                 Documentación del módulo
|
|-- protocol/                                 Protocolo de conectores
|   |-- connector.ts                          Interfaz HabilitaloConnector
|   |-- canonical-event.ts                    Esquema CanonicalEvent + helper
|   |-- registry.ts                           ConnectorRegistry + bridge
|   +-- index.ts                              Exports del protocolo
|
|-- events/                                   Sistema de eventos
|   |-- HabilitaloEventEmitter.ts             Emisor core (HMAC, retry)
|   |-- emitBankMovementCreated.ts            Constructor de evento
|   |-- index.ts                              Exports de eventos
|   +-- schemas/                              Definiciones de tipos de evento
|       |-- BankMovementCreated.ts            bank.movement.created
|       |-- AnomalyDetected.ts               analysis.anomaly.detected
|       |-- RecurrenceDetected.ts            analysis.recurrence.detected
|       +-- LedgerEntryCreated.ts            ledger.entry.created
|
|-- feeds/                                    Pipeline
|   |-- index.ts                              Exports de feeds
|   |-- types.ts                              Interfaces core
|   |-- hash.ts                               Motor de dedup SHA-256
|   |
|   |-- pipeline/                             ETL de 3 etapas
|   |   |-- ingest.ts                         Etapa 1: fuente -> RawEntry
|   |   |-- validate.ts                       Validación de entrada + balance
|   |   +-- processor.ts                      Etapa 2: RawEntry -> JournalEntry
|   |
|   |-- sources/                              Implementaciones de conectores
|   |   |-- base.ts                           Interfaz SourceAdapter + registry
|   |   |-- fintoc.ts                         Adapter Fintoc
|   |   |-- fintoc/habilitalo.connector.json  Manifiesto Fintoc
|   |   |-- csv.ts                            Adapter CSV
|   |   |-- csv/habilitalo.connector.json     Manifiesto CSV
|   |   |-- manual.ts                         Adapter Manual
|   |   +-- manual/habilitalo.connector.json  Manifiesto Manual
|   |
|   |-- ledger/                               Contabilidad partida doble
|   |   |-- journal.ts                        CRUD, anulación, consultas
|   |   +-- balance.ts                        Balance por naturaleza de cuenta
|   |
|   +-- analysis/                             Motores de análisis
|       |-- types.ts                           Config + tipos de resultado
|       |-- anomaly-detector.ts               Detección de anomalías Z-score
|       |-- recurrence-detector.ts            Detección de patrones
|       +-- index.ts                          Exports de análisis
|
+-- integrations/                             Orquestadores de terceros
    +-- fintoc/
        +-- sync-incremental.ts               Sync incremental 24 meses

Superficie de API

  • Gestión de webhooks: POST/GET /api/habilitalo/webhooks
  • Inspección de entregas: GET /api/habilitalo/webhooks/:id/deliveries
  • Operaciones de feed: Varias rutas bajo /api/v1/feeds/
  • Consultas al libro mayor: Rutas bajo /api/v1/ledger/
  • Disparadores de sync: Rutas cron job para sincronización Fintoc

Multi-Tenancy

Cada registro en el sistema pertenece a una organización. El organizationId está presente en: DataFeed, RawEntry, JournalEntry, JournalLine, Anomaly, RecurringPattern, WebhookEndpoint, WebhookDelivery.

La restricción de deduplicación está scoped a la organización: UNIQUE(organizationId, entryHash). Dos organizaciones pueden tener transacciones idénticas sin colisión.

13

Gobernanza

Licencia

La especificación del protocolo Habilitalo y la implementación de referencia serán publicadas bajo la Licencia MIT. Esto significa:

  • Cualquiera puede construir un conector sin pedir permiso.
  • Cualquiera puede correr el pipeline en su propia infraestructura.
  • Cualquiera puede hacer fork, modificar y redistribuir.
  • No hay un tier "premium" del protocolo. El protocolo completo es abierto.

Registry Abierto

Planeamos mantener un registry abierto de conectores comunitarios en habilitalo.com. Enviar un conector requiere:

  1. Un manifiesto habilitalo.connector.json.
  2. Una función de transformación que pase la validación (chequeo de tipos input/output).
  3. Al menos un par de ejemplo input/output para testing.

El registry es un catálogo, no una puerta. Si tu conector cumple los requisitos de interfaz, se lista.

Auto-Hosteable

El pipeline y el sistema de eventos están diseñados para correr en cualquier despliegue PostgreSQL + Node.js. No hay dependencia de servicios cloud propietarios más allá de lo que usa la aplicación host (Balancealo). El esquema Prisma define todas las tablas requeridas, y las migraciones son versionadas y portables.

Calidad de Conectores

Distinguimos tres niveles de calidad de conectores:

  • Fundacionales: Construidos y mantenidos por el equipo Habilitalo. Actualmente: Fintoc, CSV, Manual.
  • Verificados: Construidos por la comunidad, revisados por el equipo Habilitalo por corrección y seguridad.
  • Comunitarios: Auto-publicados. Sin revisión. Usar a discreción propia.

Versionamiento

  • La versión del protocolo sigue semver. La versión actual es 0.1.
  • Los manifiestos de conector declaran la versión del protocolo que apuntan ("habilitalo": "0.1").
  • Los esquemas de evento se versionan independientemente.
  • Los breaking changes incrementarán la versión mayor del protocolo.
14

Hoja de Ruta

v0.1 — Actual (Implementado)

Todo lo descrito en este documento está implementado y desplegado:

  • Interfaz HabilitaloConnector y ConnectorContext
  • Esquema CanonicalEvent con helper createCanonicalEvent
  • ConnectorRegistry con bridgeAdapterToConnector
  • Pipeline de 3 etapas: ingestar, procesar, analizar
  • 3 conectores fundacionales: Fintoc, CSV, Manual
  • Sistema de eventos con HMAC-SHA256, entrega al-menos-una-vez, 3 reintentos con backoff exponencial
  • 4 tipos de evento
  • Libro de partida doble con validación de balance, naturaleza de cuenta, anulación/reversión
  • Detector de anomalías: Z-score, contraparte nueva, detección de sospecha de duplicado
  • Detector de recurrencia: análisis de intervalos, clasificación de frecuencia, scoring de confianza
  • Sync incremental Fintoc de 24 meses con chunks reanudables
  • API de gestión de webhooks
  • Aislamiento multi-tenant con deduplicación scoped por organización

v0.2 — Siguiente (Planeado)

  • UI de conectores: Gestión visual de conectores en el dashboard de Balancealo.
  • Conectores adicionales: SII chileno, templates CSV por banco, wizard de mapeo manual de columnas.
  • Endpoints de API de análisis: Endpoints REST para detección de anomalías/recurrencias on-demand.
  • Cobertura de tests: Tests unitarios para cálculo de hash, transforms de adapter, validación de balance.
  • Connector SDK: Herramienta CLI para scaffoldear un nuevo conector (npx habilitalo init my-connector).
  • Documentación del protocolo: Spec OpenAPI, JSON Schema para eventos y manifiestos.

v0.3 — Futuro (Considerado)

  • Marketplace de conectores: Registry público con búsqueda, documentación e instalación.
  • Conectores operativos y comerciales: CRM, ERP, inventario, facturación electrónica, plataformas de e-commerce.
  • Pipeline streaming: Reemplazar procesamiento batch con streaming event-driven.
  • Conectores cross-organización: Transferencias inter-empresa y conciliación.
  • Categorización con IA: Categorización de datos basada en LLM como etapa del pipeline.
  • Extracción del protocolo: Extraer en un paquete npm standalone y servicio independiente.
15

Apéndice A: Esquema del Manifiesto de Conector

Cada conector viene con un archivo habilitalo.connector.json que declara sus capacidades:

JSON
{
  "id": "string",
  "name": "string",
  "version": "string (semver)",
  "category": "string",
  "tags": ["string"],
  "inbound": {
    "transport": "webhook | file | manual | api_poll",
    "events": ["string"]
  },
  "outbound": {
    "events": ["string"]
  },
  "auth": {
    "type": "api_key | oauth2 | link_token | none",
    "requiredFields": ["string"]
  },
  "habilitalo": "string (versión del protocolo)"
}

Descripción de campos

CampoRequeridoDescripción
idIdentificador único del conector. Convención: {fuente}-v{mayor}
nameNombre legible del conector
versionVersión semver de este conector
categoryCategoría amplia: financial, commerce, tax, custom
tagsNoTags buscables para descubrimiento en el registry
inbound.transportCómo entran los datos al conector
inbound.eventsTipos de evento de la fuente que maneja este conector
outbound.eventsTipos de evento canónico que emite este conector
auth.typeMecanismo de autenticación requerido
auth.requiredFieldsNoNombres de campos de credencial (para generación de UI)
habilitaloVersión del protocolo que apunta este manifiesto
16

Apéndice B: Referencia de Tipos de Evento

bank.movement.created

Se emite cuando el procesador del pipeline crea un JournalEntry desde un movimiento bancario.

TypeScript
{
  eventType: 'bank.movement.created',
  eventId: string,       // UUIDv4
  source: 'habilitalo',
  timestamp: string,     // ISO8601
  data: {
    movementId: string,
    accountId: string,
    institutionId: string,
    amount: number,
    currency: string,
    date: string,                // ISO8601
    description: string,
    counterparty: string | null,
    type: 'credit' | 'debit',
    category: string | null,
    categoryId: string | null,
    categorySource: string | null,
    rawFintocId: string,
    journalEntryId: string
  }
}

analysis.anomaly.detected

Se emite cuando el detector de anomalías marca una transacción.

TypeScript
{
  eventType: 'analysis.anomaly.detected',
  eventId: string,
  source: 'habilitalo',
  timestamp: string,
  data: {
    anomalyId: string,
    journalLineId: string,
    type: 'unusual_amount' | 'unusual_frequency'
        | 'new_counterparty' | 'duplicate_suspect',
    severity: 'low' | 'medium' | 'high' | 'critical',
    title: string,
    description: string,
    detectedValue: number,
    expectedValue: number | null,
    deviation: number | null,
    context: {
      mean?: number,
      stdDev?: number,
      zScore?: number,
      threshold?: number,
      categoryAvg?: number,
      categoryMax?: number,
      similarTransactionsCount?: number
    }
  }
}

analysis.recurrence.detected

Se emite cuando el detector de recurrencia identifica o actualiza un patrón.

TypeScript
{
  eventType: 'analysis.recurrence.detected',
  eventId: string,
  source: 'habilitalo',
  timestamp: string,
  data: {
    patternId: string,
    name: string,
    frequency: 'daily' | 'weekly' | 'biweekly'
             | 'monthly' | 'quarterly' | 'yearly' | 'custom',
    intervalDays: number,
    expectedAmount: number,
    amountVariance: number,
    currency: string,
    matchPattern: string,
    counterparty: string | null,
    categoryId: string | null,
    confidence: number,         // 0.0 a 1.0
    occurrenceCount: number,
    firstOccurrence: string,    // ISO8601
    lastOccurrence: string,     // ISO8601
    nextExpected: string,       // ISO8601
    status: 'active' | 'paused' | 'ended'
  }
}

ledger.entry.created

Se emite cuando un JournalEntry se postea al libro mayor.

TypeScript
{
  eventType: 'ledger.entry.created',
  eventId: string,
  source: 'habilitalo',
  timestamp: string,
  data: {
    journalEntryId: string,
    entryNumber: number,
    description: string,
    transactionDate: string,
    postDate: string,
    status: 'draft' | 'posted' | 'voided',
    sourceType: string | null,
    sourceRef: string | null,
    lines: Array<{
      lineId: string,
      accountId: string,
      entryType: 'debit' | 'credit',
      amount: number,
      currency: string,
      description: string | null,
      counterparty: string | null,
      categoryId: string | null
    }>
  }
}
17

Apéndice C: Flujo de Datos del Pipeline

Una traza completa de un solo movimiento Fintoc a través del sistema:

1. API FINTOC
   GET /accounts/{id}/movements?since=2026-01-01&until=2026-01-31
   Retorna: FintocMovement {
     id: "mov_abc123",
     amount: -45000,
     currency: "CLP",
     description: "COMPRA SUPERMERCADO LIDER",
     post_date: "2026-01-15",
     recipient_account: { holder_name: "WALMART CHILE SA" }
   }

2. ADAPTER FINTOC (transform)
   FintocMovement -> RawEntryInput {
     transactionDate: 2026-01-15,
     amount: -45000,
     currency: "CLP",
     description: "COMPRA SUPERMERCADO LIDER",
     counterparty: "WALMART CHILE SA",
     sourceId: "mov_abc123",
     rawData: { ...FintocMovement original }
   }

3. ETAPA DE INGESTIÓN
   a. Validar: amount != 0, fecha válida, descripción presente     [PASA]
   b. Normalizar: date="2026-01-15", amount="45000.00",
      description="compra supermercado lider"
   c. Hash: SHA-256("2026-01-15|45000.00|{accountId}|
      compra supermercado lider") = "a1b2c3..."
   d. Verificar existente: SELECT * FROM RawEntry
      WHERE organizationId=? AND entryHash="a1b2c3..."         [NO ENCONTRADO]
   e. Crear: INSERT INTO RawEntry (status='pending', ...)

4. ETAPA DE PROCESAMIENTO
   a. Obtener: SELECT * FROM RawEntry WHERE status='pending'
   b. Marcar: UPDATE RawEntry SET status='processing'
   c. Determinar tipo: amount < 0 -> gasto
   d. Crear JournalEntry:
      Línea 1: DÉBITO  {accountId} $45.000 CLP "Gasto"
      Línea 2: CRÉDITO {accountId} $45.000 CLP
               "COMPRA SUPERMERCADO LIDER" contraparte="WALMART CHILE SA"
   e. Validar balance: débito(45000) == crédito(45000)            [PASA]
   f. INSERT JournalEntry + JournalLines
   g. UPDATE RawEntry SET status='processed', journalEntryId=...

5. EMISIÓN DE EVENTOS (fire-and-forget)
   a. Construir payload BankMovementCreatedEvent
   b. Encontrar WebhookEndpoints suscritos
   c. Para cada endpoint:
      - CREATE WebhookDelivery (al-menos-una-vez)
      - Firmar payload con HMAC-SHA256
      - POST a endpoint.url con header de firma
      - Si 2xx: marcar como entregado
      - Si 5xx/timeout: reintentar (1s, 4s, 16s)
      - Si 4xx (!429): detener, marcar como fallido

6. ANÁLISIS (asincrónico)
   a. Detector de anomalías:
      - Stats categoría "supermercado": media=38000, stddev=8000
      - Z-score: |45000 - 38000| / 8000 = 0.875 < umbral 2.0
      - Resultado: NO anómalo
   b. Detector de recurrencia:
      - Grupo "compra supermercado lider|bucket_X" ahora tiene 4 ocurrencias
      - Intervalos: [7, 7, 8] días -> prom=7.33, stddev=0.47
      - Frecuencia: semanal
      - Confianza: 0.94 * 0.5 + 0.33 * 0.3 + 0.98 * 0.2 = 0.77
      - Resultado: RecurringPattern creado/actualizado

Cierre

Habilitalo existe porque lo necesitábamos. Estábamos construyendo Balancealo y nos encontramos escribiendo el mismo código de integración para cada fuente — el mismo parsing de fechas, la misma normalización de montos, la misma lógica de dedup, el mismo posteo al libro mayor. Extrajimos el patrón en un protocolo porque los protocolos componen y las librerías no.

La implementación actual son 5.500 líneas de TypeScript, 24 archivos, 3 conectores, 4 tipos de evento y 2 motores de análisis. No es un juguete. Procesa movimientos bancarios reales de bancos chilenos reales para usuarios reales. Pero tampoco está completo. No hay UI. No hay suficientes conectores. La cobertura de tests necesita trabajo. Los motores de análisis necesitan endpoints de API.

Publicamos este whitepaper y la especificación del protocolo para invitar a otros a construir sobre él. Si tienes una fuente de datos que no tiene un conector Habilitalo, escribe uno. Si al pipeline le falta una feature que necesitas, proponla. Si el diseño del protocolo tiene una falla, dínoslo.

El objetivo no es construir otra plataforma. Es hacer de la integración de datos — financieros, operativos y comerciales — un problema resuelto: un protocolo que cualquiera pueda implementar, un pipeline que cualquiera pueda correr, y un registry al que cualquiera pueda contribuir.


Protocolo Habilitalo v0.1 — Marzo 2026
Franco Danioni — Digitalo SpA — habilitalo.com
Licencia MIT