Como categorizar as transações do extrato bancário com o Jev, o modelo que só devolve valor tipado

Fluxo para categorizar transações com o Jev usando lista de categorias e resposta tipada
Resposta rápida

Para categorizar transações com o Jev você monta uma lista fixa de categorias, manda cada lançamento como state e faz uma pergunta do tipo Choice: o modelo devolve uma das suas opções, a probabilidade de cada uma e uma confiança de 0 a 1, nunca texto livre. Inclua sempre uma opção "outros", limpe os prefixos de maquininha antes de enviar e separe valor e data em campos próprios do state. Confiança alta grava direto, média vai pra fila de revisão, baixa não age (a doc usa 0,6 como corte de exemplo). O preço é US$ 0,042 por milhão de tokens de entrada, com saída zerada

Fala aí, beleza? Extrato bancário é um dos textos mais hostis que um app pode receber

PAGLOJA ABC, MP IFD, código de adquirente, nome de intermediário no lugar do estabelecimento, valor e data jogados no meio da string

E aí você pede categoria pra um LLM e recebe de volta uma frase simpática, um nome de categoria que você nunca cadastrou, às vezes um JSON meio quebrado que precisa de pós-processamento pra virar dado

O Jev ataca exatamente esse ponto: ele é o primeiro modelo da classe System One da TypeSafe AI, lançado em 15 de setembro de 2026, e NÃO gera texto

O que ele faz é avaliar perguntas tipadas contra um state e devolver valores tipados, com distribuição de probabilidade e um número de confiança, feitos pra serem consumidos direto pelo software

A parte que interessa pra finanças pessoais: a saída é sempre restrita às opções que você forneceu

Se a lista de categorias do usuário tem doze itens, a resposta é um desses doze itens, com a probabilidade de cada um. Nunca um valor fora da lista

O modelo foi construído por Diogo Almeida, ex-OpenAI e coautor do paper do InstructGPT, e o lançamento veio junto de uma rodada seed de 40 milhões de dólares liderada pela DCVC

Bora montar o app na prática?

O que você precisa antes de começar

A lista é curta, e a melhor notícia é que não tem mais fila

  • Conta na TypeSafe: o acesso ao Jev não tem mais waitlist, o cadastro está aberto pra qualquer pessoa
  • Uma chave de API: as chaves são criadas no console, em https://console.typesafe.ai/
  • A variável de ambiente TYPESAFE_API_KEY: o cliente do SDK lê a chave dali, então você não precisa passar nada no construtor
  • Python 3.10 ou superior com pip install typesafe-sdk (ou uv add typesafe-sdk, se tu já vive no uv)

Se o teu projeto é Node, existe SDK oficial em JavaScript publicado como pacote npm, o @typesafe-ai/sdk

E se tu tá em outra linguagem, dá pra falar direto com o endpoint HTTP: um POST pra https://api.typesafe.ai/v1/systemone, com header Authorization: Bearer <API_KEY> e Content-Type: application/json

O corpo tem três campos de topo: state, model e questions

Curso de Jev: Sistemas Mais Inteligentes, Rápidos e Robustos
Curso Recomendado

Curso de Jev: Sistemas Mais Inteligentes, Rápidos e Robustos

Desenvolva sistemas mais inteligentes, rápidos e robustos com Jev

Dois avisos antes de abrir o editor, pra ninguém se ferrar depois:

O Jev aceita apenas entrada de texto, seja texto puro, objeto JSON ou array de texto. Extrato em PDF, OFX, CSV do banco, tudo isso é problema teu: você extrai, normaliza e monta o state

E o inglês é a principal língua de treino, onde a acurácia é melhor hoje. Outras línguas são atendidas, mas não igualmente bem, e a própria documentação orienta testar no seu próprio conteúdo e observar a confiança em cargas fora do inglês

Traduzindo pro nosso caso: descrição de maquininha em português é entrada fora do inglês, então mede antes de confiar cegamente 🙂

Passo a passo: do extrato à categoria tipada

  1. Defina a lista fixa de categorias do usuário

