Build a Payoff Engine with Claude API

Generate personalized, structured project payoffs using Claude and FastAPI.

Introduction

30 Second Summary

Have you ever read a project description and immediately thought "I need to build this"? That pull comes from a specific, concrete promise that feels like it was made for you.

In this project, you will build a Payoff Engine: an AI-powered API that transforms dry project descriptions into emotionally compelling, personalized payoff statements. You will power it with the Claude API and serve it through FastAPI so your engineering team can start integrating immediately.

What You'll Build

Feed the engine "Learn Terraform" and a learner who loves Marvel, and your FastAPI service fires back structured JSON for a hero banner reading "Build your own J.A.R.V.I.S. that monitors your infrastructure and alerts you before anything breaks."

By the end of this project, you'll have:

  • A running Payoff Engine API where you POST a project description and learner profile and get back structured payoff statements tailored to that specific learner.
  • Multi-format payoff data including headlines, stat cards, before/after comparisons, and hero banners that a frontend team can render directly into rich visuals.
  • A personalization layer where the same project produces completely different payoffs for a Marvel fan versus a side-hustle entrepreneur.
  • Secret Mission: A community showcase mode that generates payoffs from five different persona perspectives, showing the breadth of what one project can produce.

Do I need anything before starting?

This project uses your Anthropic API key and Cursor, which you should already have set up. You will install Python packages and create a virtual environment in Step 1.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Python Environment and API Key

Your Payoff Engine needs two Python libraries to work. The Anthropic SDK lets you communicate with Claude. FastAPI serves your engine as a web API that your team can integrate with.

Before writing any code, you need a clean workspace with both libraries installed and your API key configured. This step gets your environment ready so you can focus on building.

In this step, get ready to:
  • Create a project folder and Python virtual environment.
  • Install the Anthropic SDK and FastAPI packages.
  • Verify everything works with a quick import check.
Create your project folder and virtual environment

This project requires Python 3.10 or higher because FastAPI depends on it. Let's confirm your Python version first.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Click Terminal in the top menu bar.
  • Click New Terminal.
  • Check which version of Python you have by running this command:
python3 --version

✔️ I see version 3.10 or higher

You're all set. Move on to creating your project folder below.

ⓧ I see an older version

Your installed Python is too old for FastAPI. You need version 3.10 or higher.

  • Download the latest Python from the official Python downloads page.
  • Run the installer and follow the prompts.
  • On macOS, your system may still default to the older version after installing. Run python3.12 --version (or the version you installed) to confirm the new install works. If the new version only responds to python3.12, use that command in place of python3 for the rest of this project.
  • On Windows, the installer adds both python and python3 to your PATH. Run python3 --version again after installing to confirm.

ⓧ Command not found

Python is not installed yet.

  • Download the latest Python from the official Python downloads page.
  • Run the installer and follow the prompts. On Windows, make sure to check Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal in Cursor after installing.
  • Run python3 --version again. You should now see a version number of 3.10 or higher.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir payoff-engine
cd payoff-engine
  • Create a Python virtual environment inside the project folder by running this command:
python3 -m venv .venv
  • Confirm the virtual environment was created by running:
ls -a

You should see .venv listed in the output. That folder holds your isolated Python environment.

  • Activate the virtual environment:

macOS

  • Activate the virtual environment by running:
source .venv/bin/activate

Windows

  • Activate the virtual environment by running:
.venv\Scripts\activate

Your terminal prompt now starts with (.venv). This confirms you are working inside the virtual environment.

What is a virtual environment?

A virtual environment is an isolated Python workspace. Packages you install here stay inside this project and do not affect other Python projects on your machine.

The (.venv) prefix in your terminal prompt is your reminder that the environment is active.

Don't see (.venv) in your prompt?

Make sure you ran the activate command for your operating system. macOS uses source .venv/bin/activate and Windows uses .venv\Scripts\activate.

If you see a permissions error on Windows, try running Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell first, then retry the activate command.

Help me activate my Python virtual environment.

Install packages and configure your API key

With your virtual environment active, you can now install the two libraries your Payoff Engine depends on.

  • Install both packages by running this command:
pip install anthropic "fastapi[standard]"

You should see download and installation progress for both packages and their dependencies. This may take a minute.

What did you just install?

  • anthropic is the official Python SDK for the Claude API. It handles authentication, message formatting, and structured output parsing.
  • fastapi[standard] is a modern Python web framework that auto-generates interactive API docs. The [standard] extra installs a development server with auto-reload so your API restarts whenever you save a file.

Seeing installation errors?

Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt. If not, re-run the activate command from the previous substep.

If you see a network timeout, check your internet connection and try running the install command again.

Help me fix my pip install error.

Now you need to tell the Anthropic SDK where to find your API key. The SDK reads it from an environment variable called ANTHROPIC_API_KEY.

