This is a submission for the MLH x DEV Writing Challenge
Every savings app I have ever installed asks me for the same thing: consistency. Put a little away, come back tomorrow, do it again. And every one of them eventually loses to the most powerful force in personal finance, which is forgetting to open the app.
So for the MLH Solana Hackathon I stopped asking people to be consistent and turned consistency into the game itself. If you can't stay disciplined for a reward, maybe you can stay disciplined to beat your friends.
What I Built
Last Saver Standing is a no-loss savings game on Solana. Think battle royale, except the arena is a shared vault and the weapon is showing up.
A round works like this:
- Everyone locks the same amount of SOL into the round.
- Check-in windows open at random times. You don't know when the next one is coming.
- Miss a window and you're out of the running for the prize.
- You get one shield, which quietly absorbs a single miss. If you check in on time, you keep it.
- You get one jolt, which forces a rival to make one extra personal check-in. It doesn't touch their money. It just makes their life harder.
- The last saver standing takes the prize.
- Then every single player, winners and losers alike, withdraws 100% of their deposit.
That last line is the whole point. Getting knocked out costs you a prize, never your principal. The worst outcome is that you saved some money for a while and got outplayed.
Keeping the "no-loss" promise honest
"No-loss" is an easy phrase to put on a landing page and a hard one to actually guarantee, so I tried to make the program enforce it rather than trust it.
It's one Anchor program, last_saver. Each round is a PDA, and that account is the vault. When you join, the exact entry amount is recorded as principal. Prize money can only come from a separate reserve account, never from another player. And every instruction that moves SOL ends by calling a check like this:
fn assert_backing(ai: &AccountInfo, round: &Round) -> Result<()> {
let rent_min = Rent::get()?.minimum_balance(ai.data_len());
let need = rent_min
.checked_add(round.principal_lamports)
.and_then(|v| v.checked_add(round.prize_lamports))
.and_then(|v| v.checked_add(round.prize_dust))
.ok_or(LastSaverError::MathOverflow)?;
require!(ai.lamports() >= need, LastSaverError::Accounting);
Ok(())
}
If a transfer would push the vault below rent plus principal plus prize, the transaction fails. There is no admin instruction that withdraws principal. And withdraw_principal can be cranked by anyone, for anyone, eliminated or not, so nobody's deposit depends on one person staying online.
There's also an escape hatch for when things go wrong. If no new window can be opened before a deadline, because the oracle stalls or nobody is cranking, anyone can refund the round. The prize goes back to the reserve and the deposits stay withdrawable.
Randomness, and being upfront about it
A check-in game falls apart if someone can predict the windows. Real rounds use ORAO VRF. The program commits a 32-byte seed only while the ORAO request is still pending, so whoever cranks the round can't fish for a result they already know. Once ORAO fulfills it, the program reads the 64 random bytes, checks the account owner, the PDA, and the seed, and turns the first 8 bytes into the gap before the next window opens.
The trouble is that a two-minute demo video can't sit around waiting on an oracle. So there is a second mode for short demo rounds: a slot-hash commit-reveal, where the seed is mixed with the hash of a slot that doesn't exist yet at commit time. It's fine for a demo. It is not a VRF, since a slot leader could bias it, and the program stores the mode on the account so the UI can say exactly that. I'd rather label a shortcut than let someone mistake it for the real thing.
Where the prize comes from (right now)
This is the part I want to be most careful about. The long-term idea is that pooled deposits earn real yield from lending or staking, and that yield becomes the prize. That isn't built yet. On devnet, the prize comes from a sponsor reserve that's clearly labeled as mock yield, and the UI keeps the sponsor bonus and the simulated yield as separate lines. All of the yield logic lives in one function, accrue, so a real lending adapter can replace it later without touching the principal accounting.
Demo
- Code: github.com/Mzee04/last-saver-standing
-
Program on devnet:
52MzPzQgqVGXYZNRAbF1nivLSUh7NQgYAbhEoFE7NUyb - MLH submission: Last Saver Standing
The round I'm proudest of had three players, Alice, Bob, and Cara, with 0.15 SOL locked between them. It played out like a short drama. Bob missed a window and his shield saved him. Cara missed one and was eliminated. Then Alice spent her jolt on Bob, he missed his extra check-in, and he was out too. Alice won the sponsor prize.
And then the part that matters: all three of them withdrew their exact principal, down to the lamport. Two players got knocked out and neither lost a thing.
The frontend is a Vite + React app that connects to Phantom or Solflare and points at devnet by default. It recomputes each window's gap from the stored randomness and shows you whether it matches, so you don't have to take the game's word for it.
Partner Technologies
The whole thing is built on Solana, the platform this hackathon was built around: an Anchor program on devnet with PDAs for rounds and players, native SOL deposits, and the SlotHashes sysvar for demo randomness.
For verifiable randomness I used ORAO VRF. I picked it over Switchboard On-Demand, which is also on devnet, for practical reasons. ORAO uses the same program ID on devnet and mainnet, its account layout is stable, and my program can verify a result by reading an account instead of making a verification CPI. The client sends ORAO's request_v2 and my commit_window_orao in one transaction, and the commit refuses any request that's already been fulfilled.
The code was written with Cursor as my AI coding tool. It sped up the grind of Anchor account structs, error enums, and test scaffolding, which gave me more time to think about the parts that needed a human worrying about them, like the accounting invariant and the edge cases where principal could get stuck.
Hackathon Experience
The MLH Solana Hackathon ran from November 6 to 8, 2026, and most of my memorable moments were the unglamorous ones.
The devnet faucet was the first wall. The public airdrop endpoint was closed and the faucets were rate-limited, but deploying a 400 KB program needs about 6 SOL just for rent. I ended up funding the deploy by hand, which is an oddly humbling thing to do for money that isn't real.
The second wall came when I recorded the demo. Solana Explorer blocked automated recording with a browser check, so the program page wouldn't load for the capture. I switched to SolanaFM for the recording and moved on.
The bigger lesson was about tests. The test suite runs the program in-process and covers the things I was most scared of getting wrong: a shield consumed only on a miss, jolt limits, an empty reserve paying zero yield instead of dipping into deposits, a double withdrawal being refused, and principal coming back exact to the lamport. In a game where the promise is "you can't lose your money," those tests are the product as much as the UI is.
It's honest about its limits too. It's unaudited, devnet only, capped at 16 players, and windows need someone to crank them. The next step is real yield: swap out the mock reserve for an actual lending or staking adapter, so the prize comes from the pool's own earnings and the game pays for itself.
Thanks to MLH and the Solana community for a weekend that made me think harder about money than any savings app ever has. If you try a round, I'd love to hear who jolted whom.
Top comments (0)