Esse é o primeiro passo porque ele é o contrato do app. A pergunta do tipo Choice recebe instructions (o texto da pergunta) e criteria, um dicionário em que as chaves são as opções e os valores podem ser None ou uma descrição da opção

A documentação orienta passar a lista completa de categorias, e não um subconjunto, e incluir uma opção do tipo "other" quando a lista pode não cobrir toda entrada. Em extrato bancário ela praticamente nunca cobre tudo, então a opção "outros" não é opcional

O teto por pergunta é de 255 opções, o que pra finanças pessoais é folga infinita

from typesafe_sdk import Choice, TypeSafeClient

client = TypeSafeClient()

CATEGORIAS = {
    "mercado": "supermarket, grocery store, butcher, bakery, food delivery of raw groceries",
    "restaurante": "restaurants, bars, coffee shops, food delivery of prepared meals",
    "transporte": "ride hailing, public transport, fuel, parking, tolls",
    "moradia": "rent, condo fee, electricity, water, gas, internet",
    "saude": "pharmacy, health insurance, doctors, labs, dentist",
    "assinaturas": "recurring digital services such as streaming, software, cloud, gym",
    "educacao": "courses, schools, books, tuition",
    "lazer": "cinema, travel, games, events, hobbies",
    "outros": "anything that does not clearly fit any other category",
}

O erro comum deste passo: mandar só as categorias que você "acha" que podem casar com aquele lançamento. Isso empurra o modelo a escolher a menos errada de um conjunto errado

  1. Monte o state com o lançamento

O state pode ser texto puro, objeto JSON ou array de texto, e em Python vai direto no client.system_one(state=...)

Para extrato, dict é melhor que string solta: você separa o que é descrição do que é número e data

lancamento = {
    "descricao": "PAG*LOJA ABC",
    "valor": -89.90,
    "data": "2026-09-12",
    "canal": "cartao de debito",
}

O erro comum deste passo: deixar valor e data grudados dentro da descrição. Mais adiante eu mostro por que isso é ruim justamente com esse modelo

  1. Escreva a pergunta Choice

Aqui o criteria faz o trabalho pesado de desambiguar categorias parecidas. "mercado" e "restaurante" são a briga clássica do iFood, e a descrição de cada opção é o que resolve

questions = {
    "categoria": Choice(
        instructions="Which personal finance category best describes this bank transaction?",
        criteria=CATEGORIAS,
    )
}

O erro comum deste passo: criteria com todos os valores em None e nomes de categoria genéricos. O valor None é permitido, mas quando duas opções se parecem, descrição explícita é o que separa uma da outra

  1. Chame o modelo e leia a resposta tipada

A chamada usa a classe TypeSafeClient e o método system_one, passando o state e o dicionário de perguntas. O cliente já usa o alias jev-latest como modelo padrão (a versão atual é a jev-1.13, e o campo model também aceita IDs versionados como jev-1.13.0)

response = client.system_one(state=lancamento, questions=questions)

resultado = response.choices["categoria"]

print(resultado.choice)         # "mercado"
print(resultado.confidence)     # 0.81
print(resultado.probabilities)  # {"mercado": 0.88, "restaurante": 0.12, ...}

A resposta de uma Choice traz a opção escolhida, uma probabilidade pra cada opção (somando 1) e um valor de confiança entre 0 e 1. É isso que vai pro banco de dados, sem regex, sem json.loads com try/except

O erro comum deste passo: esperar texto livre de volta. O jev-1.13 não é treinado pra gerar texto, e forçar geração encadeando choices funciona mal e é muito lento. Se você quer uma justificativa em prosa pro usuário, isso é trabalho de outro modelo

  1. Aponte a pergunta pro campo exato do JSON

Quando o state tem vários campos, a documentação recomenda apontar a pergunta pra um valor específico usando o caminho entre crases dentro do texto da pergunta

questions = {
    "categoria": Choice(
        instructions=(
            "Which personal finance category best describes the transaction "
            "described in `lancamento.descricao`?"
        ),
        criteria=CATEGORIAS,
    )
}

Se o state é uma lista de lançamentos, o caminho aceita índice, tipo lancamentos[0].descricao

