Product design · Full stack
A subscription product that analyses football matches and publishes betting entries on its own. I designed every screen and built what runs underneath.
The problem
Betting analysis in Brazil runs on tipsters. Someone reads statistics by hand, posts a pick, and screenshots the wins while the losses quietly disappear. Nobody publishes a record you can audit.
GoScout replaces that with an engine that publishes on its own and keeps every result, including the bad days. That creates the design problem: a subscriber has to trust a number they did not calculate. The interface has to earn credibility in a category built on hype, and it has to charge for that number without feeling like a trap.
People open it on a phone, one-handed, while a match is playing.
Scope
Nobody else worked on this. Each layer below was a decision I made and a thing I built.
Design decisions
My first paywall blurred whole cards. It tested badly. A wall of grey rectangles reads as a product hiding something, and people left before working out what they would even be paying for.
So I rewrote the rule. Free users see the full shape of every screen, including team names, fixtures and labels. Only the paid value gets covered. You always see what you would get, you just cannot read the number yet.
The cost: this gives away more than a hard wall does, and some people stay on the free layer forever. I took that trade because a paywall someone understands converts better than one that reads as a trick, and trust is the whole product here.
I designed the mobile navigation as a floating pill above the safe area instead of a full-width bar, so content runs underneath it and the chrome stays light on a small screen.
It looked right in the device emulator and broke on an actual iPhone. Safari drops fixed positioning when the offset mixes calc() with env(safe-area-inset). Separately, a Tailwind arbitrary class holding two tokens, justify-[safe_center], compiles to nothing at all and reports no error.
What changed: every interface change now gets checked on physical hardware before I call it done. Emulators verify layout. They do not verify the platform.
Subscribers open GoScout at night with a match on. Dark is not the alternate theme here, it is the origin. I built the palette dark and derived the light theme from it.
The system came out of studying how FotMob and Betano handle dense sports data: near-black surfaces, one brand green carrying identity, and colour reserved for meaning. The high-variance bet profile carries its own accent so nobody confuses it with the safe one while scanning.
The cost: the reference I liked used a licensed typeface I cannot redistribute. I picked a free family and pushed character through weight and scale instead. Shipping legally beat matching the reference.
Two screens do opposite jobs. The compact grid lets someone scan dozens of matches in seconds, so it runs tight rows, colour-coded probability and sortable columns. The detail view exists for the moment someone decides whether to act on one number, so it slows down and gives that number room.
Same design system, different density on purpose.
Payments
GoScout charges real money, so I designed and built the whole commercial surface rather than a pricing page.
Under the interface
A scheduled engine pulls fixtures, team and player statistics, lineups and market odds across 100+ leagues, inside a hard ceiling of 75,000 API requests a day that I enforce in code. Every job carries a per-run cap, because going over quota would take the product down for the rest of the day. Live matches get their own one-minute refresh so real-time data never competes with historical backfill.
The rule I would defend hardest is a product one. Not publishing beats publishing something weak. Every market has to clear validation against historical results before it can appear, and several stay switched off in production because they never cleared it. One of those I wanted to ship. When the entire value of a product is a credible record, showing less is worth more.
That rule has a visible consequence. The public results page leads with the validated number, breaks it down month by month including the months that lost, and lists what sits outside the consolidated figure with the reason for each exclusion. A subscriber can audit the claim instead of taking my word for it.
In production
This is the part I would point a hiring manager to first. Subscribers paid and got nothing, and the obvious explanation turned out to be wrong.
Two subscribers paid and received no access. It looked like a repeat of an auth token bug I had already fixed once.
I checked the audit table that logs every inbound webhook before processing. It held zero rows, and had since the integration went live. That ruled out the handler. My code was never running.
The webhook URL registered with the payment provider pointed at the bare apex domain, which the host redirects to www at the routing layer. The provider does not follow redirects on webhook calls. Every request died before reaching the application, with no error and no log line, because nothing ever arrived.
I corrected the URL to the canonical host and verified with a real webhook POST end to end: 200 back, event applied, row written.
A wrong URL breaks quietly again when DNS changes or someone edits a dashboard. Instead of fixing one URL I shipped a watchdog that watches for silence rather than errors. If no webhook arrives for 24 hours it alerts me. That covers this failure and every future one with the same symptom, without needing me to guess the cause.
Takeaway
I close the loop. The paywall connects to a webhook that grants access, an account page that reflects it, and an alert that fires when it breaks. Design decisions stay cheap until something has to work at 2am for someone who paid.
I debug at the right layer. Reading the handler code again would never have found that incident. It took a hypothesis, an audit trail, and accepting that the problem sat outside the application.
I build for the next failure. Watching for silence instead of errors is a small idea that covers a class of problems I have not met yet.
I work in close pair programming with Claude Code, including the engine, the incident fix and the watchdog. I say that plainly because it explains how one person covers this much ground, and because deciding what to build and what to throw away is the part that stays mine.
← All work