macOS

  • Export your API key by running this command, replacing your-api-key-here with your actual Anthropic API key:
export ANTHROPIC_API_KEY=your-api-key-here

Windows

  • Set your API key by running this command, replacing your-api-key-here with your actual Anthropic API key:
set ANTHROPIC_API_KEY=your-api-key-here

Why an environment variable?

Storing your API key in an environment variable keeps it out of your source code. If you ever share or commit your project files, the key stays private.

This variable only lasts for the current terminal session. If you close and reopen your terminal, you will need to export it again.

Verify the SDK loads correctly

Everything is installed and configured. Before you move on, take a moment to predict: do you think Python can find and import the Anthropic SDK right now?

  • Test that the SDK loads by running this command:
python3 -c "from anthropic import Anthropic; print('SDK ready')"

You should see SDK ready printed in your terminal. That single line confirms Python can find the Anthropic SDK inside your virtual environment.

Not seeing SDK ready?

If you see ModuleNotFoundError, the anthropic package is not installed in your active environment. Make sure (.venv) appears in your prompt, then re-run the pip install command from the previous substep.

If you see a syntax error, make sure you copied the full command including both quote marks around the Python code.

Help me get the Anthropic SDK import working.

Your environment is locked and loaded. Next up, you'll write your first prompt to Claude and see the Payoff Engine generate its first batch of motivating statements.

Build a Basic Payoff Generator

Your Python environment is set up and the Anthropic SDK is installed. Now it's time to prove the core idea behind the Payoff Engine.

Can Claude take a dry project description and generate a payoff statement that makes someone want to do the project? You'll write a short Python script that sends a project description to the Claude API and prints the results.

In this step, get ready to:
  • Create a generate.py script that calls the Claude API with a system prompt.
  • Run the generator and see payoff statements printed in your terminal.
  • Spot the limitations of generic, unstructured output.
Create the generate.py file

Your script needs three things: a connection to the Claude API, instructions telling Claude how to behave (the system prompt), and a function that sends a project description and returns the response.

  • In Cursor, create a new file called generate.py in your payoff-engine folder.
  • Add the imports, API client setup, and system prompt by pasting this code:
import os
from anthropic import Anthropic

# Create the Anthropic client using your API key from the environment
client = Anthropic(
    api_key=os.environ.get("ANTHROPIC_API_KEY"),
)

# Choose which Claude model to use for generation
MODEL = "claude-sonnet-4-6"

# System prompt: instructions that tell Claude how to generate payoffs
SYSTEM_PROMPT = """You are the Payoff Engine. Your job is to take a project description and generate emotionally compelling, concrete payoff statements that make someone think "I NEED to do this project."

Rules:
- Every payoff must be specific and concrete. Never say "improve your career" or "learn valuable skills."
- Generate exactly 3 payoff statements as a numbered list."""

What does this code do?

The first few lines import the Anthropic SDK and create a client that authenticates using the ANTHROPIC_API_KEY environment variable you set in Step 1.

MODEL stores the model ID claude-sonnet-4-6 so you can change it in one place later. SYSTEM_PROMPT is a set of instructions that tells Claude what kind of output to produce. Think of it as the personality and rulebook for every API call you make.

  • Now add the generation function and the entry point below the system prompt:
def generate_payoffs(project_description: str) -> str:
    # Send the project description to Claude with our system prompt
    response = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Project: {project_description}",
            }
        ],
    )
    # Return just the text from Claude's response
    return response.content[0].text

if __name__ == "__main__":
    # Test with a sample project description
    project = "Build a REST API with FastAPI"
    result = generate_payoffs(project)
    print(result)

What does this code do?

  • generate_payoffs() calls client.messages.create() with the model, a max token limit, the system prompt, and a user message containing the project description.
  • response.content[0].text extracts the plain text from Claude's response. The API returns a list of content blocks, and [0].text grabs the text from the first one.
  • The if __name__ == "__main__" block runs only when you execute the file directly. It tests the function with a hardcoded sample project: "Build a REST API with FastAPI".
  • Save generate.py.

