AI Marketing Lab

Recovering
lost demand.

A case study in turning a ticketing dead-end into a marketing channel — built with tools every team already has.

7 Tools in the
stack
12 Automation
nodes
3 Compliance
regulations
100% Opt-in · GDPR
compliant
01
Introduction

The problem, the solution, and why it matters.

A fan wants to see RC Celta play. The section is sold out. Days later a seat frees up — but the ticket office has no record that this fan wanted it. That missing memory is silent revenue lost.

The problem

Invisible lost demand

A significant share of tickets that free up (refunds, failed payments, released pre-allocations) never reach the fans who wanted them. The sale evaporates because nobody told them.

The solution

Automated recovery

Capture interest while inventory is sold out, continuously watch availability in ONEBOX, and send a personalised email with a purchase deep-link the moment the requested section has seats again.

💡

Key takeaway

Recovering lost demand is a timing game. The value isn't in owning the list — it's in reaching the customer the moment the problem that stopped them from buying has been solved.

Quick demo

A 2-minute visual walkthrough of the case.

Video not loading? Watch on YouTube →

02
Technology Stack

Seven tools every marketer already knows.

Deliberately few moving parts. Every component is either serverless, free-tier-friendly, or a tool the marketing team already has on its shelf.

🎟️
Data source

ONEBOX TDS

The platform the club uses to sell tickets online. Tells us — in real time — how many seats are still free in each section of each match.

🌐
Entry point

Landing page

Public static page with the "notify me when tickets open" form. Designed with the Celta look & feel and hosted free on GitHub Pages.

Contact list

Mailchimp

The CRM audience. Every registered fan lives here with merge fields (their data) and tags (by match and section) so the team can segment without writing SQL.

Transactional channel

Mandrill

The service that sends the one-to-one alert emails. Optimised for latency and individual tracking. Same login as Mailchimp.

Automation

n8n

The visual glue: polls ONEBOX, cross-references Mailchimp, triggers Mandrill. Free cloud tier or self-hosted. Zero code for the marketer.

Measurement

Google Analytics 4

The analytics layer: page views, custom events (generate_lead), UTM attribution, and a full funnel all the way to conversion.

Build environment

Claude Code

Anthropic's agentic CLI — the tool this entire case study was built in. Describe what you want in natural language; it reads the codebase, writes the code, runs the commands, deploys the services, and iterates visually alongside you. Every line of this platform shipped through Claude Code.

Best practice

Pick tools the team already knows > pick the technically best ones. Each new tool is a learning curve, a contract, and a hidden cost.

How it all connects

Signup Polls Reads Triggers
🌐 Landing GitHub Pages
📬 Mailchimp Audience + tags
🎟️ ONEBOX TDS Inventory truth
🤖 n8n Automation
✉️ Mandrill Transactional
📧
📊 Google Analytics 4  ·  measures every step of the funnel

Hover any tool to highlight its connections

03
Roadmap · 6 weeks

From brief to presentation, week by week.

The case study mapped to the AI Marketing Lab's six-week calendar — what you do, what you ship, and where "done" lives at the end of each week.

Phase 01 · Weeks 1–2 Foundation Understand the brief, set up tools, talk to the API.
Phase 02 · Weeks 3–4 Build & validate Wire every piece together and prove it works end-to-end.
Phase 03 · Weeks 5–6 Ship & defend Propose next steps and present to the room.
01
📋
Week 1

Briefing & setup

Read the brief, surface doubts, install the stack and verify every tool opens cleanly.

02
🌐
Week 2

API & landing

Understand the ONEBOX API. Design and integrate the branded landing page with a working form.

03
🔌
Week 3

Mailchimp & n8n

Integrate Mailchimp. Design the n8n workflow that links availability to contacts.

04
🧪
Week 4

MVP testing

End-to-end dry runs. Submit, trigger, deliver. Fix what breaks in the real flow.

05
🚀
Week 5

Next-steps proposal

Draft a growth roadmap: what scales, what hardens, what monetises from here.

06
🎤
Week 6

Presentation

Walk the room through the problem, the solution and the business case. Ship.

📅

How to use this calendar

Treat each week as a checkpoint, not a deadline. If Week 2 slips, compress Week 4's MVP testing rather than sliding everything right — the Week 6 presentation is fixed. When you're stuck, the fastest way back on pace is to look one phase ahead and ask: what would unblock that?

04
Credentials

Four credentials, none paid on the starter plan.

This is the minimum setup. Every key either has a generous free tier or is free-forever. Budget 30 minutes to have them all lined up before you start building.

What you need

client_secret
32-character string that identifies your channel in ONEBOX.
channel_id
Integer that identifies the channel inside the organisation.

Stage credentials for this course

Pre-provisioned by ONEBOX for the AI Marketing Lab. Paste these into your local .env file when Claude Code asks for them — no need to request your own keys.

ONEBOX_CLIENT_SECRET=163242167d78a369efc5272b39958803
ONEBOX_CHANNEL_ID=2287

Safe to share — stage only

These credentials point at ONEBOX's stage (pre-production) environment. They're intentionally public for course use. Any real money, real tickets, and real fans live behind a different pair of keys the club never hands out.

⚠️

For production, rules change

A production client_secret grants access to the club's full catalog and live inventory. Never embed a production key in the frontend or commit it to a public repo. Keep it in your platform's secret manager.

What you need

API key
Token to call the Marketing API from n8n and the automation layer.
Audience ID
Identifier of the audience (list) where your contacts will live.
Server prefix
Suffix of the API key (e.g. ...-us1us1). Tells you which datacenter your account lives on.

