EVERYTHING AIAI engineering, made visual
0/12 complete
LESSON 04 · SETUP & TOOLING × AI · BUILD

Send a request.
Get a response.

Every AI API works the same way: an endpoint, a key, a request body, a response body. The details change — the pattern doesn’t. Keys that never touch your source, one Python and one TypeScript client, the raw HTTP underneath, and what a 429 really means.

30 MIN · 6 CHAPTERS + CHECKPREREQ · PHASE 0 · LESSON 01
FIG. 04 / ONE REQUEST, FOUR PARTS · LIVE CYCLE
key from env 200 → tokens stream 429 → backoff
LESSON 04TYPE · BUILD~30 MINPREREQ · PHASE 0 · LESSON 01ORIGINAL LESSON ↗
THE 60-SECOND VERSIONSee the pattern ↓
01 / FOUR PARTS, ALWAYS

One exchange, four parts.

An endpoint (the URL), an API key (authentication), a request body (what you want), a response body (what you get back). Every provider spells them differently; the shape never changes. The key travels in a header — for Anthropic, x-api-key — and TLS encrypts the connection around it.

POST https://api.anthropic.com/v1/messages · x-api-key: sk-ant-…
02 / THE KEY LIVES IN THE ENVIRONMENT

Never in the code. Rotate if it leaks.

Keys go in environment variables (export ANTHROPIC_API_KEY=…) or a .env file that is listed in .gitignore. SDKs read the variable by name. If a key ever reaches a repo, a log, or a screenshot, treat it as public: revoke it in the console and issue a new one. Deleting the file does not delete the history.

export ANTHROPIC_API_KEY=sk-ant-… · .env → .gitignore
03 / READ THE STATUS, COUNT THE TOKENS

429 is a wait. 401 is a fix.

The status line is the first diagnosis: 4xx means change the request (401 key, 404 address, 429 slow down with Retry-After), 5xx means wait and retry. Back off exponentially — 1s, 2s, 4s, 8s capped — with jitter so clients don't retry in lockstep. Then count tokens: input and output are billed separately.

1 + 2 + 4 + 8 = 15s · full jitter expected 7.5s · ≈4 chars/token
MENTAL MODEL IN ONE SENTENCE

An API call is one structured message in four parts — endpoint, key, request body, response body — so learn the pattern once, keep the key in the environment (and rotate it the moment it leaks), read the status code before the message, and treat tokens as money.

By the end you will be able to make a real call with the Anthropic SDK in Python and TypeScript; explain what the SDK hides and reproduce the raw HTTP request with urllib; name the status families and their first fix (401 key, 403 permission, 404 address, 429 wait with Retry-After, 5xx retry); choose a backoff schedule — 1s, 2s, 4s, 8s capped = 15s, halved in expectation by full jitter; estimate tokens at ≈4 characters each (a 60-word prompt ≈ 80 tokens); compute an example bill from example rates; and rotate a leaked key without pretending that deleting the file was enough.

THE UNIVERSAL PATTERN

One exchange.
Four parts, forever.

Every AI API — Anthropic, OpenAI, Google, a local Ollama server — is one structured message sent to an address, and one structured message sent back. Learn the shape once and the schemas become details.

An API (application programming interface) is a contract that lets one program call another. For AI models the call goes over a network, which means it is an HTTP request (HyperText Transfer Protocol): a method, an address, some headers, and a body. The provider answers with a status code, its own headers, and a body. No browser, no clicking — just a message and a reply.

The source draws this as a sequence diagram with two participants: Your Code sends an HTTP request carrying an API key; the API Server answers with an HTTP response in JSON (JavaScript Object Notation). That is the whole picture, and it has exactly four parts:

PartWhere it livesAnthropic example (masked)
Endpointthe URL you POST tohttps://api.anthropic.com/v1/messages
API keya request headerx-api-key: sk-ant-…
Request bodyJSON sent with the POST{"model": "claude-sonnet-5", "max_tokens": 256, "messages": […]}
Response bodyJSON sent back{"content": [{"type": "text", "text": "…"}], "usage": {…}}

A URL (uniform resource locator) is more than an address: api.anthropic.com is the host, /v1/messages is the path. The v1 is the API version, pinned in the URL — when a provider ships a breaking change, it usually moves the path rather than silencing the old one. The key is not encryption: TLS (Transport Layer Security) encrypts the connection, while the key identifies your account, authorizes the request, and tells the provider who to bill. That is why losing a key is like losing a payment card, and why the next chapter is entirely about custody.

