REJCOB
← Todo el trabajo

Ledger de consumo de doble entrada para funciones de IA

Medición para productos que llaman a LLMs: el costo de una operación no se conoce hasta que termina, y aun así el saldo nunca debe quedar negativo.

Rol
Arquitectura y diseño
Período
2026
Empresa
Proyecto personal

2-phase

reservar un estimado, liquidar el uso real

Double-entry

cada movimiento suma cero

Idempotent

un request reintentado nunca cobra dos veces

#Contexto

Todo producto que pone funciones de IA detrás de un plan pago necesita responder '¿cuánto ha usado esta cuenta, y le alcanza para la siguiente llamada?'. Lo incómodo es el momento: el conteo de tokens llega recién cuando la respuesta termina, los streams se cancelan a la mitad, y los clientes reintentan ante timeouts. Este diseño toma el patrón que usan los pagos con tarjeta — autorizar primero, capturar después — y lo aplica al consumo de IA.

#Problema y restricciones

  • Cobrar después de la llamada permite que requests concurrentes gasten el mismo saldo varias veces.
  • Los reintentos y las entregas duplicadas convierten una operación en dos cobros salvo que la escritura sea idempotente.
  • Una sola columna de saldo mutable no puede explicar cómo llegó a su valor actual.

#Arquitectura

AI requestidempotency keyReserveavailable → reservedProvider calltokens · secondsSettleactual costPrice bookversionedLedgerdouble-entry · PostgresSweeperrelease stale holds
Los créditos se reservan antes de la llamada de IA y se liquidan al costo real después.

Cada cuenta tiene cuentas de ledger — disponible, reservado, gastado — y cada movimiento es un par de asientos que suma cero. Antes de una llamada de IA, el servicio estima un techo para la operación y mueve ese monto de disponible a reservado dentro de una transacción de PostgreSQL, tomando un lock de fila sobre la cuenta; si el disponible no alcanza, la llamada se rechaza de entrada. Cuando el proveedor responde, el payload de uso (tokens, segundos de audio, páginas) se tarifa con un libro de precios versionado y la reserva se liquida: el costo real pasa a gastado y el resto vuelve a disponible. Una llamada cancelada o fallida libera la reserva completa, y un proceso de barrido libera las reservas que superan un timeout. Cada request lleva una clave de idempotencia con restricción única, así un reintento devuelve el resultado original en vez de escribir de nuevo. Redis mantiene una caché de saldos solo para mostrar; las decisiones siempre leen Postgres.

#Decisiones clave y tradeoffs

01Reservar y luego liquidar

Decisión
Retener un máximo estimado antes de la llamada y conciliar contra el uso real después.
Alternativas
Debitar una vez después de la respuesta, o cobrar un precio fijo por operación sin importar el uso.
Por qué
Es la única opción que evita que el saldo quede negativo bajo concurrencia y a la vez cobra el costo real.
Costo de equivocarse
Los estimados deben ser conservadores, así que cuentas cerca de cero son rechazadas en llamadas que sí podían pagar; y las reservas colgadas necesitan un proceso de barrido.

02Doble entrada, solo inserción

Decisión
Nunca actualizar un saldo en sitio; derivarlo de pares de asientos inmutables.
Alternativas
Una columna de saldo más un log de actividad suelto.
Por qué
Asientos que deben sumar cero hacen que los errores contables se detecten con una consulta, y cualquier saldo puede reconstruirse y explicarse en cualquier momento.
Costo de equivocarse
Más filas y más joins; los saldos necesitan snapshots periódicos para seguir siendo rápidos de leer.

03Libro de precios versionado

Decisión
Guardar precios por proveedor, modelo y unidad con fechas de vigencia, y registrar la versión usada en cada liquidación.
Alternativas
Precios como constantes en el código.
Por qué
Los precios de los proveedores cambian seguido; los cobros pasados deben seguir siendo explicables al precio que aplicaba cuando ocurrieron.
Costo de equivocarse
Alguien tiene que mantener el libro de precios al día, y un precio faltante debe fallar cerrado.

#Resultados

Σ = 0

invariante verificable con una sola consulta

No overdraft

por construcción: los fondos se retienen antes de gastar

Exactly once

cobro ante reintentos, vía claves de idempotencia

#Qué haría diferente

La pregunta abierta es la contención de locks: un lock de fila por cuenta es simple y correcto, pero una cuenta que dispara muchas llamadas en paralelo hará cola sobre él. El siguiente paso es una prueba de carga con tráfico desbalanceado para decidir si eso necesita sub-saldos particionados o está bien como está.

#Stack

Node.jsTypeScriptPostgreSQLRedisOpenAIWhisperDouble-entry ledger