Where to find it


What you need

API key
Token to call the Transactional API from n8n.
Verified sending domain
A domain you control with DKIM + SPF records published and verified in Mandrill.

Where to find it


⚠️

Watch out

Without a verified domain, Mandrill rejects every send with unsigned. It's the number-one error beginners hit. Publish the TXT records before you wire up the flow.

What you need

Measurement ID
The G-XXXXXXX identifier of the web data stream.

Where to find it

This one is safe to expose in public HTML — it's designed to be visible. GA4 distinguishes sessions via cookies and URL parameters, not by the ID itself.

05
Claude Code

The AI that built this platform.

Anthropic's agentic coding assistant. Lives in your terminal or IDE, reads your codebase, edits files, runs commands, and iterates with you in plain English. Every commit in this case study's repo came from a Claude Code session.

What is Claude Code

💬
Natural language

Describe, don't specify

Tell it what you want — "build a signup form styled like Celta's site and sync submissions to Mailchimp" — and it writes, edits, tests, and iterates until it works.

🛠️
Agentic

Reads, writes, runs

Not autocomplete. It explores the codebase with grep and read, edits multiple files at once, runs builds, tests the output, and deploys when ready.

🔄
Iterative

Visual feedback loop

Paste a screenshot of a broken layout — it reads the image, finds the CSS bug, fixes it, asks you to refresh. Like pair programming with a colleague who never sleeps.

Why it matters here

This case study isn't only about AI marketing — it's a case built with AI. Every artefact you're reading — the landing page, the Mailchimp sync, the n8n workflow JSON, the legal copy, this manual itself — was produced through conversation with Claude Code. The commit history at github.com/.../commits/main is the paper trail of that conversation.

💡

Key takeaway

For a marketing team, this changes the economics of launching technical experiments. What used to require a sprint with an engineer now takes an afternoon with an AI that already knows your stack.

Setup in Visual Studio Code · 5 minutes

01
Get an Anthropic account

Sign up at console.anthropic.com. Free tier works to try it; paid plan recommended for serious use.

02
Install the extension

In VS Code, open Extensions (Cmd ⇧ X / Ctrl ⇧ X), search "Claude Code", install the official Anthropic extension.

03
Sign in

Command Palette (Cmd ⇧ P) → run Claude Code: Sign In. A browser window handles OAuth with your Anthropic account.

04
Open a project + the panel

Open any folder. Click the Claude icon in the activity bar (left sidebar) to reveal the Claude Code panel. Start typing.

First prompt to try

"Build me a single HTML page that says «¡Hola, Celta!» in huge letters in the middle, with today's date right below. Make it look clean and colourful — surprise me with the styling. Save it as hola.html." — when it's done, open the file in your browser. From first prompt to first page in under a minute. That loop is the whole magic.

Tips for effective use

Be specific

Name files + constraints

"Update generate-landing.js so the form submits to a new URL at the top. Keep the existing styling." works better than "change where the form goes".

Iterate

Screenshot, don't describe

When a UI is off, paste a screenshot into the chat. Claude reads the image and edits the CSS. Explaining "the button is too far right" in words is slower than dragging in a PNG.

Commit often

Let Claude commit for you

Ask it to commit after each working change. The history becomes a clean, reviewable log of decisions. Revert with a click if anything goes sideways.

Use CLAUDE.md to give Claude Code project context

CLAUDE.md is a special file you commit to your project that tells Claude Code (and future team members) how your project is organized, what conventions you follow, and what decisions you've made. It's like leaving instructions for yourself and for AI — so when you come back to the project weeks later, Claude knows your patterns without you explaining them again.

Why it matters

Consistency: Without it, you explain your conventions every conversation. With it, Claude knows them.

Onboarding: New team members (human or AI) read CLAUDE.md and understand your decisions immediately.

Faster iterations: Claude doesn't ask clarifying questions about architecture — it already knows.

Accountability: You've documented why things are the way they are — useful for your own memory.

Example: RC Celta CLAUDE.md

Here's what your RC Celta project's CLAUDE.md might include:

# RC Celta Avisame Platform

Fan alert system: ONEBOX → signup form → Mailchimp → n8n workflow → Mandrill emails

## Stack
- Frontend: Static HTML + JS (no build)
- APIs: ONEBOX (read-only), Mailchimp (list + tags), Mandrill (email)
- Workflow: n8n (listening to Mailchimp segments, triggering alerts)
- Hosting: GitHub Pages (landing) + Mailchimp + n8n

## Key Files
- `landing.html` — Signup form (auto-generated with real sectors)
- `generate-landing.js` — Builds landing from ONEBOX sectors
- `n8n/avisame-workflow.json` — Full workflow (import via UI)
- `.env` — API keys (gitignored)

## Rules
- Never commit `.env` to Git
- ONEBOX: read-only, no writes
- User copy in Spanish, docs in English
- Run `generate-landing.js` after sector changes
- All form validations happen client-side first

Pro tip

After building your first feature, ask Claude Code: "Write a CLAUDE.md file documenting this project's structure, conventions, and the decisions we've made so far." It will create one in ~30 seconds. Then refine it as your project grows.

⚠️

Watch out

Claude Code can run shell commands and modify files autonomously. Keep your project in a dedicated empty folder the first few times — easy to delete and restart if anything gets weird. Never paste real production secrets into the chat without thinking twice.

06
ONEBOX

The ticketing API.

ONEBOX TDS is the platform where RC Celta sells tickets online. It exposes a professional REST API that we use exclusively in read mode — we never modify data in ONEBOX.

