Atomic Betting
September 2024 → October 20245 min read

The idea came from a joke in class: we thought it would be funny to bet on who’d show up late. I figured I might as well build it for real, to experiment and learn. It’s my first personal project with real data modelling and full authentication.
Atomic Betting is an app for betting with friends or coworkers on just about anything, like how long Monday’s meeting will last or who’s bringing croissants on Friday. You create a team, invite the others, and everyone bets Atomic Coins, a play currency. There’s never any real money involved. The site isn’t online anymore, but the code is public on GitHub.
Odds that follow the stakes
A bet is a question, a few possible answers, an opening and a closing date, and a minimum and maximum stake. Everyone picks an answer and stakes coins on it. You can add more coins to the same answer later, but you can’t change your mind.
The odds are the technical part I’m happiest with. They aren’t set in advance, they depend on what people stake. That’s how pari-mutuel betting works. An answer’s odds are the total staked on the bet divided by what was staked on that answer. The more an answer is backed, the lower its odds. In the screenshot, 385 coins are in play, 250 of them on “Plus d’une heure” (over an hour). Its odds are 1.54, while “Moins de 30 minutes” (under 30 minutes), backed far less, is at 6.42.

Once the bet is over, a team lead picks the right answer. Each winner gets their stake times the final odds, rounded down. In the end, the winners split the pot, each according to their stake.
An answer nobody has backed yet would have infinite odds. I give it the number of answers instead, as if they all had the same chance.
The same function computes the odds shown in the browser and the winnings paid out by the server. It’s also the only part I covered with tests, using Vitest.
A balance and a ledger
Stakes and winnings go through everyone’s balance. A member has one balance per team, stored with their membership, and I read it as is to render a page, without recomputing anything. Alongside it, stakes, winnings and rewards are written to a ledger.
The risk is that the two end up disagreeing, for example if an operation stops halfway. So placing a bet writes the ledger line and the new balance in one go, inside a Prisma transaction. Either both go through, or neither does.
I didn’t get this ledger right the first time. At first, the table only kept stakes, then winnings came in with a type. When I added the daily reward, a coin movement no longer necessarily had a bet behind it. I replaced the table with a more general ledger, where the bet and the answer are optional.
The daily reward
Coins don’t only come from bets. You get a starting balance when you join a team. After that, you can claim a reward once a day, and the seventh day in a row brings a bonus. All these amounts are set per team. When you claim it, confetti falls across the screen. Here’s the dashboard right after, with the balance at the top left and the latest ledger lines on the right.

Teams and roles
All of this happens inside a team. You sign in with GitHub, Discord or Google through Auth.js, with no password to manage, and sessions are kept in the database. Then you join a team through an invitation link, and the request waits for a manager or an administrator to accept it.

Everyone can bet. Managers also create bets, pick the right answer and accept newcomers. Administrators can do all of that, and only they can change roles, from this table.

Every action checks the role on the server, not just in the interface. In the team settings, for example, a manager can only change the default bet duration. The other fields are greyed out for them, and if they send them anyway, the server ignores them.
Three weeks in September
I made the app on my own in three weeks, in September 2024. It’s built on Next.js 15 with React 19, both still release candidates at the time. Forms go through Server Actions validated with Zod, data through Prisma on PostgreSQL, and the interface is made with shadcn/ui and Tailwind. The site ran on Vercel, with the database on Neon.
In those three weeks, I also put together the pages people often skip, like the landing page, the FAQ, the welcome screen after the first sign-in or the terms of use.
I stopped at the end of September, and the repository is now archived.