Inspiration
Shoppers scroll endlessly, searching for deals that never pop up. Retailers list items that sit on their shelves for years. They share the same problem, they cannot find a target audience. We wanted to fix both. A shopper should be able to join without paying and leave without penalty. A retailer should be able to find out whether anyone wants a product before they put it up for sale. Both parties should leave the transaction happy, shoppers get a discount and retailers move low-sales products. The current design focuses on furniture but we believe this can be done for all high-cost, low-conversion items. Group buying works well for these kinds of big purchases. A $900 dining table has real margin to give up, and people already wait months for a specific product. This way, retailers get rid of unnecessary inventory and shoppers find products at prices they are happy about.
What it does
You type what you want into one box, something like “walnut dining table for 4, under $800”. An LLM recommends the best matching Pools.
A Pool has one to three Tiers, for example 15 people for $200 off each, or 45 for $250 off. Joining means saving a card, and you aren’t charged. You can leave any time until the Pool locks. When the first Tier fills, every Member is charged at once. Anyone who joins after that is charged on the spot. At the deadline or the stock cap, everyone ends up paying the same final price, and anyone who paid more gets a Tier Refund automatically. If the Pool never hits Tier 1, nobody pays.
Retailers can also list a product as a Hidden Listing. Shoppers never see it, but when a shopper’s Wish matches it and their budget reaches the retailer’s floor price, that counts as a Demand Hit. Once the hits reach the retailer’s threshold, they get an alert and can launch a real Pool, and everyone who matched gets notified. There’s also a demo panel that moves the clock forward and fills Pools with simulated shoppers, since nobody wants to wait 30 days to watch a deadline.
How we built it
The backend is FastAPI with SQLModel on SQLite. The frontend is Next.js with TypeScript and Tailwind. The frontend ran on mocks in the spec’s shapes until the real endpoints existed.
Every change to a Pool’s state goes through one function, evaluate_pool. Routers call it, and nothing else touches pool.status. Time comes from a simulated clock instead of the system clock, which is what lets the demo panel jump to a deadline. Every charge and refund goes through one payments adapter that runs in mock mode or against Stripe test keys, and each attempt is written to a payments table so the order history looks the same either way. Money is integer cents everywhere.
The recommender makes one OpenAI call (gpt-5-nano) with structured JSON output. We give it the Wish, the public Pools, and the Hidden Listings, and it returns matches with scores. The backend then merges in a few regex checks for budget and category, saves the Wish, and creates Demand Hits.
Challenges we ran into
Pool settlement was the hard part. A card can decline at the moment of Lock, so the Pool has to lock anyway and drop just that Member, even if that pushes the count back under Tier 1. A payment provider can go down mid-charge, which leaves Members stuck as pending. A refund can fail halfway through, so the Pool has to stay locked and retry only what’s still owed, without ever refunding someone twice. We spent more time on those cases than on any screen, and evaluate_pool has its own test file for each one.
The recommender was the other problem. The model sometimes ignored an obvious “under $200” or put a coffee table in the wrong category, and a wrong budget means a bad Demand Hit that the retailer then acts on. We ended up parsing budget and category from the text ourselves and taking the tighter of that and what the model returned.
Accomplishments that we’re proud of
The whole lifecycle works end to end: a Wish becomes a Demand Hit, the retailer launches a Pool, a few shoppers join, it locks and charges everyone, and at the deadline the Tier Refunds go out. All of it shows up in a shopper’s order history as separate charge and refund lines. An extension is that it runs against real Stripe test mode.
What we learned
Writing the contract and the vocabulary first was worth the hours it cost. Once “Lock” and “Settlement” meant one thing, most of the arguments about behavior ended in a sentence. We also learned to put all the state changes for something that involves money in one place, because the failure cases (declines, outages, partial refunds) only stay manageable when there’s exactly one function to reason about.
On the AI side, we learned not to trust the model with anything that costs real money downstream. It’s good at “why does this table fit you” and unreliable at “what is this person’s budget”, so the second job went to code.
What’s next for Quorum
Several things in the spec we didn’t get to. The retailer’s tier suggestions (the model proposing Tiers inside the price band based on demand count) and clustering similar Wishes with embeddings are both unbuilt, and the second matters because right now two people asking for a “walnut table” and a “dark wood dining table” only count as the same demand if the model happens to match them to the same listing. We also haven’t deployed anywhere beyond localhost.
The bigger open question is the Demand Threshold. Right now each retailer picks a number, and we don’t know what a good one is. We’d want real retailers to tell us how many people have to ask before they’d commit to stock. Past that: real accounts instead of the user switcher, shipping and returns, and buying more than one unit, none of which we touched.
Log in or sign up for Devpost to join the conversation.