Seeing a syntax error?

  • Check that the triple-quoted string for SYSTEM_PROMPT opens and closes with """. A missing set of quotes will break the file.
  • Make sure the messages list inside client.messages.create() has matching brackets and braces. Each opening [ and { needs a matching close.

Why is my generate.py throwing a syntax error when I try to save?

✔️ Awesome, I've got everything!

Great. Make sure you have saved generate.py before moving on.

ⓧ I'd like to double check the full code

Your generate.py should contain the two code blocks you added above, in this order:

  • Block 1: import os and from anthropic import Anthropic, the client setup, MODEL, and SYSTEM_PROMPT.
  • Block 2: The generate_payoffs() function and the if __name__ == "__main__" block that calls it with "Build a REST API with FastAPI".

Scroll up and confirm each block is present and in order, then save the file.

Run the generator

The script is ready. Before you run it, take a moment to predict what you will see.

Do you think Claude will generate payoffs that feel personalized to a specific type of learner, or will they be broad enough to apply to anyone?

  • Run the generator by entering this command in your terminal:
python3 generate.py

You see three numbered payoff statements printed to your terminal. They sound motivating at first glance.

Now read them again more carefully. The statements are broad and generic. They could apply to almost any learner taking almost any project. There is no structure a frontend team could render into a card or chart. There is nothing personal about them.

Why is this a problem?

A payoff like "Master a modern web framework" sounds fine in isolation. But it does not make a Marvel fan think of building J.A.R.V.I.S., and it does not make a side-hustle builder see dollar signs.

The output is also a plain wall of text. A frontend team cannot render a stat card, a before/after comparison, or a hero banner from a paragraph. You need both personalization and structure. Those are the two gaps you will close in the next steps.

Getting an authentication error?

  • Make sure your ANTHROPIC_API_KEY environment variable is still set. Run echo $ANTHROPIC_API_KEY in your terminal to check. If it is empty, re-export it.
  • Verify your API key is valid and has available credits on the Anthropic dashboard.

Why is my Payoff Engine script failing with an authentication error?

You have proven the core idea works. Claude can generate payoff statements from a project description. But the output is generic and unstructured. Next up, you will add learner profiles so the same project produces completely different payoffs for different people.

Add Learner Profiles for Personalized Payoffs

Your Payoff Engine can generate payoffs, but read them closely. "Master API development fundamentals." "Build production-ready backend skills." These could apply to anyone on the planet.

The same project should feel completely different to someone who loves Marvel versus someone building a side hustle. In this step, you will add a learner profile so Claude can weave someone's specific interests and goals into every payoff it generates.

In this step, get ready to:
  • Update the system prompt to incorporate learner profile data.
  • Modify the generator function to accept and send profile information to Claude.
  • Test personalization by running the same project through two very different learner profiles.
Update the system prompt to know about learner profiles

The system prompt is Claude's instruction manual. Right now it only knows about project descriptions. You need to tell it that a learner profile might arrive and that it should use that profile to make every payoff personal.

  • In generate.py, find the SYSTEM_PROMPT variable. It currently starts with:
SYSTEM_PROMPT = """You are the Payoff Engine. Your job is to take a project description and generate emotionally compelling, concrete payoff statements that make someone think "I NEED to do this project."
  • Replace the entire SYSTEM_PROMPT variable with this updated version:
SYSTEM_PROMPT = """You are the Payoff Engine. Your job is to take a project description and a learner profile, then generate emotionally compelling, concrete payoff statements that make someone think "I NEED to do this project."

Rules:
- Every payoff must be specific and concrete. Never say "improve your career" or "learn valuable skills."
- If a learner profile is provided, weave their specific interests, goals, and passions into every payoff. A Marvel fan should see Iron Man references. A side-hustle builder should see dollar amounts.
- Generate exactly 3 payoff statements as a numbered list."""

What changed in the prompt?

Two things changed. The opening line now says "a project description and a learner profile" so Claude knows to expect both inputs.

A new rule tells Claude exactly how to use the profile: weave the learner's interests into every payoff. The examples ("A Marvel fan should see Iron Man references") give Claude a concrete model to follow.

Update the function to accept and send learner profiles

The system prompt now expects profile data, but the generate_payoffs function has no way to receive or forward it. You need to update the function signature and build a user message that includes the profile details.

  • In generate.py, find the generate_payoffs function. Replace the entire function with this updated version:
def generate_payoffs(project_description: str, learner_profile: dict | None = None) -> str:
    # Build the user message with optional profile data
    user_message = f"Project: {project_description}"
    if learner_profile:
        user_message += f"\n\nLearner Profile:\n"
        user_message += f"- Interests: {learner_profile.get('interests', 'Not specified')}\n"
        user_message += f"- Goals: {learner_profile.get('goals', 'Not specified')}\n"
        user_message += f"- Excited by: {learner_profile.get('excited_by', 'Not specified')}"

    response = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": user_message,
            }
        ],
    )
    return response.content[0].text

What does this code do?

  • learner_profile: dict | None = None makes the profile optional. If no profile is passed, the function works exactly like before.
  • The if learner_profile: block appends the learner's interests, goals, and excitement triggers to the user message. Claude sees this data alongside the project description.
  • learner_profile.get('interests', 'Not specified') safely retrieves each field, falling back to "Not specified" if the key is missing.
  • Save generate.py.
  • Confirm nothing broke by running the script with its current __main__ block (which still passes no profile):
python3 generate.py

You should see 3 payoff statements printed, similar to what you saw in the previous step. The function still works without a profile, which is exactly what the None default gives you.