Understanding the data model

ONEBOX organizes ticketing data hierarchically. To the fan we speak in marketing language; internally, IDs power the machine.

📌 EVENT
Celta – Athletic · Matchday 29
A match series with multiple time slots
⏰ SESSION
A specific performance with date and time
e.g., Saturday 3pm at Balaídos
💰 PRICE TYPES
General
Junior
Season ticket
🏟️ SECTIONS
Pista B1
Río Alto
Marcador Norte
🪑 AVAILABILITY
N free seats per section
💡

Key takeaway

To the fan we speak match and section. The internal IDs (session_id, grada_id) are for the machines.

Before you start

⚙️

You need three things

① Your ONEBOX keys (two values: client_secret and channel_id)
② VS Code with Claude Code installed (see §05)
③ A new empty folder, opened in VS Code

Don't worry about Node.js or technical setup — Claude Code handles it all. Just paste the prompts below one at a time and follow along.

Pre-provisioned for the AI Marketing Lab

These keys are already configured for this lab. Paste them into your .env file:

ONEBOX_CLIENT_SECRET=163242167d78a369efc5272b39958803
ONEBOX_CHANNEL_ID=2287
ONEBOX_API_ENDPOINT=https://api.oneboxtds.net/oauth/token

📖 Documentation & API Reference

Official Resources

Developer Portal: https://developer.oneboxtds.com/

Authentication Docs: https://developer.oneboxtds.com/docs/authentication/seller-channel-clients/

Full API Documentation: https://developer.oneboxtds.com/docs/

Interactive Test Page

After running the local server, open this file to see the three API calls with real request/response examples:

http://localhost:8001/onebox-api-test.html

The three API endpoints we use

1️⃣ Authentication

POST /oauth/token

Headers: Content-Type: application/x-www-form-urlencoded

Body:
grant_type=client_credentials
channel_id=2287
client_id=seller-channel-client
client_secret=***

2️⃣ Get Sessions

GET /catalog-api/v1/sessions

Headers: Authorization: Bearer {token}

Response: List of sessions with metadata (matches, dates, times)

3️⃣ Get Availability

GET /catalog-api/v1/sessions/{sessionId}/availability

Headers: Authorization: Bearer {token}

Response: Seat availability information for each stadium section (grada)

Try it yourself — build this in an afternoon

Five prompts, copied one after the other into Claude Code (see §05), take you from API keys to a branded live-data landing page. No coding knowledge required — just copy, paste, run, and watch Claude explain what it's doing.

01
Check that your ONEBOX keys work → a small test script
I have two ONEBOX API keys — a client_secret and a channel_id. Save them in a .env file and build me a small script that calls ONEBOX, tells me if the keys are valid, and shows what the API replies.

What you'll see: Claude creates a .env file for your keys, writes the script, and runs it. If everything works, the terminal prints something like ✔ token acquired · expires in 12 hours. If it fails, Claude reads the error message and helps you figure out what's wrong.

02
Ask ONEBOX what matches are on sale → extends the same script
Great, it works. Now use those keys to ask ONEBOX for the full list of matches available on our channel. Print how many came back, and show me the name of the first one.

What you'll see: Something like 21 sessions returned · first: NUMERADO. This proves Claude can not just authenticate but also read real catalog data — the foundation for every webpage we'll build next.

03
Turn the API into a documentation webpage → onebox-api-test.html
I'd like a webpage that documents the three ONEBOX calls we'll use in this project: authentication, the list of matches, and the available seats for one match. For each, show the request and the response side by side, styled like professional API documentation with a dark theme. Hide the real secret tokens in the output so the page is safe to share. Save it as onebox-api-test.html.

What you'll see: Open the file in your browser — it looks like developer docs built from your own live data. Three neat sections with the real API request / response for each call. You can send this link to a teammate without leaking any secrets.

See the live result
04
Visualise every match as a card → onebox-sessions-cards.html
Now build a visual webpage showing every match as a card: event image, date, venue, price range, and how many seats are still free — colour-coded green / amber / red depending on how full it is. Add a search bar at the top to filter by match or venue name. Save it as onebox-sessions-cards.html.

What you'll see: Your entire catalog, rendered as a branded grid of cards. Instantly spot which matches are selling fast (red bars), which still have room (green), and filter to a specific venue with the search box.

See the live result

What's next

You can now talk to ONEBOX and visualise its data. Section 07 turns that data into the fan-facing «¡Avísame!» signup page with its own prompt-by-prompt tutorial — and then §08–§10 show how every submission becomes an automated email alert.

07
Landing Page

Static, branded, wired to ONEBOX.

A static HTML page that mirrors RC Celta's official landing look & feel. The match data (image, date, available sections) is baked in at build time by reading ONEBOX directly.

What it is

Static HTML · GitHub Pages

A page generated at build time from a Node.js template. Served directly from GitHub Pages with automatic HTTPS.

🏗️
Why static

Fewer parts = more stable

No runtime server, no database in the hot path. Free to host. Trivial to cache on a CDN.

Form submission flow

01
User fills it in

Name, email, section, consents.

02
Browser captures UTMs

Source, campaign, locale, user-agent.

03
Sent to automation

Validation + upsert to Mailchimp.

04
Thank-you + GA4

generate_lead event fires.

💡

Key takeaway

The browser never talks directly to Mailchimp. An intermediate automation validates the data, enforces business logic, and keeps API keys off the client.

Try it yourself — build the «¡Avísame!» page

