By Bonaventure Ogeto|

Handling Money in Code: Why KES Amounts Should Never Be Floats

Store money as the smallest currency unit, integer cents or whole shillings, never as a float. Floating point arithmetic introduces invisible rounding errors that compound across transactions. A Daraja STK Push that sends 99.99999999 instead of 100 will fail silently or charge the wrong amount.

The classic floating point money bug

Open a JavaScript console and type 0.1 + 0.2. You get 0.30000000000000004, not 0.3.

This is not a JavaScript bug. It is how every programming language stores decimal numbers in binary (IEEE 754 floating point). Numbers like 0.1 and 0.2 cannot be represented exactly in binary, just like 1/3 cannot be represented exactly in decimal. The tiny errors are invisible for most calculations, but they compound when you add up money.

// JavaScript
console.log(0.1 + 0.2);          // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);  // false

// Now imagine this in a real transaction:
let balance = 1000.00;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
balance -= 99.99;
console.log(balance);
// Expected: 0.10
// Actual:   0.09999999999999787
# Python
print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)   # False

# After many transactions:
balance = 1000.00
for _ in range(10):
    balance -= 99.99
print(balance)
# 0.09999999999999787

The difference is fractions of a cent, but it matters. If your code checks if (balance >= requiredAmount) and the balance is 0.0001 KES short because of floating point drift, the check fails. A customer who has enough money gets told they do not.

How this bites M-Pesa integrations

The Daraja API expects the Amount field to be a whole number. M-Pesa does not handle fractional KES. Here is what goes wrong when you use floats:

Scenario 1: rounding error in the amount

// A customer orders two items
const item1 = 149.99;  // stored as float
const item2 = 350.01;  // stored as float
const total = item1 + item2;
console.log(total);  // 500.00000000000006

// You send this to Daraja
const payload = {
  Amount: total,  // 500.00000000000006, not 500
  // ...
};

Daraja might reject this. Or it might silently truncate to 500. Or it might round to 500. The behavior depends on the API version and endpoint, and relying on any specific rounding behavior is a bug in your code.

Scenario 2: reconciliation mismatch

// Your database stores the order total as a float
const orderTotal = 1499.99;

// The callback from M-Pesa reports the amount as an integer
const callbackAmount = 1500;  // M-Pesa rounded up

// Your reconciliation check fails
if (callbackAmount !== orderTotal) {
  // MISMATCH! The payment does not match the order.
  // But the customer actually paid the right amount.
}

This is a real pattern that has caused confusion in production systems. The fix is simple: decide on a single representation (whole KES as integers) and use it everywhere.

The fix: store amounts as integers

The rule is simple: store all money values as integers representing the smallest unit of the currency. For KES, that means whole shillings (M-Pesa does not deal in cents). For USD or EUR, store cents.

TypeScript:

// BAD: float amounts
const price = 149.99;
const quantity = 3;
const total = price * quantity; // 449.96999999999997

// GOOD: integer amounts (whole KES)
const priceKES = 150;  // KES 150
const quantityInt = 3;
const totalKES = priceKES * quantityInt; // 450, exactly

// For display
function formatKES(amount: number): string {
  return `KES ${amount.toLocaleString()}`;
}
console.log(formatKES(totalKES)); // "KES 450"

// For Daraja STK Push
const stkPayload = {
  Amount: totalKES,  // clean integer, no rounding needed
};

Python:

# BAD: float amounts
price = 149.99
total = price * 3  # 449.96999999999997

# GOOD: integer amounts
price_kes = 150
total_kes = price_kes * 3  # 450, exactly

# For display
def format_kes(amount: int) -> str:
    return f"KES {amount:,}"

print(format_kes(total_kes))  # "KES 450"

# For APIs that need cents (USD, etc.)
def dollars_to_cents(dollars: float) -> int:
    """Convert a display price to cents for storage."""
    return round(dollars * 100)