Seeing a syntax error?

  • Check that the SYSTEM_PROMPT string uses triple quotes (""") and that you closed them properly.
  • Make sure the function signature has dict | None = None with the pipe symbol, not the word "or".
  • help me debug a syntax error in my generate_payoffs function
Create sample profiles and test personalization

Now for the fun part. You will create two very different learner profiles and run the same project through both. If personalization works, the payoffs should feel like they were written for two completely different people.

  • In generate.py, find the if __name__ == "__main__": block at the bottom. Replace the entire block with this:
if __name__ == "__main__":
    project = "Build a REST API with FastAPI"

    # A learner who loves Marvel and wants a tech career
    marvel_fan = {
        "interests": "Marvel, Iron Man, building cool tech",
        "goals": "Get a software engineering job at a top company",
        "excited_by": "Feeling like Tony Stark building J.A.R.V.I.S.",
    }

    # A learner focused on entrepreneurship and income
    entrepreneur = {
        "interests": "Side hustles, passive income, automation",
        "goals": "Build a $10k/month online business",
        "excited_by": "Making money while sleeping, scaling systems",
    }

    print("=== Marvel Fan Payoffs ===")
    print(generate_payoffs(project, marvel_fan))

    print("\n=== Entrepreneur Payoffs ===")
    print(generate_payoffs(project, entrepreneur))

What does this code do?

  • Two dictionaries (marvel_fan and entrepreneur) represent two learner profiles with different interests, goals, and excitement triggers.
  • The script calls generate_payoffs twice with the same project but different profiles, printing both sets of results with clear section headers.
  • This is the same project ("Build a REST API with FastAPI") both times. The only thing that changes is who the payoffs are for.
  • Save generate.py.

Before you run this, think about what you expect to see. Both profiles are looking at the same FastAPI project. How do you think the Marvel fan's payoffs will differ from the entrepreneur's?

  • Run the script:
python3 generate.py

You should see two clearly labelled sections of payoff statements. The Marvel fan's payoffs reference Iron Man, J.A.R.V.I.S., or building superhero-level tech. The entrepreneur's payoffs talk about revenue, automation, and scaling a business.

Both sets of payoffs look generic?

  • Check your SYSTEM_PROMPT includes the new rule about weaving interests into payoffs. If the old prompt is still there, Claude does not know to personalize.
  • Make sure you are passing marvel_fan and entrepreneur as the second argument to generate_payoffs. If you forgot the second argument, the profile defaults to None and Claude gets no profile data.
  • help me figure out why my payoffs are not personalized to the learner profile

Same project. Two completely different sets of payoffs. That is the power of personalization.

✔️ Awesome, I've got everything!

Great. Double check you have saved generate.py.

ⓧ I'd like to double check the full code

Scroll up through your generate.py and confirm these sections are present, in this order:

  • Imports (os and Anthropic) and the client setup at the top.
  • MODEL set to claude-sonnet-4-6.
  • SYSTEM_PROMPT with the updated first line ("and a learner profile") and the new rule about weaving interests into payoffs.
  • generate_payoffs function accepting both project_description and learner_profile, building a user_message that includes profile data when provided.
  • __main__ block defining marvel_fan and entrepreneur dictionaries, calling generate_payoffs twice, and printing both results.

The payoffs now feel personal and specific. But look at the output closely. It is still just walls of text. A frontend team cannot render a stat card or a hero metric from a paragraph. Next up, you will add structured output so every payoff comes back as a typed object your team can drop straight into a UI.

Add Multi-Format Structured Output

Your payoffs are now personalized, but they are still walls of text. A frontend team cannot render a stat card from a paragraph, or build a before/after comparison visual from a sentence.

In this step, you will define structured data models for each payoff format. Then you will tell Claude to return validated, typed objects instead of raw text. The result is data your frontend team can render directly as cards, charts, and hero banners.

In this step, get ready to:
  • Define Pydantic models for each payoff format (headline, stat, comparison, hero).
  • Switch from client.messages.create() to client.messages.parse() for structured output.
  • Run the generator and see typed JSON objects in your terminal.
Define the Pydantic models

Each payoff format needs a data model that defines exactly which fields it contains. Pydantic models act as a contract between your AI backend and your frontend team.

  • In generate.py, find the two import lines at the top of the file:
import os
from anthropic import Anthropic
  • Replace them with these four import lines, then add the four payoff type models directly below the MODEL line:
import os
import json
from anthropic import Anthropic
from pydantic import BaseModel


client = Anthropic(
    api_key=os.environ.get("ANTHROPIC_API_KEY"),
)

MODEL = "claude-sonnet-4-6"


# A punchy hook line with supporting context
class HeadlinePayoff(BaseModel):
    type: str = "headline"
    hook: str
    context: str


# A single impressive metric
class StatPayoff(BaseModel):
    type: str = "stat"
    value: str
    label: str
    context: str

What does this code do?

Each class inherits from BaseModel, which means Pydantic will validate that the data matches the shape you defined. If Claude returns a headline without a hook field, Pydantic will catch that automatically.

The type field on each model has a default value (like "headline") so Claude does not need to guess the type name. The json import will be used later to pretty-print the structured output.

  • Directly below StatPayoff, add the remaining two payoff type models by copying this code:
# A before/after that shows the transformation
class ComparisonPayoff(BaseModel):
    type: str = "comparison"
    before: str
    after: str
    insight: str


# A big metric with a Jarvis-style statement
class HeroPayoff(BaseModel):
    type: str = "hero"
    metric: str
    metric_label: str
    statement: str

What does this code do?

ComparisonPayoff captures a before/after transformation, like "3 hours deploying manually" vs. "30 seconds, one command." HeroPayoff holds a big-impact metric with a label and statement, perfect for a banner visual.

  • Now add the wrapper models below HeroPayoff. These group the individual payoff types into a single response structure by copying this code:
# Each payoff item holds one of the four types
class PayoffItem(BaseModel):
    headline: HeadlinePayoff | None = None
    stat: StatPayoff | None = None
    comparison: ComparisonPayoff | None = None
    hero: HeroPayoff | None = None


# The full response is a list of payoff items
class PayoffResponse(BaseModel):
    payoffs: list[PayoffItem]

Why are all the PayoffItem fields optional?

Each PayoffItem holds exactly one payoff type. A headline payoff will populate the headline field and leave stat, comparison, and hero as None. This is why every field defaults to None.

  • Save generate.py.
  • Verify your models load correctly by running this command:
python3 generate.py

You should see the same personalized text payoffs as before. The models are defined but not used yet.

Seeing an import error?

Make sure you have from pydantic import BaseModel at the top of your file. Pydantic is installed automatically with the anthropic package, so you do not need to install it separately.

Help me debug a Pydantic import error in my generate.py file.

Switch from client.messages.create() to client.messages.parse()

The Anthropic SDK has a dedicated method for structured output. Instead of getting raw text back and parsing it yourself, client.messages.parse() accepts a Pydantic model and returns validated, typed output directly.

  • In generate.py, find the SYSTEM_PROMPT variable (it starts with SYSTEM_PROMPT = """You are the Payoff Engine.).
  • Replace the entire SYSTEM_PROMPT variable with this updated version that tells Claude about the four structured types:
SYSTEM_PROMPT = """You are the Payoff Engine. Your job is to take a project description and a learner profile, then generate emotionally compelling, concrete payoff statements that make someone think "I NEED to do this project."

Rules:
- Every payoff must be specific and concrete. Never say "improve your career" or "learn valuable skills." Instead say "Build a system that deploys your side-hustle website in 30 seconds for $0.03/month."
- If a learner profile is provided, weave their specific interests, goals, and passions into every payoff. A Marvel fan should see Iron Man references. A side-hustle builder should see dollar amounts.
- Generate exactly 4 payoffs, one of each type:
  1. headline: A punchy hook line with supporting context
  2. stat: A single impressive metric with a label and context
  3. comparison: A before/after that shows the transformation
  4. hero: A big metric with a Jarvis-style statement

For each payoff item, populate ONLY the field matching its type and leave the others as null."""

What changed in the prompt?

The prompt now asks for exactly 4 payoffs instead of 3, one per type. It describes each type (headline, stat, comparison, hero) so Claude knows the shape of the data it needs to fill.

The last line is critical: it tells Claude to populate only the matching field and leave the others as null. This ensures each PayoffItem holds exactly one payoff type.

  • Now replace the entire generate_payoffs function (from def generate_payoffs down to the return line) with this updated version:
def generate_payoffs(
    project_description: str,
    learner_profile: dict | None = None,
) -> PayoffResponse:
    user_message = f"Project: {project_description}"
    if learner_profile:
        user_message += f"\n\nLearner Profile:\n"
        user_message += f"- Interests: {learner_profile.get('interests', 'Not specified')}\n"
        user_message += f"- Goals: {learner_profile.get('goals', 'Not specified')}\n"
        user_message += f"- Excited by: {learner_profile.get('excited_by', 'Not specified')}"

    # Use parse() instead of create() for structured output
    response = client.messages.parse(
        model=MODEL,
        max_tokens=2048,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": user_message,
            }
        ],
        output_format=PayoffResponse,
    )

    return response.parsed_output

What changed in the function?

  • The return type changed from str to PayoffResponse. The function now returns a typed Pydantic object, not raw text.
  • client.messages.create() became client.messages.parse(). The parse() method is the Anthropic SDK's built-in method for structured output.
  • output_format=PayoffResponse tells Claude the exact shape of data to return. The SDK validates the response against your Pydantic model automatically.
  • response.parsed_output gives you a ready-to-use PayoffResponse object instead of digging into response.content[0].text.

