Colocar um LLM atrás de uma tela de chat leva uma tarde. Transformar isso num produto que cobra, aguenta mais de um cliente e não quebra a sua margem leva bem mais — e o modelo é a parte fácil. O difícil é medir, cobrar e isolar.
Este guia é a planta de uma plataforma de IA multi-inquilino: as camadas, o que fica em cada uma, e as contas que decidem se o negócio fecha. Os erros listados aqui são erros reais de quem opera uma plataforma de IA cobrando em reais — inclusive os nossos.
Seis camadas: identidade → chaves → limites → roteamento → medição → cobrança. A medição é o coração; se ela estiver errada, todo o resto está. Regra de ouro: cobre pelo efeito, nunca pela chamada.
A arquitetura em seis camadas
Desenhe assim e cada problema cai numa camada só:
| Camada | Responsabilidade | Erro clássico |
|---|---|---|
| 1. Identidade | Quem é o usuário e a que conta pertence | Confundir usuário com conta — aí ninguém consegue ter equipe |
| 2. Chaves | Credenciais de máquina, revogáveis, com escopo | Guardar a chave em texto puro no banco |
| 3. Limites | Quota, rate limit, saldo mínimo | Checar saldo depois de gastar |
| 4. Roteamento | Escolher o modelo/máquina que atende | Amarrar o nome do fornecedor no identificador público |
| 5. Medição | Registrar o consumo real de cada chamada | Medir o que pediu em vez do que aconteceu |
| 6. Cobrança | Converter consumo em lançamento financeiro | Cobrar por chamada em operação idempotente |
Camada 2: chaves de API que dá para revogar
Usuário faz login; máquina usa chave. São coisas diferentes e o mesmo sistema não deve servir as duas. A chave precisa nascer com prefixo reconhecível, ser guardada como hash e poder morrer sem derrubar a conta.
import secrets, hashlib
def gerar_chave(conta_id: int):
bruta = "sk_live_" + secrets.token_urlsafe(32)
return bruta, { # a bruta aparece UMA vez, na criação
"conta_id": conta_id,
"hash": hashlib.sha256(bruta.encode()).hexdigest(),
"prefixo": bruta[:16], # p/ o usuário reconhecer na lista
"criada_em": agora(),
"ultima_uso": None,
}
def autenticar(cabecalho: str):
bruta = cabecalho.removeprefix("Bearer ").strip()
h = hashlib.sha256(bruta.encode()).hexdigest()
return db.buscar_um("SELECT * FROM chaves WHERE hash=? AND revogada_em IS NULL", [h])
Guardar só o hash não é preciosismo: significa que um vazamento do seu banco não entrega as chaves dos clientes. E o campo prefixo existe para o cliente conseguir dizer "revoga a que começa com sk_live_a3f" sem você precisar do valor completo.
Camada 3: limites — e o erro que derruba cliente
Você precisa de três limites distintos, e confundi-los é fonte de incidente:
- Rate limit: requisições por minuto. Protege a sua infraestrutura.
- Quota: teto de consumo no período. Protege o cliente dele mesmo.
- Saldo: dinheiro disponível. Protege o seu caixa.
Este erro nos custou um incidente real. Quando uma chamada está em voo, você reserva um valor estimado para não deixar o saldo furar. Se algum outro processo — o que desliga máquinas por falta de saldo, por exemplo — ler o saldo cru sem descontar as reservas, ele conclui que o cliente está no vermelho e desliga um recurso que estava pago.
Solução: exponha uma única função saldo_efetivo() = saldo − reservas em voo, e proíba qualquer código de decisão de ler a coluna de saldo diretamente.
def saldo_efetivo(conta_id: int) -> float:
saldo = db.escalar("SELECT saldo FROM contas WHERE id=?", [conta_id])
em_voo = db.escalar("""SELECT COALESCE(SUM(valor),0) FROM reservas
WHERE conta_id=? AND liquidada_em IS NULL""", [conta_id])
return saldo - em_voo
# TODO código que decide "pode gastar?" ou "desliga por falta de saldo?" usa ESTA função.
Camada 4: roteamento e a fronteira da sua marca
Se a sua plataforma serve modelos ou máquinas de terceiros, o identificador público não pode ser o identificador do fornecedor. Não é só uma questão de segredo comercial: é liberdade operacional.
Enquanto o cliente pedir modelo-x-v3-0731, você está preso àquele fornecedor e àquele nome. No dia em que trocar, ou em que ele renomear, você quebra a integração de todo mundo. Com um identificador próprio, a troca é uma linha num mapa:
MODELOS = { # público → interno
"plataforma-fast": {"upstream": "modelo-pequeno-atual", "nome": "Rápido"},
"plataforma-plus": {"upstream": "modelo-medio-atual", "nome": "Padrão"},
"plataforma-max": {"upstream": "modelo-grande-atual", "nome": "Máximo"},
}
# Aceite os nomes genéricos na ENTRADA por conveniência,
# mas NUNCA devolva o identificador interno na SAÍDA.
def resolver(pedido: str):
return MODELOS.get(pedido) or MODELOS.get(ALIASES.get(pedido, ""))
Cuidado com os vazamentos indiretos, que são os que escapam na revisão: campos de metadados que o fornecedor devolve e você repassa sem filtrar, mensagens de erro cruas, headers de resposta, e comentário em HTML ou JavaScript — view-source é público. Filtre a saída num ponto único, não em cada rota.
Camada 5: medição — o coração da plataforma
Meça o que aconteceu, nunca o que foi pedido. A diferença aparece o tempo todo: a resposta cortou no meio, o cliente cancelou a conexão, o modelo usou cache de contexto, a chamada falhou depois de processar metade.
def registrar_uso(conta_id, chave_id, modelo, uso, desfecho):
custo = (uso.entrada / 1e6) * PRECO[modelo]["entrada"] \
+ (uso.saida / 1e6) * PRECO[modelo]["saida"] \
+ (uso.cache / 1e6) * PRECO[modelo]["cache"]
db.inserir("eventos_uso", {
"conta_id": conta_id, "chave_id": chave_id, "modelo": modelo,
"tokens_entrada": uso.entrada, "tokens_saida": uso.saida,
"tokens_cache": uso.cache, "custo_brl": round(custo, 6),
"desfecho": desfecho, # ok | cancelado | erro_upstream | truncado
"quando": agora(),
})
return custo
Grave desfecho desde o primeiro dia. É o campo que responde "por que a receita não bate com o número de chamadas?" — e sem ele você reconstrói isso a mão, meses depois, sem os dados.
Arredonde a favor da casa, mas com honestidade
Chamadas individuais custam frações de centavo. Se você arredondar para baixo em cada uma, some um milhão de chamadas e o prejuízo é real. Arredonde para cima, sempre na mesma direção, e documente isso na página de preços. Cliente aceita arredondamento previsível; o que ele não aceita é conta que não fecha.
Camada 6: cobrança — cobre pelo efeito, nunca pela chamada
Uma plataforma cobra 1 hora pré-paga ao ligar uma máquina. Um cliente com pipeline automatizado chamava /start antes de cada tarefa. A máquina já estava ligada — do lado da infraestrutura, aquilo era operação sem efeito. Mas a cobrança rodava na chamada, não no efeito. Resultado: 7 horas cobradas por 2 horas de uso, estornadas depois.
A correção tem três partes: (1) checar o estado antes; (2) usar uma trava atômica, porque duas chamadas simultâneas passam pela checagem juntas; (3) responder 200 com {"jaEstavaLigada": true}, não 4xx — devolver erro quebraria a integração de quem chama de forma idempotente de propósito.
-- A trava é o UPDATE condicional, não o SELECT antes dele.
-- Só segue quem conseguiu mudar a linha (changes = 1).
UPDATE instancias SET status='ligando'
WHERE id = ? AND status NOT IN ('ligada','ligando','criando');
alterou = db.executar(SQL_ACIMA, [inst_id]).linhas_afetadas
if alterou == 0:
return {"ok": True, "jaEstavaLigada": True} # 200, sem cobrar
try:
cobrar_reserva(conta_id, valor_hora)
provedor.ligar(inst_id)
except Exception:
db.executar("UPDATE instancias SET status='parada' WHERE id=?", [inst_id])
estornar_reserva(conta_id, valor_hora) # o rollback é obrigatório
raise
O padrão vale para qualquer coisa cobrada: se a operação não produziu efeito, ela não gera lançamento. E se produziu efeito e a cobrança falhou, você precisa desfazer o efeito — senão entrega de graça.
Convenção de sinais no extrato
Decida no primeiro dia e não mude: cobrança é negativa, crédito é positivo. Parece trivial e não é. Um lançamento de cobrança gravado com valor positivo debita o saldo certo (porque outro trecho de código faz a subtração) mas aparece como crédito no extrato do cliente — e vira bomba-relógio no dia em que alguém recalcular o saldo com um SUM().
Isolamento entre inquilinos
Três níveis, do mais barato ao mais caro. Escolha por cliente, não para a plataforma inteira:
| Nível | Como | Custo | Para quem |
|---|---|---|---|
| Lógico | conta_id em toda tabela e toda consulta | Zero | Padrão, a maioria |
| Dados | Banco ou schema por cliente | Baixo | Exigência de contrato |
| Computação | Instância de GPU dedicada por cliente | Alto | Corporativo, dado sensível |
No isolamento lógico, a proteção que importa não é a boa intenção do desenvolvedor: é o middleware que injeta conta_id e a revisão que rejeita qualquer consulta a tabela multi-inquilino sem ele. Uma consulta esquecida vaza dado de um cliente para outro — o pior incidente possível numa plataforma B2B.
Para o cliente corporativo que não aceita API compartilhada, a resposta é uma instância dedicada: o modelo roda numa máquina que é só dele, e ele controla os pesos, os logs e os prompts, sem API de terceiro no caminho. Isso é argumento de controle — e é o que vale em análise de LGPD, porque define quem processa o quê, não onde a máquina está.
A conta que decide o negócio
Modelo de custo de uma plataforma de IA tem três parcelas. Duas todo mundo lembra; a terceira mata margem em silêncio.
- Variável: tokens ou horas de GPU consumidos pelos clientes. Escala com a receita — a parcela saudável.
- Fixa: banco, aplicação, monitoramento, e-mail. Pequena e previsível.
- Ociosa: GPU ligada sem ninguém usando. É esta que quebra a plataforma.
Token ou GPU dedicada, por cliente
Com números reais. Suponha um cliente que consome 40 milhões de tokens de entrada e 8 milhões de saída por mês:
| Opção | Conta | Custo/mês |
|---|---|---|
Por token (gpub-plus) | 40 × R$ 0,69 + 8 × R$ 3,99 | R$ 59,52 |
Por token (gpub-fast) | 40 × R$ 0,49 + 8 × R$ 1,09 | R$ 28,32 |
| GPU dedicada 24×7 (RTX 4090, Econômica) | R$ 2,38/h × 730 h | R$ 1.737 |
| GPU dedicada só em horário comercial (10 h/dia útil) | R$ 2,38/h × 220 h | R$ 523 |
A leitura: nesse volume, token ganha com folga. A GPU dedicada só passa a fazer sentido quando o consumo é contínuo — porque o preço da GPU é por hora ligada, e não por trabalho feito. Regra prática: token para carga irregular, GPU para carga constante, e desligue a máquina fora do horário se a carga tem hora para acontecer.
Sirva a base de clientes por token, e ofereça GPU dedicada como plano superior para quem tem volume ou exigência de isolamento. Você fica sem custo ocioso na base e cobra caro exatamente onde o custo é alto. Bônus: o mesmo código atende os dois — se a sua GPU serve uma interface compatível com a da OpenAI, muda só a URL base.
Precificação: três armadilhas
- Câmbio embutido em multiplicador fixo. Se você compra em dólar e vende em real com margem fixa, toda alta do dólar sai do seu bolso — e ninguém percebe, porque nada quebra. Coloque um monitor diário do câmbio com alerta em dois níveis, e trate o multiplicador como fonte única: se o preço de venda, o preço de exibição e a cobrança lerem valores diferentes, você vai cobrar errado. Já vimos essa divergência cobrar 32% a mais do cliente.
- Preço sem piso de custo. Se o seu preço final é calculado por regra (desconto de tier, promoção, arredondamento), coloque um piso amarrado ao custo real. Sem ele, uma combinação de regras acaba vendendo abaixo do custo — e o item mais barato do catálogo é justamente o que vende mais.
- Preço de bloco anunciado como preço unitário. Se um recurso só é vendido em lote (8 GPUs, por exemplo), o preço por unidade dele não existe na prática. Publicar esse número em artigo ou e-mail gera cliente irritado na hora de fechar.
O que monitorar desde o primeiro cliente
- Saldo na sua conta do fornecedor. Se ele zera, as máquinas dos seus clientes morrem. Alerta em dois níveis, com throttle para não virar ruído.
- Órfãos. Recurso vivo no fornecedor sem registro ativo no seu banco = sangramento puro. Precisa de reconciliação periódica em ambas as direções.
- Demanda recusada. Conte as chamadas que você negou (sem saldo, sem quota, sem estoque), não só as que atendeu. Demanda barrada é invisível no gráfico de receita e é o dado mais valioso que você tem.
- Catálogo vazio. Se um fornecedor cai, a sua vitrine esvazia e o cliente lê "não temos" em vez de "estamos com problema". Alerte quando uma categoria zerar.
Um monitor nosso lia o arquivo de ambiente por caminho relativo. Rodando na mão, funcionava. No agendador, o diretório de trabalho era outro — ele rodava sem nenhuma credencial, lia saldo como NaN e falhava em silêncio por semanas. Em qualquer tarefa agendada, resolva o caminho a partir do arquivo do próprio script.
A infraestrutura da sua plataforma, cobrada em reais
API por token compatível com a da OpenAI e GPUs sob demanda no mesmo saldo. Sem assinatura, sem câmbio, sem IOF — e suporte em português.
Criar conta →Roteiro de 30 dias
- Semana 1: identidade, chaves com hash, uma rota que chama o modelo e grava o evento de uso. Sem cobrança ainda.
- Semana 2: medição correta (com
desfecho), painel de consumo para o cliente ver. Ainda de graça. - Semana 3: saldo, reserva,
saldo_efetivo(), cobrança pelo efeito. Teste os caminhos de falha — não só o feliz. - Semana 4: limites, alertas de saldo, reconciliação e o monitor de câmbio. Só então abra para clientes pagantes.
A ordem importa. Plataforma que abre cobrança antes de ter medição confiável passa os meses seguintes estornando.
Conclusão
Uma plataforma de IA é 20% modelo e 80% contabilidade. As decisões que definem se ela sobrevive — cobrar pelo efeito, medir o que aconteceu, saldo efetivo em vez de saldo cru, piso de custo no preço, fonte única de câmbio — não têm nada a ver com IA. São as mesmas de qualquer sistema que mexe com dinheiro, só que com um custo variável que muda sozinho.
Construa a medição primeiro. Todo o resto se conserta depois; dado de consumo que você não gravou está perdido para sempre.
Continue: o agente que roda em cima dela · RAG de produção · Econômica ou Dedicada?