Pra onde foi meu dinheiro mês passado? — o que instalar e a receita do agente

Como um agente de IA responde "pra onde foi meu dinheiro mês passado?" em português brasileiro. Instala Cumbuca Open Finance Data MCP, usuário autoriza via OF, agente roda receita determinística — regra MCC + heurística de descrição + LLM apenas no resíduo — categoriza, agrega por categoria, compara com mês anterior, lista as 10 maiores. Sem ML pesado, sem dicionário externo.

Top pick
cumbuca-of-data-mcp
Last verified
Eval method
auxiliar-onde-foi-meu-dinheiro-documented-characteristics-v1
Eval score
6.5/10
Categories
personal-finance, expense-categorization, open-finance, bank-data, brazilian-data, task-template, agent-tools
Works with
claude-code, claude-desktop, chatgpt, cursor

Pra onde foi meu dinheiro mês passado? — o que instalar e a receita do agente

Resposta

Você é um usuário brasileiro (ou está construindo para usuários brasileiros) e quer perguntar ao agente de IA — Claude, ChatGPT, Cursor, qualquer um que fale MCP — “pra onde foi meu dinheiro mês passado?” e receber a resposta direto no chat. Sem app separado, sem export de extrato, sem categorizar à mão.

Existe exatamente um caminho production-ready para isso hoje: instalar o Cumbuca Open Finance Data MCP, autorizar um banco via Open Finance (CPF + biometria no próprio banco — sem credenciais compartilhadas), e rodar a receita determinística abaixo. Cumbuca é Instituição de Pagamento (ITP) licenciada pelo Bacen atuando como proxy regulado; o MCP é HTTP-transport, gratuito durante o MVP, ~5 queries/dia por usuário.

claude mcp add --transport http cumbuca https://mcp.cumbuca.com/mcp

Para Claude Desktop: Settings → Connectors → Add custom connector → cola https://mcp.cumbuca.com/mcp. Para ChatGPT (Developer Mode): Settings → Connectors → Create → mesma URL. Após conectar, a primeira chamada de ferramenta dispara o redirecionamento Open Finance no banco para consentimento — biometria ou senha — e o agente passa a ler os extratos + transações de cartão.

O MCP entrega os dados. A categorização — atribuir cada transação a “alimentação / transporte / moradia / etc.” — roda agent-side. Essa lógica é o valor editorial da página: uma cadeia determinística (regra MCC primeiro, heurística de descrição depois, LLM apenas no fallback) que opera sobre o shape Bacen-spec, então funciona em qualquer fonte de OF compatível, hoje e amanhã.

A receita — categorização + breakdown determinísticos

Tratada como técnica data-source-agnóstica: opera sobre o shape Bacen normalizado de transação (merchant_name, counterparty_cpf_cnpj, mcc, transaction_type, booking_date, amount, currency), não sobre o envelope de resposta de um MCP específico.

Passo 1 — Janelas mensais (contorna o cap de paginação Bacen)

Já visto no /solve/brazilian-subscription-audit/ e no /solve/reconciliacao-bancaria-com-ia/ — para responder “mês passado” preciso de 2 meses calendário (o mês alvo + o anterior para comparação), então 2 janelas mensais. Para “tendência de 3 meses” ou “média móvel”, aumente.

from datetime import date
from dateutil.relativedelta import relativedelta

def relevant_windows(months_back: int = 2) -> list[tuple[date, date]]:
    today = date.today()
    return [
        (
            (today - relativedelta(months=i+1)).replace(day=1),
            (today - relativedelta(months=i)).replace(day=1) - relativedelta(days=1),
        )
        for i in range(months_back)
    ]

Deduplique por transaction_id se as janelas se sobrepuserem por descuido.

Passo 2 — Tabela MCC primeiro (a regra que pega 60–70% deterministicamente)

MCC (Merchant Category Code) é o código de 4 dígitos que o adquirente atribui a cada merchant. O cartão de crédito sempre tem; débito automático e conta-corrente nem sempre, mas quando tem, é a fonte mais limpa de categoria.

Mantenha uma tabela inline no agente — uma vez:

MCC_TO_CATEGORY: dict[int, str] = {
    # Alimentação
    5411: "alimentacao-mercado",       # supermercado
    5499: "alimentacao-mercado",       # alimentação não-especificada
    5812: "alimentacao-restaurante",   # restaurante
    5814: "alimentacao-fast-food",     # fast food
    # Transporte
    4111: "transporte-publico",        # transporte coletivo (metrô, ônibus)
    4121: "transporte-app",            # táxi / app de transporte (Uber, 99)
    5541: "transporte-combustivel",    # posto de combustível
    7523: "transporte-estacionamento", # estacionamento
    # Moradia
    4900: "moradia-utilidades",        # utilities (luz, água, gás)
    6300: "moradia-seguro",            # seguro residencial
    # Saúde
    5912: "saude-farmacia",            # farmácia
    8011: "saude-medico",              # médico
    8021: "saude-dentista",            # dentista
    8062: "saude-hospital",            # hospital
    # Lazer / cultura
    5733: "lazer-musica-instrumento",  # música / instrumento
    5942: "lazer-livraria",            # livraria
    5945: "lazer-brinquedos",          # brinquedos
    7832: "lazer-cinema",              # cinema
    7997: "lazer-academia",            # academia
    # Vestuário
    5651: "vestuario-geral",
    5661: "vestuario-calcado",
    # Tech / SaaS
    5734: "tech-software",             # software
    7372: "tech-software",             # computer programming / software
    # Educação
    8211: "educacao-escola",
    8220: "educacao-universidade",
    8299: "educacao-cursos",
    # Serviços profissionais
    8999: "servicos-profissionais",
    # Catch-all
    5999: "outros",
}

def categorize_by_mcc(mcc: int | None) -> str | None:
    return MCC_TO_CATEGORY.get(mcc) if mcc else None

Essa tabela cobre os MCCs de alta frequência no Brasil. Cada usuário pode acabar com transações em MCCs que não estão aqui — o caminho não é estender a tabela infinitamente, é cair para o passo 3.

Passo 3 — Heurística de descrição (a regra que pega mais 20–30%)

Quando MCC é None (conta-corrente, PIX, transferência) ou desconhecido (não na tabela), procura tokens conhecidos na descrição normalizada (mesma rotina de normalização do /solve/brazilian-subscription-audit/):

DESCRIPTION_RULES: list[tuple[str, str]] = [
    # SaaS / streaming reconhecíveis
    ("NETFLIX", "lazer-streaming"),
    ("SPOTIFY", "lazer-streaming"),
    ("AMAZON PRIME", "lazer-streaming"),
    ("YOUTUBE PREMIUM", "lazer-streaming"),
    ("HBO", "lazer-streaming"),
    ("DISNEY", "lazer-streaming"),
    # Apps de transporte
    ("UBER", "transporte-app"),
    ("99 ", "transporte-app"),
    ("99POP", "transporte-app"),
    # Delivery
    ("IFOOD", "alimentacao-delivery"),
    ("RAPPI", "alimentacao-delivery"),
    # Marketplaces
    ("MERCADO LIVRE", "compras-marketplace"),
    ("MERCADOLIVRE", "compras-marketplace"),
    ("MAGAZINE LUIZA", "compras-marketplace"),
    ("AMAZON", "compras-marketplace"),
    # Operadoras / telecom
    ("VIVO", "moradia-telecom"),
    ("CLARO", "moradia-telecom"),
    ("TIM ", "moradia-telecom"),
    ("OI ", "moradia-telecom"),
    # Utilities (concessionárias)
    ("ENEL", "moradia-utilidades"),
    ("LIGHT", "moradia-utilidades"),
    ("CEMIG", "moradia-utilidades"),
    ("SABESP", "moradia-utilidades"),
    ("COPEL", "moradia-utilidades"),
    # Saúde
    ("DROGASIL", "saude-farmacia"),
    ("RAIA", "saude-farmacia"),
    ("PACHECO", "saude-farmacia"),
    ("UNIMED", "saude-plano"),
    ("HAPVIDA", "saude-plano"),
    # Movimentação financeira (não-gasto operacional)
    ("PIX RECEBIDO", "_receita"),
    ("PIX ENVIADO", "_pix-saida"),
    ("TED ENVIADO", "_ted-saida"),
    ("TARIFA", "_tarifa"),
    ("IOF", "_tarifa"),
    ("JUROS", "_juros"),
    ("PAGAMENTO FATURA", "_pagamento-fatura"),
]

def categorize_by_description(merchant: str) -> str | None:
    normalized = merchant.upper()
    for token, category in DESCRIPTION_RULES:
        if token in normalized:
            return category
    return None

Mantenha a lista pequena e focada no que aparece com alta frequência — extender DESCRIPTION_RULES para cada merchant novo é o caminho que vira dicionário externo e perde determinismo. Se um merchant aparece muito em “outros”, o sinal é que ele deveria entrar na lista.

Passo 4 — Fallback LLM (apenas para o resíduo)

