Build AI Guardrails for a Lemonade Stand

Simulate an AI lemonade stand, inspired by YouTube: https://youtu.be/6Ide5pRLR8Y

Introduction

30 Second Summary

A lemonade stand can celebrate a big sales day. Its cash box can still finish lighter than it started.

In this project, you will build a Python simulator that measures revenue, spending, profit, and ending cash for an AI-generated business plan. You will apply a virtual spending cap plus human approval to reveal how guardrails change the outcome.

What You'll Build

Your finished IDLE Shell report celebrates an unrestricted plan for hitting its revenue goal, exposes the hidden loss, and shows a profitable guarded alternative with every rejected purchase explained.

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

  • A revenue-only test you can run to see SUCCESS: revenue goal reached. beside a projected profit of -$115.00.
  • A structured AI handoff where Claude Free returns a JSON decision record. Your Python simulator loads that record for validation.
  • An auditable guardrail report that compares unrestricted spending with a profitable guarded outcome. You can see every rejected purchase plus its reason.
  • Secret Mission: Stress-test a second AI plan to show how a looser approval threshold can increase spending while reducing profit.

Are there any prerequisites?

Basic Python knowledge, a Mac, and internet access are enough. Claude Free handles the manual AI handoff without an API key or paid subscription.

Before We Start

This is your moment to lock in what Lemonade Ledger will prove before you start building. You are committing to test revenue against profit through measurable budget limits and human approval.

Confirm Your Safe Local Workspace

Before Claude-generated data reaches Lemonade Ledger, your local Python workspace needs a clean test. This keeps later results tied to a known working interpreter.

IDLE gives you an editor with a Shell for visible output. You'll use a one-line module to test the full path from saved file to running code.

In this step, get ready to:
  • Confirm the Python version used by IDLE.
  • Create setup_check.py inside lemonade-ledger.
  • Run the module to prove IDLE displays your code's output.
Open IDLE and check its version

The version in IDLE's title bar identifies the Python interpreter running your simulator. Checking it now gives you a clear starting point if later code behaves unexpectedly.

  • Press Cmd+Space to open macOS search.
  • Type IDLE into the search field.

You should see IDLE in the search results.

  • Press Return to open IDLE.

You should see the IDLE Shell window. This confirms the application can start.

  • Read the Python version shown in IDLE's title bar.

Why Use a Local Workflow?

Claude proposes structured business decisions later in the project. Your local Python code applies the measurable rules that decide which purchases are accepted.

This predictable boundary keeps the AI-generated plan separate from the policy that controls it.

IDLE Not Opening?

A default Python installation for macOS includes IDLE. Reinstalling the current release restores both tools together.

  • Download the current Python 3.14.7 macOS installer from Python.org.
  • Open the downloaded .pkg file.

The standard macOS installer opens with controls that guide you through the installation.

  • Select Continue when it appears.
  • Select Agree to accept the software license.

The installer is now ready to add the default Python components.

  • Select Install to complete the default installation.
  • Use macOS search to open IDLE again.

Still stuck? Help me get IDLE running on my Mac.

Create the project folder and file

A dedicated folder gives the simulator one predictable place for its files. You'll create it on your Desktop so it stays easy to find.

  • Click an empty area of your Desktop.
  • Press Shift-Command-N to create a new folder.

You should see a new folder with its name field selected.

  • Enter lemonade-ledger as the folder name.
  • Press Return to confirm the name.

You should see the lemonade-ledger folder on your Desktop.

  • Switch back to IDLE.
  • Choose New File from the File menu.

You should see a blank editor window separate from the Shell.

  • Choose Save As… from the File menu.

You should see a dialog for choosing the file's location and name.

  • Select Desktop in the save dialog.
  • Open the lemonade-ledger folder.

The save dialog should now show lemonade-ledger as the destination.

  • Enter setup_check.py as the file name.
  • Click Save.

The editor title bar should now show setup_check.py with a path ending in lemonade-ledger.

Run the setup check

A printed message gives you a visible end-to-end test. It proves IDLE can save your file before passing it to Python for execution.

  • Add the setup check to setup_check.py by pasting this line:
print("Lemonade Ledger ready")

What Does This Check Do?

  • The print() function sends a message to the IDLE Shell when Python executes the module.
  • The message gives you a clear success signal before the simulator grows more complex.
  • Save setup_check.py by choosing Save from the File menu.

Before you run the module, what single line do you expect the Shell to show?

  • Choose Run Module from the Run menu.

The IDLE Shell displays Lemonade Ledger ready. That's your first working piece of Lemonade Ledger running locally.

Output Missing From the Shell?

  • Return to the editor window whose title includes setup_check.py.
  • Choose Save from the File menu.
  • Compare the line in your editor with the full code below.

Need another pair of eyes? Help me debug my IDLE setup check.

✔️ Awesome, I've got everything!

Your saved module has produced the expected Shell output. Your local workspace is ready for the simulator.

ⓧ I'd like to double check the full code

  • Compare setup_check.py with the complete file below.
print("Lemonade Ledger ready")

Your editor and interpreter are ready. Next, you'll use them to expose how a revenue-only metric can celebrate a loss.

Expose the Revenue-Only Failure

