surfaai · parte 7 de 7
PIX, boleto e cartão: a partir de quanto cada um fica mais barato
07 de outubro de 20269 min de leituraCesar Eduardo Sturmer
Quando você monta um checkout no Brasil, a pergunta "quanto custa processar isso" tem uma resposta diferente para cada meio de pagamento. E quase toda discussão sobre isso para no lugar errado: "PIX é barato, boleto é barato, cartão é caro".
Não é falso. É só inútil, porque as três afirmações dependem do valor da venda, e o ponto onde elas trocam de ordem é calculável. Este artigo é sobre esses pontos.
O artigo 3 desta série já gastou a matemática do gross-up — a conta circular de quando você repassa a taxa ao pagador. Aqui é outra coisa: os números absolutos, de onde eles saem, e as três armadilhas que eu encontrei no caminho.
Os números
Estes são os valores no código do surfaai, referência Stripe Brasil:
| meio | percentual | fixo |
|---|---|---|
| Cartão doméstico | 3,99% | R$ 0,39 |
| Cartão internacional | 5,99% | R$ 0,39 |
| PIX | 1,19% | — |
| Boleto | — | R$ 3,45 |
Repare na forma, antes dos números: PIX é só percentual, boleto é só fixo, cartão é os dois. As três formas se comportam de modo completamente diferente conforme o ticket, e é daí que vem tudo que segue.
O custo real por ticket
A mesma tabela, aplicada. A coluna que importa é a porcentagem efetiva, não o valor:
| venda | PIX | boleto | cartão |
|---|---|---|---|
| R$ 50 | R$ 0,60 — 1,20% | R$ 3,45 — 6,90% | R$ 2,39 — 4,78% |
| R$ 80 | R$ 0,95 — 1,19% | R$ 3,45 — 4,31% | R$ 3,58 — 4,47% |
| R$ 100 | R$ 1,19 — 1,19% | R$ 3,45 — 3,45% | R$ 4,38 — 4,38% |
| R$ 290 | R$ 3,45 — 1,19% | R$ 3,45 — 1,19% | R$ 11,96 — 4,12% |
| R$ 500 | R$ 5,95 — 1,19% | R$ 3,45 — 0,69% | R$ 20,34 — 4,07% |
| R$ 1.000 | R$ 11,90 — 1,19% | R$ 3,45 — 0,34% | R$ 40,29 — 4,03% |
Três leituras:
O PIX é uma linha reta. 1,19% em qualquer valor. É o único meio cujo custo relativo não muda — o que o torna o único previsível, e o motivo de ele ser o default sensato quando você não sabe o perfil do seu ticket.
O boleto é uma curva que começa péssima. Num ticket de R$ 50, aquele R$ 3,45 fixo é 6,90% — quase o dobro do cartão. O boleto só é "barato" quando o ticket carrega a taxa fixa.
O cartão converge, nunca cai. Começa em 4,78% e desce assintoticamente para 3,99%, porque o fixo de R$ 0,39 dilui. Nunca fica competitivo, e isso é o esperado: você está pagando por crédito e por disputa.
Os dois pontos de cruzamento
Daí saem dois números que valem mais que a tabela.
Boleto passa a ser mais barato que PIX em R$ 289,92. Igualando as duas fórmulas:
0,0119 × b = 3,45
b = 3,45 / 0,0119 = R$ 289,92Abaixo disso, PIX ganha. Acima, boleto — e a diferença cresce sem limite, porque o boleto não cresce. Em R$ 1.000 o boleto custa R$ 3,45 contra R$ 11,90 do PIX.
Boleto passa a ser mais barato que cartão em R$ 76,69:
0,0399 × b + 0,39 = 3,45
b = 3,06 / 0,0399 = R$ 76,69Abaixo de R$ 76,69, o boleto é o mais caro dos três. Essa é a parte contraintuitiva: existe uma faixa de ticket baixo onde o meio que todo mundo chama de barato é o pior negócio da sua operação.
E cartão nunca vence PIX. Igualando, o b sai negativo — não existe valor em que 3,99% + R$ 0,39 fique abaixo de 1,19%.
Armadilha 1: o cap que não é cap
No código, o PIX tem um teto de taxa:
pix: {
percentageFee: 1.19,
fixedFee: 0,
capAmount: 999.0, // Stripe não menciona cap — valor alto = sem efeito
},Aquele 999.0 não é um limite da Stripe. É um "sem limite" escrito como número, porque a estrutura do código exige um capAmount e a Stripe não publica nenhum para PIX.
Faz a conta de quando ele morderia: 999 / 0,0119 = R$ 83.949,58. Nenhuma aula de surfe custa oitenta e quatro mil reais, então o campo nunca age.
Funciona, e eu manteria. Mas é uma mentira codificada, do tipo que depende de uma premissa não escrita — "nosso ticket nunca chega lá". No dia que alguém vender um pacote corporativo de R$ 90 mil, a taxa trava em R$ 999 e a plataforma come a diferença, e vai levar um tempo até alguém entender por quê. Um capAmount: null com o if tratando a ausência seria honesto. O comentário ao lado é o que salva — e é por isso que ele está lá.
Armadilha 2: parcelamento não custa percentual a mais
Essa eu errei por meses. O campo existe:
installmentExtraPercentPerMonth: 0,E ele já valeu 0,50 — meio ponto percentual por parcela adicional, que é como funciona na maioria das adquirentes brasileiras. Uma migration corrigiu, e o comentário dela é a explicação inteira:
-- Stripe Brasil não cobra taxa extra por parcela.
-- O parcelamento é liquidado pelo banco/emissor — o merchant recebe o valor cheio de uma vez.
UPDATE stripe_fee_config SET card_installment_extra_pct = 0 WHERE id = 1;O modelo é diferente do que a intuição brasileira espera. No parcelamento sem juros da Stripe, quem financia é o emissor do cartão, que cobra do portador. O merchant recebe à vista, com a taxa de cartão normal. Não há percentual por parcela porque não há crédito concedido por você.
Eu estava cobrando 0,5% por parcela de quem parcelava em 12x — seis pontos percentuais de taxa inventada — por ter assumido que a Stripe funcionava como as adquirentes que eu conhecia. O campo continua lá, zerado, porque é um gancho legítimo: se mudar, muda no banco.
Armadilha 3: taxa fixa torna o gross-up inútil
Uma consequência bonita da forma de cada taxa. A fórmula do gross-up do artigo 3:
taxa = (base × p + f) / (1 − p)Aplique no boleto, onde p = 0 e f = 3,45:
taxa = (base × 0 + 3,45) / (1 − 0) = 3,45O gross-up é idempotente para taxa puramente fixa. Faz sentido: a circularidade existia porque o percentual incidia sobre um total que já continha a taxa. Sem percentual, não há circularidade — somar R$ 3,45 não muda quanto é R$ 3,45.
No PIX, que é só percentual, o efeito é mínimo: num ticket de R$ 100, a taxa vai de R$ 1,19 para R$ 1,20. Um centavo. No cartão, que tem os dois, é onde o gross-up ganha o salário: de R$ 4,38 para R$ 4,56.
A regra geral: o gross-up importa proporcionalmente ao percentual. Quanto mais a taxa é fixa, menos ele faz — mas você aplica a mesma função nos três, porque a fórmula já trata o caso degenerado sozinha. Não precisa de if por método.
Onde esses números moram — e o problema que eu achei aí
As taxas não vêm da API da Stripe. O checkout precisa do número antes de criar a cobrança, e uma chamada externa para exibir um valor coloca latência e um ponto de falha no exato momento em que a pessoa decide se compra.
Então elas vivem numa tabela, stripe_fee_config, com um singleton (id = 1), carregadas por loadStripeFees(). Taxa de gateway muda em escala de meses; um UPDATE no painel resolve sem deploy. O valor no código é o fallback de quando o banco não responde:
export async function loadStripeFees(): Promise<StripeFeesConfig> {
try {
const { data } = await supabase
.from('stripe_fee_config')
.select('*')
.eq('id', 1)
.single();
if (!data) return STRIPE_BR_FEES;
// ... mapeia as colunas
} catch {
return STRIPE_BR_FEES;
}
}O desenho está certo. O problema é o que eu encontrei escrevendo este artigo: os DEFAULT da tabela são valores antigos, e discordam do fallback.
| campo | DEFAULT na migration |
fallback no código |
|---|---|---|
pix_pct / pix_cap |
1.00 / 15.00 | 1.19 / 999 |
boleto_pct / boleto_fixed |
1.50 / 0.75 | 0 / 3.45 |
card_installment_extra_pct |
0.50 | 0 |
E o seed foi INSERT INTO stripe_fee_config (id) VALUES (1) — sem valores, pegando todos os defaults. Só o campo de parcelamento foi corrigido depois, pela migration da armadilha 2. PIX e boleto não.
Como loadStripeFees() prefere o banco e só cai no fallback em erro, a linha do banco é a fonte de verdade — e ela foi semeada com os números errados. O código documenta uma taxa de PIX de 1,19% sem teto; a tabela, se nunca foi editada, diz 1,00% com teto de R$ 15. Todas as contas deste artigo saem do fallback.
Não cobrei ninguém errado: o surfaai lançou sem taxa, então não havia taxa a errar. O estrago é potencial, e vira real no dia que a cobrança ligar com a linha velha no banco.
O que isso ensina sobre o padrão: um DEFAULT de coluna é código, e envelhece como código — só que não aparece em nenhum diff depois do dia que foi escrito. O fallback no .ts foi revisado, atualizado e lido por mim várias vezes. O DEFAULT na migration foi escrito uma vez, em março, e nunca mais olhado — enquanto era ele, não o fallback, que decidia.
Se eu refizesse: ou o seed grava valores explícitos e a coluna não tem DEFAULT nenhum, ou existe um teste que compara a linha do banco com STRIPE_BR_FEES e grita quando divergem sem alguém ter mandado divergir. Ter duas fontes de verdade é aceitável. Não saber qual está valendo, não.
A regra
Taxa de pagamento não é um número, é uma função do ticket. "Qual meio é mais barato" não tem resposta sem o valor da venda, e os pontos de virada — R$ 76,69 e R$ 289,92 — são onde a resposta muda.
Isso tem consequência de produto, não só de engenharia: o meio mais conveniente na interface raramente é o mais barato na operação, e num ticket baixo o boleto é o pior dos três. Vale desenhar o checkout mostrando isso, em vez de empurrar cartão porque é o caminho de menor atrito.
E vale conferir, agora, qual linha o seu banco está realmente servindo.