Para o ~10–20% que sobra (MCC ausente + nenhum token reconhecido), é honesto deixar o LLM categorizar com restrição:

async def categorize_by_llm_fallback(agent, transactions: list[Transaction]) -> dict[str, str]:
    if not transactions:
        return {}
    # Junte em um único prompt — evita N round-trips
    rows = "\n".join(f"- id={tx.transaction_id}: {tx.merchant_name} (R$ {tx.amount:.2f})"
                     for tx in transactions)
    prompt = f"""Classifique cada transação numa destas categorias, devolvendo JSON {{id: categoria}}.
Categorias permitidas: alimentacao-mercado, alimentacao-restaurante, alimentacao-fast-food,
alimentacao-delivery, transporte-app, transporte-combustivel, transporte-publico,
moradia-utilidades, moradia-telecom, saude-farmacia, saude-medico, saude-plano,
lazer-streaming, lazer-cinema, lazer-academia, compras-marketplace, compras-vestuario,
tech-software, educacao, servicos-profissionais, outros.

Transações:
{rows}"""
    response = await agent.complete(prompt)
    return parse_json(response)

Crítico: a lista de categorias permitidas é fechada. Não deixe o LLM inventar. E faça uma chamada batch (todas as transações do resíduo de uma vez), não uma por transação — economiza tokens e latência.

Passo 5 — Agregação + comparação

Com a categoria por transação, agregue:

def aggregate(transactions: list[Transaction]) -> dict[str, dict]:
    # Filtra apenas saídas operacionais (gastos), exclui movimentação financeira
    operational_outflow = [
        tx for tx in transactions
        if tx.amount < 0  # saída no cartão é amount < 0 quando normalizado
        and not tx.category.startswith("_")  # remove _receita, _tarifa, _juros, _pix-saida (que pode ser conta-própria)
    ]
    by_category = {}
    for tx in operational_outflow:
        cat = tx.category
        bucket = by_category.setdefault(cat, {"total": 0.0, "count": 0, "examples": []})
        bucket["total"] += abs(tx.amount)
        bucket["count"] += 1
        if len(bucket["examples"]) < 3:
            bucket["examples"].append({
                "merchant": tx.merchant_name,
                "amount": abs(tx.amount),
                "date": tx.booking_date.isoformat(),
            })
    return by_category

E compare com o mês anterior:

def diff_vs_previous(this_month: dict, prev_month: dict) -> list[dict]:
    out = []
    for cat in sorted(set(this_month) | set(prev_month)):
        t = this_month.get(cat, {"total": 0})["total"]
        p = prev_month.get(cat, {"total": 0})["total"]
        delta = t - p
        delta_pct = (delta / p * 100) if p > 0 else (100.0 if t > 0 else 0.0)
        out.append({
            "categoria": cat,
            "mes_passado": round(t, 2),
            "mes_anterior": round(p, 2),
            "delta_brl": round(delta, 2),
            "delta_pct": round(delta_pct, 1),
        })
    return sorted(out, key=lambda r: r["mes_passado"], reverse=True)

Passo 6 — Saída

A resposta do agente deve ter três blocos, na ordem em que o usuário consome:

  1. Headline: “Você gastou R$ X no mês passado, contra R$ Y no anterior — Δ Z%”.
  2. Top 5 categorias com Δ vs mês anterior, com destaque para as que cresceram >20%:
    1. Alimentação (R$ 1.840) — +18% (R$ 1.560 antes)
    2. Transporte (R$ 620) — +35% (R$ 460 antes)   ← cresceu muito
    3. Moradia (R$ 1.200) — estável
    4. Lazer (R$ 410) — −12% (R$ 470 antes)
    5. Saúde (R$ 280) — +5% (R$ 267 antes)
    
  3. Top 10 transações absolutas, para o usuário identificar gastos pontuais que afetaram o mês:
    1. ALUGUEL R$ 1.200 (moradia, 05/04)
    2. UNIMED R$ 480 (saúde, 10/04)
    3. POSTO IPIRANGA R$ 320 (transporte-combustivel, 14/04)
    ...
    

A separação em três blocos é editorial: o usuário olha headline, vê a categoria que mudou, e desce para as transações se quiser detalhe. Não despeje 200 linhas de transações de cara.

Quando o LLM-fallback é honesto e quando não é