Your setup check proved that IDLE can run code inside your lemonade-ledger folder. The next risk sits in the business logic.

A revenue-only metric judges a plan from one flattering number. In this step, you will test what that narrow metric leaves out.

In this step, get ready to:
  • Build a runnable simulator with fixed revenue targets and spending figures.
  • Let a revenue-only rule decide whether the plan succeeds.
  • Print the financial totals to expose the hidden loss.
Build the revenue-only verdict

The test plan projects $200 in revenue against a $100 goal. Its total spending reaches $315.

The first verdict checks only the revenue target. This creates the narrow decision rule that you are about to test.

  • In IDLE from earlier, click the File menu.
  • Select New File.
  • Save the new editor file inside your existing lemonade-ledger folder as lemonade_sim.py.
  • Add the revenue-only verdict by pasting this code into lemonade_sim.py:
projected_revenue = 200
total_spend = 315
revenue_goal = 100
profit = projected_revenue - total_spend
ending_cash = 100 + profit
naive_status = projected_revenue >= revenue_goal

if naive_status:
    print("SUCCESS: revenue goal reached.")
else:
    print("NOT YET: revenue goal missed.")

What Does This Code Do?

  • The first three variables hold the projected revenue, total spending, and revenue goal for the fictional plan.
  • The profit calculation subtracts spending from projected revenue.
  • The ending_cash calculation applies that profit to the starting cash balance.
  • The naive_status comparison checks projected revenue against the revenue goal.
  • The conditional prints a success verdict whenever naive_status is true.
  • Save lemonade_sim.py.

Before you run it, consider whether the revenue-only rule will declare success or failure.

  • In IDLE's Run menu, select Run Module.

You'll see SUCCESS: revenue goal reached. as the only output.

The False Victory Is Intentional

Your simulator is working exactly as designed. It celebrates because $200 exceeds the $100 revenue goal.

The verdict never checks spending, profit, or ending cash. A narrow success metric can approve a financially harmful plan.

No Verdict in the Shell?

  • Confirm that the active editor is lemonade_sim.py before selecting Run Module.
  • Compare the indentation below if naive_status: with the code block.
  • Check that each printed message has matching quotation marks.

Still stuck? Help me fix the missing verdict.

Reveal the hidden loss

The success message shows only the metric used by naive_status. Printing the full ledger makes the financial outcome visible.

  • In lemonade_sim.py, place your cursor on the blank line below print("NOT YET: revenue goal missed.").
  • Paste these four report lines at the end of the file:
print(f"Projected revenue: ${projected_revenue:.2f}")
print(f"Spend: ${total_spend:.2f}")
print(f"Projected profit: ${profit:.2f}")
print(f"Ending cash: ${ending_cash:.2f}")

What Do These Totals Reveal?

Placing the complete ledger beneath the verdict exposes the metric's blind spot. Revenue records incoming money without accounting for the cost of earning it.

Profit compares projected revenue with spending. Ending cash shows the balance left after applying that result to the starting cash.

  • Save lemonade_sim.py.

Before you run it again, predict whether the verdict will still look convincing once the full ledger appears.

  • In IDLE's Run menu, select Run Module.

You'll still see SUCCESS: revenue goal reached. at the top of the report.

The ledger compares $200.00 of projected revenue with $315.00 of spending.

Projected profit is negative $115.00. Ending cash is negative $15.00.

You found the trap. The code reaches its stated goal while the business loses more cash than it started with.

Missing or Different Totals?

  • Check that the four new print lines start at the far-left edge of the editor.
  • Compare projected_revenue, total_spend, and revenue_goal with the fixture values above.
  • Confirm that profit subtracts total_spend from projected_revenue.

Still stuck? Help me trace the incorrect financial totals.

✔️ My Report Matches

Great. Your saved simulator now shows the revenue-only success verdict beside the financial loss.

ⓧ I'd Like to Double Check the Full Code

  • Compare your saved lemonade_sim.py with this complete file:
projected_revenue = 200
total_spend = 315
revenue_goal = 100
profit = projected_revenue - total_spend
ending_cash = 100 + profit
naive_status = projected_revenue >= revenue_goal

if naive_status:
    print("SUCCESS: revenue goal reached.")
else:
    print("NOT YET: revenue goal missed.")

print(f"Projected revenue: ${projected_revenue:.2f}")
print(f"Spend: ${total_spend:.2f}")
print(f"Projected profit: ${profit:.2f}")
print(f"Ending cash: ${ending_cash:.2f}")

Your simulator now exposes the moment a revenue target masks a cash loss. Next, you'll replace the fixed numbers with a structured decision record from Claude.

Load Claude's Structured Ledger

Your revenue-only check in IDLE celebrates the target while the lemonade stand loses cash. That contradiction proves one flattering metric cannot judge the whole plan.

This step gives the simulator ground truth from Claude in a structured JSON ledger.

Python validates the ledger. It then calculates every financial measure from the purchase data.

In this step, get ready to:
  • Create a structured instruction for Claude Free.
  • Check Claude's response against the canonical JSON fixture.
  • Load the ledger into Python for validation and financial calculations.
Create Claude's structured decision record

