Inspiration

Walk into any Indian mandi and you will find two prices for the same potatoes. There is the number on the board, and there is the number you get if you have the time, the language and the nerve to haggle. Most buyers pay the board. Most stalls, by nightfall, dump whatever is left at whatever they can get. A stall that opens at ₹40/kg is selling at ₹25 by evening, and some of the stock still goes home unsold.

That is money left on the table on both sides, and it is left there because haggling is expensive. It costs time, it costs social energy, and it only works for people who are good at it. We wanted to know what happens if both sides get an agent: a buyer's agent that bargains every item in the basket at once, and a stall's agent that holds the shopkeeper's floor while they serve the next customer. And we wanted the result to be something you could trust without trusting either agent, which is where the proof of a fair deal came from.

What it does

MolTol is a bazaar where agents haggle in the open and PayPal settles the deal in one tap.

For the shopkeeper. Simple mode is built for a busy stall: a handful of big buttons, voice prompts, and a camera that recognises what is in the crate. The shopkeeper sets today's price and a floor, the lowest they will ever accept, opens the dukaan, and pins the stall on the map. From then on their agent negotiates for them and never goes below the floor. A Modern mode adds stock, a ledger, CSV import and a deals feed for shopkeepers who want the full dashboard.

For the buyer. Nearby stalls appear on a live map. The buyer walks up (literally: within 35 metres the app offers to "enter the stall"), builds a basket, picks a haggle style from Quick Deal to Hard Bargain, and lets the agents go. Each item gets its own lane, all running in parallel, with the back-and-forth visible as chat. At any moment the buyer can take over a lane, counter, or accept.

The proof. Before the first offer, the buyer's limit and the stall's floor are sealed as SHA-256 commitments. After the deal is agreed and paid, both are revealed and checked: the agreed price never crossed either line. The receipt carries the hash, so "fair" is something you can verify rather than something you were told.

The payment. One PayPal Checkout approval (sandbox) captures the whole basket. The PayPal window closes itself and hands control back to the tab you started in, which then prints the receipt and the proof.

How we built it

The frontend is Astro 5 with React 19 islands. Every screen that needs state uses small zustand stores persisted to IndexedDB, so a stall or a half-finished haggle survives a reload and the app installs as a PWA. The buyer's live bazaar is driven by a Server-Sent Events stream per basket: each negotiation turn, agreement, payment status and proof reveal arrives as an event and is folded into the lane state by a single reducer, which is what lets the same stream carry the UI straight through checkout.

The backend is a Fastify API in TypeScript on top of Postgres, with a worker pool that runs the negotiation agents. Each lane is an LLM-backed negotiation with a persona (the shopkeeper picks a charmer, a stoic or a storyteller), a round budget set by the buyer's haggle style, and hard rails from the sealed limits. PayPal Checkout is integrated server-side: the API creates the order, the buyer approves it in a popup, and a capture plus webhook path confirms the payment and triggers the proof reveal. Email OTP handles signup, so accounts are real.

The vendor's voice comes from Cartesia text-to-speech, the camera path uses a vision model to turn a photo of a crate into a product suggestion, and shop discovery uses real GPS with a Nominatim fallback for people who would rather type a place.

The frontend ships on Vercel and the API runs on AWS behind Caddy. The demo video itself was produced the same way the product was built: a Playwright script drives the real deployed app end to end, the browser tab is recorded through the DevTools protocol, and HyperFrames assembles the footage with a Cartesia voiceover and the score.

Challenges we ran into

State that has to survive the payment. The basket stream stays open through checkout, which is great until a later event quietly overwrote the agreed price. Our first receipts showed ₹0 paid. The fix was small (keep the agreed price once it is set rather than gating on the "agreed" phase) but finding it meant tracing the whole event flow from the worker to the reducer to the receipt.

Coming back from PayPal. PayPal redirects the browser to a return URL. Opened in a new tab, that return landed the buyer in a second copy of the app with duplicated state. We moved approval into a popup that is opened synchronously on the click (so popup blockers allow it), had it report back over a BroadcastChannel, and close itself, with the original tab finishing the capture. A same-tab redirect remains as the fallback.

Hydration races. With everything persisted asynchronously in IndexedDB, a page can render before the session exists. We added an explicit "hydrated" gate for the checkout and a tiny synchronous auth hint so the pre-paint redirect to login behaves on reload, bookmarks and the back button.

Designing for a stall, not a desk. Simple mode went through several rounds before the buttons were big enough, the copy was short enough, and the voice said the right thing at the right moment. Every extra option we removed made it better.

Making it demo-able. Real OTP emails, real GPS gating, real PayPal popups and real LLM latency are all correct behaviour and all hostile to a two-minute video. Driving the production app with Playwright, injecting sessions the way the login form does, and recording the tab rather than the screen is what finally made the take repeatable.

What we learned

Negotiation is a protocol problem before it is a prompt problem. Once the limits are sealed and the rails are enforced by the server, the agents can be playful and still be safe. We also learned that proof is a product feature: the moment both numbers are revealed on the receipt, the whole experience feels different for the buyer and the shopkeeper.

On the engineering side, the biggest lesson was to treat the event stream as the source of truth and keep every derived number, from "saved ₹12" to the ledger, computed from the server's order rather than the client's memory.

What's next

Real settlement beyond the sandbox, UPI alongside PayPal, group baskets where a family's agents negotiate together, and ledger insights that show a shopkeeper how individual haggling compares to blanket end-of-day discounting over a month. The mandi math on our landing page is illustrative today; we want it to be their numbers.

Built With

Share this project:

Updates

Submission history