There's a post on this blog called Why Your Transfer Is ₦4,317. It explained why the amount you sent us was deliberately not the price on the pricing page.

That mechanism is gone. Your transfer is now the price — ₦4,000 — or anything above it. This is why we built the old one, why we took it out, and what we do instead.

On data: I don't know how many transfers arrive with an empty narration. I've never measured it, and I suspect it varies by bank. Everything below is the mechanism as it stands, including the cases where it doesn't work.

The problem is identity, not money

Two customers sign up on the same Tuesday. Both are asked to pay ₦4,000. Both transfer ₦4,000.

Your statement now shows two identical lines. If the narrations are blank, or if both customers are named A. Okafor, nothing in the payment tells you which line belongs to which account.

You cannot answer that from the money. Only a human can, one at a time, in a chat, forever.

That's the actual problem. Not whether the transfer arrives — it arrives. A flat price produces identical payments, and identical payments are indistinguishable.

So we stopped making them identical.

The amount was the identity

Every account that owed ₦4,000 was given a slightly different number to transfer: always a whole naira, always just above the price. No two pending accounts held the same one.

When the payment landed, we looked at the amount first. If exactly one pending account was waiting on that number, the match was made and the account activated. No reference field. No email typed into a narration. Nothing beyond transferring the number we showed them.

It worked when the narration was blank, which is the case it existed for.

Two things ended it.

The first was the objection we could never fully answer. A customer sees ₦4,000 on the pricing page, opens their bank app, and is asked to send ₦4,317. Those are not the same number. The reasonable reading is that someone is overcharging them by a small, oddly specific amount — small enough that complaining feels disproportionate, specific enough to look deliberate. We wrote the copy that explained it, on the pricing page and everywhere else we could put it, and it helped. But it was explaining a friction we had chosen to create.

The second was arithmetic. The amounts were drawn from a fixed band just above the price — 499 of them — and each one belongs to an account that hasn't paid yet. The limit was never how many people use Zest. It was how many people can be waiting to pay at the same moment. We were nowhere near that ceiling when we started, and we would have hit it. A payment mechanism that needs re-plumbing at 500 concurrent signups isn't a payment mechanism; it's a deadline.

The name was on the transaction the whole time

Here's the part I find slightly embarrassing.

A bank transfer carries the sender's name. It is on the row. The customer did not type it, cannot leave it blank, and cannot put an email address in it the way they can in a narration field. It arrives whether or not anything else does.

We had been reading a different column.

The narration is the field the customer fills in or doesn't — that's where references go missing, and that's what the whole amount scheme was built to survive. The sender name is the field the bank fills in. We engineered around the optional one and ignored the required one.

What we match on now

The amount went back to being the amount.

A payment activates when two things are true:

It's at least ₦4,000. That's a floor, not an identifier. It tells us the payment covers the price. It tells us nothing about who sent it, and we no longer pretend otherwise.

The sender's name matches the account. We check both the personal name and the business name, because people pay from either.

Matching the name is deliberately loose, because banks do not hand you clean data. They truncate names, drop middle names, and concatenate a surname onto the next word. So we don't compare strings — we compare words. Everything is lowercased and stripped to bare words first. A word matches if it's identical to the expected one, or if one is a prefix of the other and the shorter piece is at least five characters, so OKAFORJOHN matches OKAFOR while A can never match Adaeze.

Then it has to hold in one direction all the way through: every word we expect appears in what arrived, or every word that arrived appears in what we expect. That's what lets a dropped middle name still land.

One shared word is enough if it's at least six characters. Otherwise we need two.

There's one layer left over from the old design, and it's invisible. Accounts created before the change still have their unique amount on file, and if a transfer matches one exactly, that wins. Nobody is shown those numbers any more — but someone who was sent ₦4,317 in August can still send ₦4,317 and it will work. We'd rather carry a branch of matching that almost never fires than tell somebody their already-sent transfer doesn't count.

What actually goes wrong

Someone pays from an account that isn't theirs. A spouse, a business account in the company's name, a friend doing them a favour. The sender name doesn't match the account name, nothing activates, and a person looks at it. This is now the most common reason a payment waits, and the billing page says so plainly: transfer from the bank account in your name.

Two accounts share a name. Our candidate pool is every account that hasn't paid — which includes everyone still on a trial, some of whom will never upgrade. Two Chikezie Okafoors is enough to stop the match, and the system will flag both rather than pick one. I'd rather name this than imply it's rare: the A. Okafor problem did not go away, it moved. Names have more distinguishable parts than a ₦4,000 amount does, but this is where it lives now.

The name arrives too short to match. Banks do truncate, and if all that survives is a fragment under six characters with nothing else beside it, we refuse rather than guess. It goes to review.

The transfer is under ₦4,000. Not a matching failure — a wrong amount. It stays unmatched until the difference is sent or we refund it.

None of this is solved. It's managed. There's a human in the loop for everything the machine isn't sure about, and I think that's the right shape for a payment system at this size.

What this isn't

This isn't a claim that payment matching is solved. It's the description of a second decision — put the identity in the name, keep the amount as a floor — and the reasons the first one didn't survive.

It also isn't an argument against payment gateways. If you're taking cards, use a gateway. The point is narrower: for customers transferring directly to a bank account, the identification problem doesn't disappear because you'd prefer it did. We cover the other half of this in how to accept bank transfer payments.

And it isn't a claim that we got it right the first time. The amount scheme was a reasonable answer to a real problem at a small scale, built in a hurry by someone who hadn't looked closely enough at which field was optional. The honest summary is that we over-built it: we encoded identity in the one value every customer shares, when the value we needed was already populated on every row.

The bigger thing

Most software built for this market starts from an assumption made somewhere else: that payments arrive with a reference, that the reference is reliable, and that the customer will attach it because an interface asked them to.

The correction isn't to ask harder. It's to look at what the infrastructure already carries and use that — even if the field is one you didn't design, even if matching on it means accepting that you'll be wrong sometimes and putting a person on every case where you are.

₦4,317 was a name for an account, spelled in naira. It turns out every transfer already arrived with a real one.


If you've ever stared at a bank statement trying to work out which transfer was which, you already understand the problem. You can see how Zest handles the rest at zestbook.org.ng.