Watch one request happen

Pick the transport and the outcome, then watch the four parts travel: the key is injected from the environment, the headers and body go out, the status comes back, and words stream in. The 429 wait is accelerated 30 seconds into about one.

POST https://api.anthropic.com/v1/messages content-type: application/json x-api-key: sk-ant-… ← read from ANTHROPIC_API_KEY anthropic-version: 2023-06-01 {"model":"claude-sonnet-5","max_tokens":256, "messages":[{"role":"user","content":"What is a neural network in one sentence?"}]} ← 200 OK {"content":[{"type":"text","text":"A neural network learns patterns by adjusting weights against a loss signal."}], "usage":{"input_tokens":12,"output_tokens":28}} what raw HTTP shows you the exact URL, headers and JSON body the status line and the error body this is the call to reach for when the SDK error is cryptic

urllib.request — you write all three headers yourself. the key checked out and the body was valid. The four parts never change — only the spelling of them does.

Quick check

In the exchange, where does your question travel — and what does the API key actually protect?

KEY HYGIENE

A key is a credit card.
Treat it like one.

The source’s rule is one sentence: never put API keys in code. The value belongs in an environment variable or an ignored .env file, the code reads the variable by name, and a leaked key gets rotated — not just deleted.

A key with a spend limit is a bearer credential: whoever holds the string can spend your quota. So the first design rule is custody — the value must live somewhere that never travels with the code.

Pattern one: environment variables. The shell holds the value and passes it to every program it starts. export ANTHROPIC_API_KEY=sk-ant-… sets it for the current shell session; a new terminal starts clean, so people put the line in a shell profile such as ~/.zshrc (a file outside the repository). CI (continuous integration) systems and cloud platforms have their own secret stores that inject environment variables at run time — the mechanism is the same, only the vault differs.

Pattern two: a .env file, listed in .gitignore. Dotenv files keep KEY=VALUE lines next to the project, which is convenient for local development. The convenience is only safe because of one line in .gitignore. And a .env file does not load itself: something has to read it into the process environment. The source’s TypeScript port hand-writes a twenty-five-line loader for exactly this reason — KEY=VALUE per line, # comments, optional quotes — and it lets process.env win, so a real exported variable overrides the file without editing anything.

the clean setup — environment first, ignore rules visiblebash
# ~/.zshrc  (or a CI secret, or a cloud secret manager)
export ANTHROPIC_API_KEY="sk-ant-…"

# .env  — a convenience copy for local runs, never committed
ANTHROPIC_API_KEY=sk-ant-…
LLM_MODEL=claude-sonnet-5

# .gitignore — the line that makes .env safe
.env
.env.*

# the code never contains a value, only a name
#   Python:      os.environ["ANTHROPIC_API_KEY"]
#   TypeScript:  process.env.ANTHROPIC_API_KEY
#   SDKs:        anthropic.Anthropic() / new Anthropic() read the variable for you
The source's bash snippet: export ANTHROPIC_API_KEY=… and OPENAI_API_KEY=…, with the .env variant added to .gitignore. The .env file in this panel is masked — real keys never appear in a lesson.

What leaks actually look like. Note that the leak is rarely a dramatic hack. It is a .env that missed .gitignore; a key pasted as a test fixture; a notebook cell whose output was saved into the .ipynb JSON; a CI log line that printed the environment for debugging; a screenshot with the value visible in a variable inspector; a Dockerfile ENV line baked into every image layer; an error-tracker event carrying the request headers. Code-hosting platforms and providers scan public repositories for credential patterns, and a discovered key can be revoked automatically — but the safe assumption is that anything that reached a remote, a log, or another person is public.

If a key leaks, rotate it. Rotation means: revoke the key in the provider console, create a new one, update every place that needs it (your shell, CI secrets, a deployed service), restart the processes that cached the old value, and only then clean up the repository. Purging history with git filter-repo or BFG is worth doing — but it is cleanup, not a fix. The old key was valid the entire time it was exposed.

The secret-leak scanner

