<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CESAR EDUARDO STURMER</title>
    <description>The latest articles on DEV Community by CESAR EDUARDO STURMER (@cesarsturmer).</description>
    <link>https://dev.to/cesarsturmer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4153198%2F0e71df10-0c0f-4e67-88fa-827c2899c501.jpeg</url>
      <title>DEV Community: CESAR EDUARDO STURMER</title>
      <link>https://dev.to/cesarsturmer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cesarsturmer"/>
    <language>en</language>
    <item>
      <title>Destination charges na prática: split, estorno e a matemática das taxas</title>
      <dc:creator>CESAR EDUARDO STURMER</dc:creator>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/cesarsturmer/destination-charges-na-pratica-split-estorno-e-a-matematica-das-taxas-3he7</link>
      <guid>https://dev.to/cesarsturmer/destination-charges-na-pratica-split-estorno-e-a-matematica-das-taxas-3he7</guid>
      <description>&lt;p&gt;O artigo anterior foi sobre a decisão: por que deleguei a infraestrutura financeira à Stripe em vez de construir o split na mão. Este é sobre como isso funciona por dentro — incluindo uma conta que quase todo mundo erra na primeira tentativa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Onde a cobrança acontece
&lt;/h2&gt;

&lt;p&gt;O Stripe Connect tem dois padrões principais de split. A diferença entre eles é onde a cobrança nasce.&lt;/p&gt;

&lt;p&gt;Com &lt;strong&gt;separate charges and transfers&lt;/strong&gt; , você cobra na conta da plataforma e depois cria um &lt;code&gt;Transfer&lt;/code&gt; separado para o vendedor. São duas operações, em dois momentos, que você orquestra.&lt;/p&gt;

&lt;p&gt;Com &lt;strong&gt;destination charges&lt;/strong&gt; , a cobrança já nasce sabendo para onde parte dela vai:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountInCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payment_method_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;billingType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;billingType&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;application_fee_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationFeeAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transfer_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stripeAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerId&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uma operação. A Stripe cobra do aluno, retém &lt;code&gt;application_fee_amount&lt;/code&gt; para a plataforma e transfere o líquido para a conta Connect do provider assim que a cobrança é capturada.&lt;/p&gt;

&lt;p&gt;Três consequências que importam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O aluno vê &lt;strong&gt;uma cobrança só&lt;/strong&gt; , no nome da plataforma. Um lançamento no extrato, não dois.&lt;/li&gt;
&lt;li&gt;O provider recebe &lt;strong&gt;automaticamente&lt;/strong&gt; , sem nenhuma rotina de repasse minha.&lt;/li&gt;
&lt;li&gt;Eu nunca fico com o dinheiro dele em custódia.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  O detalhe que me fez escolher: estorno
&lt;/h2&gt;

&lt;p&gt;Essa é a parte pouco comentada, e foi decisiva.&lt;/p&gt;

&lt;p&gt;Quando você estorna uma cobrança feita com destination charge, a &lt;code&gt;application_fee&lt;/code&gt; correspondente &lt;strong&gt;é revertida automaticamente&lt;/strong&gt; , na proporção do estorno. A Stripe ajusta os dois lados numa operação só.&lt;/p&gt;

&lt;p&gt;Parece um detalhe contábil, mas olhe o que ele elimina. O surfaai tem cancelamento com direito de arrependimento: dentro de sete dias, o aluno recebe estorno real, proporcional aos créditos que não usou. Com split manual, eu teria que escrever a lógica de "devolver minha taxa também", lidar com o caso de o provider já ter sacado o valor, e reconciliar tudo isso.&lt;/p&gt;

&lt;p&gt;Com destination charges, o estorno proporcional é uma chamada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;payment_intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripe_payment_intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valorProporcional&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E a taxa volta sozinha, na proporção certa. Zero código meu para a parte financeira do cancelamento.&lt;/p&gt;

&lt;h2&gt;
  
  
  As taxas não vêm da API
&lt;/h2&gt;

&lt;p&gt;Para mostrar ao aluno quanto custa cada meio de pagamento no checkout, eu preciso saber a taxa &lt;strong&gt;antes&lt;/strong&gt; de criar a cobrança. A tentação é consultar a Stripe. Eu não consulto.&lt;/p&gt;