The last change is in the __main__ block. Since generate_payoffs now returns a Pydantic object instead of a string, you need to convert it to JSON for printing.

  • Scroll down to the if __name__ == "__main__": block. Find these four lines at the bottom:
    print("=== Marvel Fan Payoffs ===")
    print(generate_payoffs(project, marvel_fan))

    print("\n=== Entrepreneur Payoffs ===")
    print(generate_payoffs(project, entrepreneur))
  • Replace those four lines with this updated version that captures the result and prints it as formatted JSON:
    print("=== Marvel Fan Payoffs ===")
    result = generate_payoffs(project, marvel_fan)
    print(json.dumps(result.model_dump(), indent=2))

    print("\n=== Entrepreneur Payoffs ===")
    result = generate_payoffs(project, entrepreneur)
    print(json.dumps(result.model_dump(), indent=2))

What does model_dump() do?

result.model_dump() converts the Pydantic object into a plain Python dictionary. Wrapping it in json.dumps() with indent=2 prints it as nicely formatted JSON you can read in your terminal.

  • Save generate.py.

Seeing a syntax error on save?

Check that your SYSTEM_PROMPT starts and ends with triple quotes ("""). Also make sure the old SYSTEM_PROMPT and old generate_payoffs function are fully replaced, not duplicated.

Help me fix a syntax error in my generate.py file.

Run the generator and see structured JSON

Before you run this, think about what you expect the output to look like now. Will it be paragraphs of text like before, or something different?

  • Run the generator by running this command:
python3 generate.py

Instead of walls of text, you now see a JSON array of typed payoff objects. Each object has a type field ("headline", "stat", "comparison", or "hero") and type-specific structured data fields. This is data a frontend team can render directly into cards, charts, and hero banners.

Getting an API error?

Make sure your ANTHROPIC_API_KEY environment variable is still set. If you opened a new terminal, you will need to re-export it.

If you see a validation error from Pydantic, check that your model definitions match the code above exactly. Field names and types need to match for parse() to validate successfully.

Help me debug an error when running my Payoff Engine with structured output.

✔️ Awesome, I've got everything!

Great. Double check you have saved generate.py.

ⓧ I'd like to double check the full code

Your generate.py is longer than 30 lines now, so scroll through your file and confirm each section is present in this order:

  • Imports: os, json, Anthropic, and BaseModel.
  • Client and model: client = Anthropic(...) and MODEL = "claude-sonnet-4-6".
  • Six Pydantic models: HeadlinePayoff, StatPayoff, ComparisonPayoff, HeroPayoff, PayoffItem, PayoffResponse.
  • Updated SYSTEM_PROMPT: references 4 payoff types and the null instruction.
  • Updated generate_payoffs function: returns PayoffResponse, uses client.messages.parse() with output_format=PayoffResponse, returns response.parsed_output.
  • Updated __main__ block: uses result = generate_payoffs(...) and json.dumps(result.model_dump(), indent=2).

Your Payoff Engine now returns structured, typed data that any frontend can render. Next up, you will wrap this in a FastAPI web service so your engineering team can hit it as an API endpoint.

Wrap It in a FastAPI Web Service

Your generate.py script proves the concept. It takes a project description and a learner profile, and it returns structured, personalized payoffs. But a Python script is something only you can run.

Your engineering team needs an HTTP endpoint they can call from a frontend. FastAPI gives you a production-ready web service with auto-generated interactive docs at /docs that your team can explore immediately.

In this step, get ready to:
  • Create a main.py file with a FastAPI app and your Pydantic data models.
  • Add a health check endpoint and the main generation endpoint.
  • Run the server and test it through the interactive Swagger UI at /docs.
Create your FastAPI app and define the data models

You are creating a brand new file for the web service. Much of the code is the same Pydantic models and system prompt you already wrote in generate.py.

  • In Cursor, create a new file called main.py in the same folder as generate.py.
  • Add the imports, client setup, and FastAPI app creation by pasting this code:
import os
import json
from anthropic import Anthropic
from fastapi import FastAPI
from pydantic import BaseModel


# Initialize the Anthropic client using your API key
client = Anthropic(
    api_key=os.environ.get("ANTHROPIC_API_KEY"),
)

MODEL = "claude-sonnet-4-6"

# Create the FastAPI app with metadata for the docs page
app = FastAPI(
    title="Payoff Engine API",
    description="Transform project descriptions into emotionally compelling, personalized payoff statements.",
    version="0.1.0",
)

What does this code do?

The first few lines are familiar from generate.py. The new part is FastAPI(). The title, description, and version you pass here appear on the auto-generated docs page at /docs.

Notice from fastapi import FastAPI is a new import that was not in generate.py.

  • Next, add the four payoff type models below the app creation. These are the same models from generate.py:
class HeadlinePayoff(BaseModel):
    type: str = "headline"
    hook: str
    context: str


class StatPayoff(BaseModel):
    type: str = "stat"
    value: str
    label: str
    context: str


class ComparisonPayoff(BaseModel):
    type: str = "comparison"
    before: str
    after: str
    insight: str


class HeroPayoff(BaseModel):
    type: str = "hero"
    metric: str
    metric_label: str
    statement: str

Why repeat the models?

Each file in this project is self-contained. main.py is your web service. generate.py is your standalone script. Both need the same Pydantic models to define the shape of payoff data.

  • Save main.py.
  • Confirm the app loads by running this command in your terminal:
fastapi dev main.py

You should see output that includes Server started at http://127.0.0.1:8000 and Application startup complete.

  • Press Ctrl+C in the terminal to stop the server before continuing.

Server did not start?

  • Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt.
  • Check that main.py is saved in the same folder where you activated the virtual environment.
  • If you see an import error, confirm you installed fastapi[standard] in Step 1.

Still stuck? Help me debug why fastapi dev main.py is not starting my server.

Add the request models, system prompt, and health check

The payoff type models define the output shape. Now you need models for the incoming request, plus the system prompt and a simple health check endpoint.

  • In main.py, add these models below HeroPayoff:
class PayoffItem(BaseModel):
    headline: HeadlinePayoff | None = None
    stat: StatPayoff | None = None
    comparison: ComparisonPayoff | None = None
    hero: HeroPayoff | None = None


class PayoffResponse(BaseModel):
    payoffs: list[PayoffItem]


# New: define the shape of incoming learner profile data
class LearnerProfile(BaseModel):
    interests: str = ""
    goals: str = ""
    excited_by: str = ""


# New: define the shape of the full API request
class GenerateRequest(BaseModel):
    project_description: str
    learner_profile: LearnerProfile | None = None

What is new here?

PayoffItem and PayoffResponse are the same container models from generate.py. The two new models are LearnerProfile and GenerateRequest.

LearnerProfile is a proper Pydantic model instead of a plain dictionary. This gives your API automatic validation and documentation. GenerateRequest bundles the project description and optional learner profile into one request object.

  • Below GenerateRequest, add the system prompt and the health check endpoint:
SYSTEM_PROMPT = """You are the Payoff Engine. Your job is to take a project description and a learner profile, then generate emotionally compelling, concrete payoff statements that make someone think "I NEED to do this project."

Rules:
- Every payoff must be specific and concrete. Never say "improve your career" or "learn valuable skills." Instead say "Build a system that deploys your side-hustle website in 30 seconds for $0.03/month."
- If a learner profile is provided, weave their specific interests, goals, and passions into every payoff. A Marvel fan should see Iron Man references. A side-hustle builder should see dollar amounts.
- Generate exactly 4 payoffs, one of each type:
  1. headline: A punchy hook line with supporting context
  2. stat: A single impressive metric with a label and context
  3. comparison: A before/after that shows the transformation
  4. hero: A big metric with a Jarvis-style statement

For each payoff item, populate ONLY the field matching its type and leave the others as null."""


@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "payoff-engine"}

What does this code do?

SYSTEM_PROMPT is the same prompt from generate.py. It tells Claude how to behave.

The @app.get("/health") decorator turns health_check() into a GET endpoint. This is a standard pattern for APIs. It lets monitoring tools confirm the service is running without triggering any expensive logic.

  • Save main.py and start the server again:
fastapi dev main.py
  • Open http://127.0.0.1:8000/health in your browser.

You should see {"status":"healthy","service":"payoff-engine"} displayed in the browser. Your first endpoint is live.

  • Press Ctrl+C in the terminal to stop the server.

Seeing an error instead of JSON?

  • If you see a syntax error, check that the SYSTEM_PROMPT string starts and ends with triple double quotes """.
  • Make sure @app.get("/health") appears directly above async def health_check(): with no blank lines between them.

Need help? Help me fix the error I see when running fastapi dev main.py after adding my health check endpoint.

Add the generation endpoint and test your API

The health check confirms the server runs. Now you need the real endpoint: the one that accepts a project description and learner profile, calls Claude, and returns structured payoffs.

  • In main.py, add the generation endpoint below the health_check function:
@app.post("/generate-payoffs", response_model=PayoffResponse)
async def generate_payoffs(request: GenerateRequest):
    # Build the user message from the request data
    user_message = f"Project: {request.project_description}"
    if request.learner_profile:
        user_message += f"\n\nLearner Profile:\n"
        user_message += f"- Interests: {request.learner_profile.interests}\n"
        user_message += f"- Goals: {request.learner_profile.goals}\n"
        user_message += f"- Excited by: {request.learner_profile.excited_by}"

    # Call Claude with structured output parsing
    response = client.messages.parse(
        model=MODEL,
        max_tokens=2048,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": user_message,
            }
        ],
        output_format=PayoffResponse,
    )

    return response.parsed_output