Pick a snippet, or paste your own and say where it lives. The scanner is a teaching heuristic — a real repository wants gitleaks or trufflehog in CI — but the leak paths it names are the ones that actually happen.

SNIPPET — CLICK ONE, OR EDIT BELOW

The classic first draft: the key typed straight into the file that gets committed. Nothing looks wrong until the repo is public.

LEAK · 1 FAILING FINDINGCredential exposed (agent.py) — rotate it, then fix the custody.
  • LEAK a key literal appears in this text — anything committed will carry it
verdict HARDCODED literal yes — a sk-ant-… value appears env lookup no the fix, in order 1 · Move the value out: export ANTHROPIC_API_KEY=sk-ant-… (the shell) or put it in a .env file. 2 · Add .env (and .env.*) to .gitignore — the file git must never track. 3 · Rotate the key now, in the provider console: deleting the line does not remove it from git history, and clones, forks and caches keep copies. 4 · Read it by name in code: os.environ["ANTHROPIC_API_KEY"] or process.env.ANTHROPIC_API_KEY.

A key value itself is not the only leak: where it flows matters. Exposure in git history keeps the old key valid until you rotate it in the provider console.

Quick check

You committed a .env with a live key, deleted the file in the next commit, and added .env to .gitignore. Are you safe?

THE FIRST CALL, THROUGH AN SDK

Twelve lines
to your first answer.

An SDK (software development kit) is a library that wraps the raw HTTP exchange in typed functions. You create a client, ask for a message, and read the text out of the response — with the key supplied by the environment, never by the code.

Install once, then let the client find the key: pip install anthropic for Python, bun add @anthropic-ai/sdk (or npm) for TypeScript. The source’s Python call is the whole program in miniature — this is the canonical upstream snippet, with the usage line added from the source’s script:

first_api_call.py — the source's Python callpython
import os
import anthropic

client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY from the environment

MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-5")

response = client.messages.create(
    model=MODEL,
    max_tokens=256,
    messages=[{"role": "user", "content": "What is a neural network in one sentence?"}],
)

print(response.content[0].text)
print(f"Tokens used: {response.usage.input_tokens} in, {response.usage.output_tokens} out")
Adapted from reference/lesson-sources/00-setup-and-tooling/04-apis-and-keys/code/first_api_call.py. The model id comes from LLM_MODEL with the source's un-dated Sonnet alias as the default.

Read it one line at a time, because every line is a design decision:

  1. client = anthropic.Anthropic() — the client looks for ANTHROPIC_API_KEY in the process environment. No argument means no key anywhere in your source, and no key in the traceback if something fails.
  2. MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-5") — the model id is configuration, not code. The source’s default is the un-dated Sonnet alias claude-sonnet-5; a dated name pins an exact snapshot, an alias follows the provider’s current build.
  3. max_tokens=256 — a hard cap on the output, not the input. The source picks 256 because a one-sentence answer is small; it is also required, so omitting it returns 400.
  4. messages=[{"role": "user", "content": "…"}] — a list of turns, with roles user and assistant. One message is still a list; a conversation is the same list with more entries.
  5. response.content[0].text — the response’s content is a list of blocks. The first block is a text block here; a model may return several, which is why the index is explicit.
  6. response.usage — input and output tokens for this call. This is the receipt; keep an eye on it from your very first request.

The TypeScript port is the same program wearing different syntax. Note that await appears because the call is asynchronous — the program does nothing until the answer arrives:

first_api_call.ts — the same call in TypeScripttypescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();          // reads ANTHROPIC_API_KEY

const MODEL = process.env.LLM_MODEL ?? "claude-sonnet-5";

const response = await client.messages.create({
  model: MODEL,
  max_tokens: 256,
  messages: [{ role: "user", content: "What is a neural network in one sentence?" }],
});