&lt;p&gt;As taxas vivem numa tabela do banco, carregadas por &lt;code&gt;loadStripeFees()&lt;/code&gt;, com um fallback hardcoded no código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STRIPE_BR_FEES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StripeFeesConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;domestic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percentageFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.39&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;international&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percentageFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.39&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;installmentExtraPercentPerMonth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percentageFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;999.0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;boleto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;percentageFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fixedFee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.45&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duas decisões empilhadas aqui.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por que não a API:&lt;/strong&gt; o checkout precisa ser instantâneo. Uma chamada externa só para exibir um número coloca latência e um ponto de falha num lugar onde o usuário está decidindo se compra. Taxas de gateway mudam em escala de meses, não de segundos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por que no banco, e não só no código:&lt;/strong&gt; quando a Stripe reajusta uma taxa, eu quero corrigir pelo painel administrativo, não abrir um pull request e esperar deploy. O valor hardcoded é a rede de segurança para quando o banco não responde.&lt;/p&gt;

&lt;h2&gt;
  
  
  A conta que quase todo mundo erra
&lt;/h2&gt;

&lt;p&gt;Aqui está a parte interessante. O surfaai permite configurar quem paga a taxa do gateway: a plataforma, o provider ou o aluno. Quando a taxa é repassada ao aluno, aparece um problema circular.&lt;/p&gt;

&lt;p&gt;A intuição diz: calcule a taxa sobre o preço e some.&lt;/p&gt;

&lt;p&gt;Com um serviço de R$ 100 e cartão a 3,99% + R$ 0,39:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taxa = 100 × 0,0399 + 0,39 = R$ 4,38
total cobrado = R$ 104,38
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Está errado. A Stripe não cobra a taxa sobre os R$ 100 — ela cobra sobre o valor da transação, que agora é R$ 104,38:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taxa real = 104,38 × 0,0399 + 0,39 = R$ 4,56
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Faltam R$ 0,18. Pouco numa venda, relevante em milhares delas, e sempre saindo do seu bolso.&lt;/p&gt;

&lt;p&gt;O valor correto exige resolver a circularidade. Chamando o preço de &lt;code&gt;base&lt;/code&gt;, a taxa percentual de &lt;code&gt;p&lt;/code&gt; e a fixa de &lt;code&gt;f&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total = base + taxa
taxa = total × p + f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Substituindo e isolando:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total = base + (total × p + f)
total − total × p = base + f
total = (base + f) / (1 − p)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E a taxa, que é o que eu preciso somar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taxa = (base × p + f) / (1 − p)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Que é exatamente o que está no código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateStripeFeeGrossUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;percentageFee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fixedFee&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getStripeFee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;billingType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;installments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isInternational&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;percentageFee&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// PIX tem cap — verificar se o gross-up também atingiria o cap&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;billingType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round2&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;baseAmount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fixedFee&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capAmount&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;fees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capAmount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conferindo com os números de antes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;taxa = (100 × 0,0399 + 0,39) / (1 − 0,0399)
     = 4,38 / 0,9601
     = R$ 4,56

total = R$ 104,56
verificação: 104,56 × 0,0399 + 0,39 = R$ 4,56 ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fecha. Esse padrão tem nome — &lt;strong&gt;gross-up&lt;/strong&gt; — e aparece em qualquer lugar onde um valor percentual é calculado sobre um total que o inclui: imposto retido, comissão sobre comissão, taxa repassada.&lt;/p&gt;

&lt;p&gt;Repare também no tratamento do cap do PIX. Um método com teto de taxa precisa verificar o limite &lt;strong&gt;depois&lt;/strong&gt; do gross-up, não antes: a fórmula pode empurrar o valor acima do teto, e aí o teto é que vale.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que eu levei disso
&lt;/h2&gt;

&lt;p&gt;Destination charges substituiu, com duas propriedades de um objeto, o que seria um módulo inteiro de custódia e reconciliação. O estorno automático da taxa sozinho já pagaria a escolha.&lt;/p&gt;

