Card fees under fire: what devs can do with Bun
Visa, Mastercard and some of the biggest banks in the United States are back in court. The reason is an old one: the card fees merchants pay on every purchase. The new lawsuit says these fees are anticompetitive. Merchants pay them and can't negotiate. In the end the consumer foots the bill, because everything costs more. The news came out on ClassAction.org. It sounds like a topic for lawyers and economists. But it lands right in the lap of whoever writes the checkout. And since most of my backends run on Bun these days, I'll use it for the examples.
What happened with Visa, Mastercard and the banks
The short version fits in three sentences. Merchants accuse the card networks and issuing banks of effectively fixing the interchange fee. That fee comes out of the merchant's pocket on every card transaction. The lawsuit says the networks' rules stop merchants from fighting back, either by charging more to customers who pay with premium cards or by nudging them toward a cheaper payment method.
This isn't a new fight. The best-known class action by US merchants against the card networks started in 2005. Since then it has gone through a multibillion-dollar settlement, appeals, a settlement rejected by a judge and a new round of negotiations. The numbers are huge. Industry surveys estimate that US merchants pay more than 100 billion dollars a year in card fees alone.
Maybe nothing changes tomorrow. A case this big takes years. But the debate already tells devs something useful: card fees look fixed, and they aren't.
Why card fees are a dev problem, not just a finance one
In Brazil the name is different, but the logic is the same. Here we talk about MDR, the fee the acquirer deducts from the merchant. It bundles the issuing bank's interchange, the card network's cut and the acquirer's margin. A R$ 100 single-payment credit sale can lose between R$ 2 and R$ 4 along the way, depending on the contract. With installments, it goes up. With early settlement of receivables, it goes up again.
Now think about who decides how the customer pays. It's not the CFO. It's the checkout component you wrote on a Friday afternoon.
- Which payment method shows up first on the screen?
- Does Pix, Brazil's instant payment system, get a discount, or is it hidden in a tab?
- Are interest-free installments the default for any amount?
- Do you know how much each order cost in fees, or just the gross amount?
Each of these decisions is code. And each one moves the business margin more than a lot of the query optimizations we love doing.
A fee nobody sees is a fee nobody negotiates.
How to calculate the card fee per order with Bun
The first step is to stop treating fees as something "the gateway handles". Store the cost of every order. With Bun you can do this without installing anything beyond the runtime itself, because it ships with a built-in HTTP server and SQLite.
One detail that saves lives: always work in cents, with integers. Money in floating point is a bug waiting to become a support ticket.
// fees.ts
type Method = "pix" | "debit" | "credit" | "credit_installments";
// Illustrative values. Use the ones from your contract.
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;
}
Now an endpoint that records each sale with its estimated fee:
// 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 });
},
},
},
});
In production you'll want to validate the request body and use PostgreSQL instead of a SQLite file. The idea stays the same: every order is born with its cost right next to it. After a month, a simple query shows how much went out the door in fees:
SELECT method, COUNT(*) AS orders, SUM(fee_cents) / 100.0 AS fees_brl
FROM orders
GROUP BY method
ORDER BY fees_brl DESC;
I've seen a client find out with a query like this that interest-free installments on R$ 40 purchases cost more than shipping. Nobody had looked because the number didn't exist anywhere.
What changes in the checkout when you see the cost
With the data in hand, the interface decisions become obvious. Many of them are allowed in Brazil, something US merchants are still fighting for in court.
Pix discount. Since 2017, Brazilian law has allowed different prices depending on the payment method. If Pix costs you a third of what credit does, you can pass part of that difference back to the customer. In code, it's one line:
const pixPrice = Math.round(amountCents * 0.95); // 5% off with Pix
Installments with a floor. Offering 12 interest-free installments on a R$ 30 product is being generous with someone else's money. Set a minimum amount per installment and keep the rule in one place:
const MIN_INSTALLMENT_CENTS = 5000; // R$ 50 per installment
const maxInstallments = Math.min(12, Math.floor(amountCents / MIN_INSTALLMENT_CENTS)) || 1;
Button order. Whatever shows up first gets picked more often. Highlighting Pix isn't a dark pattern, as long as the card option is still easy to find. If you hide the card, you trade fees for abandoned carts, and then the math gets worse.
Routing between acquirers. Once you have volume, it makes sense to have two providers and send each transaction to the cheapest one for that card type. I'm cautious here. This adds complexity in reconciliation, refunds and support. It's only worth it when the volume pays for that.
Is Bun the point of the story?
No, and I'd rather be honest about it. This code runs on Node with two or three extra dependencies. What Bun brings here is less friction: built-in bun:sqlite, Bun.serve with routes, TypeScript without a build step and tests with bun test. For a small payments service, or a script that reprocesses the acquirer's statement every night, that matters. Fewer dependencies in the money path means less surface to break and fewer packages to audit.
And auditing matters. Payment code is the worst possible place for an abandoned or compromised dependency. If the runtime already handles HTTP, a local database, hashing and tests, you have less in your package.json to keep an eye on.
A minimal test for the fee function already prevents the classic rounding regression:
// fees.test.ts
import { expect, test } from "bun:test";
import { feeCents } from "./fees";
test("single-payment credit on R$ 100", () => {
expect(feeCents(10000, "credit")).toBe(299);
});
My take on the lawsuit and on your checkout
The lawsuit against Visa, Mastercard and the banks will probably end in a settlement, like the earlier ones. Maybe with a new rule that lets US merchants charge more for premium cards or refuse certain cards. Maybe with a small fee discount for a few years. I'm not expecting a revolution.
What I take from the news is something else. If even giant merchants can't negotiate with the card networks, the only lever left for a small business is the product itself. Show the real cost, offer the cheaper option and design the checkout with that in mind. That part is dev work, and almost nobody does it.
So before you switch UI frameworks for the third time this year, store fee_cents on every order. I bet that column will spark more conversation at your next meeting than any refactor. If you want to see how I usually build lean backends like this, take a look at my projects.
LinkedIn summary
Visa, Mastercard and major US banks have been sued again. The people who should be paying attention are the ones who write checkout code. The claim: anticompetitive card fees that merchants can't negotiate. In the US, that adds up to more than 100 billion dollars a year. In Brazil we call it MDR. How much of it a business pays depends on the checkout component you wrote on a Friday afternoon. I've seen a client find out that interest-free installments on R$ 40 purchases cost more than shipping. Nobody saw it because the number didn't exist anywhere. My suggestion: store `fee_cents` on every order. With Bun you can do it with the built-in server and SQLite, without installing anything else. I wrote a step-by-step guide with code on my blog. If your checkout still doesn't show how much each sale pays in fees, it's worth a read. #Bun #Payments #SoftwareDevelopment #Ecommerce #TypeScript