Commission 02 · Rexona
Rexona Tribuna
A brand about movement needed proof that fans actually moved, so the mechanic became a screenshot, a ticket, and a human being who checks.
World Cup 2026 · rexonatribuna.club · dashboard.rexonatribuna.club
- 2days from first commit to strangers entering
- 11contests, one per day, none carrying over
- 1entry per person per day, guaranteed by the database
- 299tests across the project, 203 in the backend alone
The second phone call
The Visa project was already underway when the second brief arrived. Different agency, different brand, different game, same tournament, same deadline.
I said yes to that one too.
I want to be honest about why, because the sensible answer was no. I love this sport, and I have watched every World Cup since I was a child. The chance to build inside one does not come around often, and when it does it does not wait for you to have a comfortable schedule.
So I took the second one on top of the first, and for the week they overlapped I was building both at once. This is that second one.
Attention is easy to buy. Effort is not.
Rexona's line across Latin America is nunca te abandona, it never abandons you. The whole brand rests on movement and endurance. It is a promise about the tenth kilometer, not the first.
A World Cup hands a brand an enormous amount of attention and almost no way to spend it on that particular promise. You can buy impressions. You can run a contest where somebody taps a button and enters a raffle. None of it says anything about movement. A tap costs nothing.
So the brief I took was narrower than "run a World Cup campaign." It was: build something that makes fans physically walk, and then proves they walked. Every decision downstream comes from the second half of that sentence.
What came out is rexonatribuna.club: eleven independent daily contests spread across the tournament, each running on a Tribuna Club event day from noon to nine at night, Bogotá time. One winner per day, one prize, a chance to meet Richard Ríos. Days did not accumulate: yesterday's leader started the next day at zero. The tournament is over now, so the site stands with sample data, which keeps the mechanics visible without exposing any real entrant.
Proof, held up to a camera
Here is the entire mechanic. You were at Tribuna Club, the ticketed World Cup event Rexona was sponsoring in Bogotá. On an operation day you opened your phone's health app, screenshotted your step count, photographed the ticket that got you in, and submitted both with your name and email at the QR code on the Rexona stand. Later a human being opened the operator console, looked at your screenshot, and checked whether it matched what you had claimed.
That is deliberately low technology, and the low technology is the design.
I considered the sophisticated versions and threw all of them out. A step-tracking SDK means an app install. A health-API integration means a permissions dialog, a platform review, and two separate builds for iOS and Android. Both of those assume the fan is somewhere calm, holding their own phone, with ten spare minutes and patience.
The actual situation is the floor of a fan event between matches. It is loud. The phone might be borrowed. You have about forty seconds of that person's attention before they go back to the match. A camera roll and a web form survive that. An elegant SDK integration does not.
A tap proves nothing. A screenshot of a step count, held up next to a ticket, proves somebody walked.
When the screenshot does not match the claim, the operator disqualifies the entry and the participant sees a plain sentence in Spanish explaining why. No appeals process, no arbitration screen. One operator, one judgment, one sentence.
The email is the person
Every participant hands over a cédula, the Colombian national ID number. It is the obvious thing to identify people by, and I did not use it.
I used the email address instead, stored as a hash rather than as text. The reasoning is short: a cédula is easy to fake and easy to mistype, but a working email address has to be real, because that is where the prize announcement goes. Had I keyed on the cédula, somebody who invented one would still have won. Somebody who invents an email never finds out they won. The incentive does the enforcement, so I do not have to.
That only holds if one inbox cannot pretend to be several people, so the system collapses the aliases that genuinely reach the same place and nothing else. It ignores capitalization. It strips the +something trick that most providers support. And it removes dots from the part before the @ only for Gmail, because Gmail is the one major provider that documents dots as meaningless. Applying that rule everywhere would quietly merge two different human beings who happen to work at the same company. That restraint is pinned down by a test named for the case it protects.
Because the identifier is a hash, no email address appears in a file path, a folder name, or a log line anywhere in the system.
The day is the address
Each person's entry for a given day is stored at an address built from their hash and the date. The date is not a field inside the record. It is part of where the record lives.
One entry per person per day is not a rule the code enforces. It is the shape of the database. There is no path through the system that can violate it.
A duplicate entry cannot be a bug, because a duplicate entry is an attempt to write to an address that is already occupied. The system looks, sees it is taken, and says no. There are a couple of checks around that to make the failure fast and tidy, but neither of them is the guarantee. The address is the guarantee.
That mattered enormously given the timeline. Anything I could make structurally impossible was one less thing I had to find time to test.
Two sources, one leaderboard
The public leaderboard is a published snapshot: a masked top twenty, rewritten whenever somebody submits and again every sixty seconds. Your own position is calculated live, against the real data, at the moment you ask for it. Two sources, two clocks.
They agree nearly always. The interesting moment is the few hundred milliseconds after you submit a number good enough for the top of the board, when your live position says first and the published board has not caught up yet. Which produced a real bug, reported in exactly these words: I am number one, but I am shown at the bottom of the list.
The naive fix, sticking your row onto the end of the list, is what caused it in the first place. The real fix was to stop rendering two sources side by side and reconcile them into one view that is internally consistent.
That is fifty five lines of pure logic with ten tests around it. It finds your row by an anonymous identifier the board now carries. When the board is older than that, it falls back to matching on your masked name and step count, but only when exactly one entry matches, because masked names collide and I would rather highlight nobody than highlight a stranger who happens to share your first name and your step total. And when the board is simply behind, it inserts you at your correct position rather than gluing you to the bottom.
The email that succeeded without sending
One thing went wrong in production, and it is the most instructive part of the build.
Confirmation emails stopped arriving when a registration was reset and re-created on the same day, which is what happens when an operator reopens a contest day. Not intermittently. Reliably. And there was no error anywhere. No log line, no alert, nothing. The system reported a successful send, the inbox stayed empty, and every instrument I had said everything was fine.
The cause was one line I had written myself. Each confirmation carried a key so that an internal retry would not send twice. The email provider remembers those keys for twenty four hours. So the re-created registration matched a key it had already seen, and returned a cached success without sending anything at all. From my side that is indistinguishable from delivery.
I found it by elimination. The logs showed the registration succeeding with no email error, so that path was healthy. A test message with a fresh key from the same domain arrived instantly, so the account and the domain were healthy. The only variable left was the key.
The fix is one line. The key is now unique to each registration rather than to each person-and-day. It still prevents a retry from double sending, which is what the mechanism is actually for, and it can never suppress a legitimate second attempt.
Never make the key stable for an event that can genuinely happen twice.
The corollary is that I should not over-correct. Stable keys are still right for the winner announcements and the daily batch, because those are operator-triggered, and if an operator has to re-run one it must resume rather than send everyone a second copy. The distinction is not that stable keys are dangerous. It is whether the underlying event can legitimately repeat.
The part I keep coming back to is what it says about monitoring. I had just finished wiring error reporting through all three applications, and it was good coverage. It was also completely blind to this, because a cached success is a success. There is no error to catch. Error monitoring cannot see a failure that never throws, which means "sends succeed but mail does not arrive" is a category you have to go looking for on purpose.
Three omissions on purpose
There is no rate limit by IP address on the public form. That looks like something I forgot. It is in the code as a note: in Colombia most mobile traffic goes through carrier-grade NAT, so thousands of legitimate people share a handful of public addresses. A per-IP limit would block real users and concentrate database writes on a hot spot at the same time, buying worse security and worse performance together. Abuse is bounded by the thing that actually bounds it, which is one entry per email per day.
Uploads are identified by reading the first few bytes of the file, never by the label the browser attaches. The browser is describing its own upload, and there is no reason to take its word for it.
And the check that an email domain can actually receive mail deliberately fails open. If a domain is definitively dead, the entry is rejected. If my own lookup merely hiccups, the entry goes through. A person standing in a stadium should not lose their entry because a server had a bad second. That is my problem, not theirs.
The parts that were not code
I designed all of it. The user experience, the interface, the type, the operator console, the architecture, the database, the deployment. The palette came from the client's Richard Ríos artwork rather than from my own taste, deep petrol teal with a single sharp gold, because the activation had to look like it belonged to the brand rather than to me.
I also did the legal research. A prize promotion in Colombia is regulated. Where chance is involved it requires authorization from the state gambling regulator before it can run, and the regulator has fifteen business days to decide, which is a clock nobody on the project could compress. The terms and conditions are not marketing copy either. They form part of the filing, due before launch rather than after it. And Colombia's Habeas Data regime shaped the interface directly: I asked separately for the data the contest actually needs and the data a brand would like to have, so that entering was never quietly bundled with agreeing to be marketed to.
How the work got done
I build with a system of my own called LLM Orchestrator: a team of specialized AI agents with a controller routing work between them, an architect, an implementer, two independent reviewers, a debugger, and a researcher that verifies external APIs against current documentation before anything is specified. Nothing is called finished without the output of the command that proves it.
I worked test first throughout. There are 203 tests in the backend alone, and 299 across the whole project.
I also had agents watching the running system on a schedule and reporting anomalies, wired so I could reach them from my phone.
I would take both briefs again tomorrow.
Two days
The first commit landed on a Thursday morning. The first public operation day opened at noon on Saturday. Fifty four commits, a public site, an operator console, and a set of cloud functions, built and reviewed and deployed and taking live entries from strangers inside two days.
That pace is why the structural decisions had to be right the first time. There was no week in which to discover that duplicate entries were slipping through, or that identity was forgeable, or that the leaderboard was wrong about who was winning.
Everything I had made structural held through the tournament. The one thing that needed fixing was the one place I had been clever with a string.