FreshRoute: Building a USSD-First Rescue System for Perishable Logistics
June 8, 2026
A pickup truck carrying 45 crates of tomatoes leaves a collection point in Kibaha, bound for Tandika Market in Dar es Salaam. The cargo is worth roughly TZS 1,200,000. The buyer expects delivery by early afternoon.
Then something ordinary and catastrophic happens: a vehicle problem on Morogoro Road. The driver is stuck. The coordinator is on another call. The buyer does not know yet.
By the time everyone coordinates through WhatsApp messages and scattered phone calls, a meaningful share of the load may already be unsellable.
This is the moment we built for.
Not the happy path of tracking dots on a map. The exception moment — delay, breakdown, roadblock — where perishable logistics actually loses money.
The Enemy Is Not a Missing Dashboard
Most small produce aggregators do not fail because they lack software. They fail when one of ten daily shipments goes wrong and coordination collapses into:
Calls to drivers who may not answer immediately.
WhatsApp groups that scroll past the urgent message.
Handwritten driver lists with no structured escalation.
Apologies to buyers after spoilage, not alerts before it.
A month ago, at another hackathon, my team and I built NyumbaConnect — a landlord platform that taught us something we carried into this project: the people you’re building for don’t all have smartphones, and they don’t all have data. But they all have a SIM card.
That lesson became the spine of everything we built next.
The Hackathon Constraint
The event was the Africa’s Talking Open Community Transportation & Logistics Hackathon (East Africa Hub, Dar es Salaam, May 30, 2026). The judges were not looking for a clone of a global courier app. They wanted four things:
A real logistics pain grounded in African operating realities.
Africa’s Talking as core infrastructure, not a notification afterthought.
A believable sandbox demo.
Enough market clarity to imagine a post-hackathon pilot.
We pressure-tested five ideas — proof-of-delivery, port coordination, fleet safety, commuter reporting — and ranked FreshRoute Rescue first. It had the best combination of specific pain, demo drama, feature-phone accessibility, and natural use of SMS, USSD, Voice, Airtime, and WhatsApp.
We built with Cursor as our AI-assisted IDE, running a phased PRD workflow that kept scope disciplined. After NyumbaConnect, we knew what scope creep looked like, and we were determined not to repeat it.
The product promise was simple: meet drivers where they already are. Do not ask informal drivers to become app users. Put the rescue workflow on the network that is already in every pocket.
Architecture: Two Servers, One Story
We split the system into two Node.js services on purpose.
Service Port Role AT Demo Backend 3000 Low-level wrappers for SMS, Voice, Airtime, WhatsApp FreshRoute Rescue API 3001 Shipments, spoilage, rescue logic, USSD menus, audit trail
FreshRoute does not embed the Africa’s Talking SDK directly. Instead, africaTalking.js proxies outbound calls to the AT backend over HTTP. Credentials live in one .env. FreshRoute stays focused on domain rules.
Driver / buyer phones
│
▼
Africa's Talking sandbox
│ USSD webhook
▼
FreshRoute API (:3001)
│ HTTP: /sms, /voice, /airtime, /whatsapp
▼
AT demo backend (:3000)
For demos behind a single ngrok tunnel, FreshRoute also proxies inbound webhooks (SMS, Voice events, WhatsApp) to the AT server on localhost. USSD is the one callback that must hit FreshRoute directly — the generic AT demo menu on port 3000 is not our product.
Storage is in-memory (store.js). No database migrations during a hackathon. Demo reset is one API call. Stack: Express, Axios, dotenv, Morgan, UUID. Deliberately boring. Fast to reason about under time pressure.
This two-server design was not cleverness for its own sake. It was defensive. If the AT demo backend broke during a rehearsal, FreshRoute’s domain logic — the spoilage engine, the USSD state machine, the rescue broadcast — stayed untouched. Everything that could fail independently, did.
USSD as Product Surface
USSD was the most important UX decision in the project.
Many drivers in this segment do not have smartphones or reliable data. A native app was out of scope. SMS alone cannot carry a structured multi-step workflow. USSD gives you a menu on any GSM phone, no installation, no internet.
Africa’s Talking posts form data to POST /api/ussd. The handler returns plain text starting with CON (continue) or END (terminate). Session state is encoded in the text field itself, with menu choices joined by *. For example, 2*3*Kibaha means: main menu option 2 (report late delivery), submenu option 3 (police/roadblock), location input “Kibaha.”
Here is the skeleton of how that handler works:
router.post('/api/ussd', (req, res) => {
const { sessionId, phoneNumber, text } = req.body;
if (!text) {
return res.send(`CON FreshRoute Rescue\\n1. Check in\\n2. Report late delivery\\n3. Report breakdown\\n4. Complete delivery`);
}
const steps = text.split('*');
const choice = steps[0];
if (choice === '2') {
if (steps.length === 1) {
return res.send(`CON Why are you late?\\n1. Traffic\\n2. Vehicle problem\\n3. Police/roadblock\\n4. Loading delay\\n5. Weather/road\\n6. Other`);
}
if (steps.length === 2) {
const reason = steps[1];
return res.send(`CON Enter your current location or nearest landmark:`);
}
if (steps.length === 3) {
const reason = steps[1];
const location = steps[2];
// Match driver to active shipment, calculate spoilage, trigger alerts
return res.send(`END Delay logged. Spoilage risk being calculated. Buyer and dispatcher will be alerted if needed.`);
}
}
// ... other menu branches
});
Rescue offers intercept this normal menu. When a backup driver has a pending offer, they bypass the main menu entirely and see:
CON FreshRoute Rescue Offer
Shipment: Tomatoes to Tandika
1. Accept rescue
2. Decline
We learned the hard way that USSD is unforgiving. If the gateway does not receive valid CON/END text within roughly fifteen seconds — or if ngrok points at the wrong port — the user sees a generic telco error. Our fixes:
Strict response formatting: every branch must return
CONorENDas a complete string, no trailing whitespace, no HTTP headers leaking into the response body.ngrok always on port 3001, never the AT backend port.
phonesMatch()comparing the last nine digits so+254and+255sandbox numbers still match seeded shipments.A
link-sandbox-pairdemo endpoint to bind the driver’s real sandbox SIM before dialing.
USSD is not a fallback feature in this market. It is infrastructure. Treating callback formatting, session parsing, and phone-number matching as first-class engineering — not plumbing — was one of the most important technical decisions we made.
The Spoilage Engine: Honest Math, Not Fake AI
We refused to bolt on “AI risk scoring” for demo credibility. The spoilage engine is rule-based, and every decision is explainable.
The core formula:
spoilageRate = min(95, baseRate[cargo] × exposureHours × reasonMultiplier)
valueAtRisk = cargoValue × (spoilageRate / 100)
Where exposureHours combines how late the shipment already is with the estimated remaining travel time from the driver’s reported zone to the destination.
Cargo sensitivity:
Cargo Base spoilage per hour Fish 12% Milk 10% Leafy vegetables 8% Tomatoes 5% Bananas 4% Grains 1%
Delay reason multipliers:
Reason Multiplier Why Vehicle problem 1.5 High uncertainty, possible long stop Weather/road 1.4 Can worsen and affect remaining travel Breakdown 1.5 Same as vehicle problem, different trigger Police/roadblock 1.2 Delay may clear, timing uncertain Traffic 1.1 Usually moving slowly, not fully stopped Loading delay 1.0 Cargo may not yet be exposed long
Risk levels drive automated actions:
Spoilage Level Action 0–15% Normal Log only 16–35% Watch SMS + WhatsApp to buyer 36–60% High Above + Voice call to dispatcher 61–95% Critical Above + automatic rescue broadcast
Worked example. Tomato shipment, Kibaha to Tandika:
hoursLate = 1.5 (expected 14:00, now 15:30)
remainingHours = 1.58 (Kibaha ≈ 95 minutes to Tandika)
exposureHours = 3.08
reasonMultiplier = 1.5 (vehicle problem)
spoilageRate = 5 × 3.08 × 1.5 = 23.1%
valueAtRisk = TZS 1,200,000 × 0.231 = TZS 277,200
Risk level: Watch.
Same shipment, same delay, but fish instead of tomatoes: 55.4%. High. Dispatch the rescue.
The function contract is straightforward:
function calculateSpoilageRisk(input: {
cargoType: string; cargoValue: number;
expectedArrivalAt: Date; currentTime: Date;
delayReason: DelayReason; reportedLocation: string;
destination: string;
}): {
spoilageRate: number; valueAtRisk: number;
riskLevel: 'normal' | 'watch' | 'high' | 'critical';
recommendedAction: string;
}
Every calculation includes an explanation string — base rate, exposure hours, reason multiplier — so a judge or buyer can understand why the system made the decision it did. That is not a feature. That is the product. A spoilage score with a visible formula earned more trust than a black-box “AI risk” label ever would.
Location is not GPS. The driver types a zone or landmark. We fuzzy-match against a sandbox table mapping places like Kibaha (~95 minutes to Tandika) and Kariakoo (~20 minutes). That is a feature, not a limitation: it matches how coordinators actually talk to drivers on the phone.
Five Africa’s Talking Products, One Audit Trail
The differentiator for hackathon judges is depth of integration, not a single SMS at the end.
Product Use in FreshRoute USSD Driver check-in, delay reporting, breakdown, delivery completion, rescue accept/decline SMS Driver assignment, buyer alerts, rescue broadcast, airtime confirmation Voice Dispatcher escalation on high/critical spoilage Airtime Real credit to driver on completed delivery WhatsApp Parallel buyer alerts, formatted breakdown messages
Every call is logged to GET /api/activity with product type, direction, recipient, status (sent, failed, mocked, skipped), reason code, and shipment ID. When sandbox limits bite — and they do — the log still tells the truth. That panel was our proof-of-work for API usage.
Mock mode (AFRICASTALKING_MOCK=true) lets the full workflow run when credentials or connectivity fail. Useful for rehearsals, but we always aimed to show real sandbox sends when possible. The activity log does not lie: if a call was mocked, it says so. If it failed with a sandbox error, it says so. Honesty in the audit trail was more important than pretending every API call succeeded.
The Five-Minute Demo
Our narrative was intentionally small and dramatic. One shipment. One delay. One rescue.
The coordinator creates a tomato shipment: 45 crates, Kibaha to Tandika, TZS 1,200,000. Driver gets an assignment SMS.
The driver dials USSD. Checks in. Then something goes wrong.
Vehicle problem. Location: Kibaha. The spoilage engine runs. Risk crosses into High. SMS and WhatsApp fire to the buyer. The activity log updates. The dashboard shifts amber to red.
Critical scenario triggers: voice call to dispatcher, SMS broadcast to five backup drivers. A second phone dials USSD — “You have a rescue offer. 1. Accept. 2. Decline.” The backup driver hits 1. Shipment status: Rescue Assigned. Buyer notified.
Delivery completed. Airtime API credits the driver. Confirmation SMS. Done.
Under five minutes. No maps. No app. No GPS. Just a feature phone, five APIs, and a workflow that asks nothing of the driver except what they can already do.
We also built API-only simulation endpoints (/api/demo/simulate) to recover if live USSD stumbled on stage. Hackathon demos need backup plans. NyumbaConnect taught us that.
What Went Wrong
The product worked. Every feature. Every API call. Every USSD menu branch. Before showtime, the five-minute arc ran flawlessly on localhost.