O erro comum deste passo: pergunta ambígua num state cheio de campos. Você acha que está perguntando sobre a descrição e o modelo está pontuando a coisa toda

Descrições bagunçadas de maquininha: o que quebra e como contornar

Agora a parte que ninguém conta nos tutoriais bonitinhos: extrato brasileiro é sujo

A página oficial de jaggedness do jev-1.13 lista três fraquezas conhecidas, e as três batem exatamente nos problemas de extrato. Se liga nisso

**Sintoma: PAGLOJA ABC ou MP IFD viram "outros"**

Causa: o nome que aparece é o do intermediário de pagamento, não o do estabelecimento. O modelo lê literalmente o que tá ali, e o que tá ali não é comércio nenhum

Solução: limpe os prefixos de intermediário no SEU código, antes de enviar. Uma tabela de prefixos conhecidos e um strip resolvem boa parte, e o que sobrar vira campo separado no state (algo como intermediario e estabelecimento), em vez de uma string única confusa

Sintoma: a pergunta parece certa e a resposta insiste em outra categoria

Causa: interpretação muito literal da pergunta escrita. Escopo, negação e condições implícitas são lidos ao pé da letra

Solução: escreva criteria dizendo explicitamente o que cai em cada categoria, e evite negação na pergunta ("which category, not counting…" é pedido pra dar errado). Se você quer excluir algo, tira do conjunto de opções ou descreve a exclusão dentro do critério da opção

Sintoma: pedir pro modelo decidir por faixa de valor dá resultado errático

Causa: precisão numérica é uma fraqueza documentada. -89.90 não é tratado como quantidade comparável do jeito que você espera

Solução: a faixa de valor é if no seu código, não pergunta pro modelo. Separe valor em campo próprio do state pra dar contexto, e faça a comparação em Python

Sintoma: "é recorrente?" com base na data dá resposta instável

Causa: datas são lidas como texto, não como quantidades ordenadas, e piora quando os formatos vêm misturados

Solução: não peça cálculo nem comparação de datas. Se você precisa saber se o mesmo lançamento aparece todo mês, isso é uma query no seu banco. E mantenha UM formato de data só no state (ISO em todos os registros, sem 12/09 em um e 2026-09-12 em outro)

A regra de ouro aqui: normalize a string antes de enviar e deixe aritmética e calendário do lado do seu código. O Jev é bom em decidir entre opções, não em fazer conta

O que fazer quando a confiança é baixa

A confiança colapsa a distribuição de probabilidades num número de 0 a 1, e é ela que decide se o teu app age sozinho ou chama o usuário

O padrão recomendado na documentação divide em três faixas

Faixa de confiança O que a doc recomenda No app de finanças
Alta agir automaticamente grava a categoria direto no lançamento
Média confirmar, sinalizar pra revisão ou buscar mais contexto entra numa fila de revisão com a sugestão já preenchida
Baixa não agir: mandar pra humano, pedir esclarecimento ou cair em outro sistema fica como "outros" pendente de confirmação do usuário

O exemplo de corte que a documentação usa é rotear pra um atendente humano quando a confiança fica abaixo de 0,6, e ela permite explicitamente thresholds diferentes por ação, conforme o custo do erro

Na prática fica assim (o corte de cima é escolha sua, calibre olhando os seus dados)

ALTA = 0.85   # valor ilustrativo: calibre com a sua distribuicao
BAIXA = 0.6   # corte de exemplo da documentacao

r = response.choices["categoria"]

if r.confidence >= ALTA:
    salvar(lancamento, categoria=r.choice)
elif r.confidence >= BAIXA:
    fila_de_revisao(lancamento, sugestao=r.choice, alternativas=top2(r.probabilities))
else:
    salvar(lancamento, categoria="outros", pendente=True)

E tem um detalhe que deixa a tela do app muito melhor: você não precisa mostrar só a categoria escolhida

Como a resposta traz a probabilidade de cada opção, dá pra oferecer as duas mais prováveis como botão de escolha rápida

def top2(probabilities):
    return sorted(probabilities.items(), key=lambda kv: kv[1], reverse=True)[:2]

