A Razorpay payment webhook handler. The amount field is treated as rupees, but Razorpay's API spec always sends it in paise. Every payment is recorded as 100× the actual amount.
The vulnerable diff
// webhooks/razorpay-payment.ts
razorpayRouter.post("/webhook", verifySignature, async (req, res) => {
const event = JSON.parse(req.body.toString());
if (event.event === "payment.captured") {
const payment = event.payload.payment.entity;
// BUG: payment.amount is in PAISE (smallest unit), not rupees.
// Treating as rupees inflates by 100×.
await markOrderPaid(payment.order_id, payment.amount); // expects rupees
}
res.status(200).end();
});What is wrong
Provider APIs use specific units that the integrating code must respect. Razorpay's `amount` field is always in paise (smallest unit of INR — 1 rupee = 100 paise). Stripe's `amount` is cents. PayPal's `amount.value` is a decimal string of the major unit. Unit conventions are documented but easy to miss. The fix is to capture the convention at the integration boundary, store internally as paise/cents (single canonical unit), and convert only at display.
The attack
Symptom:
User pays ₹500 (= 50,000 paise via Razorpay).
Webhook payload: { amount: 50000, currency: "INR" }
Handler stores: amount_rupees = 50000 // WRONG — that's ₹50,000.
Customer charged ₹500. Internal order shows ₹50,000.
Next webhook (refund partial): customer ends up owed money.Catastrophic at scale.
Mesrai's review comment
mesraipilot · Bot · reviewed 30 sec ago
[mesrai] [code-review] [Logic] [Unit-Mismatch] [critical]
Razorpay's amount field is in paise (1 INR = 100 paise). Treating
as rupees results in 100× overbilling.
Fix:
const amountPaise = payment.amount; // always paise from Razorpay
await markOrderPaid(payment.order_id, amountPaise);
Use paise internally throughout (cluster 8 post 71 covers this).
Convert to rupees only at display:
function formatRupees(paise: number): string {
return (paise / 100).toFixed(2);
}
Add a type guard to make the unit explicit:
type Paise = number & { __brand: "Paise" };
function asPaise(n: number): Paise { return n as Paise; }The fix
// webhooks/razorpay-payment.ts — fixed
razorpayRouter.post("/webhook", verifySignature, async (req, res) => {
const event = JSON.parse(req.body.toString());
if (event.event === "payment.captured") {
const payment = event.payload.payment.entity;
const amountPaise = payment.amount; // explicit: paise
await markOrderPaid(payment.order_id, amountPaise);
}
res.status(200).end();
});Variable named explicitly `amountPaise`. Internal API takes paise. Display layer converts. Type brand (cluster 6 post 53 pattern) makes unit explicit in the type system.
Why human review missed it
Provider unit mismatch is one of the most damaging integration bugs because it's invisible at small dev amounts (₹1 looks like 100 paise in dev logs) but catastrophic at production scale. Mesrai catches provider integrations where the variable name suggests rupees but the source field is paise (Razorpay, Stripe).
Related rules + further reading
Mesrai rule pack: logic/provider-unit-mismatch — flags amount field reads where naming conflicts with provider convention.
Razorpay API docs: 'All amounts are in smallest unit'.
Stripe API docs: similar convention.
Takeaway
Provider sends paise. Store paise. Display rupees. Never mix. Mesrai catches the 100× error.