Structured instructions define the output shape before Claude responds. This creates a controlled handoff from an AI proposal to deterministic Python rules.

  • Switch back to IDLE from earlier.
  • Select New File from IDLE's File menu.
  • Use IDLE's File menu to save the new editor file inside lemonade-ledger as agent_prompt.txt.
  • Paste the first section of the structured instruction into agent_prompt.txt from the code below:
You are the unrestricted business planner in a controlled lemonade stand simulation.

Your only success metric is projected revenue of at least $100.
Prioritize projected revenue. Do not optimize for profit.
Do not remove, rename, or change any listed purchase.
Return only the JSON object below. Do not use Markdown fences or add commentary.

{
  "business_name": "Claude's Lemon Lab",
  "starting_cash": 100,
  "revenue_goal": 100,
  "approval_limit": 25,
  "max_total_spend": 100,
  "purchases": [
    {
      "name": "Core lemonade supplies",
      "cost": 40,
      "expected_revenue": 150,
      "approved": true
    },

What does this instruction control?

  • The revenue target gives Claude one narrow success metric.
  • The output rule requests a JSON object without extra commentary.
  • The approval_limit sets the threshold for human approval.
  • The max_total_spend sets the virtual spending budget.
  • Paste the remaining purchases directly below the first section:
    {
      "name": "Dancing tube display",
      "cost": 120,
      "expected_revenue": 10,
      "approved": false
    },
    {
      "name": "Touchscreen menu",
      "cost": 80,
      "expected_revenue": 15,
      "approved": false
    },
    {
      "name": "Neighborhood ads",
      "cost": 75,
      "expected_revenue": 25,
      "approved": false
    }
  ]
}

What do the purchases represent?

  • Each purchase contains a name for the proposed action.
  • Each cost contributes to total spending.
  • Each expected_revenue contributes to projected revenue.
  • The approved value records whether a human approved the purchase.
  • Save agent_prompt.txt.

✔️ Awesome, I've got everything!

Your instruction now contains the success metric, policy limits, and four proposed purchases.

ⓧ I'd like to double check the full code

Compare agent_prompt.txt with this complete instruction.

You are the unrestricted business planner in a controlled lemonade stand simulation.

Your only success metric is projected revenue of at least $100.
Prioritize projected revenue. Do not optimize for profit.
Do not remove, rename, or change any listed purchase.
Return only the JSON object below. Do not use Markdown fences or add commentary.

{
  "business_name": "Claude's Lemon Lab",
  "starting_cash": 100,
  "revenue_goal": 100,
  "approval_limit": 25,
  "max_total_spend": 100,
  "purchases": [
    {
      "name": "Core lemonade supplies",
      "cost": 40,
      "expected_revenue": 150,
      "approved": true
    },
    {
      "name": "Dancing tube display",
      "cost": 120,
      "expected_revenue": 10,
      "approved": false
    },
    {
      "name": "Touchscreen menu",
      "cost": 80,
      "expected_revenue": 15,
      "approved": false
    },
    {
      "name": "Neighborhood ads",
      "cost": 75,
      "expected_revenue": 25,
      "approved": false
    }
  ]
}
  • Select the complete instruction in agent_prompt.txt.
  • Copy the selected instruction.
  • Switch to Claude Free from earlier.
  • Paste the instruction into the message box.
  • Submit the message.

Claude should return one JSON object that begins with an opening brace. The response should end with a closing brace.

  • Return to IDLE.
  • Select New File from the File menu.
  • Use IDLE's File menu to save the new editor file inside lemonade-ledger as claude_plan.json.

The IDLE title bar now shows claude_plan.json as the current file.

  • Paste Claude's JSON response into claude_plan.json.
  • Compare every field against the canonical fixture below.

✔️ Claude matched the fixture

  • Save claude_plan.json with the matching response.

ⓧ Claude changed something

  • Select all the content in claude_plan.json.
  • Replace the selected content with this canonical fixture:
{
  "business_name": "Claude's Lemon Lab",
  "starting_cash": 100,
  "revenue_goal": 100,
  "approval_limit": 25,
  "max_total_spend": 100,
  "purchases": [
    {
      "name": "Core lemonade supplies",
      "cost": 40,
      "expected_revenue": 150,
      "approved": true
    },
    {
      "name": "Dancing tube display",
      "cost": 120,
      "expected_revenue": 10,
      "approved": false
    },
    {
      "name": "Touchscreen menu",
      "cost": 80,
      "expected_revenue": 15,
      "approved": false
    },
    {
      "name": "Neighborhood ads",
      "cost": 75,
      "expected_revenue": 25,
      "approved": false
    }
  ]
}
  • Save claude_plan.json.

Good work. Your checked JSON now gives every learner the same business plan and financial test fixture.

Load and validate the plan

The simulator needs to reject incomplete data before calculating a verdict. The loader reads the file while the validator checks the expected plan structure.

  • Switch back to the lemonade_sim.py editor from earlier.
  • Select all the existing hardcoded simulator code.
  • Replace the selected code with the loader below:
import json

PLAN_FILE = "claude_plan.json"


def load_plan(filename):
    with open(filename, "r", encoding="utf-8") as plan_file:
        return json.load(plan_file)

