Taxas de cartão em xeque: o que o dev faz com Bun
Visa, Mastercard e alguns dos maiores bancos dos Estados Unidos estão de novo no banco dos réus. O motivo é velho conhecido: as taxas de cartão cobradas do lojista a cada compra. A nova ação diz que essas tarifas são anticompetitivas. Os lojistas pagam e não conseguem negociar, e no fim quem banca é o consumidor, que paga tudo mais caro. A notícia saiu no ClassAction.org. Parece assunto de advogado e economista, mas cai direto no colo de quem escreve checkout. E como boa parte dos meus backends hoje roda em Bun, vou usar ele nos exemplos.
O que aconteceu com Visa, Mastercard e os bancos
O resumo cabe em três frases. Comerciantes acusam as bandeiras e os bancos emissores de combinarem, na prática, o valor da taxa de intercâmbio. Essa taxa sai do bolso do lojista em toda transação com cartão. A acusação diz que as regras das bandeiras impedem o lojista de reagir, seja cobrando a mais de quem paga com cartão premium, seja empurrando o cliente para um meio de pagamento mais barato.
Não é briga nova. O processo coletivo mais famoso dos comerciantes americanos contra as bandeiras começou em 2005 e já passou por acordo bilionário, recurso, acordo rejeitado por juíza e nova rodada de negociação. Os números são enormes: levantamentos do setor estimam que os lojistas americanos pagam mais de 100 bilhões de dólares por ano só em taxas de cartão.
Pode ser que nada mude amanhã. Processo desse tamanho leva anos. Mas o debate em si já diz algo útil para o dev: a taxa de cartão parece fixa, e não é.
Por que taxa de cartão é problema de dev, e não só do financeiro
No Brasil o nome muda, mas a lógica é a mesma. Aqui a gente fala em MDR, a taxa que a adquirente desconta do lojista. Dentro dela vai o intercâmbio do banco emissor, a parte da bandeira e a margem da adquirente. Uma venda de R$ 100 no crédito à vista pode deixar entre R$ 2 e R$ 4 no caminho, dependendo do contrato. Parcelado, sobe. Antecipação de recebíveis, sobe de novo.
Agora pensa em quem decide como o cliente paga. Não é o diretor financeiro. É o componente de checkout que você escreveu numa sexta à tarde.
- Qual meio de pagamento aparece primeiro na tela?
- O Pix tem desconto ou está escondido numa aba?
- O parcelamento sem juros é o padrão para qualquer valor?
- Você sabe quanto cada pedido custou em taxa, ou só sabe o valor bruto?
Cada uma dessas decisões é código. E cada uma mexe na margem do negócio mais do que muita otimização de query que a gente adora fazer.
Taxa que ninguém vê é taxa que ninguém negocia.
Como calcular a taxa de cartão por pedido com Bun
O primeiro passo é parar de tratar taxa como algo que "o gateway resolve". Grave o custo de cada pedido. Com Bun dá para fazer isso sem instalar nada além do próprio runtime, porque ele já traz servidor HTTP e SQLite embutidos.
Um detalhe que salva vidas: trabalhe sempre em centavos, com inteiros. Dinheiro em ponto flutuante é bug esperando para virar chamado de suporte.
// fees.ts
type Method = "pix" | "debit" | "credit" | "credit_installments";
// Valores ilustrativos. Use os do seu contrato.
const RATES: Record<Method, { pct: number; fixedCents: number }> = {
pix: { pct: 0.0099, fixedCents: 0 },
debit: { pct: 0.0149, fixedCents: 0 },
credit: { pct: 0.0299, fixedCents: 0 },
credit_installments: { pct: 0.0459, fixedCents: 0 },
};
export function feeCents(amountCents: number, method: Method) {
const { pct, fixedCents } = RATES[method];
return Math.round(amountCents * pct) + fixedCents;
}
Agora um endpoint que registra cada venda com a taxa estimada:
// server.ts
import { Database } from "bun:sqlite";
import { feeCents } from "./fees";
const db = new Database("orders.db");
db.run(`CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
amount_cents INTEGER NOT NULL,
method TEXT NOT NULL,
fee_cents INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`);
const insert = db.query(
"INSERT INTO orders (amount_cents, method, fee_cents) VALUES (?, ?, ?)"
);
Bun.serve({
routes: {
"/orders": {
POST: async (req) => {
const { amountCents, method } = await req.json();
const fee = feeCents(amountCents, method);
insert.run(amountCents, method, fee);
return Response.json({ amountCents, method, feeCents: fee });
},
},
},
});
Em produção você vai querer validar o corpo da requisição e usar PostgreSQL em vez de um arquivo SQLite. A ideia continua a mesma: cada pedido nasce com o custo do lado. Depois de um mês, uma query simples mostra quanto foi embora em taxa:
SELECT method, COUNT(*) AS pedidos, SUM(fee_cents) / 100.0 AS taxa_reais
FROM orders
GROUP BY method
ORDER BY taxa_reais DESC;
Já vi cliente descobrir com uma consulta dessas que o parcelamento sem juros em compras de R$ 40 custava mais do que o frete. Ninguém tinha olhado porque o número não existia em lugar nenhum.
O que muda no checkout quando você enxerga o custo
Com o dado na mão, as decisões de interface ficam óbvias. E várias delas são permitidas no Brasil, coisa que o lojista americano ainda briga na Justiça para conseguir.
Desconto no Pix. Desde 2017 a lei permite cobrar preço diferente conforme o meio de pagamento. Se o Pix custa um terço do crédito para você, dá para devolver parte dessa diferença ao cliente. Em código, é uma linha:
const pixPrice = Math.round(amountCents * 0.95); // 5% off no Pix
Parcelamento com piso. Liberar 12x sem juros para um produto de R$ 30 é generosidade com o dinheiro dos outros. Coloque um valor mínimo por parcela e deixe a regra num lugar só:
const MIN_INSTALLMENT_CENTS = 5000; // R$ 50 por parcela
const maxInstallments = Math.min(12, Math.floor(amountCents / MIN_INSTALLMENT_CENTS)) || 1;
Ordem dos botões. Quem aparece primeiro é escolhido mais vezes. Colocar o Pix em destaque não é golpe de UX, desde que o cartão continue fácil de achar. Se esconder o cartão, você troca taxa por carrinho abandonado, e aí a conta piora.
Roteamento entre adquirentes. Para quem já tem volume, faz sentido ter dois provedores e mandar cada transação para o mais barato naquele tipo de cartão. Aqui eu sou cauteloso: isso traz complexidade de conciliação, de estorno e de suporte. Só vale quando o volume paga essa conta.
Bun é o ponto da história?
Não, e prefiro ser honesto sobre isso. Esse código roda em Node com duas ou três dependências a mais. O que o Bun traz para esse caso é menos atrito: bun:sqlite embutido, Bun.serve com rotas, TypeScript sem etapa de build e testes com bun test. Para um serviço pequeno de pagamentos, ou para um script que reprocessa o extrato da adquirente toda madrugada, isso conta. Menos dependência no caminho do dinheiro é menos superfície para quebrar e menos pacote para auditar.
E auditar importa. Código de pagamento é o pior lugar possível para uma dependência abandonada ou comprometida. Se o runtime já resolve HTTP, banco local, hash e testes, você tem menos coisa no package.json para vigiar.
Um teste mínimo para a função de taxa já evita a clássica regressão de arredondamento:
// fees.test.ts
import { expect, test } from "bun:test";
import { feeCents } from "./fees";
test("crédito à vista em R$ 100", () => {
expect(feeCents(10000, "credit")).toBe(299);
});
Minha opinião sobre o processo e sobre o seu checkout
O processo contra Visa, Mastercard e os bancos provavelmente vai terminar em acordo, como os anteriores. Talvez com alguma regra nova que deixe o lojista americano cobrar mais caro no cartão premium ou recusar certos cartões. Talvez com um desconto pequeno na taxa por alguns anos. Não espero revolução.
O que eu levo da notícia é outra coisa. Se nem lojistas gigantes conseguem negociar com as bandeiras, a única alavanca que sobra para o negócio pequeno está no próprio produto: mostrar o custo real, oferecer a alternativa mais barata e desenhar o checkout pensando nisso. Essa parte é trabalho de dev, e quase ninguém faz.
Então antes de trocar de framework de UI pela terceira vez no ano, grava o fee_cents em cada pedido. Aposto que essa coluna vai gerar mais conversa na próxima reunião do que qualquer refatoração. Se quiser ver como eu costumo montar backends enxutos assim, dá uma olhada nos meus projetos.
Resumo para o LinkedIn
Visa, Mastercard e grandes bancos americanos foram processados de novo, e quem deveria prestar atenção é quem escreve checkout. A acusação: taxas de cartão anticompetitivas que o lojista não consegue negociar. Por lá, isso passa de 100 bilhões de dólares por ano. Aqui no Brasil o nome é MDR, e quem decide quanto dele o negócio paga é o componente de checkout que você escreveu numa sexta à tarde. Já vi cliente descobrir que o parcelamento sem juros em compras de R$ 40 custava mais que o frete. Ninguém via porque o número não existia em lugar nenhum. Minha sugestão: grave o `fee_cents` em cada pedido. Com Bun dá pra fazer com servidor e SQLite embutidos, sem instalar mais nada. Escrevi o passo a passo com código no blog. Se o seu checkout ainda não mostra quanto cada venda paga de taxa, vale a leitura. #Bun #Pagamentos #DesenvolvimentoDeSoftware #Ecommerce #TypeScript