What does this endpoint do?

The @app.post("/generate-payoffs") decorator creates a POST endpoint. The response_model=PayoffResponse tells FastAPI to document and validate the response shape automatically.

The function body is nearly identical to the generate_payoffs() function in generate.py. The difference is that it reads from a typed GenerateRequest object instead of raw arguments. FastAPI parses the incoming JSON into this model before your function runs.

  • Save main.py.

Before you start the server, what do you think you will see at http://127.0.0.1:8000/docs?

  • Start the server by running:
fastapi dev main.py
  • Open http://127.0.0.1:8000/docs in your browser.

You should see an interactive Swagger UI page titled Payoff Engine API. It lists your two endpoints: GET /health and POST /generate-payoffs.

  • Click on the POST /generate-payoffs row to expand it.
  • Click Try it out.
  • Replace the JSON in the request body with this test payload:
{
  "project_description": "Build a REST API with FastAPI",
  "learner_profile": {
    "interests": "Marvel, Iron Man, building cool tech",
    "goals": "Get a software engineering job at a top company",
    "excited_by": "Feeling like Tony Stark building J.A.R.V.I.S."
  }
}
  • Click Execute.

After a few seconds, the response body appears below the button. You should see a JSON array of payoff objects, each with a type field like headline, stat, comparison, or hero and structured data tailored to a Marvel fan.