&lt;p&gt;Mas a lição que ficou é outra: &lt;strong&gt;delegar a infraestrutura não te isenta de entender a mecânica&lt;/strong&gt;. O gross-up não é problema da Stripe, é meu. Se eu tivesse tratado o split como caixa-preta, estaria perdendo centavos em cada transação — e centavos em volume são a diferença entre margem e prejuízo.&lt;/p&gt;

&lt;p&gt;No próximo artigo, um bug de verdade: como um aluno apertando F5 duas vezes gerou crédito dobrado, e o que isso me ensinou sobre onde o dinheiro pode ser criado.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>pagamentos</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Postmortem: o crédito duplicado que um F5 criava</title>
      <dc:creator>CESAR EDUARDO STURMER</dc:creator>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/cesarsturmer/postmortem-o-credito-duplicado-que-um-f5-criava-1mh8</link>
      <guid>https://dev.to/cesarsturmer/postmortem-o-credito-duplicado-que-um-f5-criava-1mh8</guid>
      <description>&lt;p&gt;Este é um relato de incidente real do surfaai. O artigo seguinte da série generaliza o padrão; aqui eu conto o que aconteceu comigo, incluindo a parte em que meu diagnóstico inicial estava errado.&lt;/p&gt;

&lt;h2&gt;
  
  
  O sintoma
&lt;/h2&gt;

&lt;p&gt;Um aluno comprou um pacote de créditos e ficou com o dobro do que pagou. Um pagamento, dois registros em &lt;code&gt;user_credits&lt;/code&gt;, cada um com a quantidade cheia.&lt;/p&gt;

&lt;p&gt;Não era erro de arredondamento nem bug de contagem. Eram duas linhas no banco, criadas com segundos de diferença, idênticas exceto pelo id.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como estava construído
&lt;/h2&gt;

&lt;p&gt;No desenho original, o fluxo de compra era assim:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;o front cria o PaymentIntent&lt;/li&gt;
&lt;li&gt;o aluno paga&lt;/li&gt;
&lt;li&gt;a Stripe devolve a confirmação ao front&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;o front chama a rota que cria o crédito&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;O passo 4 é o problema, e levei um tempo para enxergar — porque o código do passo 4 estava correto. Ele validava o pagamento, checava o valor, criava o registro certo. Revisei várias vezes procurando o erro na lógica.&lt;/p&gt;

&lt;p&gt;O erro não estava na lógica. Estava no fato de o passo 4 existir onde existia.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que realmente acontecia
&lt;/h2&gt;

&lt;p&gt;A página de confirmação disparava a criação do crédito ao montar. Se ela montasse duas vezes, o crédito era criado duas vezes.&lt;/p&gt;

&lt;p&gt;E ela montava duas vezes com uma facilidade constrangedora:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;o aluno aperta F5 na tela de confirmação&lt;/li&gt;
&lt;li&gt;a conexão oscila, o navegador reenvia a requisição&lt;/li&gt;
&lt;li&gt;o React remonta o componente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nenhum desses casos é exótico. O primeiro é o que qualquer pessoa faz quando a tela demora.&lt;/p&gt;

&lt;p&gt;Minha primeira hipótese foi condição de corrida entre duas requisições simultâneas, e cheguei a pensar em resolver com um lock. Estava olhando para o lugar errado: não era concorrência, era &lt;strong&gt;repetição&lt;/strong&gt;. O cliente podia chamar a operação quantas vezes quisesse, e nada no sistema dizia que ela só podia valer uma vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que o cliente nunca deveria ter esse poder
&lt;/h2&gt;

&lt;p&gt;Tem um ponto mais profundo aqui, e ele só ficou claro quando fui integrar PIX e boleto.&lt;/p&gt;

&lt;p&gt;Cartão tem um momento em que o front "sabe" que deu certo. PIX e boleto &lt;strong&gt;não têm&lt;/strong&gt;. O aluno fecha a aba, paga pelo app do banco quarenta minutos depois, e nunca mais volta ao site. Não existe nenhum instante no navegador em que esse pagamento confirma.&lt;/p&gt;

&lt;p&gt;Ou seja: mesmo que o fluxo client-side funcionasse perfeitamente para cartão, ele era estruturalmente incapaz de atender metade dos meios de pagamento. O bug do F5 foi o sintoma que apareceu primeiro, mas a arquitetura já estava errada antes dele.&lt;/p&gt;