console.log(response.content[0].text);
Adapted from reference/lesson-sources/00-setup-and-tooling/04-apis-and-keys/code/first_api_call.ts. That port also parses a .env file by hand and supports MOCK=1 to go through the whole program with no network — useful for a first run before you have a key.
example output (the source's mock fixture)bash
$ python first_api_call.py
A neural network is a stack of differentiable functions that learns patterns
by adjusting weights against a loss signal.
Tokens used: 12 in, 28 out
The sentence varies run to run; the shape does not. The numbers come from the source's TypeScript fixture, which matches the real /v1/messages response shape so the surrounding code is identical with or without the network.
Worked check — estimating the tokens before you send (and why the fixture says 12)

The prompt "What is a neural network in one sentence?" is 41 characters. At the lesson’s rule of thumb — ≈4 characters per token for English — that is 41 ÷ 4 = 10.25, or about 11 tokens. The fixture reports 12 input tokens.

rule of thumb 41 characters ÷ 4 = 10.25 → ≈ 11 tokens actual fixture 12 input tokens the 1-token gap is the point: ÷4 is an estimate, and the tokenizer that the provider actually runs is the authority. Estimates are for planning; usage is for billing.

Same arithmetic on the max_tokens cap: 256 output tokens ≈ 256 × 4 = 1,024 characters ≈ 193 English words at the prompt’s 5.3 characters per word. A one-sentence answer needs roughly 20–40 tokens, so the cap is generous here — and it is a cap, not a target: you are billed for the tokens the model actually produces, not for 256.

WHAT THE SDK HIDES

Three headers, one JSON body.
This is the real call.

An SDK is a convenience wrapper around one HTTP POST. When the wrapper’s error is cryptic, reproducing the call by hand is the fastest way to see the status line and the response body — the two things that name the problem.

The source says it plainly: “This is what the SDKs do under the hood. Understanding the raw HTTP call helps when debugging.” Python’s standard library is enough — no package to install:

raw_http.py — the same request without the SDKpython
import os
import json
import urllib.request

url = "https://api.anthropic.com/v1/messages"
headers = {
    "Content-Type": "application/json",
    "x-api-key": os.environ["ANTHROPIC_API_KEY"],
    "anthropic-version": "2023-06-01",
}
body = json.dumps({
    "model": os.environ.get("LLM_MODEL", "claude-sonnet-5"),
    "max_tokens": 256,
    "messages": [{"role": "user", "content": "What is a neural network in one sentence?"}],
}).encode()

req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req) as resp:
    result = json.loads(resp.read())
    print(result["content"][0]["text"])
The source's raw-HTTP snippet, verbatim apart from formatting. urllib.request ships with Python; json.dumps(...).encode() turns the dict into the bytes the network needs.

The three headers are the whole ceremony. Content-Type: application/json tells the server how to parse the body. x-api-key carries the credential — Anthropic’s API uses this header rather than the more common Authorization: Bearer, which is why the quiz question about it exists. And anthropic-version: 2023-06-01 pins the API contract to a date, so a server-side change cannot silently reshape your request or response. Note where the key is not: never in the URL. Query strings land in logs, proxies and shell history; headers are the safe place for credentials.

The response is ordinary JSON — with no SDK, it is an ordinary Python dictionary, which is exactly the point: result["content"][0]["text"] and result["usage"] are visible with no types in between. TypeScript’s equivalent is global fetch with the same three headers and await resp.json(); the source’s TypeScript port does exactly that, with a small .env loader in front and a MOCK=1 switch so the whole program runs with no network.

Worked check — one exchange, read like a wire

Reconstructing an exchange from the headers up is the debugging habit this section teaches. This is a teaching reconstruction — header order and exact bodies vary by API version — but every line here has a counterpart in the panel above:

POST /v1/messages HTTP/1.1 host: api.anthropic.com content-type: application/json x-api-key: sk-ant-… ← from ANTHROPIC_API_KEY anthropic-version: 2023-06-01 {"model":"claude-sonnet-5","max_tokens":256, "messages":[{"role":"user","content":"What is a neural network in one sentence?"}]} ↓ one network round trip ↓ HTTP/1.1 200 OK content-type: application/json {"content":[{"type":"text","text":"…"}], "usage":{"input_tokens":12,"output_tokens":28}} what each header buys content-type the server parses the bytes as JSON x-api-key identity + billing (not encryption — TLS encrypts) anthropic-version the contract stays pinned to a known date what the SDK adds around this header assembly · JSON encode/decode · version pinning retries on 429/5xx with backoff · typed errors with status + body streaming helpers · explicit timeouts

One practical loop: when an SDK error message is too high-level, run the raw call once. The HTTP status and the JSON error body usually name the missing field or the limit that was crossed — a 400 that says max_tokens: required, a 404 that names the model string, a 429 with retry-after: 30.

WHEN THINGS GO WRONG