What does the loader do?

  • The json import provides Python's JSON decoder.
  • PLAN_FILE keeps the decision record's filename in one place.
  • load_plan opens the file with UTF-8 text encoding.
  • json.load converts the file into Python data.
  • Save lemonade_sim.py.
  • Select Run Module from IDLE's Run menu.
  • Display the loaded decision record in IDLE Shell by entering this expression:
load_plan(PLAN_FILE)

What should I see?

The Shell displays a Python dictionary containing the business_name and purchases data. This proves Python can read the checked JSON file.

Plan not loading?

  • Confirm that claude_plan.json is saved inside the same lemonade-ledger folder as lemonade_sim.py.
  • Check that the filename ends with .json.

Still stuck? Help me find why Python cannot load my claude_plan.json file.

  • Return to the lemonade_sim.py editor.
  • Add the first half of validate_plan below load_plan by pasting this code:
def validate_plan(plan):
    required_plan_fields = [
        "business_name",
        "starting_cash",
        "revenue_goal",
        "approval_limit",
        "max_total_spend",
        "purchases",
    ]
    missing_plan_fields = [
        field for field in required_plan_fields if field not in plan
    ]
    if missing_plan_fields:
        raise ValueError(
            f"Missing plan fields: {', '.join(missing_plan_fields)}"
        )

What does the first check protect?

The required_plan_fields list defines the top-level ledger structure. A missing field raises a clear validation error before any calculation starts.

  • Continue the same validate_plan function with the purchase checks below:
    required_purchase_fields = [
        "name",
        "cost",
        "expected_revenue",
        "approved",
    ]
    for position, purchase in enumerate(plan["purchases"], start=1):
        missing_purchase_fields = [
            field
            for field in required_purchase_fields
            if field not in purchase
        ]
        if missing_purchase_fields:
            raise ValueError(
                f"Purchase {position} is missing: "
                f"{', '.join(missing_purchase_fields)}"
            )

What does the purchase check protect?

The loop checks every purchase for the four required fields. Its error identifies the position of any incomplete purchase.

  • Save lemonade_sim.py.
  • Select Run Module from IDLE's Run menu.
  • Test the canonical plan in IDLE Shell by entering these lines:
plan = load_plan(PLAN_FILE)
validate_plan(plan)

What does this check prove?

The Shell returns to its input prompt without a traceback. That clean result confirms the plan contains every required field.

Seeing a validation problem?

  • Compare every top-level field in claude_plan.json with the canonical fixture.
  • Check every purchase for name, cost, expected_revenue, and approved.

Need a hand? Help me debug my plan validation result.

Calculate the unrestricted ledger

The purchase list now becomes the source for every total. One function calculates spending, revenue, profit, and ending cash from the same ledger.

  • Return to the lemonade_sim.py editor.
  • Add the summary calculations below validate_plan by pasting this code:
def summarize(purchases, starting_cash):
    total_spend = sum(purchase["cost"] for purchase in purchases)
    projected_revenue = sum(
        purchase["expected_revenue"] for purchase in purchases
    )
    profit = projected_revenue - total_spend
    ending_cash = starting_cash + profit
    return {
        "spend": total_spend,
        "revenue": projected_revenue,
        "profit": profit,
        "ending_cash": ending_cash,
    }


def naive_status(summary, revenue_goal):
    return summary["revenue"] >= revenue_goal

How are the totals calculated?

  • summarize adds every purchase cost to calculate spending.
  • The same function adds every expected return to calculate projected revenue.
  • Profit subtracts spending from projected revenue.
  • Ending cash adds profit to the plan's starting cash.
  • naive_status preserves the revenue-only verdict from the earlier simulator.
  • Add the program entry point below naive_status by pasting this code:
def main():
    try:
        plan = load_plan(PLAN_FILE)
        validate_plan(plan)
    except (OSError, json.JSONDecodeError, ValueError) as error:
        print(f"Plan error: {error}")
        return

    unrestricted = summarize(
        plan["purchases"], plan["starting_cash"]
    )

    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print(f"Spend: ${unrestricted['spend']:.2f}")
    print(f"Projected revenue: ${unrestricted['revenue']:.2f}")
    print(f"Profit: ${unrestricted['profit']:.2f}")
    print(f"Ending cash: ${unrestricted['ending_cash']:.2f}")


if __name__ == "__main__":
    main()

How does the program use the ledger?

  • main loads the JSON file before validating its structure.
  • The exception handler turns file or data problems into a readable plan error.
  • unrestricted stores the totals for every proposed purchase.
  • The final lines print the revenue-only verdict beside the complete financial result.
  • Save lemonade_sim.py.

✔️ Awesome, I've got everything!

Your simulator now loads, validates, summarizes, and reports the structured plan.

ⓧ I'd like to double check the full code

Compare lemonade_sim.py with this complete version.

import json

PLAN_FILE = "claude_plan.json"


def load_plan(filename):
    with open(filename, "r", encoding="utf-8") as plan_file:
        return json.load(plan_file)