&lt;h2&gt;
  
  
  A correção
&lt;/h2&gt;

&lt;p&gt;Crédito passou a nascer &lt;strong&gt;exclusivamente&lt;/strong&gt; do evento &lt;code&gt;payment_intent.succeeded&lt;/code&gt; do webhook da Stripe. O front não cria mais nada — ele só consulta o estado.&lt;/p&gt;

&lt;p&gt;Isso muda a natureza do problema. A origem passa a ser única e confiável: a Stripe assina cada evento, e cada evento tem um id estável. Cartão, PIX e boleto convergem para o mesmo caminho, porque todos geram o mesmo evento.&lt;/p&gt;

&lt;p&gt;Mas trocar o cliente pelo webhook &lt;strong&gt;não resolve a duplicação sozinho&lt;/strong&gt; — e esse foi o segundo aprendizado. Webhooks são entregues com garantia &lt;em&gt;at-least-once&lt;/em&gt;: a Stripe reenvia o evento se não receber &lt;code&gt;2xx&lt;/code&gt; rápido o bastante. Eu troquei "o usuário pode chamar duas vezes" por "a Stripe pode chamar duas vezes".&lt;/p&gt;

&lt;p&gt;A diferença é que agora eu tinha com o que trabalhar: um identificador único por evento.&lt;/p&gt;

&lt;p&gt;A primeira barreira ficou na entrada do handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existingEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe_webhook_events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe_event_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maybeSingle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingEvent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Already processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E a última, no banco, como rede de segurança — uma constraint de unicidade que falha se o crédito já existir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creditError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 23505 = unique_violation: crédito já criado por webhook concorrente — OK&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;creditError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23505&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;creditError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse &lt;code&gt;return&lt;/code&gt; silencioso no &lt;code&gt;23505&lt;/code&gt; é deliberado. Se dois webhooks passarem pela primeira barreira ao mesmo tempo — e passam, porque entre o &lt;code&gt;select&lt;/code&gt; e o &lt;code&gt;insert&lt;/code&gt; existe uma janela —, o banco é o único árbitro confiável. O segundo apanha a violação e desiste sem erro, porque o resultado desejado já aconteceu.&lt;/p&gt;

&lt;p&gt;Entre essas duas há uma terceira camada. O artigo seguinte destrincha as três, por que cada uma pega um caso diferente, e por que remover qualquer uma reabre o buraco.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que eu faria diferente
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Teria começado pelo webhook.&lt;/strong&gt; Não por prever o bug, mas porque PIX e boleto já estavam no roadmap desde o início. Se eu tivesse desenhado para o meio de pagamento mais restritivo em vez do mais conveniente, o fluxo client-side nunca teria existido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teria colocado a constraint antes do código.&lt;/strong&gt; A unicidade no banco não foi a primeira coisa que fiz — foi a última. Devia ter sido a primeira. Constraint de banco é a única garantia que não depende de eu ter pensado em todos os caminhos do código.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teria desconfiado antes do "o código está certo".&lt;/strong&gt; Passei tempo demais relendo uma função correta. A pergunta útil não era &lt;em&gt;"esse código está certo?"&lt;/em&gt;, era &lt;em&gt;"o que acontece se isso rodar duas vezes?"&lt;/em&gt;. Para qualquer operação que mexe com dinheiro, essa deveria ser a primeira pergunta, não a última.&lt;/p&gt;

&lt;h2&gt;
  
  
  A regra
&lt;/h2&gt;

&lt;p&gt;Toda operação que cria, move ou destrói dinheiro precisa de uma origem única e de um identificador que permita reconhecê-la se ela voltar. O cliente nunca pode ser essa origem — não porque o código dele seja ruim, mas porque você não controla quantas vezes ele roda.&lt;/p&gt;

</description>
      <category>postmortem</category>
      <category>webhooks</category>
      <category>idempotencia</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Por que Stripe Connect Express em vez de split manual</title>
      <dc:creator>CESAR EDUARDO STURMER</dc:creator>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/cesarsturmer/por-que-stripe-connect-express-em-vez-de-split-manual-5603</link>
      <guid>https://dev.to/cesarsturmer/por-que-stripe-connect-express-em-vez-de-split-manual-5603</guid>
      <description>&lt;p&gt;Quando você monta o pagamento de um marketplace, a primeira ideia que aparece é quase sempre a mesma: cobrar o valor cheio do comprador, guardar o dinheiro na conta da plataforma e, depois, transferir a parte do vendedor.&lt;/p&gt;