Seeing a 500 error in the response?

  • Check your terminal for the full error traceback. The most common cause is a missing or invalid ANTHROPIC_API_KEY environment variable.
  • If the error mentions PayoffResponse or parsing, make sure all six Pydantic models (HeadlinePayoff through PayoffResponse) are defined in main.py before the endpoint.
  • If you stopped and restarted your terminal, re-export your API key with export ANTHROPIC_API_KEY= followed by your key value.

Still stuck? Help me debug a 500 error from my /generate-payoffs FastAPI endpoint.

✔️ Awesome, I've got everything!

Great. Double check that you have saved main.py and that your server is still running.

ⓧ I'd like to double check the full code

Your main.py should contain these sections in order. Scroll up and confirm each one is present:

  • Imports (os, json, Anthropic, FastAPI, BaseModel) and client setup.
  • FastAPI app creation with title, description, and version.
  • Four payoff type models: HeadlinePayoff, StatPayoff, ComparisonPayoff, HeroPayoff.
  • Container models: PayoffItem and PayoffResponse.
  • Request models: LearnerProfile and GenerateRequest.
  • SYSTEM_PROMPT string.
  • GET /health endpoint.
  • POST /generate-payoffs endpoint with response_model=PayoffResponse.

generate.py is unchanged from the previous step.

Secret mission

Add Community Showcase Mode

Sometimes the most motivating payoff is not a stat card or a headline. It is seeing what five completely different people built from the same project. In this mission, you will add a community showcase endpoint that generates diverse persona-based payoff stories.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with no ongoing costs. You are only charged for Claude API calls when you actively send requests.

Resources you used:

  • Your project folder on the Desktop, containing generate.py, main.py, and the .venv virtual environment with installed packages.
  • Claude API usage during development (billed per call, no ongoing charges when idle).
  • The running FastAPI dev server in your terminal.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with new prompts and learner profiles.

  • Your FastAPI server continues running at http://127.0.0.1:8000/docs until you stop it.
  • Your virtual environment stays active in the current terminal session.
  • You are only charged for Claude API calls when you actively send requests through the endpoints.

Pause - I'll come back to this later

Shut down running processes to free up your terminal. Your project files stay on disk so you can pick up where you left off.

  • Stop the FastAPI dev server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • When you are ready to come back, open a terminal in your project folder and reactivate the environment.

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate
  • Re-export your API key before making any Claude API calls.

macOS

export ANTHROPIC_API_KEY=your-api-key-here

Windows

$env:ANTHROPIC_API_KEY="your-api-key-here"
  • Then restart the server with fastapi dev main.py.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code and virtual environment permanently.

  • Stop the FastAPI dev server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name"]]
  • Your Anthropic API key remains active on your Anthropic account. No further action is needed unless you want to revoke it from the Anthropic console.

Nice Work!

Nice Work!

Nice work! You just built a Payoff Engine that takes dry project descriptions and turns them into personalized, emotionally compelling motivation powered by the Claude API.

You learned how to:

  • Use the Claude API with Pydantic models and client.messages.parse() to generate typed, multi-format payoff data (headlines, stats, comparisons, hero banners) that a frontend team can render directly.
  • Add a personalization layer through learner profiles that transforms generic AI output into something that feels custom-made for each person's interests and goals.
  • Wrap everything in a FastAPI web service with auto-generated interactive docs at /docs, ready for your engineering team to integrate with.
  • Secret Mission: Added a community showcase mode that generates diverse persona-based payoff stories, showing the breadth of what one project can produce.

Ready to quiz yourself?