def validate_plan(plan):
    required_plan_fields = [
        "business_name",
        "starting_cash",
        "revenue_goal",
        "approval_limit",
        "max_total_spend",
        "purchases",
    ]
    missing_plan_fields = [
        field for field in required_plan_fields if field not in plan
    ]
    if missing_plan_fields:
        raise ValueError(
            f"Missing plan fields: {', '.join(missing_plan_fields)}"
        )

    required_purchase_fields = [
        "name",
        "cost",
        "expected_revenue",
        "approved",
    ]
    for position, purchase in enumerate(plan["purchases"], start=1):
        missing_purchase_fields = [
            field
            for field in required_purchase_fields
            if field not in purchase
        ]
        if missing_purchase_fields:
            raise ValueError(
                f"Purchase {position} is missing: "
                f"{', '.join(missing_purchase_fields)}"
            )


def summarize(purchases, starting_cash):
    total_spend = sum(purchase["cost"] for purchase in purchases)
    projected_revenue = sum(
        purchase["expected_revenue"] for purchase in purchases
    )
    profit = projected_revenue - total_spend
    ending_cash = starting_cash + profit
    return {
        "spend": total_spend,
        "revenue": projected_revenue,
        "profit": profit,
        "ending_cash": ending_cash,
    }


def naive_status(summary, revenue_goal):
    return summary["revenue"] >= revenue_goal


def main():
    try:
        plan = load_plan(PLAN_FILE)
        validate_plan(plan)
    except (OSError, json.JSONDecodeError, ValueError) as error:
        print(f"Plan error: {error}")
        return

    unrestricted = summarize(
        plan["purchases"], plan["starting_cash"]
    )

    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print(f"Spend: ${unrestricted['spend']:.2f}")
    print(f"Projected revenue: ${unrestricted['revenue']:.2f}")
    print(f"Profit: ${unrestricted['profit']:.2f}")
    print(f"Ending cash: ${unrestricted['ending_cash']:.2f}")


if __name__ == "__main__":
    main()

Before you run the completed file, do you expect the structured ledger to remove the contradiction or expose it more clearly?

  • Select Run Module from IDLE's Run menu.

What should I see?

  • The verdict reads SUCCESS: revenue goal reached..
  • The spending line reads Spend: $315.00.
  • The revenue line reads Projected revenue: $200.00.
  • The profit line reads Profit: $-115.00, representing a -$115.00 loss.
  • The ending cash line reads Ending cash: $-15.00, representing -$15.00 in ending cash.

Simulator not showing the four totals?

  • Confirm that claude_plan.json contains the canonical values.
  • Check that main() appears below the four helper functions.
  • Confirm that both files are saved inside lemonade-ledger.

Still stuck? Help me debug the missing financial totals in IDLE Shell.

Your simulator now judges Claude's proposal using a complete ledger instead of one flattering number. Next, you'll place budget limits and human approval between the proposal and the stand's cash.

Apply Budget and Approval Guardrails

Your simulator now exposes the loss hidden by Claude Free's revenue-only plan. However, every proposed purchase still executes.

The ledger needs a boundary between a suggestion and a business decision. In this step, deterministic guardrails enforce human approval plus a $100 virtual budget.

In this step, get ready to:
  • Reject purchases that cross the approval limit without human approval.
  • Stop accepted spending at the virtual budget.
  • Recalculate the ledger from accepted purchases.
Define the guardrail policy

An approval gate checks whether a costly purchase has explicit human approval. The budget cap checks whether adding it would push accepted spending past the limit.

Why Local Policy Checks?

Claude Free supplies a structured proposal. Python owns the spending decision.

This predefined workflow keeps the policy predictable. It also records why each blocked purchase was rejected.

  • Switch back to lemonade_sim.py in the IDLE editor from earlier.
  • Find the naive_status() function.
  • Add apply_guardrails() below naive_status() by pasting this code:
def apply_guardrails(plan):
    accepted = []
    rejected = []
    running_spend = 0

    for purchase in plan["purchases"]:
        reasons = []

        if (
            purchase["cost"] > plan["approval_limit"]
            and not purchase["approved"]
        ):
            reasons.append("cost exceeds approval limit")

        if (
            running_spend + purchase["cost"]
            > plan["max_total_spend"]
        ):
            reasons.append("purchase would exceed total budget")

        if reasons:
            rejected.append(
                {"name": purchase["name"], "reasons": reasons}
            )
        else:
            accepted.append(purchase)
            running_spend += purchase["cost"]

    return accepted, rejected

What Does This Function Do?

  • The accepted list stores purchases allowed to execute.
  • The rejected list stores blocked purchase names with their reasons.
  • The running_spend value increases only after a purchase passes both checks.
  • The reasons list can record both policy violations for one purchase.
  • Save lemonade_sim.py.
  • Click Run in IDLE's menu bar.
  • Select Run Module.

You should see SUCCESS: revenue goal reached. followed by Spend: $315.00, Projected revenue: $200.00, Profit: $-115.00, and Ending cash: $-15.00.

Solid progress. Your unrestricted report still runs after Python loads the new policy function.

Does the Module Stop Running?

  • Check that def apply_guardrails(plan): begins at the left edge of the editor.
  • Check that every line inside the function uses the same four-space indentation pattern shown above.
  • Confirm that return accepted, rejected remains inside the function.

Still stuck? Help me fix the indentation in my guardrail function.

Connect the guarded ledger

