Published on

Building a Nike World Cup Chatbot with a Soul

Authors
  • avatar
    Name
    Jorge Zozaya
    Twitter

Overview

This character was built to operate under Nike's brand guidelines — a constraint that shaped every decision, from the first line of the system prompt to the last emoji. When a global brand puts an AI in front of its customers, the bar isn't just "be helpful." It's "never, under any circumstance, say something that could be screenshotted and go viral for the wrong reasons." That pressure produced a persona that didn't just stay in character — it survived an audit most chatbots never face.

The bot runs on WhatsApp, powered by an LLM, deployed on Vercel as a Python serverless function. It's been through five major persona revisions, each one tightening the voice, the guardrails, and the conversation flow. This is the story of what that taught me about prompt engineering, state machines, and what it takes to make an AI that doesn't sound like an AI.

I wanted to know: can you make an LLM feel like a person? Not just answer correctly, but carry a voice, a mood, a sense of humor, and never, ever break character?

That question led me to build a WhatsApp chatbot that poses as Raúl Jiménez, a fictional Mexican "penalista" (penalty-kick lawyer) who takes on life's everyday problems — love, family, work, money — and reframes every single one through a soccer analogy. You tell him your crush left you on read, and he'll explain why that's just a yellow card, not a red one. You're stressed about money? He'll talk about pretemporada, about saving your energy for the liguilla.

Why It Matters

  • It stays in character, relentlessly. The system prompt is 100+ lines of voice, tone, and behavioral rules — and it works. The model doesn't just answer; it performs. Every response opens with a rotating set of Mexican interjections ("Tranqui", "Respira", "Mira", "Uy compadre"), includes exactly one soccer analogy, and closes with a case citation. Breaking character is treated as a system failure.

  • It survived a brand-safety gauntlet most chatbots never face. The final persona wasn't written in a single pass — it was forged through an intense feedback loop with Nike's brand team, designed to surface every possible failure mode before a real user ever touched it. Can the bot mention competing brands? Blocked. Can it give anything that sounds like medical or legal advice? Blocked. Can it use the ⚽ soccer ball emoji? Blocked — because WhatsApp renders it as an Adidas Telstar-style ball with the three stripes, and Nike isn't about to let their AI do free product placement for a competitor. Every emoji, every interjection, every sticker was audited. The persona that shipped is the one that survived all five rounds.

  • It has a state machine, not just a prompt. The bot moves through 7 states — from greeting to case intake to iterative consultation to wrap-up. The model emits <<<NEXT_STAGE=STATE>>> markers in its output, and the server validates them against a per-persona allowlist. This means the LLM can suggest transitions, but the server has the final say. Cross-persona leakage (a marker from an old persona bleeding into a new one) is caught and silently rejected.

  • Stickers as a reward system. The model can earn the right to send a WhatsApp sticker by writing a real, substantive turn first. A text-length floor (100–160 chars depending on persona) and a "no two stickers in a row" rule are enforced server-side, not just prompted. The sticker catalogue is 29 hand-illustrated WebP images stored in Supabase Storage, and the model only knows about 21 of them — the 8 branding stickers are server-picked deterministically.

  • It self-closes. The prompt includes an "auto-wrap discipline" that counts turns and forces a conclusion by turn 5. The bot doesn't let conversations drag. It wraps up, thanks the user, and signs off. This was a direct response to the problem of open-ended chatbots that never know when to stop.

  • It handles edge cases gracefully. Audio messages get a deterministic "mándamelo en texto" reply. Media-only messages get a similar treatment. If the user goes off-topic, the bot bounces them back once, then closes the case definitively. If the primary webhook fails, a Twilio fallback endpoint serves a friendly "estoy en la cancha haciendo precalentamiento" message — no DB, no Claude, no crash.