# Reverse for display
def cents_to_dollars(cents: int) -> str:
    return f"${cents / 100:.2f}"

The key practices:

  • Database columns: use INTEGER or BIGINT for money columns, never FLOAT, DOUBLE, or REAL. If your ORM or schema uses DECIMAL, that is also safe but integer is simpler for KES.
  • API responses: accept money as integers in your API. If a frontend sends "amount": 150.50, reject it or round immediately at the entry point.
  • Calculations: do all arithmetic on integers. Apply discounts and taxes as integer operations. Round at each step, not just at the end.
  • Display: convert to a formatted string only at the moment of display. The formatKES function above is the only place a human-readable amount should be created.

Database schema patterns

Here is how to set up your database columns correctly.

PostgreSQL:

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_phone TEXT NOT NULL,
  amount_kes INTEGER NOT NULL,  -- whole KES, never FLOAT
  status TEXT NOT NULL DEFAULT 'pending',
  mpesa_receipt TEXT,
  checkout_request_id TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- For systems that need sub-shilling precision (rare in Kenya)
CREATE TABLE usd_transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  amount_cents INTEGER NOT NULL,  -- store cents, display dollars
  currency TEXT NOT NULL DEFAULT 'USD'
);

Supabase / JavaScript ORM:

// When inserting
await supabase.from('orders').insert({
  customer_phone: '254712345678',
  amount_kes: 1500,  // integer
  status: 'pending',
});

// When reading and displaying
const { data } = await supabase
  .from('orders')
  .select('amount_kes')
  .eq('id', orderId)
  .single();

const display = `KES ${data.amount_kes.toLocaleString()}`;

If you are inheriting a codebase that already stores money as floats, migrate carefully. Write a migration script that rounds each value to the nearest whole shilling and converts the column type. Test the migration against a copy of production data before running it live.

When to use a money library

For straightforward KES-only applications (most Kenyan startups), storing integers and formatting at display time is sufficient. You do not need a library.

Consider a money library when:

  • Your app handles multiple currencies (KES, USD, EUR, UGX)
  • You need currency conversion with proper rounding
  • You handle sub-unit arithmetic (cents, fractions)
  • You need to split amounts and handle remainders (e.g., splitting a bill of KES 100 three ways: 34 + 33 + 33)

JavaScript/TypeScript options:

  • dinero.js: well-maintained, supports multiple currencies and subdivision
  • currency.js: simpler API, good for single-currency apps

Python options:

  • py-moneyed: money and currency handling with proper arithmetic
  • decimal.Decimal: built into Python, avoids float issues but requires discipline to use consistently
# Python: using Decimal for precise arithmetic
from decimal import Decimal, ROUND_HALF_UP

price = Decimal("149.99")
quantity = Decimal("3")
total = price * quantity  # Decimal("449.97"), exact

# Round to whole KES for M-Pesa
kes_amount = int(
    total.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
)
print(kes_amount)  # 450

The simplest and most reliable approach for Kenyan M-Pesa integrations: store everything as whole KES integers. No library needed. No rounding surprises.

Frequently Asked Questions

Does KES have cents?
Officially, 1 KES equals 100 cents. But in practice, M-Pesa and virtually all Kenyan digital payment systems operate in whole shillings only. You cannot send KES 10.50 via M-Pesa. Store KES amounts as whole integers unless your system specifically needs sub-shilling precision (extremely rare in Kenyan applications).
What about libraries like dinero.js or python-money?
Money libraries enforce integer storage and safe arithmetic internally. They are a good choice for apps that handle multiple currencies or complex rounding rules. For a KES-only app with M-Pesa integration, plain integers are simpler and just as correct.
Will Daraja reject a float amount?
Daraja expects the Amount field to be a number without decimal places. A value like 100.50 may be silently truncated to 100, rounded to 101, or cause a validation error depending on the endpoint and API version. Do not rely on any specific behavior. Send a clean integer every time.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.

See Programs

Also available: M-Pesa Integration course