Build · API · Module 02
Auth, Models, the Why
After this, your key lives somewhere safer than your shell history, you can pick the right model for a given workload without guessing and you've written a loop where Claude calls a tool, your code runs the tool and Claude reads the result.
Auth, in order of how much you trust the environment
The first script in API-01 read the key from an environment variable. That's fine for a laptop. It stops being fine the moment the code runs anywhere else. Three patterns, ranked by where they belong.
Local development. A .env file in the project, loaded by the SDK or by a library like python-dotenv. Add .env to .gitignore on the same minute you create it. Never commit a key. If you suspect you've committed one, rotate it immediately at console.anthropic.com; the old one is no longer a secret.
Shared dev or staging. A secret manager. AWS Secrets Manager, Google Secret Manager, Doppler, 1Password Connect, whatever your team uses. The point is that the key is fetched at runtime from a system with access logs, not baked into the deploy artefact. Cost: a few lines of bootstrap. Payback: when the key leaks, you know when and from where.
Production. Same as staging, plus least-privilege keys and rotation. Anthropic supports multiple keys per workspace; give each environment its own. When you rotate, you can swap one without taking the others down. If a key shows up in a log, you know which environment to chase.
The most expensive mistake at this layer isn't a leak; it's not knowing one happened. Set a billing alert on the workspace. Set a rate-limit alert. Both take five minutes in the console. They pay back the first time a runaway loop or a leaked key starts spending faster than you'd notice.
Models, in order of cost
Three current models in the Claude 4.x line, as of 2026-06-18:
claude-haiku-4-5 fastest, cheapest, bounded tasks
claude-sonnet-4-6 general default, moderate cost
claude-opus-4-7 hardest reasoning, highest cost
The wrong question is "which is the best." The right question is "which is good enough for this task, given my volume." A classifier running on a million messages a day on Opus is a budget problem. The same classifier on Haiku is sensible engineering. A two-step reasoning task on Haiku is a quality problem; the same task on Sonnet is fine. Match the model to the work, not your ego.
Decision · which model for which job
Pick the model. ├─ Is the task bounded and structural? │ └─ classification, extraction, transformation ──── Haiku ├─ Is the task open-ended but standard? │ └─ drafting, summarising, ordinary code ────────── Sonnet (default) ├─ Is the task ambiguous, multi-step, novel? │ └─ planning, architecture, complex review ──────── Opus └─ Don't know yet? └─ Start at Sonnet. Profile. Move only on evidence.
"Profile. Move only on evidence" is the load-bearing line. The instinct to optimise too early is what produces budget problems. Run the work on Sonnet for a week. Read the output. Read the cost. If the quality holds at Haiku, switch. If the quality doesn't hold and the work matters, switch to Opus. Don't switch on hunch.
The tool_use round-trip
So far you've sent a message and read text back. The other shape the model returns is a tool_use block — a structured request for your code to run something and report back. The model is asking. Your code answers. The model continues.
The minimal example is a calculator. The model doesn't do arithmetic well; if you give it a tool, it learns to call the tool instead of guessing. Save this as tool_loop.py:
from anthropic import Anthropic
client = Anthropic()
tools = [
{
"name": "calculator",
"description": "Evaluate a simple arithmetic expression like '12 * 47' or '(100 - 12) / 4'. Returns the numeric result.",
"input_schema": {
"type": "object",
"properties": {
"expression": {"type": "string"}
},
"required": ["expression"],
},
}
]
def run_calculator(expression):
return str(eval(expression)) # toy executor; do not use eval in real code
messages = [
{"role": "user", "content": "What is 487 multiplied by 21, then divided by 7?"}
]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
for block in response.content:
if block.type == "text":
print(block.text)
break
if response.stop_reason == "tool_use":
# Add the model's turn to the conversation
messages.append({"role": "assistant", "content": response.content})
# Run each tool the model asked for, append the results
tool_results = []
for block in response.content:
if block.type == "tool_use" and block.name == "calculator":
result = run_calculator(block.input["expression"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
continue
print(f"Unexpected stop_reason: {response.stop_reason}")
break
Run it. The model receives the question, decides it needs to call the calculator, returns a tool_use block. Your code runs the expression, returns the result as a tool_result message. The model receives the result and produces its final answer.
Three things to notice the first time you run a tool loop:
You wrote the loop. The while True is your code. The API does not iterate on your behalf. The model decides whether to call a tool, your code decides whether to keep running. Both halves are required.
The conversation grows on every turn. Each messages.append adds to what you send next time. The model needs the whole history to reason. You pay for it on every turn. This is why long agent loops can get expensive — you're re-sending everything.
The stop condition is your responsibility. The loop above exits on end_turn, but a buggy tool or a confused model could keep requesting tool calls forever. Always have at least one ceiling that isn't the model's choice. A turn cap is the simplest:
for turn in range(10): # never run more than 10 turns
response = client.messages.create(...)
# ... existing logic ...
else:
print("Hit turn limit. Stopping.")
Pick a number you can live with. The number itself isn't important; having one is.
The eval warning
The calculator above uses Python's eval() as the executor. That is a security disaster outside a tutorial. eval("__import__('os').system('rm -rf /')") is a valid expression. The model is unlikely to send something malicious in this exact loop, but if you ever expose a tool that takes a string and runs it on your machine, treat the model's output as untrusted user input. Parse it. Validate it. Never eval it.
This is the cross-axis lesson from A-00 in API form. On the Use axis, the rule is "never approve an action you don't understand." On the Build axis, the rule is "never execute model output you haven't validated."
If you skipped the Use-axis equivalent
The mental model for what tools are and when to reach for them is A-07: Tool Fluency. That module covers the judgement side — which tool to ask Claude Code to use. This module is the construction side — how to define one yourself. Both readings together are the full picture.
Next
Tools-01 (MCP from the API side) takes the loop you just wrote and shows you how the same shape generalises into the Model Context Protocol — the standard for exposing tools and data sources to Claude without rewriting the loop for every integration.