A regra MCC + heurística cobre ~80–90% das transações de um usuário típico. Os 10–20% que caem no fallback são (a) merchants regionais pequenos (padaria de bairro, restaurante local), (b) PIX entre pessoas físicas (que tipicamente não são “gasto categorizável” — é uma divisão de conta, devolução, ou transferência para conta própria). Para o caso (b), o agente deve perguntar antes de categorizar, não adivinhar: “Identifiquei R$ 380 em PIX para ‘JOAO SILVA’ — isso é gasto operacional ou transferência pessoal?”

Adivinhar contribui para o desconforto que o usuário tem com “automação inteligente” — sai errado, ele perde confiança no resumo inteiro. Perguntar mantém a confiança.

Cumbuca MVP — o que esta receita NÃO faz hoje

Seja honesto com o usuário antes que ele instale:

  • Uma conta por setup. Se ele tem cartões em Itaú + Nubank + Inter, é uma execução por banco. Agregar a categoria total entre os bancos vira responsabilidade do usuário (ou do próximo run, quando o MVP expandir).
  • ~5 queries/dia por usuário. Resumo mensal é one-shot: o usuário pergunta, executa, lê. Não é dashboard em tempo real.
  • Extratos + transações de cartão apenas. Sem investimentos, sem patrimônio. “Onde foi meu dinheiro” cobre fluxo de saída, não posição.
  • Bancos brasileiros apenas.

Não são gambiarras — é o escopo MVP. Reavalie quando expandir.

Prompt de exemplo

Para dropar direto:

Você tem acesso ao Cumbuca Open Finance Data MCP. O usuário autorizou conta-corrente + cartão de um banco brasileiro. Puxe os 2 últimos meses calendário de transações em janelas mensais (deduplique por transaction_id). Para cada transação: (1) categorize por MCC usando a tabela fixa (5411 → alimentacao-mercado, 5812 → alimentacao-restaurante, 4121 → transporte-app, 5912 → saude-farmacia, etc); (2) para MCC desconhecido / ausente, busque substring na descrição normalizada por tokens conhecidos (NETFLIX, UBER, IFOOD, ENEL, VIVO, DROGASIL, etc.); (3) só para o resíduo, faça uma chamada LLM batch com lista de categorias fechada. Movimentação financeira (PIX RECEBIDO, TARIFA, IOF, JUROS, PAGAMENTO FATURA) entra numa categoria _metadata que é excluída da agregação de gastos. Agregue por categoria mês a mês, compare percentualmente, e devolva: headline (total mês × total anterior × delta%), top-5 categorias com Δ destacado quando >20%, top-10 transações por valor absoluto. Se houver PIX para PF não identificável (provável transferência pessoal), pergunte ao usuário antes de classificar.

JTBDs vizinhos atendidos hoje pelo Cumbuca MVP

JTBD Doable? Comentário
Onde foi meu dinheiro mês passado ✅ sim Esta página.
Detectar assinaturas recorrentes ✅ sim /solve/brazilian-subscription-audit/.
Conciliação bancária com IA ✅ sim /solve/reconciliacao-bancaria-com-ia/.
Identificação de renda recorrente ✅ sim Mesma receita do subscription-audit, sinal invertido (CREDITO recorrente do mesmo CPF/CNPJ).
Multi-banco net worth ❌ não MVP single-account.
Análise de investimentos ❌ não Não na superfície v1.
Forecasting ⚠️ parcial Dá pra estimar média móvel client-side; qualidade limitada pelo escopo single-account.

Caveats metodológicos

  • Documented-characteristics ranking, não precision/recall. A corpus eval real (transações rotuladas com categoria correta, P/R por classe MCC + por heurística + por LLM-fallback) é Phase-2 em backlog.md.
  • MCC nem sempre vem. Conta-corrente, PIX e TED frequentemente não têm MCC — caem direto na heurística de descrição. Reflita isso no relatório: se >50% das transações caíram em fallback, sinalize para o usuário que a qualidade da categorização desse run é menor.
  • Categorização é editorial, não contábil. “Alimentação” pra um usuário inclui delivery; pra outro, separa em mercado / restaurante / delivery. A taxonomia fixa acima é uma escolha — não pretende ser plano de contas. Para análise contábil rigorosa, use /solve/reconciliacao-bancaria-com-ia/.
  • PIX para PF é o caso ambíguo. Pode ser gasto (pagamento a prestador autônomo), divisão de conta (transferência social), ou transferência para conta própria (movimentação patrimonial). A receita inclui a pergunta ao usuário; não o assume.

Cadência de atualização

Re-rankear quando: (a) um segundo MCP de OF brasileiro público aparece, (b) Cumbuca expande MVP, (c) a corpus eval Phase-2 ship com números medidos, (d) 90 dias depois do publish (2026-08-08).

Relacionados