KITKIT 19 ago 2026Aug 19, 2026
Seguimiento de gastos: de tus estados de cuenta a un panel tuyo Expense tracking: from your statements to your own panel
Los bancos ya te dan tus movimientos. Con un par de scripts en Python puedes categorizarlos, ver qué te cuesta al año cada suscripción y qué estás pagando dos veces — sin entregarle tus datos a nadie, ni a una app ni a la IA que escribe el código. Banks already give you your transactions. With a couple of Python scripts you can categorize them, see what each subscription costs per year and what you are paying for twice — without handing your data to anyone, not to an app and not to the AI that writes the code.
Los promptsThe prompts
Archivos de texto. Ábrelos, cópialos y pégalos en el asistente que uses. Plain text files. Open them, copy and paste into whichever assistant you use.
Todo el mundo sabe que debería llevar un registro de gastos. Casi nadie lo hace porque las apps piden acceso a la cuenta o porque clasificar a mano es aburrido. Esta receta usa los CSV que ya puedes descargar de tu banco: clasifica automáticamente, detecta suscripciones y te muestra dónde se va el dinero.
Quién ve tus movimientos
Nadie. Y conviene explicar por qué, porque “confía en mí” no es una respuesta.
Yo no veo nada. No porque prometa no mirar, sino porque no hay nada que mirar. Esta página es HTML estático: no tiene formulario, ni cuenta, ni botón para subir archivos, ni analítica. Mi servidor registra que alguien pidió esta página, como cualquier sitio web, y ahí se acaba. Nada de lo que construyas con esta receta pasa por él, ni ahora ni después.
La IA tampoco necesita ver tus movimientos. Eso no es suerte, es cómo están escritos los prompts: le pides un script, no un análisis de tus datos. Pegas un texto genérico que no contiene un solo peso tuyo, te devuelve código, y el código lo corres tú contra tu CSV, en tu máquina. El asistente nunca ve una transacción.
Dónde sí se verían: si pegas el estado de cuenta en el chat, o si abres el CSV con una herramienta que procesa archivos en servidores ajenos. Eso ya es decisión tuya, y es exactamente por lo que esta receta está marcada para correrse en local.
Y no hace falta que me creas: se revisa. Un script que solo lee CSV y escribe JSON necesita csv, json, re, pathlib y poco más. Si en las primeras líneas aparece requests, urllib, httpx o socket, pregunta para qué está ahí. Un script sin librerías de red no puede mandar nada a ningún lado, y eso se ve en diez segundos sin saber Python.
Lo que vas a montar
Un pipeline que:
- Lee todos los CSV de un directorio.
- Limpia las descripciones (quita ciudad, números de referencia, espacios dobles).
- Clasifica cada fila: cargo, reembolso, pago a tarjeta, interés/comisión, control (meses sin intereses, reversión).
- Filtra solo los cargos reales.
- Categoriza por comercio con reglas regex.
- Detecta suscripciones recurrentes.
- Escribe un
gastos.jsonque una página HTML lee.
Dos trampas que arruinan el análisis
1. Confundir “cargo” con “pago”
En un estado de cuenta aparecen líneas como “GRACIAS POR SU PAGO”, “INTERÉS FINANCIERO”, “CUOTA ANUAL”, “REVERSIÓN DE CARGO” o “MESES EN AUTOMÁTICO”. Si sumas todo lo que tiene importe positivo, estás contando tu propio pago como gasto, duplicando ingresos como egresos y escondiendo las comisiones reales.
La regla: antes de categorizar, clasifica cada fila en una de estas clases:
| Clase | Qué es | Cuenta como gasto |
|---|---|---|
cargo |
Compra normal | Sí |
reembolso |
Devolución | No (resta) |
pago |
Abono a tarjeta | No |
interes_comision |
Intereses, anualidad, comisiones | Sí, pero aparte |
control |
MSI, reversión, redención | No |
2. El mismo comercio aparece escrito de formas distintas
“UBER EATS *MEXICO DF”, “UBER EATS 1234” y “UBER EATS CDMX” son el mismo comercio. Si agrupas por descripción cruda, las suscripciones y los comercios frecuentes se dispersan en docenas de líneas.
La solución es una función de normalización:
- Pasa a mayúsculas y quita espacios al inicio/final.
- Quita todo a partir de un doble espacio (suele ser ciudad o tienda).
- Quita números de 3 o más dígitos y el asterisco con lo que sigue.
- Trunca a 24 caracteres.
Después agrupa por la descripción normalizada, no por la original.
Cómo detectar suscripciones (gastos hormiga)
Las suscripciones no son solo Netflix y Spotify. También son membresías de gimnasio, apps, seguros, servicios de nube y cargos anuales. La forma más simple y robusta de detectarlas es una lista de palabras clave sobre la descripción original:
NETFLIX, SPOTIFY, HBO, OPENAI, CHATGPT, NOTION, APPLE.COM, YOUTUBE,
AUDIBLE, GRAMMARLY, NORDVPN, GOOGLE ONE, PRIME VIDEO, TELMEX, TELCEL,
AT&T, IZZI, ZWIFT, STRAVA, MEMBERSHIP, CUOTA ANUAL, SEGUROtext
Luego agrupa por comercio normalizado y suma cuántas veces aparece en N meses. Si aparece más de una vez con importes similares, es recurrente.
Cinco preguntas que el total mensual no responde
Saber que gastaste 18,400 el mes pasado no cambia nada. Estas cinco sí, y todas salen del mismo JSON.
1. ¿Cuánto me cuesta lo que ni siento?
El gasto hormiga no aparece en el total porque cae repartido en “Otros”. Aparece cuando ordenas por número de cargos en vez de por monto. Un cargo de 68 pesos veintiún veces al mes son 1,428 al mes y 17,136 al año: más que casi cualquier suscripción, y nadie lo tiene en la cabeza.
Ordena top_comercios por count y no por total. Ahí está.
2. ¿Cuánto es eso al año?
Las suscripciones se deciden en mensual y se pagan en anual. 149 al mes son 1,788 al año; cuatro servicios de 149 son 7,152. Pon la cifra anual junto a la mensual en el panel — es el mismo dato y cambia la decisión.
3. ¿Cuáles subieron de precio sin que me enterara?
De lo más fácil de detectar y de lo que menos se mira. Mismo comercio, importes distintos a lo largo del periodo: compara el primer cargo con el último. Si subió, tu panel lo dice. Tu correo probablemente también lo dijo hace ocho meses.
4. ¿Qué estoy pagando dos veces?
Agrupa las suscripciones por lo que hacen, no por cómo se llaman: video, música, almacenamiento, asistentes de IA, gimnasio, respaldo de fotos. Dos renglones en el mismo grupo son una pregunta que vale dinero. Tres son una respuesta.
5. ¿Qué de esto ya viene incluido en algo que ya pago?
Aquí el panel llega hasta la mitad y el resto lo haces tú una vez. Él sabe exactamente qué pagas, cuánto, cada cuándo y con qué tarjeta. Lo que no sabe es qué incluye ya esa tarjeta o ese plan.
Con la lista en la mano, las preguntas son cuatro:
- ¿Mi plan de celular incluye algún servicio de streaming o de música?
- ¿Mi tarjeta trae membresías, seguros o meses de cortesía que estoy pagando por separado?
- ¿Existe un paquete del mismo proveedor que englobe lo que compro suelto (almacenamiento + video + música)?
- ¿Lo estoy pagando mensual pudiendo pagarlo anual, o al revés?
No busques la respuesta en esta receta: los paquetes cambian cada temporada y son distintos en cada país. Lo que el panel te da es la lista exacta de lo que pagas, que es justo lo que hace falta para ir a preguntar.
Estructura del JSON
{
"gastos": {
"actualizado": "2026-08-19",
"meses": ["enero", "febrero", "marzo"],
"total_neto": 45000,
"promedio_mensual": 15000,
"por_categoria": [
{"cat": "Restaurantes", "total": 8200, "promedio_mensual": 2733, "pct": 18.2},
{"cat": "Suscripciones", "total": 2100, "promedio_mensual": 700, "pct": 4.7}
],
"suscripciones": [
{"merchant": "NETFLIX", "total": 447, "count": 3, "promedio": 149,
"anualizado": 1788, "cadencia_dias": 30, "grupo": "video",
"primer_importe": 129, "ultimo_importe": 149, "subio_pct": 15.5,
"tarjeta": "Visa ***4821"}
],
"hormiga": [
{"merchant": "OXXO", "count": 21, "ticket_promedio": 68,
"mensual": 1428, "anualizado": 17136}
],
"solapamientos": [
{"grupo": "video", "servicios": ["NETFLIX", "HBO", "PRIME VIDEO"],
"mensual": 447, "anualizado": 5364}
],
"top_comercios": [
{"merchant": "OXXO", "total": 1200, "count": 12}
],
"intereses_comisiones": 450,
"alertas": [
"Pagas ~700/mes en suscripciones: 8,400 al año.",
"3 servicios de video al mismo tiempo, 447/mes.",
"NETFLIX subió 15.5% durante el periodo.",
"Intereses y comisiones: 450 en el periodo."
]
}
}json
Qué puede salir mal
- CSV con columnas distintas. Cada banco nombra las columnas a su manera: “Descripción”, “Concepto”, “Movimiento”, “Cargo”, “Abono”. Tu script debe aceptar un mapeo de columnas o detectarlas por nombre aproximado.
- Decimales y signos invertidos. Algunos bancos ponen los cargos como negativos y los abonos como positivos; otros al revés. Revisa el signo con un mes que conozcas.
- Meses sin intereses. Una compra de 12,000 pagada en 6 meses aparece como 6 cargos de 2,000. Si no los marcas como
control, parece que gastaste 12,000 en un mes. -
Suscripciones anuales. Aparecen una vez al año. Si tu ventana es de 6 meses, no las detectas. Considera agrandar la ventana o mantener un histórico acumulado.
-
“Reversión” y “reembolso” se pisan, y el orden decide. Una reversión que trae abono cumple las dos reglas a la vez: está en la lista de
controlpor la palabra, y en la dereembolsopor el signo. Si ganacontrol, el importe devuelto se ignora y el neto queda inflado por esa cantidad. Corriendo esta receta sobre datos de muestra eran 1,250 pesos fantasma. Regla que sí funciona: si vuelve dinero a la cuenta es reembolso, aunque la descripción diga reversión. - El umbral del gasto hormiga no puede salir de la mediana de todos los cargos. Si un comercio te cobra 159 veces, esos 159 tickets baratos arrastran la mediana hacia abajo y acaban excluyendo a comercios que sí son hormiga. El umbral se muerde la cola. Saca la mediana del ticket promedio por comercio, donde cada comercio pesa lo mismo, y el corte deja de depender de quién te cobra más seguido.
Límite honesto: si tu banco no permite exportar CSV o solo da PDF, esta receta se complica. Necesitas una etapa previa de extracción de PDF que no cubrimos aquí.
Everyone knows they should track expenses. Almost nobody does it because apps ask for account access or because manual classification is boring. This recipe uses CSV files you can already download from your bank: classify automatically, detect subscriptions and see where the money goes.
Who sees your transactions
Nobody. And that deserves an explanation, because “trust me” is not an answer.
I see nothing. Not because I promise not to look, but because there is nothing to look at. This page is static HTML: no form, no account, no upload button, no analytics. My server logs that someone requested this page, like any website, and that is where it ends. Nothing you build with this recipe ever passes through it.
The AI does not need to see your transactions either. That is not luck, it is how the prompts are written: you ask it for a script, not for an analysis of your data. You paste generic text that contains not a single amount of yours, you get code back, and you run that code against your CSV, on your machine. The assistant never sees one transaction.
Where they would be seen: if you paste a statement into the chat, or open the CSV with a tool that processes files on someone else’s servers. That is your call, and it is exactly why this recipe is marked to run locally.
And you do not have to take my word for it — check. A script that only reads CSV and writes JSON needs csv, json, re, pathlib and little else. If requests, urllib, httpx or socket show up in the first lines, ask what they are doing there. A script with no networking libraries cannot send anything anywhere, and you can see that in ten seconds without knowing Python.
What you will build
A pipeline that:
- Reads all CSVs in a directory.
- Cleans descriptions (removes city, reference numbers, double spaces).
- Classifies each row: charge, refund, card payment, interest/fee, control (installments, reversal).
- Filters only real charges.
- Categorizes by merchant with regex rules.
- Detects recurring subscriptions.
- Writes a
gastos.jsonthat an HTML page reads.
Two traps that ruin the analysis
1. Confusing “charge” with “payment”
Statements include lines like “THANK YOU FOR YOUR PAYMENT”, “FINANCE CHARGE”, “ANNUAL FEE”, “REVERSAL” or “AUTOMATIC INSTALLMENTS”. If you sum everything with a positive amount, you are counting your own payment as spending, duplicating income as expense and hiding real fees.
The rule: before categorizing, classify each row into one of these classes:
| Class | What it is | Counts as spending |
|---|---|---|
charge |
Normal purchase | Yes |
refund |
Return/credit | No (subtracts) |
payment |
Card payment | No |
interest_fee |
Interest, annual fee, penalties | Yes, but separate |
control |
Installments, reversal, redemption | No |
2. The same merchant appears in many different spellings
“UBER EATS *MEXICO CITY”, “UBER EATS 1234” and “UBER EATS NYC” are the same merchant. If you group by raw description, subscriptions and frequent merchants scatter into dozens of lines.
The solution is a normalization function:
- Uppercase and trim spaces.
- Drop everything from a double space onward (usually city or store).
- Remove numbers of 3+ digits and the asterisk with what follows.
- Truncate to 24 characters.
Then group by normalized description, not the raw one.
How to detect subscriptions
Subscriptions aren’t just Netflix and Spotify. They also include gym memberships, apps, insurance, cloud services and annual charges. The simplest robust way to detect them is a keyword list on the raw description:
NETFLIX, SPOTIFY, HBO, OPENAI, CHATGPT, NOTION, APPLE.COM, YOUTUBE,
AUDIBLE, GRAMMARLY, NORDVPN, GOOGLE ONE, PRIME VIDEO, TELMEX, TELCEL,
AT&T, IZZI, ZWIFT, STRAVA, MEMBERSHIP, ANNUAL FEE, INSURANCEtext
Then group by normalized merchant and count how many times it appears in N months. If it appears more than once with similar amounts, it’s recurring.
Five questions the monthly total does not answer
Knowing you spent 18,400 last month changes nothing. These five do, and they all come out of the same JSON.
1. What is the stuff I never notice costing me?
Small recurring spending does not show up in the total because it scatters into “Other”. It shows up when you sort by number of charges instead of by amount. A 68-peso charge twenty-one times a month is 1,428 a month and 17,136 a year: more than almost any subscription, and nobody has that number in their head.
Sort top_comercios by count, not by total. There it is.
2. What is that per year?
Subscriptions are decided monthly and paid annually. 149 a month is 1,788 a year; four services at 149 is 7,152. Put the annual figure next to the monthly one in the panel — same data, different decision.
3. Which ones raised their price without me noticing?
One of the easiest things to detect and one of the least looked at. Same merchant, different amounts across the period: compare the first charge with the last. If it went up, your panel says so. Your inbox probably said so eight months ago.
4. What am I paying for twice?
Group subscriptions by what they do, not by what they are called: video, music, storage, AI assistants, gym, photo backup. Two rows in the same group is a question worth money. Three is an answer.
5. What of this is already included in something I already pay for?
Here the panel gets you halfway and you do the rest once. It knows exactly what you pay, how much, how often and with which card. What it does not know is what that card or plan already includes.
With the list in hand, there are four questions:
- Does my phone plan include a streaming or music service?
- Does my card come with memberships, insurance or complimentary months I am paying for separately?
- Is there a bundle from the same provider covering what I buy piecemeal (storage + video + music)?
- Am I paying monthly when I could pay annually, or the other way round?
Do not look for the answer in this recipe: bundles change every season and differ by country. What the panel gives you is the exact list of what you pay, which is precisely what you need in order to go and ask.
JSON structure
{
"gastos": {
"actualizado": "2026-08-19",
"meses": ["january", "february", "march"],
"total_neto": 45000,
"promedio_mensual": 15000,
"por_categoria": [
{"cat": "Restaurantes", "total": 8200, "promedio_mensual": 2733, "pct": 18.2},
{"cat": "Suscripciones", "total": 2100, "promedio_mensual": 700, "pct": 4.7}
],
"suscripciones": [
{"merchant": "NETFLIX", "total": 447, "count": 3, "promedio": 149,
"anualizado": 1788, "cadencia_dias": 30, "grupo": "video",
"primer_importe": 129, "ultimo_importe": 149, "subio_pct": 15.5,
"tarjeta": "Visa ***4821"}
],
"hormiga": [
{"merchant": "OXXO", "count": 21, "ticket_promedio": 68,
"mensual": 1428, "anualizado": 17136}
],
"solapamientos": [
{"grupo": "video", "servicios": ["NETFLIX", "HBO", "PRIME VIDEO"],
"mensual": 447, "anualizado": 5364}
],
"top_comercios": [
{"merchant": "OXXO", "total": 1200, "count": 12}
],
"intereses_comisiones": 450,
"alertas": [
"You pay ~700/mo in subscriptions: 8,400 a year.",
"3 video services at once, 447/mo.",
"NETFLIX went up 15.5% during the period.",
"Interest and fees: 450 in the period."
]
}
}json
What can go wrong
- Different CSV columns. Each bank names columns differently: “Description”, “Concept”, “Movement”, “Charge”, “Credit”. Your script should accept a column mapping or detect them by approximate name.
- Inverted decimals and signs. Some banks list charges as negative and credits as positive; others do the opposite. Check the sign against a month you know.
- Installments. A 12,000 purchase paid in 6 months appears as 6 charges of 2,000. If you don’t mark them as
control, it looks like you spent 12,000 in one month. -
Annual subscriptions. They appear once a year. If your window is 6 months, you won’t detect them. Consider extending the window or keeping an accumulated history.
-
“Reversal” and “refund” overlap, and evaluation order decides. A reversal carrying a credit satisfies both rules at once: it is in the
controlkeyword list, and in therefundlist by sign. Ifcontrolwins, the returned amount is ignored and the net is inflated by that amount. Running this recipe over sample data it was 1,250 pesos of phantom spending. The rule that works: if money comes back to the account it is a refund, whatever the description says. - The small-spending threshold cannot come from the median of all charges. If one merchant bills you 159 times, those 159 cheap tickets drag the median down and end up excluding merchants that genuinely are small recurring spending. The threshold bites its own tail. Take the median of the average ticket per merchant, where every merchant weighs the same, and the cut stops depending on who bills you most often.
Honest limit: if your bank only allows PDF exports, this recipe gets harder. You need a prior PDF extraction step that we don’t cover here.