&lt;p&gt;Chamo isso de &lt;strong&gt;split manual&lt;/strong&gt;. Parece simples porque você já sabe fazer as duas pontas: receber um pagamento e mandar uma transferência. O que não é óbvio é tudo que mora no meio.&lt;/p&gt;

&lt;p&gt;Este artigo é sobre por que não fiz isso no surfaai — e o que eu teria assumido se tivesse feito.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que "split manual" realmente exige
&lt;/h2&gt;

&lt;p&gt;No momento em que o dinheiro do provider passa pela sua conta, você deixa de ser uma plataforma de software. Você vira, na prática, um intermediário financeiro. Isso traz uma lista de responsabilidades que não aparecem no diagrama de arquitetura:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custódia.&lt;/strong&gt; O dinheiro do provider está na sua conta, misturado com o seu. Se a empresa tiver qualquer problema de fluxo de caixa, o dinheiro de terceiros está exposto. Separar isso corretamente é um problema contábil e jurídico, não técnico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KYC e KYB.&lt;/strong&gt; Para repassar dinheiro a alguém, você precisa saber quem é essa pessoa. Documento, validação, verificação de titularidade da conta bancária, checagem de listas restritivas. E isso não é "uma vez": precisa ser monitorado e reverificado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconciliação.&lt;/strong&gt; Para cada pagamento, você precisa saber quanto entrou, quanto é seu, quanto é do provider, quanto já foi repassado e quanto falta. Com estorno parcial, chargeback e parcelamento no meio, essa conta fica difícil rápido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing.&lt;/strong&gt; Transfere quando? Na confirmação do pagamento? Depois do serviço prestado? Em D+30? Cada resposta é uma regra de negócio que vira código, e cada regra tem um caso de borda que você vai descobrir em produção.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Falha e retentativa.&lt;/strong&gt; A transferência falhou porque a conta do provider estava errada. E agora? Quem detecta, quem notifica, quem reprocessa, e como você garante que a retentativa não transfere duas vezes.&lt;/p&gt;

&lt;p&gt;Nenhum desses problemas é sobre o seu produto. Todos consomem tempo de engenharia que não vai para o produto.&lt;/p&gt;

&lt;h2&gt;
  
  
  As três portas do Connect
&lt;/h2&gt;

&lt;p&gt;O Stripe Connect resolve isso, mas oferece três tipos de conta, e a escolha importa:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Standard&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Custom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quem faz o onboarding&lt;/td&gt;
&lt;td&gt;o próprio provider, na Stripe&lt;/td&gt;
&lt;td&gt;Stripe, dentro do seu fluxo&lt;/td&gt;
&lt;td&gt;você, via API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard do provider&lt;/td&gt;
&lt;td&gt;completo, da Stripe&lt;/td&gt;
&lt;td&gt;reduzido, da Stripe&lt;/td&gt;
&lt;td&gt;nenhum, você constrói&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Responsabilidade por KYC&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sua&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Esforço de implementação&lt;/td&gt;
&lt;td&gt;baixo&lt;/td&gt;
&lt;td&gt;baixo&lt;/td&gt;
&lt;td&gt;alto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controle da experiência&lt;/td&gt;
&lt;td&gt;baixo&lt;/td&gt;
&lt;td&gt;médio&lt;/td&gt;
&lt;td&gt;total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Standard&lt;/strong&gt; exige que o provider tenha ou crie uma conta Stripe completa. Para um instrutor que quer só receber pelas aulas, é fricção demais — ele abandona no meio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom&lt;/strong&gt; devolve para você exatamente o que eu queria evitar: a responsabilidade de KYC e a construção de toda a interface de onboarding, extrato e saque.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express&lt;/strong&gt; fica no meio: o provider passa por um fluxo de onboarding hospedado pela Stripe, curto e em português, e ganha um painel simples para ver os recebimentos. Eu não vejo nem armazeno documento nenhum. A Stripe assume o KYC.&lt;/p&gt;