With your ONEBOX credentials from §06 already in the .env file, two prompts take you from an empty folder to a live, branded signup form with the real stadium sections in the dropdown. We split it on purpose — first the look, then the data — so each step is easy to verify.

01a
Create the HTML structure → landing.html
Create a basic HTML landing page for RC Celta «¡Avísame!» signup. No styling yet — just clean semantic HTML. Include: a hero section with an image placeholder for the Celta crest, a title «¡Avísame!», a short Spanish intro line, and a form with fields for name, surname, email, a grada dropdown with three placeholder options ("Pista Alta", "Río Alto", "Marcador"), and a privacy policy checkbox. Add a submit button. When submitted, log the form data to the browser console. No CSS yet — just plain HTML with semantic structure. Save it as landing.html.

What Claude will do: create landing.html with proper semantic HTML structure (header, form elements, button). No styling, just clean markup.

How to verify: open landing.html in your browser. The page is plain and unstyled, but all text, inputs, and the button are visible. Form submits and logs data to DevTools console.

01b
Add RC Celta brand colors & layout → landing.html (CSS)
Now add CSS styling to landing.html. Use RC Celta's sky blue (#7AB1D9) as the hero background, white text for the title «¡Avísame!», and dark navy text for the form. Make the page responsive and centered. The submit button should use red (#E4002B) with white text. Add basic padding and spacing to make it readable, but don't worry about fine details yet. Keep it simple and focused on the brand colors.

What Claude will do: add embedded <style> CSS to the HTML with the Celta color palette, hero background, form styling, and button colors.

How to verify: refresh landing.html. The page now has the sky-blue hero background, white title, and red button. The form is laid out nicely with proper spacing.

01c
Add the Celta crest image → landing.html
Replace the image placeholder in the hero with the actual Celta crest SVG from assets/celta-crest.svg. Make the crest about 120px wide, centered in the hero section, and add some margin below it before the title. The SVG should scale responsively on smaller screens. Keep the rest of the styling intact.

What Claude will do: embed the SVG crest in the hero section with appropriate sizing and responsive scaling.

How to verify: refresh landing.html. The Celta crest now appears in the hero section above the «¡Avísame!» title. It looks good on desktop and mobile.

01d
Refine the form styling → landing.html (CSS)
Improve the form styling in landing.html. Make input fields have a light border, subtle focus states, and consistent padding. Style the dropdown to match the other inputs. Add hover effects to the submit button. Make the privacy checkbox label clear and clickable. Ensure the form fields stack nicely on mobile. All changes should be CSS-only — no HTML changes.

What Claude will do: update the <style> block with better input styling, focus states, and hover effects.

How to verify: refresh landing.html. Click into the input fields — they should have clear focus states. Hover over the button — it should have a darker red. The form looks polished and professional.

01e
Add form validation & feedback → landing.html (JavaScript)
Enhance the form submission in landing.html with client-side validation. Check that all fields are filled before submitting. If validation passes, show a success message and log the data to the console. If it fails, show a helpful error message. Keep this simple — just basic HTML5 validation plus a custom message.

What Claude will do: add JavaScript validation logic to check required fields and show success/error feedback.

How to verify: try submitting the form with empty fields — you should see an error. Fill all fields and submit — you should see a success message and data in the console.

02
Wire the grada dropdown to ONEBOX → generate-landing.js
Now replace the three placeholder gradas with the real ones from ONEBOX. Pick one match to watch — use session ID 240895 (or whichever session from the cards page in §06 I want to watch). Write a short Node script called generate-landing.js that reads the ONEBOX credentials from my .env, gets an access token, calls /catalog-api/v1/sessions/240895/availability, collects every sector from the response (id + name), and rewrites landing.html so the grada dropdown's <option> list reflects those real sectors. Each option's value should be the sector ID, the visible text the sector name. Leave the rest of landing.html untouched. At the end, print the number of sectors baked in so I know it worked.

What Claude will do: create generate-landing.js, run it once so the file actually ships into landing.html, and print something like "✔ baked 8 sectors into landing.html".

How to verify: refresh landing.html in the browser — the dropdown now shows the real RC Celta stadium sections (Pista A1, Río Alto, Marcador Norte…). Right-click the dropdown → Inspect: each <option value="..."> carries a real sector ID.

See the live result
⚠️

Watch out — this is a build-time fetch, not runtime

The page served to fans is plain static HTML with no ONEBOX call at page-load. generate-landing.js runs once in your terminal and bakes the sectors into the file. That's why your ONEBOX secret never leaves your laptop: it only lives in .env during the build. Rerun node generate-landing.js any time the catalog changes, then commit the regenerated landing.html.

What's next

Your form now captures fan intent locally. Section 08 shows how we send every submission to Mailchimp so the marketing team has a real segmentable audience — and the rest of the alerting pipeline unlocks from there.

08
Mailchimp

The central list, segmentable without SQL.

Every registered fan lives here. Two distinct mechanisms organise their data: merge fields (their profile) and tags (their interest history). Each has different semantics, and that changes how we use them.

Merge fields Overwrite

The contact's "profile". Each field holds one value: the most recent one. Ideal for data that's by its nature unique per contact.

Example: Ana signs up for Pista B1 today. Tomorrow she signs up for Río Alto.

GRADA_NAME: Pista B1Río Alto
GRADA_ID: 224549224562

Tags Stack

They preserve the full history. A contact can carry dozens of tags at once. Ideal for segmenting by interests or behaviour.

Same case: Ana signs up for Pista B1 and later for Río Alto.

grada:224549 (Pista B1)
grada:224562 (Río Alto)
event:4587
source:avisame
💡

Key takeaway