The status line
is the diagnosis.

HTTP answers with a three-digit code before it answers with words. Read the code and you know who has to act: your request (4xx), their server (5xx), or the clock (429, Retry-After).

The first digit carries the meaning. 4xx means the request is the problem — retrying an unchanged request will produce an unchanged error. 5xx means the provider’s side failed — the request was fine, and waiting helps. The one hybrid is 429: your traffic crossed a limit, so you must wait, but the wait is specified.

CodeMeaningFirst thing to doRetry?
400Bad request — malformed JSON or a missing required fieldPrint the body you sent; compare with the schemaNo
401Missing or invalid keyCheck the variable name and restart the processNo
403The key is valid but not permitted to do thisCheck model access and key scopes in the consoleNo
404Wrong endpoint or unknown model idPrint the exact URL and model stringNo
429Rate limit — too many requests or tokens per minuteWait for Retry-After, then back off with jitterYes, after the wait
500 / 503Server error or overload — not your requestRetry with backoff and a small cap; check the status pageYes
timeoutThe client gave up first (connect vs read)Check the network; raise the read timeout or streamDepends on where it stopped

Exponential backoff, worked out. A 429 usually comes with a Retry-After header in seconds. When it does not, the standard policy doubles the wait each time and caps it. Start at one second, cap at eight, and stop after a few attempts:

attempt 1 t = 0s → 429 wait min(cap, base × 2⁰) = min(8, 1) = 1s attempt 2 t = 1s → 429 wait min(8, 1 × 2¹) = 2s attempt 3 t = 3s → 429 wait 4s attempt 4 t = 7s → 429 wait 8s attempt 5 t = 15s → stop and surface the error total wait, no jitter 1 + 2 + 4 + 8 = 15s expected with full jitter 15 ÷ 2 = 7.5s (a random wait in [0, ceiling]) expected with equal jitter 15 × 0.75 = 11.25s (ceiling/2 + random half) retrying immediately 0s of waiting — and the same 429, from a bigger burst with "retry-after: 30" 30s > every ceiling, so each retry waits 30s: 4 retries = 4 × 30 = 120s, whatever the schedule says

Jitter is the part beginners skip and production systems require. If a thousand clients receive a 429 in the same second and every one waits exactly one second, they all retry together and trip the limit again — a thundering herd. Randomising each wait (full jitter picks uniformly from zero to the ceiling) spreads the retries out and halves the expected total wait, without changing the worst case. And a retry is not free: every attempt consumes rate-limit budget and tokens, so cap the number of attempts and surface the error instead of looping forever.

Streaming: the third mode. A normal call returns one response body when the whole answer is ready. A streamed call keeps the connection open and sends the answer token by token — words arrive one by one. The billing does not change (every token still counts), but the experience does: a 300-token answer generated at roughly 50 tokens per second takes about 6 seconds end-to-end, yet the first tokens appear after about 0.3–0.5 seconds (illustrative numbers). That gap between time-to-first-token and total time is what streaming hides — and it also keeps long generations from tripping a read timeout, because data keeps arriving.

The 429 backoff timeline

One rate-limited request, three retry policies. Each circle is an attempt; the bars are the waits. A Retry-After header turns into a wall you must respect — ignore it and the retries are just more traffic.

