James was testing TutorClaw from WhatsApp. He typed: "Teach me chapter 10." The tutor sent back chapter 10 content.
He stopped. "Wait. The spec says free tier is chapters 1 through 5 only. Why did it let me through?"
Emma leaned over his shoulder and read the tool output. "You built the gating in Module 9.3, Chapter 4 for content tools. But you did it inside each tool separately. And you did not count exchanges."
"What do you mean, count exchanges?"
"A free-tier learner gets 50 tool calls per day. Right now, they get unlimited. Your content gating blocks chapter 10, but your pedagogy tools serve guidance for chapter 10 content without checking the tier at all. And submit_code has no daily limit." She pointed at the test output. "Your gating has gaps."
You are fixing exactly what James found. Your tier gating exists in some tools but not others. There is no shared function, no exchange counter, and no consistent error message. In this chapter, you build all three: a check_tier() function that every gated tool calls, exchange counting that enforces a daily limit, and a consistent upgrade prompt that the agent can rely on.
Before writing any prompts, study the access rules. This table is the product specification for tier gating:
Three tools are always available regardless of tier: register_learner, get_learner_state, update_progress. These are infrastructure. Blocking them would break the product.
Three tools are content-gated: get_chapter_content, get_exercises, and submit_code. Free-tier learners can only access content for chapters 1 through 5, and submit_code has a separate daily cap of 10 runs.
Two tools are exchange-counted but not content-gated: generate_guidance and assess_response. Free-tier learners can call them, but each call costs one exchange out of the daily 50.
One tool is free-only: get_upgrade_url. Paid learners do not need it.
This function is the single source of truth for tier enforcement. Every gated tool calls it before doing anything else. Send this to Claude Code:
Claude Code responds with a spec. Check these elements:
If the spec does not include the daily reset logic, steer:
Once the spec looks right:
After the build finishes, test the function directly. Ask Claude Code:
You should see your mock learner's tier (free) and 50 exchanges remaining. If the tier field is missing or the exchange count is wrong, describe the problem to Claude Code and have it fix the function.
Now wire check_tier() into every tool that needs it.
Why do all gated tools need the SAME error message? Because the agent reads tool error responses to decide what to suggest next. If get_chapter_content says "Upgrade to unlock" but get_exercises says "Please subscribe for full access," the agent cannot reliably suggest the next step. Consistent messages mean the agent always knows: when it sees the upgrade error, it should call get_upgrade_url.
Send this to Claude Code:
This is a longer prompt than usual, and that is deliberate. Tier gating touches multiple tools. Describing all the rules in one message gives Claude Code the complete picture so it can make the enforcement consistent across tools.
Run four tests to confirm the gating works:
Test 1: Free-tier learner requests chapter 1 (should work)
Ask Claude Code to call get_chapter_content with chapter 1 and your mock learner ID. The tool should return content and your exchange count should decrease by 1.
Test 2: Free-tier learner requests chapter 10 (should be blocked)
Ask Claude Code to call get_chapter_content with chapter 10. You should see the upgrade message, not chapter content.
Test 3: Free-tier learner calls generate_guidance (should work but count)
Call generate_guidance with a valid learner ID. Check your exchange count afterward. It should be lower than before the call.
Test 4: Free-tier learner exhausts exchanges (should be blocked everywhere)
This is the important test. Ask Claude Code:
Both calls after exhaustion should return the upgrade message. If get_chapter_content works but generate_guidance does not check the exchange count, the enforcement is incomplete. Describe the gap to Claude Code:
The upgrade messages must be identical across all tools. This matters for two audiences: the learner and the agent.
The learner sees the message in WhatsApp. If get_chapter_content says "upgrade to access premium content" and submit_code says "daily limit reached," the learner gets confused about what they are paying for.
The agent reads the message too. If the agent gets an upgrade prompt from one tool, it should know to suggest calling get_upgrade_url. A consistent message format makes the agent's job straightforward.
Ask Claude Code to audit the messages:
After Claude Code reports and fixes any inconsistencies, run your tests again to confirm the messages match.
Your test suite from Module 9.3, Chapters 11 and 12 should still pass. The tier gating changes touched existing tool logic, so regressions are possible.
If any tests fail, they likely fail because the tests were not expecting the exchange decrement. Describe the failures to Claude Code:
After the existing tests pass, add tier-specific tests:
Run the full suite again:
All tests green means your tier gating is complete and your existing tools still work.
Ask Claude Code to audit your entire tool surface for gating gaps:
What you are learning: Every product has at least one tool that forgot to check the tier. Auditing the full surface after implementation catches gaps that testing individual tools misses.
Send a message through WhatsApp (or Claude Code acting as the agent) that should trigger the upgrade flow:
What you are learning: Tier gating is not just a server feature. The agent needs to read the error message and know what to recommend. If the agent does not suggest the upgrade, the error message may need a stronger call-to-action.
Think about what happens at midnight when the exchange counter resets:
What you are learning: Daily resets depend on stored state, not server memory. If the reset date is stored in the JSON file, a server restart does not lose the counter. If it is stored in memory, it does. This is why check_tier() reads from the JSON file every time.
James set his exchanges to 1 and called get_chapter_content. Chapter 1 came back. He called generate_guidance. Blocked.
"Fifty calls. Then the wall." He tried chapter 10. Blocked on tier. He tried chapter 1 again. Blocked on exchanges. "All gated tools, same message. Consistent."
Emma nodded. "Now the free tier means something. The upgrade is not a request. It is the natural consequence of using the product." She pulled up her own notes. "I will tell you something. I have never gotten tier gating right on the first try. Every product I have built had at least one tool that forgot to check the tier. That is why you test." She closed the notes. "You have a gated product. Free users get a real taste. Paid users get everything. But get_upgrade_url still returns a placeholder URL."
James looked at the mock response from get_upgrade_url. A hardcoded string. "So the wall exists, but the door is painted on."
"Module 9.3, Chapter 14. Stripe. The real door."