Use tags to segment. Use merge fields for the visible profile in the Mailchimp UI. They're complementary, not alternatives.

Typical segment: Sector 316 of Celta–Athletic opens

tag contains  grada:224515         AND
tag contains  event:4587           AND
tag NOT ∋     notified:event-4587-grada-224515

Marketing builds this segment in Mailchimp's UI. n8n consumes it to send the alert at most once per fan and section.

Set up Mailchimp · 10 minutes

Before connecting the landing, you need a Mailchimp audience ready to receive contacts. All UI clicks — no code yet.

01
Create a Mailchimp account

Sign up at mailchimp.com/signup. The free tier is plenty to build this case study.

02
Create the audience

Audience → Create Audience. Name it "RC Celta – Avisos de entradas". Default from-name and from-email are the club's official sender.

03
Add the merge fields

Audience → Settings → Audience fields and *|MERGE|* tags. Add the four custom fields shown in the table below.

04
Grab your three credentials

Account & billing → Extras → API keys (create one). Then Audience → Settings → Audience name and defaults for the Audience ID. The server prefix is the suffix of the API key (e.g. -us1).

The merge fields to add

Mailchimp gives you FNAME, LNAME and EMAIL out of the box. Add these four yourself so every contact remembers which match and section they signed up for:

API identifierLabel (shown in Mailchimp UI)Type
EVENT_IDMatch IDNumber
EVENT_NAMEMatch nameText
GRADA_IDGrada IDNumber
GRADA_NAMEGrada nameText

The full list used in the production repo also tracks LOCALE, CHANNEL, SIGNUPAT, SRC, LANDING and a few more — start with these four; add the rest when marketing asks.

⚠️

Keep your API key secret

The Mailchimp API key lets anyone modify your entire audience. Never commit it to Git or embed it in the landing HTML. Store it in the .env file Claude Code already created for your ONEBOX keys — that file is gitignored by default.

Connect the landing to Mailchimp · 1 prompt

Your audience is ready. One prompt in Claude Code wires every form submission into it, complete with merge fields and segmentation tags.

01
Wire the form to Mailchimp → updates landing.html + adds a server function
Connect my landing page form (from §07) to my Mailchimp audience. Whenever a fan submits the form, create them as a contact in Mailchimp with their name, surname, email, and their chosen match and grada stored in the merge fields EVENT_ID, EVENT_NAME, GRADA_ID, GRADA_NAME. Also add three tags to each contact for later segmentation: event:{id}, grada:{id}, and source:avisame. My Mailchimp API key, audience ID and server prefix are in the .env file. Keep the API key server-side — the browser must never see it.

What Claude will do: add a small server-side function that receives the form submission and forwards it to Mailchimp's API. It'll update landing.html to POST to that function instead of just logging to the console, and handle success / error states (duplicate emails, missing fields, failed network).

How to verify: submit the form with your own email. Open Mailchimp → Audience → All contacts. Your entry appears within seconds — with the four merge fields populated and the three tags attached. That's the full loop, working.

What's next

Your audience now grows automatically with every signup, and each contact is tagged for segmentation. Section 09 introduces Mandrill — the service that reads from this audience and sends the actual alert email when seats open up.

09
Mandrill

Transactional, not bulk.

Mailchimp is for scheduled campaigns to large lists. Mandrill is for immediate, personalised sends with per-message open and click analytics.

Anatomy of the email

Email body kept in Spanish — it's the actual output for Spanish-speaking fans.

What we measure per send

Tracking enabled

Open rate · CTR

Mandrill open pixel + redirect-wrapping for clicks. Visible in its own dashboard and in GA4 via UTMs.

Optional upgrade

Tags per match + section

Enable Mandrill Analytics filtering by tagging every send with avisame, event-{id}, grada-{id}. Not set in the starter workflow — add it in the Mandrill node's Additional Fields → Tags when you need per-grada breakdowns.

⚠️

Watch out

The domain you send from (from_email) must be verified in Mandrill with DKIM + SPF. If it isn't, every send is rejected with reject_reason: unsigned. It's the first mistake everyone makes — so step 02 of the setup below is the most important.

Set up Mandrill · 15 minutes

Mandrill ships as a paid add-on to Mailchimp — prices start around $20/month for 25 000 emails. For a student project that's often overkill; see the alternatives callout below if you'd rather use a free transactional service. The setup steps look the same for any provider Claude Code wires in.

01
Enable Mandrill

Inside Mailchimp: Automations → Transactional Email → Add Mandrill as an add-on. (Existing Mandrill accounts sign in at mandrillapp.com.)

02
Verify your sending domain

Mandrill → Settings → Sending Domains → add the domain you'll send from. Publish the two TXT records (DKIM + SPF) at your DNS registrar, then click Verify. Allow up to an hour for DNS to propagate.

03
Create an API key

Settings → SMTP & API InfoNew API key. Give it a memorable name like "avisame-alerts" so you can rotate or revoke it later without guessing.

04
Add it to .env

Open the .env file Claude Code already uses for ONEBOX and Mailchimp. Add MANDRILL_API_KEY=… on a new line. That file stays gitignored — the key never leaves your machine.

💡

Free alternative — same tutorial, different provider

If you don't want to pay for Mandrill, substitute any free transactional-email service: Resend (3 000/month), SendGrid (100/day), Postmark (trial), or Mailgun (trial). Sign up, verify your domain, grab an API key, and add it to .env as RESEND_API_KEY (or the equivalent). In §10 you'll swap the n8n Mandrill node for the matching one (Resend / SendGrid / SMTP) — the HTML body and tags stay identical.