attempts (incl. the first) 5 ceilings 1.0 · 2.0 · 4.0 · 8.0s no jitter total wait 15s full jitter total wait 12s expected for this policy 7.5s immediate retries total ≈0s (all attempts at once — don't) at t=6.0s (jitter lane) attempts so far 4 of 5 next attempt in 5.5s the rule that ties it together wait = max(retry-after, min(cap, base · 2^attempt)) × jitter then stop after a few attempts and surface the error.

Jitter is not decoration. If a thousand clients get a 429 at the same second and all wait exactly 1s, they retry together and trip the limit again. Randomising the wait spreads them out — the average total wait halves with full jitter.

The error triage board

Pick a status family, or paste the error text you actually got. Every card answers three questions in order: whose fault is it, should you retry, and what is the first thing to change.

SELECTED · HTTP 401Authentication — the key is missing or invalid

What happened · The server did not recognize your credential: the variable was never set in this process, the name is misspelled, the value is truncated, or the key belongs to a different environment.

First fix · Check the variable name (ANTHROPIC_API_KEY), confirm the process actually inherited it (a new terminal needs the export again), and restart the program after fixing the environment.

Retry? · No. Retrying the same wrong key just repeats the 401 — fix the credential first.

Anthropic's header is x-api-key: sk-ant-… · a missing header and a wrong key both answer 401
status families, in one line each 400 bad request your JSON or your fields — fix it, don't retry 401 unauthenticated missing/invalid key — fix the key, don't retry 403 forbidden valid key, no permission — fix access, don't retry 404 not found wrong URL or model id — fix the address, don't retry 429 rate limited wait for retry-after, then backoff with jitter 5xx server failed retry with backoff and a small cap timeout connect: check the network · read: raise it or stream the triage order 1. read the status code before the message 2. separate your input (4xx) from their state (5xx) 3. retry only what waiting can fix: 429 and 5xx 4. log the status, the request id and the body — not the key matched: 401 · retry-after: absent

The word “error” is not a diagnosis. A 401 retried a thousand times is a thousand identical failures; a 529 retried with backoff is a request that succeeds.

Quick check

Your script gets a 429 with retry-after: 30. What is the right move?

TOKENS AND COST

≈4 characters per token.
Every one is billed.

A token is the unit a model actually reads and writes — and the unit you pay for. Input and output are counted separately, the context window caps their sum, and the rule of thumb for English is about four characters per token.

A token is not a word. A tokenizer (the piece of the pipeline that turns text into numbers, trained with algorithms such as byte-pair encoding, BPE) splits text into subword pieces: common words are one token, rare words split into fragments, and punctuation adds its own. That is why the same sentence can cost different amounts in different models. The practical rule of thumb, and the one this lesson uses everywhere: ≈4 characters per token in English. It is an estimate for planning — the provider’s tokenizer is the authority, and the first place to see it is the usage object on every response. Code and non-English text typically spend more tokens per character, so treat ÷4 as a floor, not a guarantee.

Piece of textCharactersEstimated tokens (÷4)Rough English equivalent
The one-line question41≈11 (the fixture bills 12)a short sentence
The lesson’s 60-word prompt320≈80a paragraph
A 300-token reply1,200300≈230 words
A 4,000-token document16,0004,000≈3,000 words
A 200,000-token context window800,000200,000≈150,000 words — a long book

Two meters, one window. Every response reports input tokens (the prompt plus the conversation you sent) and output tokens (what the model wrote). They are billed at different rates — output is usually the more expensive meter. And there is a hard ceiling: input + output must fit inside the model’s context window. On the example 200,000-token window, max_tokens=4096 caps the answer at 4,096 tokens (2.05% of the window), leaving at most 200,000 − 4,096 = 195,904 tokens for the prompt. This lesson’s worked-example call (the 60-word prompt) — an ≈80-token prompt asking for at most 300 output tokens — uses 380 of 200,000, which is 0.19% of the window. Small talk is small; documents are not.

A worked bill, with example rates. Rates change and vary by model, so the numbers below use invented example prices — $3 per million input tokens and $15 per million output tokens — purely to show the arithmetic. Always read the provider’s live pricing page for real numbers.

the first call 80 input tokens × $3 / M = $0.000240 300 output tokens × $15 / M = $0.004500 total = $0.004740 ≈ half a cent at 1,000 calls a day 1 day = $4.74 30 days = $142.20 the second worked example — a 16,000-character document document alone 4,000 input tokens × $3 / M = $0.012000 the same 300-token answer 300 × $15 / M = $0.004500 total = $0.016500 ≈ 1.7 cents (a 20-token question on top adds $0.000060 — noise next to the document) ≈ the input grew 50× (80 → 4,000 tokens) while the bill grew 3.5× ($0.0047 → $0.0165), because output is the pricier meter one more scale check a full 200,000-token window sent once as input = 200,000 × $3 / M = $0.60 the same 200,000 tokens as output = 200,000 × $15 / M = $3.00

Two cost controls live in the request itself. Caching: several providers can cache a repeated prefix — a long system prompt, a document you ask about in several calls — and bill that prefix at a reduced rate; the discount and the rules differ, so check the pricing page. Retry discipline: a retried call is a billed call, and a messages request is not idempotent, so a blind retry can pay twice for two answers. That is the same discipline the last chapter built: retry only what waiting can fix (429, 5xx), with backoff and a cap.

Tokens are money — estimate before you send

Type a prompt and a reply length. The estimate is the lesson’s rule of thumb — characters ÷ 4 for English — not a tokenizer. The rates are examples for arithmetic; the live prices live on the provider’s pricing page.

THE PROMPT (INPUT)
EXAMPLE CONTEXT WINDOW · 200,000 TOKENS

80 input + 300 max output = 380 tokens · 0.19% of the example window.

the text characters 320 words 60 est. tokens 80 (320 ÷ 4 = 80.00 — an estimate) the reply output cap 300 tokens ≈ 1,200 characters ≈ 226 words example rates (invented for arithmetic) input $3.00 per million tokens output $15.00 per million tokens this call input 80 × $3.00/M = $0.000240 output 300 × $15.00/M = $0.004500 total $0.004740 scale 1 day $4.74 30 days $142.20 context window example 200,000 tokens this call 380 tokens = 0.19% of the window caching, in one line a repeated prefix (system prompt, long document) can be cached by some providers and billed at a reduced rate — check the live pricing page for whether caching applies to your model and how big the discount is.

These rates are examples, not today’s prices. Token counts themselves are estimates too: the provider’s tokenizer is the authority, and code or non-English text usually spends more tokens per character. See live pricing ↗

Quick check

With the example rates ($3 per million input, $15 per million output), which costs more: 2,000 input tokens of a document sent once, or a 300-token answer?

CHECK YOURSELF

Five questions.
Then the terms worth keeping.

Answer before you look. The key-hygiene question and the rate-limit question are the two that separate “I read the chapter” from “I could debug this at 2 a.m. with a 401 in one hand and a 429 in the other”.

0 / 5 answered · 0 correct

01What is an API key used for when calling an LLM service?

02Why should API keys never be hardcoded directly in source code?

03What is the recommended way to store API keys for local development?

04In the raw HTTP API call to Anthropic, which header carries the API key?

05What happens when you exceed an API's rate limit?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Four problems with exact numbers — make the first call, compare the raw HTTP and SDK shapes, read a wrong-key error like an engineer, and write the rotation plan for a repo that leaked. Try first; a worked answer is one click away.

  1. Get an Anthropic API key and make your first call (the source's Exercise 1). Print the answer text and the usage line. Then do it again in a fresh terminal without the export and explain the difference.
    Show one worked answer

    Export the variable (masked here — the real value never appears in a lesson): `export ANTHROPIC_API_KEY=sk-ant-…`, then run the lesson's first_api_call.py. Expected output shape: one sentence of answer, then `Tokens used: 12 in, 28 out` for the source's fixture. The fresh-terminal repeat fails differently depending on where the key was missing: with no value at all the SDK client raises before any network call — a local configuration error, not a 401 — because Anthropic() refuses to construct without a key. If the key is present but wrong, the request goes out and the server answers 401 with an authentication_error body. Both are fixed the same way: put the export in the shell you actually run the program from (or load the .env), then restart the process. Numbers to notice: the prompt 'What is a neural network in one sentence?' is 41 characters ≈ 11 estimated tokens and the fixture bills 12 — the ÷4 rule is planning, the usage object is billing.

  2. Run the raw HTTP version and compare the response format to the SDK version (the source's Exercise 2). Write down three things the SDK did for you.
    Show one worked answer

    The raw call returns plain Python dicts: `result["content"][0]["text"]` and `result["usage"]["input_tokens"]`. The SDK returns typed objects: `response.content[0].text` and `response.usage.input_tokens` — same JSON underneath, friendlier names on top. Three things the SDK handled: (1) it set exactly the three headers (Content-Type: application/json, x-api-key, anthropic-version: 2023-06-01) and JSON-encoded the body; (2) it retries some failures (429/5xx and connection errors) with backoff, so a transient error may never reach your code; (3) it raises typed errors that carry the status and the response body, and exposes streaming helpers. The comparison also shows the debugging move: when the SDK's message is cryptic, run the raw call once and read the status line and error body yourself.

  3. Intentionally use a wrong API key and read the error (the source's Exercise 3). Classify it: whose fault, retry or fix, and what exactly do you change?
    Show one worked answer

    With a wrong-but-present key the server answers HTTP 401 and a body shaped like `{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}`; the Python SDK surfaces it as its authentication error type carrying the same body. Classification: 401 is a 4xx, so it is your request's fault and retrying changes nothing — a thousand retries are a thousand identical failures. The fix is credential custody, not code: confirm the variable name (ANTHROPIC_API_KEY, not ANTHROPIC_KEY or ANTHROPIC_API_TOKEN), confirm the shell that runs the process actually inherited it, confirm the value was not truncated by a stray newline in the .env, then restart the process because environment variables are read at client construction. With an empty value the failure happens even earlier, locally, when Anthropic() is constructed — a useful distinction: no key is a configuration error, a wrong key is a server 401.

  4. Write the rotation plan for a repository that leaked a key: list the steps in order, and say what each step fixes and what it does not fix. (Original to this page — the source's exercises stop at reading the error.)
    Show one worked answer

    1 · Revoke the key in the provider console. This is the only step that ends the exposure: the old string stops working immediately, whatever copies exist anywhere. 2 · Create a replacement and update every consumer — the shell profile, the .env, CI secrets, deployed services — so the work continues. 3 · Restart the processes that cached the old value; a running process never re-reads the environment. 4 · Audit the usage and billing dashboards for requests you did not make, and note the time window for the incident. 5 · Clean up the repository: add .env to .gitignore, purge the blob from history with git filter-repo or BFG, force-push, and tell collaborators to re-clone — this fixes the future of the repo, not the past of the key. 6 · Prevent the repeat: a pre-commit secret scanner (gitleaks, trufflehog) plus the same scanners in CI, and a .env.example that documents the variable names without values. What each step does not fix: steps 5 and 6 never restore the old key's secrecy, and step 1 alone does not clean the history — the plan is ordered because ending the exposure comes before tidying up.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • .gitignoreThe rules file that keeps .env (and .env.*, model checkpoints, virtualenvs) out of git. This lesson leans on it heavily; the file itself, its patterns and history rewriting get their proper treatment in Phase 0, Lesson 02 (Git & Collaboration).
  • Virtual environmentThe isolated Python install where `pip install anthropic` should land. Keep the SDK per-project instead of polluting the system interpreter; Phase 0, Lesson 06 (Python Environments) builds one properly.
  • TokenizationHow text becomes tokens: byte-pair encoding (BPE) and related algorithms that learn subword pieces from a corpus. The ÷4 rule is only a rule of thumb — Phase 10 (LLMs from Scratch) teaches the tokenizers and why code and non-English text behave differently.
  • Context windowThe maximum number of tokens a model can read at once (input + output). This lesson does the arithmetic — 200,000 − 4,096 = 195,904 input tokens — while Phase 7 (Transformers Deep Dive) explains the attention mechanism that creates the limit.
  • Environment variables in the shellexport, inheritance, shell profiles, and why a running process never sees later exports. Phase 0, Lesson 10 (Terminal & Shell) covers process environments and the commands around them.
  • Server-sent streamsThe transport behind 'words arrive one by one': a long-lived HTTP response that emits events. Phase 11 (LLM Engineering) goes into the streaming APIs, partial deltas and how to assemble them.
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Lesson text adapted from AI Engineering from Scratch (Phase 00, Lesson 04) and the Math Foundations Notebook reference build. The five labs (the request-lifecycle animator, the secret-leak scanner, the 429 backoff timeline, the token-and-cost calculator and the error triage board) are original to this page, as are the second worked numeric examples: the token arithmetic behind the ÷4 rule (the 60-word, 320-character = 80-token prompt; the 800,000-character ≈ 150,000-word context window; the 16,000-character document; 256 output tokens ≈ 1,024 characters), the worked bills at clearly-labelled example rates, the backoff arithmetic (1 + 2 + 4 + 8 = 15s, 7.5s expected with full jitter, 11.25s with equal jitter, Retry-After 30 dominating every ceiling at 4 × 30 = 120s), the context-window subtraction (200,000 − 4,096 = 195,904 input tokens), the streaming latency numbers, the agent-cost framing, the memory hooks and the rotation checklist. Everything ported — the four-part pattern, key hygiene, both SDK snippets, the raw HTTP call, the status taxonomy and the quiz — traces back to the source. Every rate in this lesson is an invented example for arithmetic; today's prices live on the provider's live pricing page, never here.