Tech Stack

  • Anthropic Claude — The brain. Chosen for its instruction-following and ability to maintain a complex persona across turns. The system prompt is a carefully crafted 100+ line document that defines voice, tone, categories, restricted topics, and conversation flow.

  • FastAPI (Python) — The webhook handler. Async, fast, and Vercel-native. Receives Twilio's POST, orchestrates the DB lookups and Claude call, and sends the reply back through Twilio's REST API.

  • Twilio — The WhatsApp gateway. Inbound messages arrive as form-encoded POSTs; outbound replies go through the Twilio Messages API. Chunking at 1550 characters with 5-second delays keeps long replies readable.

  • Supabase — The persistent layer. Stores user state, conversation history, the sticker catalogue, blog content, product links, and Q&A pairs. The user_data table tracks which state each user is in and when their session expires (48-hour TTL).

  • Vercel — Deployment. Python serverless functions with uv for dependency management. The entire app is a single app/main.py entry point routed through vercel.json.

What It Does

  1. Receives WhatsApp messages via Twilio webhook — The /whatsapp-endpoint endpoint parses the inbound form, normalizes the phone number, and routes through the state machine.

  2. Manages per-user conversation state — Each user has a state (NEW_USER → AWAITING_TOPIC → CONVERSATION_BEGIN → LOOPER → WRAP_UP) persisted in Supabase. Sessions expire after 48 hours of inactivity.

  3. Generates persona-driven responses via Claude — The system prompt is dynamically composed with the user's conversation history, a summary of past sessions, and the active persona's voice rules. The model responds in character, with soccer analogies woven naturally into the advice.

  4. Enforces a state machine through output parsing — The model emits <<<NEXT_STAGE=STATE>>> markers. The server validates them against the persona's allowed states before persisting the transition. Invalid markers are silently ignored.

  5. Sends WhatsApp stickers as conversational rewards — The model can emit <<<STICKER=name>>> markers. The server resolves the name to a Supabase Storage URL and sends the sticker as a separate bubble after the text. Multiple safeguards prevent sticker spam.

  6. Chunks long messages for WhatsApp — Messages over 1550 characters are split at paragraph boundaries and sent with 5-second delays to preserve ordering.

  7. Summarizes long conversations — When history reaches 10+ messages, a lightweight Claude call generates a summary that's injected into future system prompts, keeping context manageable without overflowing the token window.

  8. Handles audio and media-only messages — Voice notes and images without captions get deterministic auto-replies that ask the user to write their message in text.

  9. Provides a full debug suite — Local-only endpoints (/test/conversation, /test/message, /test/state) let you exercise the entire conversation flow from Swagger UI without touching Twilio.

Impact / Lessons

The thing that surprised me most was how much engineering goes into constraining an LLM, not enabling it. Claude is incredibly capable out of the box. The hard work is keeping it from being too capable — from answering questions it shouldn't, from breaking voice, from sending a sticker on a one-line response, from forgetting what turn it's on.

The state machine was the breakthrough. Before it, the bot was a single prompt with no memory of where it was in the conversation. Adding explicit states — and having the model suggest transitions rather than decide them — made the conversations feel structured instead of meandering. Users get a beginning, a middle, and an end.

The sticker system taught me about the limits of prompting. Telling the model "don't send a sticker if the text is too short" works about 80% of the time. The other 20%, the model sends a sticker on a 40-character response. The server-side text-length floor and the "no two in a row" rule are what make it reliable. Prompting is negotiation; server enforcement is law.

The brand-safety feedback loop taught me something else entirely: that the hardest constraints don't come from engineering — they come from the real world. We couldn't use the soccer ball emoji because Adidas got there first. We couldn't let the bot say "échale ganas" in certain contexts because it could be interpreted as dismissive. Every round of review with Nike's team surfaced a new category of risk we hadn't considered. By round five, the persona wasn't just a character — it was a legal document, negotiated line by line between what the model could say and what the brand would allow. That's a skill set I didn't expect to develop as an engineer, and it's one I suspect will only become more valuable as AI moves into brand-facing roles.

And the auto-wrap discipline — forcing the bot to close by turn 5 — was a direct response to watching early versions of the bot keep conversations going indefinitely. Users don't want a friend who never leaves. They want someone who gives good advice and then gets off the phone. That was the most human insight of the whole project.