← Back to the project on the home page

ZUNELI
A matching platform for VTC drivers and vehicle owners in Dakar. It connects people and nothing more: no commission, no guarantee of any deal.
- Sole designer and engineer

The scoring engine, in your hands
ZUNELIWhat is real?
Simulation built on the code of ZUNELI, fictional data.
- The score engine (four criteria, fixed weights), the order of the list (subscribed profiles first, then the score) and the phone-number masking come from ZUNELI's code, with their behaviour unchanged. The score gauge is copied as is.
- The eight driver profiles, the areas and the owner's listing are invented. No real person and no phone number.
- The score lab screen and its “how it is calculated” panel are written for this site. ZUNELI has no such screen.

ZUNELI
The demo loads only when you ask for it.
The problem
An owner in Dakar wants to entrust a car. A driver wants to earn from one. Each has to trust a stranger, and nothing tells them whether the arrangement suits them. ZUNELI puts the two sides in touch and says why a profile fits. It takes no commission and guarantees no deal.
The search chain
Four stages, always in this order. The request: the owner's listing and the filters. The candidates: the driver profiles that pass the filters. The score: four fixed criteria, each a yes or a no. The ranking: subscribed profiles first, then the score orders each group. Change the request in the demo and watch the list move.
A score with its reason
The score comes from four fixed criteria: payment, area, availability, and whether the other party is verified. They do not all count the same: payment and verification weigh the most, area comes next, availability least. Under "Pourquoi ce match ?" each card lists which criteria matched. The exact weights are not published. The compared fields are closed lists, never free text.
A phone number stays masked
A number is never shown in the clear, whatever the plan. The rule lives in one function that every screen calls. Type any made-up number in the demo and see what a card would show. Nothing you type leaves your browser.
Under the hood
The masking rule: four digits stay visible, the rest becomes stars.
export function maskPhone(phone: string): string { const country = AVAILABLE_COUNTRIES.find((c) => phone.startsWith(c.dialCode)); if (!country) { const visible = phone.slice(0, 4); const masked = "*".repeat(Math.max(phone.length - 4, 0)); return `${visible}${masked}`; } const local = phone.slice(country.dialCode.length); const visible = local.slice(0, 4); const masked = "*".repeat(Math.max(local.length - 4, 0)); return `${country.dialCode} ${visible}${masked}`;}Try it yourself
Change the owner's area and payment, filter on verified drivers, open "How it is calculated" on any profile. Everything runs in your browser on made-up profiles. What happens after a match is not simulated: accepting a proposal is a single step in the real code, shown below.
Under the hood
Accepting a proposal happens once: the status only moves if it is still pending, so two simultaneous acceptances cannot both go through.
await prisma.$transaction(async (tx) => { const updated = await tx.proposal.updateMany({ where: { id: proposalId, status: "PENDING" }, data: { status: "ACCEPTED" }, }); if (updated.count === 0) { throw new Error("Cette proposition a deja ete traitee."); } const existingCollaboration = await tx.collaboration.findUnique({ where: { proposalId } }); if (existingCollaboration) { throw new Error("Une collaboration existe deja pour cette proposition."); } // Anti-survente FleetOffer (roadmap Phase 5, 2026-09-18) : impossible // pour un Vehicle individuel (un seul exemplaire par definition), mais // une FleetOffer peut avoir plusieurs Proposal PENDING simultanees sur // des Match differents — verifie qu'il reste au moins une unite libre // au moment precis de l'acceptation (dans la transaction, pour eviter // une course entre deux acceptations concurrentes). if (proposal.fleetOfferId) { const activeCount = await tx.collaboration.count({ where: { isActive: true, proposal: { fleetOfferId: proposal.fleetOfferId } }, }); const fleetOffer = await tx.fleetOffer.findUniqueOrThrow({ where: { id: proposal.fleetOfferId } }); if (activeCount >= fleetOffer.totalUnits) { throw new Error("Aucune unite disponible sur cette offre groupee : toutes les unites sont deja affectees a une collaboration active."); } }
What you can do here
- Change the owner's area and daily payment and watch eight made-up drivers re-rank
- Read the reason behind each rank, in words
- Filter on verified or subscribed profiles
- Open a conversation and try the masked phone number
- Switch between light and dark mode
Stack
Next.js 16 · React 19 · TypeScript · Tailwind CSS v4 · Prisma 7 · Neon Postgres · NextAuth v5 · Cloudflare R2 · Vercel
Context
In Dakar, an owner who wants to entrust a vehicle and a driver who needs a car mostly find each other through word of mouth and messaging apps. ZUNELI is a platform that puts the two sides in touch. V1 is delivered and runs as a private beta. It takes no commission and guarantees no transaction: its job ends where the conversation between two people begins.
The problem
Each side has to trust a stranger. The owner hands over a vehicle, the driver stakes their income on it. Nothing lets either of them verify the other, or tell whether the proposed arrangement suits them. ZUNELI is designed around verified profiles, a compatibility score that can be read at a glance, and phone numbers that never circulate in the clear.
Constraints
- The Dakar market, with mostly mobile use
- Trust between strangers: a vehicle is handed over, a livelihood is committed
- Phone numbers are sensitive data
- Sign-in by phone number and one-time code, with SMS delivery of the codes not yet connected
What I built
- Sign-up and profiles for drivers and for vehicle owners, in private beta
- Discovery as a list, with a compatibility score on each profile. In the list of drivers shown to owners, subscribed profiles come first and the score orders them within each group
- In-app chat between matched profiles
- Proposals, then collaborations, then reviews once a collaboration ends
- Document verification, reviewed from an admin page
- Countries and trades stored as database tables, so new ones can be added without changing the code
Key decisions
A score anyone can read
The compatibility score rests on four fixed criteria: the payment, the area, availability and whether the other party is verified. They are not equally important, and the exact weights are not published. These are plain, fixed rules. The fields being compared are closed lists, never free text, so two profiles can always be compared the same way. Profile completeness and vehicle quality stay out of the score and only break ties, so the number remains something you can explain to a driver or an owner. The score is not the whole ordering: in the list of drivers shown to owners, subscribed profiles are listed first and the score orders them within each group.
A score that folds profile completeness and vehicle quality in. It would have made the number harder to explain.
Phone numbers are never shown in the clear
A phone number is sensitive, so ZUNELI never displays one, not even to a subscriber. The connection between the two sides goes through the platform.
Unlocking numbers for premium subscribers. Even for them, the connection stays on the platform.
Trust comes from verification, and the badges stay separate
Verification documents go to a private Cloudflare R2 bucket and can only be opened through a signed link that expires after five minutes. They are validated from an admin page. Public photos live in a second, separate bucket, and the 5 MB size limit is checked again on the server. The "Verified" badge and the "Complete profile" badge are always shown apart.
A single badge for both. A complete profile is not a verified one, and mixing them would have told users something untrue.