All articles
Zonova Tech 10 min read

We turned custom orders into a bidding marketplace

Commissioning a custom cake or bouquet in Sri Lanka means ringing shops one by one. Here is how we replaced that with an auction, and the design decisions it forced.

EngineeringProduct
Cover image for “We turned custom orders into a bidding marketplace”

Say you want a character cake in Sri Lanka, your daughter’s favourite cartoon, for her birthday on Saturday. You do not go to a website. You go to Facebook. There is a large, genuinely good market of bakers there, and the way you reach them is WhatsApp. Send a reference photo, describe the size, wait for a price.

Then, because you have no idea whether that price is fair, you do it again with another baker. And another. Same photo, same description, three separate conversations, three different prices arriving whenever each baker gets to their phone.

The bakers are not enjoying it either. Every quote is a photo, a back-and-forth about size and date and delivery, and most of them end in nothing. Consultative selling over WhatsApp works, and that is why the market runs on it, but it does not scale past the hours in a day.

We built Zonova Shop to bring occasion gifting into one place. Custom orders were the part that did not fit. You cannot put “a cake shaped like a cricket bat” in a catalogue.

Our first answer was the obvious one, a quote request form. The customer describes what they want, it lands in the shop’s inbox, and the conversation carries on from there. It works, and we kept it for weddings, where hundreds of boxed cake pieces genuinely need a human conversation.

But for everything else it just moved the WhatsApp thread inside our app. The customer still described their idea to one shop and waited. We had digitised the form and left the actual problem completely untouched. You only ever get one price.

So we inverted it. The customer designs what they want, drops a pin on the map, and the shops come to them.

How it works

You build your design in the app, a bouquet or a cake or a printed shirt, then submit it with a quantity, a delivery location and any notes. Nearby shops that can actually make it get notified. They bid. You see the offers come in, pick one, and the winning shop’s price drops into your cart like any other item.

Design it Drop a pin Shops notified Sealed bids Into the cart
A bid request from design to cart.

That is the whole idea. Most of the interesting work was in the rules around it.

Bids are sealed

The first real decision was whether shops could see each other’s offers.

We said no. Open bidding sounds good for the customer, because shops undercut each other and prices fall. But it turns every request into a race to the bottom, and the shops that lose money on it stop bidding. Then there is nobody left to bid. A marketplace that burns its own supply is not a marketplace.

Sealed bids mean each shop quotes what the job is actually worth to them. The customer still gets competition, because nobody knows what the others said.

This is enforced in the database rules, not the interface. A shop can read exactly one bid document per request, the one whose id is its own branch id. There is no endpoint, no query and no client bug that can expose a competitor’s price, because the permission to read it does not exist.

That id choice does something else for free. Because a bid’s document id is the shop’s id, a shop cannot submit two bids on the same request. The second write lands on the same document. One bid per shop, with no uniqueness check anywhere in the code.

Matching is different for cakes than for shirts

We assumed one matching rule would cover everything. It does not, and the reason is physical rather than technical.

A printed t-shirt can go in a courier bag to anywhere in Sri Lanka. A three-tier cake or a fresh bouquet cannot. It has to be driven, and it has to survive the trip. Distance is a soft preference for one and a hard constraint for the other.

So the rules diverged.

Clothing starts with shops within 20 km of the pin. If nobody bids, the radius widens by 20 km every half hour, notifying only the newly reached shops, until either every clothing shop has seen it or the radius covers the island. A quiet request slowly gets louder rather than dying at the first ring.

Cakes and flowers never escalate. Instead, each shop declares its own delivery radius, and a shop is matched only if the customer’s pin falls inside it. The shop decides how far it will travel. We do not widen a ring until someone reluctantly agrees.

Escalation would have been actively wrong here. Widening the search for a fresh bouquet just finds shops that should not be delivering it.

Clothing Cakes and flowers 20 km 40 60 80 in range out of range One ring, widening 20 km every 30 minutes Each shop sets how far it will travel
The customer pin is the same in both. What changes is who counts as nearby.

Requests expire

Every request dies after 48 hours, whether or not anyone bid.

An auction with no deadline is just a form that never gets a reply. The expiry gives the customer a definite answer, even when the answer is that nobody can make this, and it stops shop inboxes filling with stale requests from people who bought elsewhere a week ago.

Bid notifications are also grouped into 30-minute windows. Four shops bidding within a few minutes should be one notification, not four. The fastest way to make someone mute your app is to buzz them once per bid.

What we got wrong

Shops set a delivery radius, and we anchored it to their address. Obvious. Wrong.

A shop’s storefront is not necessarily where its deliveries leave from. A bakery may have a kitchen on the other side of town, and a florist may run deliveries from a depot. Anchoring the radius to the storefront quietly mis-matched exactly the shops that had invested in real delivery capacity, the ones we most wanted bidding.

Where the radius belongs Same shop Storefront Where we anchored it Where deliveries leave from Customer
The customer sits inside the circle that matters and outside the one we were actually using.

The fix was small. Shops now pick a separate origin pin, and matching uses it, falling back to the storefront when unset. The lesson was not small. We had encoded an assumption without ever noticing it was an assumption. A shop is where its address says it is.

There was a smaller one of the same flavour. Submissions were failing Firestore validation because our snapshot cleaner only stripped empty fields at the top level, and bouquet designs nest them several levels down. It only surfaced when we tested the real submit flow with a real design. Unit tests had been passing on tidier data than reality produces.

What we are watching

Every decision above is a bet on how people will behave. Whether shops undercut each other or hold their prices. How quickly the first offer arrives. How often a request finds nobody at all. Those are the questions a design review cannot settle, and the ones we are most interested in.

Some of what is written here will turn out to be wrong in exactly that way. When it does, we will write the follow-up.

If you are building something with this shape, whether that is matching or quoting or marketplace mechanics, the decisions above are the ones worth arguing about early. They are much harder to change once real money is moving through them.

Written by the Zonova Tech team in Sri Lanka. We are a small, senior studio that designs and builds web platforms, mobile apps and cloud systems end to end, including the products we ship ourselves.