What's next

Mandrill is verified and ready to send. Section 10 introduces n8n — the automation that watches ONEBOX continuously, reads Mailchimp for the right subscribers, and calls Mandrill directly (via its native n8n node) to alert exactly the fans who asked to be notified.

10
n8n

The brain that connects everything.

n8n is a visual automation tool: drag boxes, connect arrows, run. The open-source equivalent of Zapier, but without execution caps and with full control.

Set up n8n · 10 minutes

n8n is where every previous tool meets. You won't write any glue code — you'll import a workflow, paste a handful of keys, and press Execute. The free cloud tier is enough for this case study; self-hosting via Docker is an option if you'd rather keep data on-prem.

01
Create your n8n workspace

Go to n8n.cloud and sign up for the free tier (14-day trial, then free community plan). Prefer self-hosting? Run npx n8n or docker run -p 5678:5678 n8nio/n8n — same editor, on localhost:5678.

02
Import the reference workflow

Download the reference file from the case-study repo: n8n/avisame-workflow.json (open it on GitHub, click Raw, then save the page as a file). Back in n8n: Workflows → Import from File → pick the JSON. The 11 nodes below appear pre-wired, ready for your credentials.

03
Fill in the Vars node

Open the Vars node (step 02 in the pipeline). Replace every REPLACE_WITH_* placeholder with your own values: ONEBOX client secret + channel, Mailchimp list ID + Basic-Auth token, Mandrill API key, and the event_id of the match to watch.

04
Link credentials & run

Open the Mailchimp Members node → Credential to connect with → paste your Mailchimp API key. Repeat on the Mandrill Send node with your Mandrill key. Click Execute workflow. The arrows light up left-to-right; green ticks appear under each node as items flow through.

⚠️

Watch out

The Mailchimp Basic-Auth token in Vars is the Base64 encoding of anystring:YOUR_API_KEY. Compute it once (Claude Code: "give me the base64 for anystring:<key>") and paste — n8n expressions don't expose a $base64() helper, so doing this at runtime will 401.

What you'll see after Execute

The canvas looks like this on a successful run. Read it left to right — each grey label between boxes shows how many items passed through at that step. Watching the item counts is the fastest way to spot where a pipeline starts leaking.

n8n canvas showing the 11-node avisame workflow executing successfully, with item counts on each connection.
Successful run · 1 fan matched × 6 gradas with seats → 6 personalised Mandrill sends

The pipeline — 11 nodes

01🎬
Manual Trigger
Entry point
02📌
Vars
Central config
03📬
Mailchimp Members
Fetch contacts
04📚
Aggregate
Collapse to a list
05🔐
ONEBOX Auth
Request token
06📅
ONEBOX Sessions
List matches
07🔎
Filter event_id
Keep the target
08🎫
ONEBOX Availability
Query availability
09🧮
Sections w/ seats
Flatten
10🤝
Find leads
Cross-reference × tags
11✉️
Mandrill Send
Send the alert

Best practice — one source of truth

Every ID and key in this workflow lives in the Vars node (step 02 of the pipeline). Need to watch a different match, swap Mailchimp audience, or rotate an API key? Edit Vars and the 9 downstream nodes pick it up automatically — no other node needs touching.

Anatomy of each node

The workflow reads left to right: collect fans → collect seat availability → cross-reference the two → send personalised emails to the matches. Each node does exactly one thing, so when a run fails you can see on the canvas which step leaked.

01🎬

Manual Trigger

n8n-nodes-base.manualTrigger

The on/off switch. Pressing Execute workflow fires one item into the pipeline — useful while building and debugging. In production you swap this for a Schedule Trigger that runs every 10 minutes.

In
Out1 item (the start signal)
02📌

Vars

n8n-nodes-base.set · Edit Fields

The single source of truth for every key, ID, and template used downstream: event_id, channel_id, onebox_client_secret, mailchimp_list_id, mandrill_api_key, mandrill_from_email, mandrill_from_name, and purchase_url_template. Change any value once here — every other node references it via {{ $('Vars').first().json.<field> }}.

In1 item from Trigger
Out1 item with 8 config fields
GotchaAfter import, replace every REPLACE_WITH_* placeholder before running.
03📬

Mailchimp Members

n8n-nodes-base.mailchimp · List Member · Get All

Pulls every subscriber in your audience that has status = subscribed. Each member arrives as its own n8n item with email_address, merge_fields (FNAME, LNAME, EVENT_ID, EVENT_NAME, GRADA_ID, GRADA_NAME…), and the tags array we'll use to match fans to seat openings.

In1 item
OutN items (one per subscriber)
CredentialLink the Mailchimp API credential after import — together with the Mandrill credential on step 11, these are the only two the workflow needs.
04📚

Aggregate members

n8n-nodes-base.aggregate · All Item Data

Collapses N subscriber items into a single item with a members array. We do this because the downstream Find leads step needs to loop over every subscriber inside a bigger loop over gradas — working with one bundled list is faster than exploding the cartesian product across the canvas.

InN items
Out1 item · json.members[]
05🔐

ONEBOX Auth

n8n-nodes-base.httpRequest · POST /oauth/token

Exchanges the ONEBOX client secret for a 12-hour bearer token using the seller-channel-client OAuth2 flow (grant_type=client_credentials). The resulting access_token is reused by every subsequent ONEBOX call via {{ $('ONEBOX Auth').first().json.access_token }} — no need to re-authenticate per request.

In1 item
Out1 item · access_token
06📅

ONEBOX Sessions