&lt;p&gt;Para um produto tocado por uma pessoa, essa divisão é a diferença entre o projeto existir e não existir.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que o código vira
&lt;/h2&gt;

&lt;p&gt;Com Express e destination charges, o repasse inteiro cabe em duas propriedades do PaymentIntent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountInCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;application_fee_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationFeeAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transfer_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stripeAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerId&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;application_fee_amount&lt;/code&gt; é o que fica comigo. &lt;code&gt;transfer_data.destination&lt;/code&gt; é a conta do provider. A Stripe cobra do aluno, retém minha taxa e credita o líquido ao provider — sem nenhuma rotina de repasse minha, sem o dinheiro passar pela minha conta como custódia.&lt;/p&gt;

&lt;p&gt;O artigo seguinte da série detalha essa mecânica e a matemática das taxas.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que eu abri mão
&lt;/h2&gt;

&lt;p&gt;Seria desonesto apresentar isso como escolha sem custo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perdi o controle do timing.&lt;/strong&gt; Com destination charges, o repasse acontece quando o pagamento é capturado. Se eu quisesse segurar o valor até a aula acontecer — um escrow — precisaria de outro desenho, provavelmente separate charges and transfers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fiquei preso à Stripe.&lt;/strong&gt; As contas Connect são da Stripe. Migrar de gateway com providers já onboardados significa refazer o onboarding de todos. É um custo de saída real, e eu já paguei um parecido quando migrei de Asaas para Stripe — o artigo 6 conta essa história.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependo do onboarding deles.&lt;/strong&gt; Se o fluxo da Stripe rejeitar um provider, ou pedir um documento que ele não tem à mão, eu não tenho como contornar. Já aconteceu.&lt;/p&gt;

&lt;p&gt;Foram trade-offs aceitáveis &lt;strong&gt;para este produto&lt;/strong&gt;. Num marketplace onde o dinheiro precisa ficar retido até a entrega, a conta seria outra.&lt;/p&gt;

&lt;h2&gt;
  
  
  A regra que ficou
&lt;/h2&gt;

&lt;p&gt;A decisão de arquitetura mais importante quase nunca é qual ferramenta usar. É &lt;strong&gt;onde traçar a linha entre o que você constrói e o que você delega&lt;/strong&gt; — e, principalmente, quais responsabilidades você aceita junto com o código que escreve.&lt;/p&gt;

&lt;p&gt;Custódia de dinheiro de terceiros e verificação de identidade são responsabilidades caras, contínuas e completamente fora do meu produto. Delegar isso não foi preguiça de engenharia. Foi a escolha de gastar o tempo de engenharia no que o aluno e o provider realmente veem.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>marketplace</category>
      <category>arquitetura</category>
      <category>pagamentos</category>
    </item>
    <item>
      <title>Construindo o surfaai do zero: stack, decisões e o que eu faria diferente</title>
      <dc:creator>CESAR EDUARDO STURMER</dc:creator>
      <pubDate>Wed, 30 Sep 2026 22:33:00 +0000</pubDate>
      <link>https://dev.to/cesarsturmer/construindo-o-surfaai-do-zero-stack-decisoes-e-o-que-eu-faria-diferente-33p5</link>
      <guid>https://dev.to/cesarsturmer/construindo-o-surfaai-do-zero-stack-decisoes-e-o-que-eu-faria-diferente-33p5</guid>
      <description>&lt;p&gt;Depois de alguns anos como engenheiro de software em empresas — arquitetura de frontend, sistemas distribuídos, produtos que outras pessoas definiam — decidi construir algo do zero, sozinho: do primeiro commit até estar rodando em produção, com usuários reais e dinheiro de verdade passando pelo sistema.&lt;/p&gt;

&lt;p&gt;Esse algo é o &lt;strong&gt;surfaai&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Esta é a primeira de uma série de artigos técnicos sobre as decisões que tomei construindo o surfaai. Não é um tutorial de "como fazer X" — é um relato real de arquitetura: os trade-offs que pesei, os erros que cometi, e o que eu faria diferente se começasse hoje. Se você já pensou em sair de só integrar sistemas dos outros para construir o seu, ou só curte ver decisão técnica real — com o porquê, não só o resultado final —, essa série é para você.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por que o surfaai
&lt;/h2&gt;