O usuário toca em uma das duas e segue a vida, sem abrir um seletor com doze categorias

O erro comum deste bloco: usar um threshold único pro cafezinho da esquina e pro pagamento do aluguel

Errar a categoria do café da esquina custa nada. Errar a categoria de um pagamento alto bagunça o relatório do mês inteiro, então o corte pra "agir automaticamente" pode ser mais alto quando o valor é alto. Essa é a ideia de threshold por ação aplicada a dinheiro

Várias perguntas por lançamento em uma chamada só

Categoria é só a primeira pergunta. Num app de finanças você normalmente quer mais: é recorrente? é despesa dedutível? é pessoal ou da empresa?

A notícia boa é que cada pergunta é pontuada isoladamente contra o state. Uma chamada com N perguntas dá as mesmas respostas que N chamadas de uma pergunta cada, só que sem reenviar o state

questions = {
    "categoria": Choice(
        instructions="Which personal finance category best describes `lancamento.descricao`?",
        criteria=CATEGORIAS,
    ),
    "recorrente": Choice(
        instructions="Does `lancamento.descricao` look like a recurring subscription or monthly bill?",
        criteria={
            "sim": "clearly a subscription or a monthly bill from a known provider",
            "nao": "a one-off purchase",
            "indefinido": "not enough information in the description",
        },
    ),
    "dedutivel": Choice(
        instructions="Could `lancamento.descricao` be a deductible business expense for a freelancer?",
        criteria={"provavel": None, "improvavel": None, "outros": "unclear from the description"},
    ),
}

response = client.system_one(state=lancamento, questions=questions)

O ganho de agrupar aparece no cookbook oficial: com 13 perguntas sobre um artigo de cerca de 54.000 caracteres, juntar tudo numa chamada saiu 12,2x mais barato e 10,0x mais rápido, sem mudar as respostas

No nosso caso o state é minúsculo, então o ganho é proporcionalmente menor, mas a lógica é a mesma: o state é o que pesa, as perguntas viajam de graça junto

Isso também define quantos lançamentos cabem por state. O orçamento de contexto do jev-1.13 é de 64k cobrindo o state mais todas as perguntas somadas, e 32k pro state mais a pergunta mais longa

Ou seja: dá pra mandar um lote de lançamentos num array e perguntar sobre cada um pelo caminho indexado, desde que você respeite esses dois tetos. Se o teu app de finanças já está rodando e você quer encaixar essa camada sem refazer o resto, o caminho de adotar o Jev em projeto existente é um assunto por si só

E tome cuidado com o loop ingênuo no extrato inteiro: os rate limits são medidos em tokens por segundo e requisições por minuto, e estourar qualquer um deles devolve 429 Too Many Requests

Ou seja, um for cru em cima de 400 lançamentos vai bater no 429. Trate o 429 com backoff e reenvio, do jeito que você já faria com qualquer API

Quanto custa rodar isso no seu extrato (e onde mais isso serve)

Aqui a conta é quase engraçada de tão pequena

O preço do Jev 1.13 é de US$ 0,042 por 1 milhão de tokens de entrada e US$ 0,00 por milhão de tokens de saída

Com o dólar a cerca de R$ 5,14, isso dá aproximadamente 22 centavos de real por milhão de tokens de entrada, e saída zerada mesmo

E tem o empurrão inicial: contas novas começam com 5 dólares em crédito grátis, o equivalente a cerca de 120 milhões de tokens

Descrição de lançamento bancário é texto curtíssimo, então um uso pessoal de extrato cabe folgado nesse crédito. Faz a conta com o teu volume real: número de lançamentos, tamanho médio do state e as perguntas que você manda junto

O padrão que você acabou de montar não serve só pra extrato, e é aqui que a coisa fica massa

Qualquer lugar onde existe uma lista fixa de opções definida pelo negócio é caso de Choice:

  • triagem de notas fiscais por natureza de despesa
  • classificação de recebíveis por origem
  • marcação de assinaturas recorrentes dentro de uma base de cobranças
  • roteamento de pedidos de reembolso pro time certo
  • classificação de lançamentos por centro de custo