Then we connected to the projector via HDMI.
The cursor lagged. Everything lagged. The localhost dashboard that had been snappy moments earlier now crawled across the screen like it was rendering through molasses. We could not navigate the demo at the pace the narrative required. The USSD simulation worked. The spoilage engine calculated correctly. The activity log showed every API call. But to the judges, it looked like a system that was struggling to function — when in reality, it was a laptop struggling to drive an external display through a bad connection.
We later found out that presentation counted highly in the scoring. The product itself? Solid. The delivery mechanism? A single point of failure we had not even considered.
Other things went wrong in development, same as every hackathon:
USSD is unforgiving. Pointing ngrok at the wrong port meant the callback hit the generic Africa’s Talking demo menu instead of our rescue workflow. Sandbox airtime has sharp edges — amounts like 500 can fail silently. The phonesMatch() problem surfaced at 1am when sandbox numbers formatted differently (+254 versus +255) didn’t match seeded shipment records. We fixed all of it.
What we didn’t fix — what we didn’t even think to fix — was the presentation layer. We treated deployment as optional. We treated the demo machine as reliable. Both assumptions were wrong.
What We Cut (and Why That Helped)
Scope discipline was part of the architecture.
Cut:
Real GPS and maps
Route optimization
Driver mobile app
Payments and wallets
ML spoilage models
Production database and auth
Kept:
End-to-end exception workflow
Transparent risk scoring
Multi-channel escalation
Rescue assignment with USSD accept/decline
API audit trail
Demo reset and scenario loaders
Saying no to a flashy map freed time for USSD reliability and Africa’s Talking integration depth — which is what the judging criteria actually rewarded.
What We Learned
1. Deploy. Always deploy. A working localhost demo is not a working demo. The HDMI cable, the projector, the borrowed laptop — these are not environmental variables. They are the presentation surface, and if they fail, the product fails with them. Next time, we deploy to Vercel or Railway. The URL is the backup plan.
2. USSD is the product surface. Treat callback formatting, session parsing, and phone-number matching as first-class engineering, not plumbing. One malformed CON response and the entire product disappears behind a telco error message.
3. Two servers require clear documentation. We documented callback URLs on GET /api/webhooks and in a team runbook. Pointing USSD at the wrong backend menu burns hours. Write it down before anyone touches ngrok.
4. Sandbox airtime has limits. Amounts like 500 may fail. Parse the AT response. Log failures honestly. The activity log is your audit — don’t lie in it.
5. Explainable beats impressive. A spoilage score with a visible formula earned more trust than a black-box “AI risk” label. Judges want to understand the decision, not admire the model.
6. Build API-first for parallel work. While we hardened USSD and spoilage, Ehud wired the admin dashboard to /api/shipments and /api/activity without blocking the core story. The REST API was the interface between our work streams.
7. AI tools accelerate PRD-to-code loops, but human decisions on scope still matter. Cursor helped us move through phased PRDs quickly. But the choices — what to cut, which APIs to integrate deeply, how to structure the demo narrative — were ours. The tool writes code. The team decides what’s worth building.
What Comes Next
The hackathon build is a prototype, not a company. Our next phase is a two-week concierge pilot: two real produce aggregators, twenty monitored shipments, metrics on check-in rate, buyer alert value, rescue interventions, and airtime-driven reporting.
The hypothesis: coordinators lose enough money on late perishable deliveries that they will adopt a lightweight USSD/SMS exception workflow — especially if drivers are nudged with small airtime rewards and buyers get proactive alerts before spoilage.
If that holds, FreshRoute is a credible candidate for the Africa’s Talking Marketplace Program: not because we built every feature, but because we proved the workflow on the channels people already use.
NyumbaConnect taught us the fundamentals: the landlord is the customer, USSD is not a side quest, demo loops beat feature lists. FreshRoute Rescue took those lessons and shipped tighter — every feature worked, every API fired, every menu branch resolved correctly. We built something that could have won.
We lost on presentation. A lagging HDMI connection made a working system look broken. And at a hackathon, if the judges cannot see it, it does not exist.
That stings. But the project survived the weekend, and so did the lessons. Next time, the demo runs on a deployed URL, not a localhost. Next time, the presentation is part of the architecture, not an afterthought.
Logistics software optimizes for visibility on the good days. FreshRoute Rescue optimizes for the bad hour — when the fish is warming, the tomatoes are softening, and everyone is already on their phones but not speaking the same language.
We did not solve African logistics in a weekend. We showed that a narrow wedge — perishable exception response over USSD — can orchestrate SMS, voice, airtime, and WhatsApp into something that feels operational, not decorative.
We’ll keep building. Next time we deploy. Then we podium.

Built at the Africa’s Talking Transportation & Logistics Hackathon (East Africa Hub, Dar es Salaam, May 2026) by Gadi Josephat Daniel , Ehudi Joha Fugutilo, Junior Jovin, and Bryson Nkinda
