← BackMar 2023

Your Payment Is My GPS: Using COD Transactions as a Real-Time Rider Location Signal

If you run an e-commerce operation in Nigeria, you know the drill: most of your orders are Cash on Delivery.

A dispatch rider leaves the warehouse with a box full of packages and a list of addresses. Whether the customers actually pick up is another story.

The problem? Customers miss calls. They step out. They forget they ordered something three days ago. By the time the rider arrives, nobody's home - and now you're eating the cost of a return trip.

The obvious fix is real-time rider tracking. Build a custom app where riders can view their assigned packages and customer details, ping their GPS location back to the server, and pipe that into customer notifications. But that means a rider-facing app they actually keep open, a real-time backend to ingest location data, and ongoing API costs. And that's before you account for the riders themselves - most aren't comfortable with custom apps. Getting them to consistently call customers through WhatsApp was already an uphill battle. For a lean operation, the whole approach is overkill.

Then I realized I already had a location signal. I just wasn't using it.

How COD actually works here

In Nigeria, "Cash on Delivery" doesn't usually mean literal cash anymore. When a COD order is created, it gets assigned a unique Paystack virtual bank account number. The customer doesn't hand over naira notes - they make a bank transfer to that account while the rider waits.

The moment that transfer lands, Paystack fires a webhook. The server receives it, credits the payment, and auto-settles the order - status flips from shipped to delivered, no human intervention needed.

That webhook tells me three things: the rider just completed a delivery, they're standing at that customer's address, and it happened right now - not when the rider remembered to update an app, but the instant the money moved.

Riders don't need to tap a button or keep an app open. The customer triggers the signal by paying, not the rider. Short of actual GPS, it's about as reliable as you can get.

Chaining notifications off payments

The system needs a starting point. Mine is the warehouse.

When a batch of orders is marked as shipped and assigned to a rider, I know exactly where that rider is - the warehouse. That's my zero point. I find the customer whose address is nearest to the warehouse and send the first notification: your rider is on the way.

From there, every delivery triggers the next:

  1. Rider leaves the warehouse -> notify the nearest customer.
  2. That customer pays -> Paystack webhook fires, order auto-delivers -> find the next nearest customer from the remaining orders -> notify them.
  3. That customer pays -> repeat.

Each payment kicks off the next notification. The rider doesn't leave until the payment clears - the server sends them an SMS confirming the package has been paid for. So even if a customer takes 10 minutes to transfer, the next customer's notification fires right as the rider is packing up to head their way. The whole route runs on bank transfers that were already happening.

Geocoding

To calculate "nearest," I need coordinates. My orders store addresses as free text - and if you've ever seen a Nigerian delivery address, you know why that's a problem.

"3rd floor, adjacent to [random landmark only someone living around this neighborhood would know], off the express, Ajah"

Google's Geocoding API isn't going to do much with that. Ajah addresses in particular are terrible - half of them reference landmarks that don't exist on any map. So rather than geocoding on the fly, I geocode early and let humans correct the result.

My system already has a delivery confirmation flow: before shipping, the customer gets an SMS & email with a link to confirm their address. This is where I add a map with a draggable pin. The customer sees where Google thinks they are and drags the pin to the right spot. Now I have reliable coordinates before the rider ever leaves.

The coordinates get cached on the order record - two new fields, latitude and longitude. Geocoding happens once, at confirmation time, not at delivery time. At ~$5 per 1,000 requests, the API cost is negligible.

Computing Distance

With coordinates on each order, finding the nearest one is a Haversine calculation:

function haversineKm(
  lat1: number, lon1: number,
  lat2: number, lon2: number
): number {
  const R = 6371;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
    Math.sin(dLon / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

Query all shipped orders for the same rider, compute the distance from the just-delivered address to each, sort, and pick the closest.

Guarding Against False Positives

The obvious failure mode: the rider delivers their last order, and the system notifies some customer 30km away that the rider is "nearby." A few safeguards:

  • Distance threshold. Only send the notification if the nearest order is within 5km. Beyond that, "shortly" is just a lie.
  • Deduplication. Don't notify the same customer twice. If they got a "rider is nearby" SMS 20 minutes ago, don't send another one.
  • Time of day. If it's past 7pm, the rider is probably heading home, not to the next delivery. Skip the notification.

There's also the case where a customer confirms an address but drags the pin to the wrong spot entirely - I've seen pins end up in the ocean. I don't have a great fix for that beyond clamping coordinates to Lagos bounds and flagging outliers for manual review.

Architecture

The implementation touches three parts of the system:

  1. Schema - Two new nullable fields on the order model, plus coordinates for each warehouse:

latitude Decimal?
longitude Decimal?

  1. Delivery confirmation flow - When the customer confirms their address, geocode it and store coordinates. Render a map with a draggable pin so they can correct the result.

  2. Shipping trigger - When orders are assigned to a rider and marked as shipped, find the customer nearest to the warehouse and send the first "your rider is on the way" SMS. This kicks off the chain.

  3. Payment webhook handler - The flow is already automated: Paystack webhook fires -> payment credited -> order auto-delivers. After that auto-delivery, query the rider's remaining shipped orders, compute distances, and enqueue an SMS to the nearest customer. The notification fires within seconds of the bank transfer - fast enough that the rider is likely still at the previous address packing up.

The whole thing runs on a few database fields, a geocoding API call, and a well-timed text message. No new infrastructure beyond what was already there.

What I'd do differently

I was close to building a full rider tracking app before landing on this. Would have been weeks of work for something the riders would have ignored anyway.

The approach has real gaps - the late payment issue, bad geocodes, the fact that it only works for COD orders at all. But it covers the majority case with almost no operational overhead, and that's been enough.

I was overthinking it. The data was already there.