Inspiration

I got tired of not being able to find anyone to play with. Discord servers move too fast, LFG posts vanish in minutes, and every app I tried matched people by location. Location is the one thing that doesn't matter when the game is online.

What actually matters is whether someone owns the same games, plays on the same platform, and enjoys a compatible play style. Nothing was doing that, so I built it.

What it does

GameBuddy shows you a deck of gamers ranked by how much your taste overlaps. You swipe, and if you both say yes you can chat.

There are also lobbies: you post something like "ranked, 9pm, chill" and pick who joins. Plus friends, badges, a run of missions to work through, and a shop of profile frames, banners and card themes you buy with coins you earn by playing.

GameBuddy uses three optional monetization paths while keeping its core match-and-chat experience free:

  • Gold, an optional subscription. No daily like limit, you can see who liked you, better filters, you can host lobbies, and you get coins every month.
  • Coin packs, for people who'd rather buy than grind.
  • Rewarded videos, for people who'd rather grind than buy.

Matching, chatting, browsing and joining lobbies, and earning badges are free. Gold members can host lobbies.

How I built it

I built all of it myself: the app, the backend, the recommendation model, the original badge and cosmetic artwork, and the servers.

The app is React Native and Expo. It's translated into seven languages, and the translations are typed, so if I forget a string in Finnish, the build fails instead of shipping a blank label.

The backend is Spring Boot, split into modules for auth, profiles, matching, lobbies, billing, moderation and so on. There's a test that fails the build if those modules start depending on each other in circles, which has saved me from myself more than once.

Recommendations come from a small Python service that ranks people by taste. I score it against held-out synthetic player pairs it never saw during training, so "is the model any good" is a number and not an opinion.

Payments are RevenueCat. It handles all seven products and tells my backend what happened over a webhook. That webhook is the only way anyone can ever get Gold or coins. I used to have an endpoint where the app told the server "I bought this," but I deleted it, because that's just a free subscription for anyone willing to send the request themselves. Neither my app nor my server ever touches a receipt now.

Ads are AdMob. Google calls my server directly when someone finishes a video, and the reward is recorded before any coins move, so watching one video can never pay twice.

It all runs in Docker on a single Hetzner box. There are 42 database migrations, and production refuses to start if the database doesn't match what the code expects. I test it with 13 suites that hit the real running stack, plus scripted runs on a real phone.

The original badge and cosmetic catalogue is generated by Python scripts: every custom frame, banner, badge and theme — 28 badges and more than 30 cosmetics — comes from two files. I can redraw that catalogue with one command. Third-party game cover art and sound assets retain their original sources and licenses.

Challenges I ran into

A test key that killed the app on launch. RevenueCat gives you separate keys for the real store and for testing. If a release build gets a test key, the SDK shows a dialog and then kills the process. That happens in native code, so there's nothing I can catch in JavaScript, and it never happens while you're developing. All I saw was my automated tests dying a few seconds after login with no error. Now I check the key before starting RevenueCat: test keys are fine in a debug build and rejected in a release one.

I was giving away four times what I earned. I sat down and added up what a active free player could earn in a week. It came to about 950 coins. The target was 250 to 400. The whole gap was one setting: rewarded videos were paying 20 coins five times a day instead of 15 coins three times. A video earns me maybe two cents, and I was handing back about forty cents worth of coins for it. Every single view lost money. I moved all those numbers into config and wrote a test that adds up the week, so it can't quietly drift again.

Rewards that go up forever are free money. I wanted missions like Subway Surfers: three at a time, finish them all and three more show up. Later ones should pay more, since they're harder. But if they keep paying more forever, that's an infinite money printer. So I made it end. There are exactly 8 sets, drawn from a pool of 24 missions, each one dealt once. The whole run is worth:

$$3 \times 45 \;+\; 3 \times 75 \;+\; 2 \times 105 \;=\; 570 \text{ coins}$$

Once, ever. After that the missions keep coming but go back to paying the starting rate, so grinding forever earns you less than finishing did.

A shortcut that quietly broke old accounts. My badge code skipped the whole check if you already had as many badges as exist. That sounds fine until you remember I'd retired three badges from an old feature but kept people's rows. So an old account could have 13 badge rows against a catalogue of 10, hit the shortcut, and never earn another badge again. Silently. Forever. The people most likely to reach the hardest badges were exactly the ones who couldn't. It compares the actual badges now, not the count.

The deck kept running out. I was filtering out people you'd already swiped on after the model ranked them. The ranking is the same every time, so you'd get the same page over and over and it looked like there was nobody left. I had to send that list to the model instead, so it ranks past them.

Accomplishments that I'm proud of

  • Nobody can fake a subscription. The app has no way to write to billing at all.
  • Nothing can be claimed twice. Every reward is locked before the coins move, and there's a test that fires two claims at the same code at the same time and checks only one wins.
  • Seven languages, and the compiler won't let me forget one.
  • The recommendation model has an actual score instead of me insisting it works.
  • The badge and cosmetic art is code, so drawing 15 new badges cost me an afternoon rather than a budget.
  • I built the whole thing on my own.

What I learned

The hard part of making money in an app isn't taking the payment. It's working out what the phone in someone's hand is allowed to claim is true. Every real bug I hit was some version of that: the app asserting something, or a reward that could be taken twice, or a number living somewhere it could drift without me noticing.

RevenueCat removed a whole category of that problem by making receipt checking someone else's job and just telling my server the answer. The rest I had to be that careful about myself, which is why the ads, the promo codes and the missions all ended up working the same way: lock it first, pay second.

I also learned that an economy is a budget, not a list of numbers. It was off by a factor of four for months and I only found out because I finally sat down and added it up.

What's next for GameBuddy

  • iOS. Android is live on Google Play; the App Store is next.
  • Trying different prices using RevenueCat's experiments. Right now there's one set of prices and I'm mostly guessing.
  • Training on real data. The model learns from a made-up population at the moment. The code to train it on real swipes is written but I haven't run it properly yet.
  • Recommending lobbies and games the same way I recommend people.
Share this project:

Updates

Submission history