n8n-nodes-base.httpRequest · GET /catalog-api/v1/sessions

Asks ONEBOX for every upcoming match (session) visible to this sales channel. Returns the full catalogue in a single response — typically a few dozen fixtures with nested event metadata we'll filter on next.

In1 item (token)
Out1 item · data[] with ~21 sessions
07🔎

Filter by event_id

n8n-nodes-base.code · Run Once for All Items

Keeps only the sessions whose event.id matches the event_id you set in Vars — the match you're watching right now. Emits one item per matching session with the fields we need downstream (session_id, session_name, event_id, event_name, on_sale, sold_out).

In1 item (full catalogue)
Out1–N items (matching sessions)
WhyAlso rescues event_name — ONEBOX's /availability response lacks it, so we pass it through here.
08🎫

ONEBOX Availability

n8n-nodes-base.httpRequest · GET /sessions/{id}/availability

For each session kept in step 07, fetches its per-sector availability breakdown. The response nests sectors[] → price_types[] → availability (available + total seats). n8n runs this node once per input item automatically — so 3 sessions means 3 API calls, all in parallel.

InN items
OutN items · nested sectors[]
09🧮

Gradas with availability

n8n-nodes-base.code · Run Once for All Items

Flattens each session's nested sector tree and keeps only the sectors with at least one free seat. Sums available across price types so each output item represents one grada-session combo with real inventory: {event_id, session_id, event_name, grada_id, grada_name, available, total}.

InN session responses
OutM items (gradas with seats)
10🤝

Find leads

n8n-nodes-base.code · Run Once for All Items

The heart of the pipeline. For every grada with availability, iterates over the aggregated Mailchimp members and keeps those whose tags contain all three of source:avisame, event:<id>, and grada:<id>. Each match becomes one output item with fan details + seat context + a ready-to-click purchase_url (the Vars template with {sessionId} / {eventId} / {gradaId} substituted).

InM gradas · N members
OutK items (fans to email)
GotchaIf K = 0 your fans' tags don't match the grada IDs — check the landing form's tag writer.
11✉️

Mandrill Send

n8n-nodes-base.mandrill · Message · sendHtml