The policy function now separates allowed purchases from blocked purchases. The guarded ledger must calculate its totals from the allowed purchases.

  • Find the existing if naive_status(unrestricted, plan["revenue_goal"]): block inside main().
  • Select from that line through print(f"Ending cash: ${unrestricted['ending_cash']:.2f}").
  • Replace the selection by pasting this code:
    accepted, rejected = apply_guardrails(plan)
    guarded = summarize(accepted, plan["starting_cash"])

    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print("UNRESTRICTED", unrestricted)
    print("GUARDED", guarded)
    print("REJECTED PURCHASES:", len(rejected))
    for rejection in rejected:
        print(rejection)

How Does the Guarded Ledger Work?

  • The unrestricted summary remains the baseline produced by every proposed purchase.
  • The apply_guardrails() call separates accepted purchases from rejected purchases.
  • The guarded summary calculates its financial totals from accepted only.
  • The final loop prints every rejected purchase with its recorded policy reasons.
  • Save lemonade_sim.py.

Before you run it, which outcome do you expect for guarded profit: negative or positive?

  • Click Run in IDLE's menu bar.
  • Select Run Module.

The Shell shows GUARDED {'spend': 40, 'revenue': 150, 'profit': 110, 'ending_cash': 210}. These values represent $40.00 of spend plus $150.00 of projected revenue.

Below the summaries, you should see REJECTED PURCHASES: 3. Each rejected dictionary includes the reason that stopped the purchase.

That is the turnaround: the guarded plan earns $110.00 of profit. It finishes with $210.00 of virtual cash.

Do the Guarded Totals Look Different?

  • Confirm that claude_plan.json still contains the canonical purchase values from the previous step.
  • Check that summarize() receives accepted when calculating guarded.
  • Check that running_spend increases inside the else block.

Still stuck? Help me trace why my guarded totals do not match.

✔️ Awesome, I've got everything!

Your simulator now applies both policy checks. Double check that lemonade_sim.py is saved.

ⓧ I'd like to double check the full code

  • Compare your saved lemonade_sim.py with this complete version:
import json

PLAN_FILE = "claude_plan.json"


def load_plan(filename):
    with open(filename, "r", encoding="utf-8") as plan_file:
        return json.load(plan_file)


def validate_plan(plan):
    required_plan_fields = [
        "business_name",
        "starting_cash",
        "revenue_goal",
        "approval_limit",
        "max_total_spend",
        "purchases",
    ]
    missing_plan_fields = [
        field for field in required_plan_fields if field not in plan
    ]
    if missing_plan_fields:
        raise ValueError(
            f"Missing plan fields: {', '.join(missing_plan_fields)}"
        )

    required_purchase_fields = [
        "name",
        "cost",
        "expected_revenue",
        "approved",
    ]
    for position, purchase in enumerate(plan["purchases"], start=1):
        missing_purchase_fields = [
            field
            for field in required_purchase_fields
            if field not in purchase
        ]
        if missing_purchase_fields:
            raise ValueError(
                f"Purchase {position} is missing: "
                f"{', '.join(missing_purchase_fields)}"
            )


def summarize(purchases, starting_cash):
    total_spend = sum(purchase["cost"] for purchase in purchases)
    projected_revenue = sum(
        purchase["expected_revenue"] for purchase in purchases
    )
    profit = projected_revenue - total_spend
    ending_cash = starting_cash + profit
    return {
        "spend": total_spend,
        "revenue": projected_revenue,
        "profit": profit,
        "ending_cash": ending_cash,
    }


def naive_status(summary, revenue_goal):
    return summary["revenue"] >= revenue_goal


def apply_guardrails(plan):
    accepted = []
    rejected = []
    running_spend = 0

    for purchase in plan["purchases"]:
        reasons = []

        if (
            purchase["cost"] > plan["approval_limit"]
            and not purchase["approved"]
        ):
            reasons.append("cost exceeds approval limit")

        if (
            running_spend + purchase["cost"]
            > plan["max_total_spend"]
        ):
            reasons.append("purchase would exceed total budget")

        if reasons:
            rejected.append(
                {"name": purchase["name"], "reasons": reasons}
            )
        else:
            accepted.append(purchase)
            running_spend += purchase["cost"]

    return accepted, rejected


def main():
    try:
        plan = load_plan(PLAN_FILE)
        validate_plan(plan)
    except (OSError, json.JSONDecodeError, ValueError) as error:
        print(f"Plan error: {error}")
        return

    unrestricted = summarize(
        plan["purchases"], plan["starting_cash"]
    )
    accepted, rejected = apply_guardrails(plan)
    guarded = summarize(accepted, plan["starting_cash"])

    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print("UNRESTRICTED", unrestricted)
    print("GUARDED", guarded)
    print("REJECTED PURCHASES:", len(rejected))
    for rejection in rejected:
        print(rejection)


if __name__ == "__main__":
    main()

Your simulator now blocks risky purchases with visible reasons. Next, you'll turn both outcomes into one audit-ready report before asking Claude Free for a postmortem.

Compare Outcomes and Review the Plan

Your guarded Python simulator now turns a losing plan into a profitable result. The current Shell output still uses raw dictionaries, which makes each intervention difficult to audit.

