Tasas de tarjeta en la mira: qué hace el dev con Bun
Visa, Mastercard y algunos de los bancos más grandes de Estados Unidos están otra vez en el banquillo. El motivo es conocido: las tasas de tarjeta que se le cobran al comercio en cada compra. La nueva demanda dice que esas tarifas son anticompetitivas. Los comercios pagan y no pueden negociar. Al final las paga el consumidor, que compra todo más caro. La noticia salió en ClassAction.org. Parece tema de abogados y economistas, pero le cae directo a quien escribe el checkout. Y como buena parte de mis backends hoy corre en Bun, lo voy a usar en los ejemplos.
Qué pasó con Visa, Mastercard y los bancos
El resumen cabe en tres frases. Los comercios acusan a las marcas de tarjeta y a los bancos emisores de fijar, en la práctica, el valor de la tasa de intercambio. Esa tasa sale del bolsillo del comercio en cada transacción con tarjeta. La acusación dice que las reglas de las marcas le impiden reaccionar. No puede cobrar más a quien paga con tarjeta premium. Tampoco puede empujar al cliente hacia un medio de pago más barato.
No es una pelea nueva. La demanda colectiva más famosa de los comercios estadounidenses contra las marcas empezó en 2005. Desde entonces pasó por un acuerdo millonario, una apelación, un acuerdo rechazado por una jueza y una nueva ronda de negociación. Las cifras son enormes. Estudios del sector estiman que los comercios estadounidenses pagan más de 100 mil millones de dólares al año solo en tasas de tarjeta.
Puede que nada cambie mañana. Un juicio de este tamaño dura años. Pero el debate ya le dice algo útil al dev: la tasa de tarjeta parece fija, y no lo es.
Por qué la tasa de tarjeta es problema del dev, y no solo de finanzas
En Brasil cambia el nombre, pero la lógica es la misma. Allá se habla de MDR, la tasa que la adquirente le descuenta al comercio. Incluye el intercambio del banco emisor, la parte de la marca y el margen de la adquirente. Una venta de R$ 100 con crédito en un solo pago puede dejar entre R$ 2 y R$ 4 en el camino, según el contrato. En cuotas, sube. Si se adelantan los cobros, sube otra vez.
Ahora piensa en quién decide cómo paga el cliente. No es el director financiero. Es el componente de checkout que escribiste un viernes por la tarde.
- ¿Qué medio de pago aparece primero en la pantalla?
- ¿El Pix tiene descuento o está escondido en una pestaña?
- ¿Las cuotas sin interés son la opción por defecto para cualquier monto?
- ¿Sabes cuánto costó cada pedido en tasas, o solo conoces el valor bruto?
Cada una de esas decisiones es código. Y cada una mueve el margen del negocio más que muchas optimizaciones de query que tanto nos gusta hacer.
Tasa que nadie ve es tasa que nadie negocia.
Cómo calcular la tasa de tarjeta por pedido con Bun
El primer paso es dejar de tratar la tasa como algo que "resuelve el gateway". Guarda el costo de cada pedido. Con Bun se puede hacer sin instalar nada más que el propio runtime. Ya trae servidor HTTP y SQLite integrados.
Un detalle que salva vidas: trabaja siempre en centavos, con enteros. Dinero en punto flotante es un bug esperando para convertirse en ticket de soporte.
// fees.ts
type Method = "pix" | "debit" | "credit" | "credit_installments";
// Valores ilustrativos. Usa los de tu 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;
}
Ahora, un endpoint que registra cada venta con la tasa 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 });
},
},
},
});
En producción vas a querer validar el cuerpo de la petición y usar PostgreSQL en lugar de un archivo SQLite. La idea sigue siendo la misma: cada pedido nace con su costo al lado. Después de un mes, una query simple muestra cuánto se fue en tasas:
SELECT method, COUNT(*) AS pedidos, SUM(fee_cents) / 100.0 AS tasa_reales
FROM orders
GROUP BY method
ORDER BY tasa_reales DESC;
Ya vi a un cliente descubrir con una consulta así que las cuotas sin interés en compras de R$ 40 costaban más que el envío. Nadie lo había visto porque el número no existía en ningún lado.
Qué cambia en el checkout cuando ves el costo
Con el dato en la mano, las decisiones de interfaz se vuelven obvias. Y varias están permitidas en Brasil, algo que el comercio estadounidense todavía pelea en los tribunales.
Descuento en Pix. Desde 2017 la ley brasileña permite cobrar precios distintos según el medio de pago. Si el Pix te cuesta un tercio del crédito, puedes devolverle parte de esa diferencia al cliente. En código, es una línea:
const pixPrice = Math.round(amountCents * 0.95); // 5% de descuento en Pix
Cuotas con mínimo. Ofrecer 12 cuotas sin interés en un producto de R$ 30 es generosidad con el dinero ajeno. Define un valor mínimo por cuota y deja la regla en un solo lugar:
const MIN_INSTALLMENT_CENTS = 5000; // R$ 50 por cuota
const maxInstallments = Math.min(12, Math.floor(amountCents / MIN_INSTALLMENT_CENTS)) || 1;
Orden de los botones. Lo que aparece primero se elige más. Destacar el Pix no es un truco sucio de UX, siempre que la tarjeta siga siendo fácil de encontrar. Si escondes la tarjeta, cambias tasa por carrito abandonado. Y ahí la cuenta empeora.
Ruteo entre adquirentes. Si ya tienes volumen, tiene sentido trabajar con dos proveedores. Así mandas cada transacción al más barato para ese tipo de tarjeta. Aquí soy cauteloso: suma complejidad en conciliación, reembolsos y soporte. Solo vale la pena cuando el volumen paga esa cuenta.
¿Bun es el centro de la historia?
No, y prefiero ser honesto. Este código corre en Node con dos o tres dependencias más. Lo que Bun aporta en este caso es menos fricción: bun:sqlite integrado, Bun.serve con rutas, TypeScript sin paso de build y tests con bun test. Para un servicio pequeño de pagos, eso cuenta. También para un script que reprocesa el extracto de la adquirente cada madrugada. Menos dependencias en el camino del dinero significa menos superficie para romperse y menos paquetes que auditar.
Y auditar importa. El código de pagos es el peor lugar posible para una dependencia abandonada o comprometida. Si el runtime ya resuelve HTTP, base de datos local, hash y tests, tienes menos cosas que vigilar en el package.json.
Un test mínimo para la función de tasa ya evita la clásica regresión de redondeo:
// fees.test.ts
import { expect, test } from "bun:test";
import { feeCents } from "./fees";
test("crédito en un pago de R$ 100", () => {
expect(feeCents(10000, "credit")).toBe(299);
});
Mi opinión sobre el juicio y sobre tu checkout
El juicio contra Visa, Mastercard y los bancos probablemente termine en un acuerdo, como los anteriores. Quizás con alguna regla nueva que deje al comercio estadounidense cobrar más por la tarjeta premium o rechazar ciertas tarjetas. Quizás con un pequeño descuento en la tasa por algunos años. No espero una revolución.
Lo que me llevo de la noticia es otra cosa. Si ni los comercios gigantes logran negociar con las marcas, la única palanca que le queda al negocio pequeño está en su propio producto. Mostrar el costo real. Ofrecer la alternativa más barata. Diseñar el checkout pensando en eso. Esa parte es trabajo del dev, y casi nadie la hace.
Así que antes de cambiar de framework de UI por tercera vez en el año, guarda el fee_cents en cada pedido. Apuesto a que esa columna va a generar más conversación en la próxima reunión que cualquier refactor. Si quieres ver cómo suelo armar backends simples como este, échale un vistazo a mis proyectos.
Resumen para LinkedIn
Visa, Mastercard y grandes bancos estadounidenses fueron demandados otra vez, y quien debería prestar atención es quien escribe el checkout. La acusación: tasas de tarjeta anticompetitivas que el comercio no puede negociar. Allá, eso supera los 100 mil millones de dólares al año. En Brasil se llama MDR. Y cuánto paga el negocio lo decide el componente de checkout que escribiste un viernes por la tarde. Ya vi a un cliente descubrir que las cuotas sin interés en compras de R$ 40 costaban más que el envío. Nadie lo veía porque el número no existía en ningún lado. Mi sugerencia: guarda el `fee_cents` en cada pedido. Con Bun se puede hacer con servidor y SQLite integrados, sin instalar nada más. Escribí el paso a paso con código en el blog. Si tu checkout todavía no muestra cuánto paga de tasa cada venta, vale la pena leerlo. #Bun #Pagos #DesarrolloDeSoftware #Ecommerce #TypeScript