Sends one personalised email per lead using n8n's built-in Mandrill node. The node's four fields are set straight from the current item: From Email (from Vars), To Email (the fan's address from Find leads), Subject ("¡Hay entradas en {grada} para {event}!"), and HTML (the full branded body with the sky-blue header, red title, seat recap card, and the red "Comprar entrada" CTA built as a single n8n expression that concatenates the template with first_name / event_name / grada_name / available / purchase_url).

InK lead items
OutK Mandrill responses
CredentialLink the Mandrill API credential after import — along with Mailchimp, it's one of the two credentials this workflow needs.
GotchaIf every response is reject_reason: unsigned, your from_email domain isn't DKIM-verified in Mandrill (see §09 step 02).

Possible evolutions

Avoid duplicates

Mark as notified

Add a notified:event-X-grada-Y tag after sending so the same fan isn't alerted twice for the same section.

Full automation

Scheduler

Replace the manual trigger with a Schedule node running every 10 min during peak demand windows.

Internal alerts

Slack channel

A second output that notifies the internal team in real time when top sections open up.

Analytics granularity

Tag every send

Add avisame, event-{id}, grada-{id} in the Mandrill node's Additional Fields → Tags. Unlocks per-match and per-grada open / click breakdowns in Mandrill Analytics.

Tracking toggles

Opens + clicks

Flip Track Opens and Track Clicks on the Mandrill node to get per-recipient engagement signals back into Mandrill's dashboard and the API response.

What's next

Your workflow runs on demand. Section 11 introduces GitHub — where the whole case study (landing page, scripts, n8n JSON) lives so classmates can fork it, and where GitHub Pages serves the signup form for free.

11
GitHub

Version control + free hosting.

GitHub plays two roles here at the same time: it keeps the code's change history (version control) and it serves landing.html over HTTPS for free (GitHub Pages).

📁
Repository

Public · Single branch

github.com/.../rc-celta-seat-availability-alert-platform. A single main branch — for a small team, that's enough.

🚀
Automatic deploy

GitHub Pages

Every git push triggers a re-deploy. 30–60 seconds later the changes are live with an auto-renewed HTTPS certificate.

Complete Guide · 15 minutes

Create a landing page, save it on GitHub, and publish it on the internet—all in 15 minutes. GitHub is like Google Drive for code — with two superpowers: it remembers every change forever, and it can publish any HTML file in your folder as a real HTTPS website for free.

Phase 1: Prepare Your GitHub Account

01
Create a GitHub Account

GitHub is a website where you store code and automatically publish websites. You need a free account.

  1. Go to github.com
  2. Click "Sign up"
  3. Enter your email, create a password, and choose a username (e.g., yourname-web)
  4. Check your email and click the verification link

⏸ Once you've created your account, tell me you're ready to continue.

02
Create a Security Token

GitHub needs a special "key" to trust commands from your computer. It's like a secure password.

  1. Log into GitHub (github.com)
  2. Click your profile picture (top right) → Settings
  3. In the left menu, click Developer settingsPersonal access tokensTokens (classic)
  4. Click "Generate new token (classic)"
  5. Fill in:
    • Note: Claude Code
    • Expiration: 30 days
  6. Check these 3 boxes:
    • ☑️ repo
    • ☑️ workflow
    • ☑️ admin:repo_hook
  7. Scroll down and click "Generate token" (green button)
  8. Copy the token (the long code starting with ghp_) — you'll only see it once!

⏸ Paste your token here when you're ready.

Phase 2: Connect to GitHub

I'll now connect your computer to GitHub using your token. This is automatic.

⏳ Connecting...

Phase 3: Save Your Code to GitHub

I'll now:

  1. Create a "save point" of your landing page
  2. Upload it to GitHub
  3. Create a repository (a folder on GitHub for your project)

⏳ Uploading to GitHub...

Phase 4: Make It Live on the Internet

GitHub can automatically publish your landing page on the internet for free. I'm enabling that now.

⏳ Publishing to the internet...

Your Website is Live!

🎉 Congratulations!

Your landing page is now live at:

👉 https://[your-github-username].github.io/rc-celta-landing/landing.html

What just happened:

  • Your landing page is now on the internet
  • It's backed up safely on GitHub
  • Every time you make changes, it updates automatically
  • It's completely free

Future: Making Changes

Whenever you want to update your landing page:

  1. Edit landing.html on your computer
  2. Save it
  3. Tell me: "Push my changes"

I'll automatically update your live website. No need to remember all these steps!

Troubleshooting

Q: My website doesn't show up
A: Wait 1-2 minutes and refresh your browser. GitHub needs time to publish.

Q: I want to change the form fields
A: Edit landing.html, save it, and tell me to push the changes.

Q: What if I forgot my token?
A: No problem—create a new one following Step 2 above.

Q: Can I share this link with others?
A: Yes! Anyone can visit your website. It's public.

What You Now Have

  • ✅ A live website on the internet
  • ✅ Automatic backups on GitHub
  • ✅ A skill most people don't have
  • ✅ Free hosting forever

Next steps? Customize your landing page, add more content, or share the link with friends!

⚠️

Watch out — never commit .env

Your .env file has real API keys. Anyone who copies it can use your services. The project's .gitignore already excludes it — but before the first push, tell Claude Code: "make sure my .env file is in .gitignore and not staged for the commit". Two seconds of paranoia prevents a leaked-key incident.

12
Google Analytics 4

Measuring what matters.

The measurement_id is embedded in the landing. GA4 captures traffic and events automatically; we add a custom generate_lead event when the form submits successfully.

Set up GA4 · 15 minutes

GA4 is Google's analytics product — free, and the tool most marketing teams already look at daily. The setup has four clicks-only steps inside Google's admin, then one prompt to wire it into your landing page.

01
Create a GA4 property

Go to analytics.google.com → sign in → Admin (gear icon, bottom-left) → Create → Property. Name it "RC Celta Avisame", pick your timezone and currency, click through.

02
Add a Web data stream

When prompted, pick Web → enter your GitHub Pages URL from §11 (<user>.github.io/<repo>) → Create stream. At the top of the stream's page you'll see a Measurement ID starting with G- — copy it.

03
Register custom dimensions

Admin → Custom definitions → Create custom dimensions. Add one dimension per field you want to slice reports by: event_id, event_name, grada_id, grada_name, is_duplicate. Scope: Event. Without this step the data is captured but not visible in reports.

04
Mark generate_lead as Key Event

Admin → Events → once generate_lead has fired at least once it appears in the list. Toggle Mark as Key event (previously called conversion). That's what turns a signup into a countable business outcome in the reports.

💡

About that G- Measurement ID

This is the only GA4 secret you'll paste into code — and it's not actually a secret. It's published in every site's HTML source. Anyone can see yours by viewing source on the live page; that's by design. So unlike ONEBOX / Mailchimp / Mandrill keys, this one doesn't need to live in .env.

Wire GA4 into the landing · 1 prompt

One prompt adds the GA4 loader to your landing and fires the generate_lead event with all the custom parameters the dashboard needs.

01
Add GA4 tracking to the landing page → landing.html
Add Google Analytics 4 tracking to my landing.html. My Measurement ID is G-XXXXXXXXXX (replace this with yours). Inject the standard gtag.js loader in the <head>. Then, when the form is submitted successfully (after the fan's email is accepted by Mailchimp), fire a generate_lead event with these parameters: method (always "avisame_form"), event_id, event_name, grada_id, grada_name, is_duplicate (true if the backend returns a duplicate, false otherwise), and locale (from the browser's navigator.language). Don't fire it on validation errors — only on confirmed signup. Keep the existing page_view automatic.

What Claude will do: add the gtag.js snippet to the <head> of landing.html, locate the success-branch of the form submit handler, and insert a gtag('event', 'generate_lead', {…}) call with the seven parameters.

How to verify: open GA4 → Reports → Realtime. Submit your own form in another tab with test data. Within ~30 seconds a page_view and a generate_lead event appear. Click generate_lead to see the custom parameters attached.

Recovery dashboard — illustrative numbers

Ticket availability alerts · Last 30 days
Signup rate
24.3%
page_view → generate_lead
Total leads
830
+412 last matchday
Emails sent
412
via Mandrill
Recovery rate
12.8%
alert → purchase

Custom parameters on generate_lead

ParameterExampleCustom dim
methodavisame_form
event_id4587
event_nameCelta – Athletic · J29
grada_id224549
grada_namePista - B1
is_duplicatefalse
localees-ESoptional
💡

Key takeaway

Without registering the parameters as custom dimensions in Admin → Custom definitions, GA4 still captures them but won't expose them as breakdowns in the reports. It's a required step.

What's next

The technical build is complete: data flows ONEBOX → form → Mailchimp → n8n → Mandrill → GA4. Section 13 covers the non-negotiable layer that turns a demo into something you can legally operate in Spain — GDPR, LOPDGDD, and LSSI-CE.