Tu factura (de luz)
Visita tufactura.raulrc.com para hablar con tu factura de la luz.
Wrestling with Our Electricity Bill
Cuando recibo la factura encuentro lo que llamo «el muro de información»; una avalancha de datos que me saturan a nivel cognitivo y me distraen del mensaje final, que es el pico que hay que pagar. Al final tomas la solución de la avestruz; asumo que es así, que todo está bien calculado y sigo con mi vida, no sin algo de desazón.
Existe muchísima opacidad en toda la factura. ¿Qué significa «peajes T+D», «coste de comercialización», «margen de comercialización fijo», «bono social», «financiación del bono social», «alquiler de equipos de medida»? Y sobre todo, ¿pago más de lo que debería?
Así pues y con el poco tiempo libre que tengo, un día dije, mira, vamos a aprender mientras nos divertimos. Vamos a pedirle a un LLM que nos explique como si fuésemos tontos qué significan todas estas cosas a través de una aplicación. El objetivo: subes un PDF de tu factura de luz, una IA lo lee, te lo desglosa, te dice qué cargos son raros y compara para saber si estás pagando de más. Además puedes charlar con el asistente para preguntarles qué demonios es eso del T+D o que te dé consejitos sobre cómo lidiar con toda esa información.
¿Podrías hacerlo con ChatGPT o similar? No del todo. ChatGPT & compañía no se conectan con ESIOS para comparar en ese mismo período cuánto hubieses pagado en otra modalidad de contrato.
El resultado está en un respositorio público: Bill Advisor. De esta manera comprobarás que tu información sensible no queda almacenada. Además, haciendo un pequeño pacto con el diablo, Anthropic asegura que no entrena sus modelos con los datos que envías a su API (ver términos comerciales y su página de uso de datos). Ok :‘D
Ahora, te pido ayuda para mejorar esto y hacerlo llegar a más gente. Échale un vistazo, juega con ello, aprende y oye, quizás te ahorras unas pelillas en el camino. Cualquier cosa, me comentas.
Nota: Haciendo honor a la verdad y puesto que quiero que el contenido que se publique aquí sea auténtico y humano, he de decir que el texto a partir de aquí me he ayudado un poquito de Open Code + Big Pickle. Se me hace bola escribir documentación como a todo buen developer. Espero que no lo tomen como un EMOSIDO ENGAÑADO, sino como una pequeña licencia.
Qué hace
En esencia, Bill Advisor es una pipeline de tres fases:
- Extracción — subes un PDF de una factura eléctrica española y Claude Opus lo lee con visión artificial, extrayendo cada campo estructurado en un modelo de datos tipado.
- Auditoría — 10 comprobaciones deterministas (ninguna usa la IA) que detectan anomalías: el IVA no cuadra, el contador está amortizado pero sigues pagando alquiler, tienes servicios adicionales que quizá no uses, etc.
- Comparador PVPC — coge los precios horarios de la tarifa regulada desde ESIOS, distribuye tu consumo uniformemente, y calcula cuánto habrías pagado con PVPC vs tu tarifa actual.
Todo se puede usar desde línea de comandos, desde una interfaz web con Streamlit, o desde una API REST con FastAPI.
Por qué visión y no OCR
Este proyecto se llama Bill Advisor y no «otro parser de PDF con regex». Podría haber ido por la ruta clásica: extraer el texto del PDF con PyPDF2 o pdfplumber, y luego aplicar expresiones regulares y heurísticas para cada comercializadora. Eso funciona si tienes seis comercializadoras y veinte formatos de factura — el día que llega una factura de Octopus Energy con un diseño que no has visto, todo se rompe.
En su lugar, la extracción usa Claude Opus 4.7 con visión: le pasas el PDF directamente como imagen (bueno, como base64), y el modelo entiende la estructura visual sin importar el diseño. Iberdrola, Endesa, Naturgy, Octopus, Repsol, COR/PVPC… da igual. El modelo sabe dónde mirar.
La extracción no es libre — usa tool-use forzado: el modelo tiene una única herramienta disponible (record_factura_extraida) cuyo esquema de entrada es el modelo Factura entero. Claude no puede responder con texto, solo llamar a esa herramienta con los datos que ha extraído. Esto tiene una historia detrás.
Inicialmente intenté usar messages.parse() con Pydantic, que es el camino más directo con el SDK de Anthropic. Pero el esquema de Factura tiene suficientes campos opcionales y modelos anidados (11 modelos, 60+ campos) como para que el compilador de gramática de strict mode explotase con un error de «compiled grammar is too large». Con tool-use sin strict mode, el modelo genera JSON libremente y Pydantic lo valida en cliente — misma garantía, sin límite de complejidad. ADR-0002 lo cuenta con más detalle.
El system prompt es extenso (~3000 tokens) y contiene reglas de dominio muy específicas del sector eléctrico español: cómo se descompone una factura PVPC vs libre, qué actores hay, qué impuestos aplican… Está marcado con cache_control: ephemeral, así que tras la primera extracción del día, las siguientes reusan el prefix cache y ahorran ~90% del coste en input tokens. ADR-0008 tiene los números.
response = client.messages.create(
model=MODEL,
max_tokens=MAX_TOKENS,
system=[
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}
],
tools=[_RECORD_TOOL],
tool_choice={"type": "tool", "name": RECORD_TOOL_NAME},
messages=[
{
"role": "user",
"content": [
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": pdf_b64}},
{"type": "text", "text": "Extrae todos los datos estructurados de esta factura…"},
],
}
],
)
El modelo de dominio: Pydantic
El núcleo del sistema es Factura y sus 11 modelos anidados. La definición completa está en schemas.py, pero las partes jugosas son:
Contrato: CUPS, comercializadora, distribuidora (no confundir — es un error clásico), tarifa de acceso (casi siempre 2.0TD), modalidad (PVPC, libre fija, libre indexada).
Energía: Tres periodos horarios (punta, llano, valle). Aquí hay una sutileza importante. Bajo PVPC, los importes de energía contienen SOLO los peajes T+D regulados (~15-20% del coste real). El coste del mercado mayorista y el margen de comercialización viven en un sub-bloque aparte (CostesPVPC). En mercado libre, todo está integrado en el precio €/kWh y costes_pvpc es null. Si no capturas bien esta diferencia, el comparador PVPC te da resultados absurdos.
Otros conceptos: Bono social (que en realidad son dos conceptos distintos — el descuento que reciben los vulnerables y el recargo que pagamos todos para financiarlo), alquiler del contador, excedentes de autoconsumo, y una lista de servicios adicionales que la comercializadora ha ido añadiendo.
Conceptos no mapeados: Un safety net. Cualquier línea de la factura que el extractor no sepa clasificar va aquí. La auditoría lo detecta y lo señala. Esto evita que cargos desconocidos pasen desapercibidos.
class Factura(BaseModel):
contrato: Contrato
periodo: PeriodoFacturacion
potencia: Potencia
energia: Energia
otros: OtrosConceptos
impuestos: Impuestos
totales: Totales
conceptos_no_mapeados: list[ConceptoNoMapeado]
notas_extraccion: list[str]
La auditoría: 10 checks, cero IA
La auditoría es deliberadamente determinista. Sin LLM, sin llamadas externas (excepto el comparador PVPC). Cada check es una función pura que recibe una Factura y devuelve una lista de Finding.
Los hallazgos se clasifican en tres niveles:
- Critical: «el total de la factura no cuadra». Si la suma de los conceptos extraídos no coincide con el total declarado, es un error de extracción o la factura tiene un cargo que no hemos contabilizado.
- Warning: «pagas alquiler del contador», «el IVA aplicado no coincide con el porcentaje declarado», «tienes servicios adicionales que probablemente no usas».
- Info: «precio efectivo medio €X/kWh», «tienes autoconsumo solar con excedentes».
Cada hallazgo con impacto cuantificable lleva un ahorro_estimado_eur_mes. Por ejemplo, si pagas €1,20 al mes por un seguro de mantenimiento que nunca has usado, el finding te dice «€1,20/mes (~€14,40/año) que puedes ahorrar con una llamada».
_CHECKS = [
_check_effective_kwh_price,
_check_total_balance,
_check_iva_pct_valid,
_check_iva_calculation,
_check_alquiler_contador,
_check_otros_servicios,
_check_conceptos_no_mapeados,
_check_excedentes_solares,
_check_pvpc_comparison,
_check_notas_extraccion,
]
La decisión de mantener la auditoría sin IA está documentada en ADR-0011. La razón principal es predecibilidad: una auditoría debe dar el mismo resultado siempre para la misma entrada. Si un LLM decidiese qué es «anómalo», la experiencia de usuario sería inconsistente y las recomendaciones, difíciles de auditar.
El comparador PVPC
Esta es la parte estrella. Si tu factura es de mercado libre (no PVPC), el sistema va a ESIOS (indicador #1001), obtiene los precios horarios PVPC 2.0TD para el periodo facturado, y recalcula cuánto habrías pagado con la tarifa regulada.
El problema es que no tenemos tus datos horarios de consumo (eso requiere Datadis y autorización del usuario — es la Fase 2). Así que distribuimos tu consumo de cada periodo uniformemente entre todas las horas de ese periodo. Si consumiste 100 kWh en horas punta, se distribuyen equitativamente entre las 8 horas diarias de punta del periodo facturado.
Es una aproximación, claro. Pero para una primera pasada, da una idea bastante fiable de si estás en una tarifa competitiva o te están cobrando de más. El resultado se presenta con matices:
Con tu mismo consumo distribuido uniformemente, bajo la tarifa regulada PVPC habrías pagado €45.20 por la energía en lugar de €67.80 — un ahorro estimado de €22.60 este periodo (~€271/año).
O, si PVPC sale más caro:
En este periodo concreto, la tarifa PVPC habría salido €12.30 más cara. Tu tarifa fija te protegió de la volatilidad del mercado mayorista.
Y siempre con un disclaimer: «sin tus datos horarios reales, el resultado es una aproximación».
El cliente de ESIOS es mínimo — una función que llama a la API con httpx y devuelve un dict[date, dict[int, float]] con precios en €/kWh para el periodo solicitado.
Dos interfaces, un núcleo
Una de las decisiones de las que más contento estoy es tener el núcleo (bill_advisor/) completamente desacoplado de la interfaz. El mismo extract_factura() y audit() se usan desde:
-
Streamlit (
app.py) — una interfaz de una sola página, perfecta para prototipado rápido y demos. Subes el PDF, ves el resumen, los hallazgos, el detalle completo y un chat RAG para preguntar dudas. Usa@st.cache_datapara no repetir la llamada a la API de Claude si subes el mismo PDF dos veces. -
FastAPI (
api/main.py) — la API de producción. Un endpointPOST /api/analyzeque recibe un multipart con el PDF y devuelve{factura, findings}como JSON. Con CORS para localhost:3000 (preparado para un frontend Next.js), rate limiting (2 peticiones/minuto por IP), y validación por Pydantic.
El rate limiter (ADR-0013) es importante: Claude cuesta dinero por llamada, y no quiero que un script automático me seque la API key. Es un sliding window en memoria — nada de Redis, nada de infraestructura extra para un proyecto personal.
El chat RAG
Cada factura tiene conceptos que la gente no entiende: «¿Qué son los peajes T+D?», «¿Este cargo de €4,39 es normal?», «¿Puedo ahorrar en potencia?». Para responder esto, hay un sistema RAG mínimo: siete documentos Markdown en bill_advisor/rag/corpus/ que explican estos conceptos en español llano.
Cuando el usuario hace una pregunta, Claude Sonnet 4 busca en esos documentos y responde con citas contextuales de la factura. El system prompt también usa cache_control, así que la segunda pregunta en la misma sesión ya es más barata.
De nuevo, es una solución ligera. Sin base de datos vectorial, sin embeddings, sin ChromaDB. Los documentos caben en el context window de Claude sin problema.
El despliegue
Bill Advisor vive en en una instancia de AWS Lightsail. El plan de despliegue (docs/deployment-plan.md) es elegante: Caddy como reverse proxy en un docker-compose.yml que enruta por Host header:
Cloudflare ──HTTPS──▶ bill-advisor:8501
raulrc.com sirve el blog, tufactura.raulrc.com sirve Bill Advisor. Cloudflare termina el TLS en ambos. Bill advisor vive en un contenedor de docker. Sin certificados en el servidor, sin complicaciones.
El Dockerfile de Bill Advisor es sencillo: imagen python:3.12-slim, instala las dependencias con pip, copia el código. Expone el puerto 8501 (Streamlit) y arranca la aplicación. La imagen final pesa unos 200 MB — no está mal para un proyecto que lleva Python, Anthropic SDK y todo el stack. Las variables de entorno necesarias se pasan a través del pipeline de despliegue en Github Actions.
Las métricas (Prometheus)
Bill Advisor expone métricas en el puerto 8001 usando prometheus_client:
bill_sessions— sesiones de navegador únicasbill_extractions— facturas extraídasbill_extraction_errors— errores de extracciónbill_pvpc_savings_found— veces que PVPC era más barato que la tarifa actualbill_pvpc_more_expensive— veces que la tarifa actual era más barata que PVPCbill_chat_questions— preguntas hechas en el chat RAGbill_extraction_duration_seconds— histograma de duración de extracción + auditoría
Con esto puedo ver cuánta gente está usando la herramienta, cuánto ahorro potencial se está detectando, y si hay errores recurrrentes. Prometheus se despliega aparte (o se consulta desde un servicio como Grafana Cloud).
Lo que no hay (y por qué)
- Tests. Lo sé. El código está diseñado para ser testeable (las funciones aceptan clientes por inyección de dependencias), pero no hay ni un solo test. ADR-0012 documenta que es una decisión consciente: el MVP tiene más valor que la cobertura de código. Llegarán.
- Datadis. Los datos horarios reales de consumo abrirían un montón de posibilidades: recomendaciones precisas de cambio tarifario, optimización de potencia contratada, consejos de desplazamiento de carga… Es la Fase 2.
- Comparador multi-tarifa. Ahora solo compara contra PVPC. Lo ideal sería comparar contra 6-8 tarifas de referencia de distintas comercializadoras. ADR-0006 explica por qué no está.
- Historial. Si subes varias facturas a lo largo del tiempo, no hay vista de tendencias. Baja prioridad.
- Interfaz bonita. Streamlit es funcional, no bonito. El frontend en Next.js está en el backlog.
IA Off
Epílogo
La idea de todo esto era usar un proyecto divertido para aprender unos cuantos conceptos, seguir entrenando en habilidades de este mundillo tan fascinante y quizás echar una mano a alguien en el camino. Si te animas a probar y encuentras algo interesante en tu factura, me alegraré mucho. No dudes en compartir esto si te ha resultado útil y dejarme un feedback bien por Whatsapp o e-mail. Juntos podemos mejorarlo. No hay nada lucrativo detrás, solamente un sentimiento altruista de ayudar a los demás a entender lo que yo no entendía y ganas de divertirme trasteando con tecnología.
En el futuro intentaré sacar tiempo para una interfaz algo más bonita y profesional, pero eso ya será otra historia.