This is a submission for Weekend Challenge: Passion Edition
What I Built
Loyalty Ledger — a fan loyalty tracker where your check-in streak, badges, and history live on Solana instead of some app's database.
Live app: https://loyalty-ledger-blond.vercel.app
Here's the problem I kept coming back to. Every sports app wants you to check in, engage, "prove your loyalty" — collect points, build a streak, unlock a badge. And every single one of them throws that history away the moment you stop using it. Switch apps and your streak resets to zero. Get banned, or the app shuts down, or they just decide to wipe inactive accounts — your history is gone, because it was never really yours. It was a number in someone else's database, and they could reset it, inflate it, or delete it whenever they felt like it, and you'd have zero recourse.
That felt like a weirdly solvable problem to just... not solve. We figured out how to make ownership portable for money, for domain names, for digital art. But "I've supported Argentina since 2019" still lives and dies inside one company's backend.
So the scope for the weekend was deliberately narrow: prove one fan's loyalty to one team, for real, end to end, rather than sketch out ten features that are all half-fake. You connect a wallet, pick a sport and team, and check in. FIFA World Cup is the fully working path — that check-in sends a real transaction that creates or updates a program-owned account, not a row in my database. Your streak count, your badge tier, the actual badge tokens — none of it exists anywhere I control.
Once that core loop worked, I built out the rest of the identity around it: a Fan Passport that shows your streak, a derived "Fan Score," your tier (Rookie → Devoted → Veteran → Legend), a progress bar toward the next tier, an achievements grid with locked/unlocked states, a recent-activity feed pulled from real on-chain transaction history, and a leaderboard ranking real fans by real streaks. There's also a "Demo Preview" toggle that shows the passport filled in with sample numbers — clearly labeled as sample data — because a screen full of zeros doesn't demo well, and I'd rather be upfront about that than pretend it's real.
NBA and "Other International Sport" are in the app too, using the exact same UI and flow, but they're honestly stubbed — sample fixtures, no chain writes, and the app says so directly rather than hiding it. I'd rather ship one path that's completely real than three that are all a little bit fake.
Demo
Live app: https://loyalty-ledger-blond.vercel.app
To actually try it:
- Install Phantom if you don't have it, and switch it to Devnet — Settings → Developer Settings → Change Network → Devnet. This is the step almost everyone misses first.
- Grab free devnet SOL from the faucet to cover transaction fees, which run a fraction of a cent per check-in. No real money touches this project anywhere.
- Open the app and click Connect Wallet, top right.
- Pick FIFA World Cup and a team.
- Hit check-in and approve the transaction Phantom pops up.
- Watch the streak and Fan Score update, confetti fire, and — if you crossed a threshold — a badge-unlocked toast appear.
- Scroll down to see the full Fan Passport fill in: tier, progress bar, achievements, recent activity, and your real rank on that team's leaderboard.
- Once you've crossed tier 1 (3 check-ins), hit Claim Badge — it mints an actual SPL token to your wallet. Check Phantom's token list afterward; it's sitting there, permanently, not just a UI element.
Code
How I Built It
The account design
The core piece is a small Anchor program. Every fan gets a PDA — a Program Derived Address, an account owned by the program itself, not by me — keyed to (wallet, sport, team):
pub struct FandomRecord {
pub wallet: Pubkey,
pub streak_count: u32,
pub last_checkin_ts: i64,
pub bump: u8,
pub highest_tier_claimed: u8,
#[max_len(32)]
pub sport: String,
#[max_len(32)]
pub team: String,
}
Check-in doesn't touch a database. It sends a check_in instruction that either creates this account (first check-in) or updates it (extends the streak, or resets it if too much time passed between check-ins). That's the entire point of the project in one line: the streak isn't something my frontend can fake, inflate, or quietly reset, because it isn't my frontend's data to begin with. Anyone — not just this app — can read the account directly and verify exactly what it says.
The leaderboard problem
This was the part that took longer than expected. My first cut of the account only stored wallet, streak_count, and last_checkin_ts — sport and team lived purely in the PDA seeds. Which works fine if you already know a wallet and want to look up their record, but it's a dead end if you want to ask "who are the top Argentina fans." Seeds let you derive one specific account when you already know the inputs; you can't run that backwards to enumerate every account matching a team.
So I added sport and team as actual fields on the account data, not just seed inputs. That meant a full account-struct change, a rebuild, and a redeploy — annoying mid-weekend, but it's what makes a real getProgramAccounts call (filtered client-side by sport/team) able to answer "rank every Argentina fan by streak" using actual chain state instead of a cached guess or a fabricated list.
Why Solana, specifically
I wanted this to be a project where pulling out the blockchain would actually break something, not just remove a buzzword from the pitch. Check-ins are frequent and individually worthless — potentially one per match, per fan, across a lot of fans over a tournament. That only works as a real product, not a novelty, if each check-in is near-free and confirms fast enough that it feels like a UI action instead of a bank transfer. Solana is one of the few chains where both of those are true at once. A check-in here confirms in about a second and costs a fraction of a cent, which is the actual reason "check in every match, forever" is a plausible thing to build instead of something that only works in a demo video.
There's a side effect of the PDA design I didn't fully plan for going in: because the fan record is owned by the program and not by my database, any other app that knows the program id can read it directly — no API key, no integration meeting with me, no trusting my numbers. A ticketing platform could check the same record to offer a loyalty discount. A streaming service could unlock a perk off the same tier. None of that requires my permission or my infrastructure staying online. That's a meaningfully different thing from "an app that happens to use Solana," and it more or less fell out of choosing PDAs over a normal database rather than being the original plan.
What actually broke along the way
In the spirit of being honest about this instead of writing it up like everything went smoothly: I burned a genuinely embarrassing amount of time on devnet SOL. The official faucet rate-limits per IP, and I hit that limit hard while iterating on deploys — every account-struct change meant a full redeploy, and every redeploy needed enough SOL to cover rent for a fresh program buffer. I ended up bouncing between the CLI faucet, a couple of alternate faucets, and eventually just asking around, before I could get back to actually building.
Prize Categories
Best Use of Solana — the PDA-per-fan account design and the on-chain leaderboard aren't decorative. Pull Solana out of this project and the core claim — that a fan's streak can't be faked, inflated, or reset by the app itself — stops being true. That was the whole point of building it this way instead of with a normal backend.




Top comments (3)
Ngl the PDA seed vs field bug is such a real "learned this by actually trying to ship it" moment, not something you'd catch just planning on paper. also respect for straight up saying NBA/Other is stubbed instead of quietly hiding it — makes the rest of the writeup way more believable. faucet rate limit pain is real too 😅
haha yeah that bug cost me a solid chunk of the weekend lol. and didn't want to be the "fully on-chain" project quietly faking two sports — felt like that'd defeat the whole point. thanks for reading through the actual build, appreciate it 🙏
Not me getting genuinely invested in whether a fan streak app "counts" as decentralized enough lol but yeah the PDA-per-fan thing checks out, this isnt just solana as a buzzword 💯