Yuri Roberto

Product design · Full stack

GoScout

A subscription product that analyses football matches and publishes betting entries on its own. I designed every screen and built what runs underneath.

Year
2026
Role
Alone: product, UX, UI, design system, frontend, backend, payments, infrastructure
Stack
Next.js, TypeScript, Tailwind, PostgreSQL, Prisma, Hubla
GoScout match analysis screen

The problem

Selling a number nobody has a reason to trust

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

What I owned

Nobody else worked on this. Each layer below was a decision I made and a thing I built.

Product definitionScope and what to leave out
Information architectureRoutes, navigation, hierarchy
Interface designEvery screen, phone first
Design systemTokens, type, dark-first palette
FrontendNext.js, TypeScript, Tailwind
Backend and schemaPostgreSQL, Prisma, REST
Data pipeline100+ leagues, live ingestion
PaymentsCheckout, webhooks, account states
InfrastructureDeploys, cron, monitoring
Growth trackingAttribution through checkout

Design decisions

Four calls that shaped it

Hide the number, keep the noun

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.

A floating nav pill, and a bug that only existed on real hardware

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.

Dark first, because of where people use it

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.

Different density for scanning and for deciding

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.

GoScout compact probability grid with locked paid columns
The compact grid. Fixtures stay readable, the paid columns carry a lock.
GoScout match screen on mobile with the floating bottom navigation
Phone view, with the floating navigation pill.
GoScout fixtures list grouped by league

Payments

The flow most designers never touch

GoScout charges real money, so I designed and built the whole commercial surface rather than a pricing page.

  • Checkout where the limit appears. The upgrade prompt sits at the point someone hits the wall, not only on a separate route. Intent and offer happen in the same moment.
  • Skeleton before lock. Gated screens render their real skeleton and then resolve into a locked state, instead of redirecting on load. A redirect feels like an error. This feels like a door.
  • Visible subscription management. Plan, renewal date and self-serve cancellation, none of it buried behind a support email.
  • Attribution that survives the funnel. I built click-ID capture and signup attribution into the middleware after finding a real sale the ads platform never recorded. A design decision in a funnel is only as good as the measurement that proves it.
GoScout checkout screen with monthly and quarterly plans

Under the interface

What the screens sit on

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.

GoScout public results page showing the validated record and a month-by-month breakdown
The public results page. Validated markets sit above the line, markets still in validation sit below it with the reason.

In production

The payment webhook that never made a sound

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.

Symptom

Two subscribers paid and received no access. It looked like a repeat of an auth token bug I had already fixed once.

Investigation

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.

Root cause

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.

Fix and proof

I corrected the URL to the canonical host and verified with a real webhook POST end to end: 200 back, event applied, row written.

The part that mattered

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

What this project shows

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