É sempre a mesma receita: lista completa de opções, uma opção "outros", state limpo e threshold de confiança

Próximo passo

Recapitulando o que faz esse app funcionar:

  • lista fixa e completa de categorias, nunca um subconjunto
  • uma opção "outros" pra absorver o que não cabe
  • state limpo, com valor e data em campos próprios e descrição normalizada
  • threshold de confiança em três faixas, com corte mais rígido quando o erro custa mais
  • nada de conta e comparação de data do lado do modelo

E se você quer escrever essas integrações com apoio do agente, existe uma agent skill oficial da TypeSafe

No Claude Code ela é instalada como plugin pelo marketplace:

claude plugin marketplace add typesafe-ai/skills
claude plugin install typesafe@typesafe-ai

Depois você invoca com o comando de barra /typesafe:typesafe-ai

Em outros agentes, a mesma skill entra via npx, com instalação local ao projeto por padrão:

npx skills add typesafe-ai/skills --skill typesafe-ai

Use -g se você quiser a instalação global

Minha sugestão pra começar: pega UM mês de extrato, roda com a tua lista de categorias e, antes de automatizar qualquer coisa, olha a distribuição de confiança que voltou

É esse histograma que te diz onde colocar o corte, e não o chute de ninguém 😀

Até o próximo post!

Perguntas frequentes

Quanto custa categorizar transações com o Jev em produção?

O Jev 1.13 cobra US$ 0,042 por 1 milhão de tokens de entrada e US$ 0,00 por milhão de tokens de saída, o que no câmbio de R$ 5,14 dá centavos de real por milhão de tokens de entrada. Como a saída não é cobrada, categorizar transações fica praticamente limitado ao tamanho do state e das perguntas que você envia. Contas novas ainda ganham US$ 5 de crédito grátis, equivalente a cerca de 120 milhões de tokens, o que dá pra rodar bastante teste antes de gastar de verdade.

Dá pra categorizar várias transações de uma vez só, numa chamada?

Dá, e é a forma recomendada: cada pergunta é pontuada isoladamente contra o state, então uma chamada com N perguntas devolve as mesmas respostas de N chamadas separadas, só que sem reenviar o state a cada vez. No cookbook oficial, agrupar 13 perguntas em uma única chamada saiu 12,2x mais barato e 10,0x mais rápido que chamar uma por vez, sem mudar as respostas.

O Jev categoriza bem descrição de transação em português?

O inglês é a língua primária de treino do Jev e onde a acurácia é melhor hoje; outras línguas são atendidas, mas não igualmente bem. A própria documentação orienta testar no seu conteúdo real e observar o valor de confiança antes de confiar cegamente em cargas fora do inglês. Descrição de maquininha em português entra nessa categoria, então vale medir a confiança antes de automatizar tudo.

O que fazer quando a confiança da categoria vem baixa?

A confiança é um número de 0 a 1 que colapsa a distribuição de probabilidade das opções, e o padrão recomendado divide o resultado em três faixas: alta pra agir automaticamente, média pra confirmar ou revisar, e baixa pra não agir sozinho. A documentação usa como exemplo de corte mandar para um humano quando a confiança fica abaixo de 0,6, mas o threshold pode variar por ação conforme o custo do erro.

Quantas categorias posso mandar numa pergunta Choice do Jev?

Uma pergunta do tipo Choice aceita até 255 opções, o que pra categorização de extrato bancário costuma ser folga de sobra. A recomendação da documentação é sempre mandar a lista completa de categorias do usuário, nunca um subconjunto, e incluir uma opção tipo ‘outros’ quando a lista pode não cobrir toda entrada, o que é praticamente sempre o caso em extrato.

O Jev funciona igual a um LLM comum pra classificar transações?

Não. O Jev não gera texto: ele avalia perguntas tipadas contra um state e devolve valores tipados, com distribuição de probabilidade e confiança, restritos às opções que você forneceu. Isso é diferente de pedir categoria pra um LLM comum, que pode devolver frase solta, categoria inventada ou JSON malformado que precisa de pós-processamento.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

Blog | Mais populares