&lt;p&gt;O surfaai nasceu de um problema simples: conectar alunos a providers numa plataforma que cuidasse de agendamento, pagamento e repasse automático — sem que cada provider precisasse resolver seu próprio checkout, suas taxas e seu split.&lt;/p&gt;

&lt;p&gt;Toda plataforma que conecta duas pontas com dinheiro no meio esbarra cedo ou tarde na mesma pergunta difícil: quem fica responsável pela parte financeira? Deixar cada provider resolver isso por conta própria significa fricção de onboarding, inconsistência de experiência para o aluno e nenhum controle real sobre a operação. Foi esse problema que me fez construir a camada de pagamento dentro do produto, em vez de empurrá-la para fora dele.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + React + TypeScript&lt;/strong&gt; no front e nas rotas de API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind&lt;/strong&gt; para a UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; (Postgres + Auth + RLS) como backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Connect Express&lt;/strong&gt; para pagamentos e repasse aos providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cada peça foi escolhida pensando em um time pequeno mantendo um produto com dinheiro real passando por ele — produtividade de desenvolvimento sem abrir mão de segurança e corretude financeira.&lt;/p&gt;

&lt;h2&gt;
  
  
  O desafio central: marketplace de verdade precisa de split
&lt;/h2&gt;

&lt;p&gt;A parte mais delicada de um marketplace não é o cadastro nem o agendamento — é garantir que o dinheiro do aluno chegue certo ao provider, com a taxa da plataforma descontada corretamente, de forma auditável e sem condições de corrida.&lt;/p&gt;

&lt;p&gt;Isso significou usar &lt;strong&gt;Stripe Connect com destination charges&lt;/strong&gt;. O PaymentIntent é criado na plataforma, com a taxa de aplicação e o destino do repasse apontando para a conta Connect do provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountInCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;application_fee_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationFeeAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transfer_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stripeAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;São duas linhas que carregam muita coisa: o Stripe faz a transferência do líquido para o provider automaticamente assim que o pagamento confirma, e a plataforma nunca custodia o dinheiro de terceiros. Detalho essa arquitetura — e por que não fiz o split na mão — nos próximos dois artigos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uma decisão que valeu a pena: créditos só nascem no webhook
&lt;/h2&gt;

&lt;p&gt;Desde cedo, créditos de compra &lt;strong&gt;nunca são criados no client-side&lt;/strong&gt; — só a partir do evento &lt;code&gt;payment_intent.succeeded&lt;/code&gt; do webhook do Stripe.&lt;/p&gt;

&lt;p&gt;Isso resolve dois problemas de uma vez. O primeiro é duplicação por condição de corrida: o usuário atualiza a página de confirmação duas vezes e ganha crédito dobrado. O segundo é mais sutil — métodos assíncronos como PIX e boleto simplesmente não têm um "momento no front" em que o pagamento confirma. O webhook é o único lugar onde cartão, PIX e boleto convergem para o mesmo caminho.&lt;/p&gt;

&lt;p&gt;Esse é o assunto dos artigos 4 e 5 da série: o incidente real que me ensinou isso, e o padrão que saiu dele.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migração de gateway em produção
&lt;/h2&gt;

&lt;p&gt;O surfaai não nasceu no Stripe. Começou no Asaas, e migrei com usuários ativos e compras acontecendo. Foi a decisão mais arriscada do projeto, e o artigo 6 conta como foi feito sem interromper o fluxo de compra de quem estava usando.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que vem a seguir
&lt;/h2&gt;

&lt;p&gt;Nos próximos artigos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;por que Stripe Connect Express em vez de split manual&lt;/li&gt;
&lt;li&gt;destination charges na prática, com o código e a matemática das taxas&lt;/li&gt;
&lt;li&gt;o bug de crédito duplicado: um postmortem&lt;/li&gt;
&lt;li&gt;idempotência em webhook de pagamento, o padrão completo&lt;/li&gt;
&lt;li&gt;migração de gateway com a plataforma em produção&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Se você constrói produtos com pagamento embutido, ou só curte ver decisão real de arquitetura com os trade-offs e os erros no caminho, essa série é para você.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>stripe</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