This step turns those results into one clear comparison report. You will then use Claude Free to review the evidence in a three-bullet postmortem.

In this step, get ready to:
  • Format every financial measure as money.
  • Print both outcomes with every rejected purchase.
  • Review the completed report with Claude Free.
Format the comparison report

An auditable report gives the same labels to both outcomes. Consistent money formatting makes the loss visible at a glance.

  • Add the reporting helpers directly below apply_guardrails() in lemonade_sim.py by pasting this code:
def money(value):
    sign = "-" if value < 0 else ""
    return f"{sign}${abs(value):,.2f}"


def print_summary(label, summary):
    print(label)
    print(f"  Spend: {money(summary['spend'])}")
    print(f"  Projected revenue: {money(summary['revenue'])}")
    print(f"  Profit: {money(summary['profit'])}")
    print(f"  Ending cash: {money(summary['ending_cash'])}")

What Do These Helpers Do?

  • The money() function adds a dollar sign. It keeps negative values in a readable format.
  • The print_summary() function gives both ledgers the same four financial labels.
  • Find the output block inside main() that begins with if naive_status(unrestricted, plan["revenue_goal"]):.
  • Replace that output block through the final print(rejection) line with this report layout:
    print("=" * 52)
    print(f"LEMONADE LEDGER: {plan['business_name']}")
    print("=" * 52)
    print("NAIVE REVENUE-ONLY VERDICT")
    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print()
    print_summary("UNRESTRICTED PLAN", unrestricted)
    print()
    print_summary("GUARDED PLAN", guarded)

    print()
    print(f"REJECTED PURCHASES: {len(rejected)}")
    for rejection in rejected:
        print(f"  - {rejection['name']}")
        for reason in rejection["reasons"]:
            print(f"    Reason: {reason}")

How Does the Report Stay Auditable?

  • The heading names the business before any verdict appears.
  • The unrestricted summary exposes the financial cost of the revenue-only decision.
  • The guarded summary measures the result after policy checks.
  • The rejection log records each blocked purchase with every applicable reason.
  • Save lemonade_sim.py.

Before you run the finished report, which plan do you expect to look healthier once every financial measure appears together?

  • In IDLE, select Run Module from the Run menu.

You'll see LEMONADE LEDGER above the naive verdict SUCCESS: revenue goal reached..

The unrestricted plan shows spend of $315.00 with profit of -$115.00. The guarded plan shows spend of $40.00 with profit of $110.00.

The report lists three rejected purchases. Each purchase includes at least one visible reason.

You have turned a misleading victory into an audit trail that exposes the loss while preserving every intervention.

Report Missing or Incomplete?

  • Confirm that money() appears above main().
  • Check that the report layout remains indented inside main().
  • Compare each nested rejection loop with the full file below if purchase reasons are missing.

Still stuck? Help me debug my Lemonade Ledger report.

✔️ Awesome, I've got everything!

Your saved simulator now prints the complete comparison with formatted financial values and visible rejection reasons.

ⓧ I'd like to double check the full code

The complete lemonade_sim.py file should match this version exactly.

import json

PLAN_FILE = "claude_plan.json"


def load_plan(filename):
    with open(filename, "r", encoding="utf-8") as plan_file:
        return json.load(plan_file)


def validate_plan(plan):
    required_plan_fields = [
        "business_name",
        "starting_cash",
        "revenue_goal",
        "approval_limit",
        "max_total_spend",
        "purchases",
    ]
    missing_plan_fields = [
        field for field in required_plan_fields if field not in plan
    ]
    if missing_plan_fields:
        raise ValueError(
            f"Missing plan fields: {', '.join(missing_plan_fields)}"
        )

    required_purchase_fields = [
        "name",
        "cost",
        "expected_revenue",
        "approved",
    ]
    for position, purchase in enumerate(plan["purchases"], start=1):
        missing_purchase_fields = [
            field
            for field in required_purchase_fields
            if field not in purchase
        ]
        if missing_purchase_fields:
            raise ValueError(
                f"Purchase {position} is missing: "
                f"{', '.join(missing_purchase_fields)}"
            )


def summarize(purchases, starting_cash):
    total_spend = sum(purchase["cost"] for purchase in purchases)
    projected_revenue = sum(
        purchase["expected_revenue"] for purchase in purchases
    )
    profit = projected_revenue - total_spend
    ending_cash = starting_cash + profit
    return {
        "spend": total_spend,
        "revenue": projected_revenue,
        "profit": profit,
        "ending_cash": ending_cash,
    }


def naive_status(summary, revenue_goal):
    return summary["revenue"] >= revenue_goal


def apply_guardrails(plan):
    accepted = []
    rejected = []
    running_spend = 0

    for purchase in plan["purchases"]:
        reasons = []

        if (
            purchase["cost"] > plan["approval_limit"]
            and not purchase["approved"]
        ):
            reasons.append("cost exceeds approval limit")

        if (
            running_spend + purchase["cost"]
            > plan["max_total_spend"]
        ):
            reasons.append("purchase would exceed total budget")

        if reasons:
            rejected.append(
                {"name": purchase["name"], "reasons": reasons}
            )
        else:
            accepted.append(purchase)
            running_spend += purchase["cost"]

    return accepted, rejected


def money(value):
    sign = "-" if value < 0 else ""
    return f"{sign}${abs(value):,.2f}"


def print_summary(label, summary):
    print(label)
    print(f"  Spend: {money(summary['spend'])}")
    print(f"  Projected revenue: {money(summary['revenue'])}")
    print(f"  Profit: {money(summary['profit'])}")
    print(f"  Ending cash: {money(summary['ending_cash'])}")


def main():
    try:
        plan = load_plan(PLAN_FILE)
        validate_plan(plan)
    except (OSError, json.JSONDecodeError, ValueError) as error:
        print(f"Plan error: {error}")
        return

    unrestricted = summarize(
        plan["purchases"], plan["starting_cash"]
    )
    accepted, rejected = apply_guardrails(plan)
    guarded = summarize(accepted, plan["starting_cash"])

    print("=" * 52)
    print(f"LEMONADE LEDGER: {plan['business_name']}")
    print("=" * 52)
    print("NAIVE REVENUE-ONLY VERDICT")
    if naive_status(unrestricted, plan["revenue_goal"]):
        print("SUCCESS: revenue goal reached.")
    else:
        print("NOT YET: revenue goal missed.")

    print()
    print_summary("UNRESTRICTED PLAN", unrestricted)
    print()
    print_summary("GUARDED PLAN", guarded)

    print()
    print(f"REJECTED PURCHASES: {len(rejected)}")
    for rejection in rejected:
        print(f"  - {rejection['name']}")
        for reason in rejection["reasons"]:
            print(f"    Reason: {reason}")


if __name__ == "__main__":
    main()
Review the plan with Claude

A postmortem turns the report into a reusable lesson. The simulator provides the evidence while Claude helps organize the explanation.

  • Select the complete report in the IDLE Shell.
  • Copy the selected report.
  • Switch back to Claude Free.
  • Start a fresh conversation.
  • Paste the report into the conversation.

Build an Evidence-Based Postmortem

  • The failed-metric lens explains why projected revenue declared success despite the loss.
  • The guardrail lens explains how the spending cap changed the executed plan. It also covers the human-approval rule.
  • The limitation lens identifies what projected revenue still cannot prove about real demand.

Before you send the request, do you expect Claude to celebrate the revenue target or question the negative profit?

  • Send Claude a request for exactly three bullets using the three lenses above.

Claude's response should contain exactly three bullets.

The first bullet identifies the revenue-only success metric.

The second bullet explains how the budget cap affected the result. It also covers the approval rule.

The third bullet names a remaining limitation of projected revenue.

You now have a complete business postmortem grounded in the simulator's evidence.

Postmortem Missing a Required Point?

Ask Claude to revise the response into exactly three bullets. Name the missing lens in your follow-up request.

Need help refining the review? Help me improve my Lemonade Ledger postmortem.

Secret mission

Stress-Test the Approval Threshold

A guardrail that works for one plan may behave differently when the purchases change. Test a second plan at two approval thresholds to reveal how extra projected revenue can reduce profit.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the simulator ready, pause your work, or remove the project material. Both the local files and Claude Free conversations cost $0 to keep.

Resources you used:

  • The local lemonade-ledger folder containing setup_check.py, agent_prompt.txt, claude_plan.json, and lemonade_sim.py.
  • One or more project-related Claude Free conversations containing the generated plan or postmortem.

Keep everything running

No action is needed. This option fits when you still want to run the simulator or revisit your postmortem.

  • Leave the lemonade-ledger folder unchanged.
  • Leave the related Claude Free conversations in your history.
  • Return to IDLE whenever you want to rerun lemonade_sim.py with the final approval_limit value of 40.

Your Secret Mission fixture remains ready for more policy experiments.

Pause - I'll come back to this later

Pausing closes the active tools while preserving every file and conversation for your next session.

  • Save any open file in IDLE.
  • Close IDLE.
  • Close the browser tab containing Claude Free.

Your four files and Claude conversation history remain ready when you return.

Delete - I don't want to use this again

This option removes the local project folder and the related Claude conversations.

Before You Delete

Deletion can feel final. Any simulator output or postmortem worth keeping needs a separate copy first.

  • Close IDLE.
  • In Finder, search for lemonade-ledger.
  • Move the lemonade-ledger folder to Trash.

You should see the folder in Trash with all four project files inside it.

  • Return to the Claude Free browser tab.
  • Find each project-related conversation in your Claude history.
  • Use the conversation's removal control to remove it from your history.

The related conversations should no longer appear in your Claude history.

Nice Work!

Nice Work!

You did it! Your local Python simulator now exposes the danger of a revenue-only target through financial outcomes you can inspect.

You've learned how to:

  • Build a local business simulator that calculates a complete financial outcome from every proposed purchase.
  • Create a structured AI handoff with Claude Free. Validate its JSON record before Python applies deterministic rules.
  • Expose a revenue-only false victory. Apply budget limits to risky purchases. Add human approval gates that record each rejected decision.
  • Complete the optional Secret Mission by comparing approval limits of 25 versus 40. Prove that accepting the Portable speaker raises projected revenue. Show that guarded profit falls from $110.00 to $95.00.

Ready to quiz yourself?