Anthropic SDK
Claude is the default model at Ekinoxis. @anthropic-ai/sdk direct in most apps;
@ai-sdk/anthropic where the Vercel AI SDK is already in play.
The bundled claude-api skill is authoritative for model ids, pricing and
parameters — read it before answering anything about models or cost, rather than
relying on memory.
The one exception in the portfolio: an older agent app uses LangChain + OpenAI, inherited
from the Coinbase AgentKit starter (../ekx-coinbase-cdp/SKILL.md).
Environment
ANTHROPIC_API_KEY= # SECRET — server only, never NEXT_PUBLIC_
ANTHROPIC_MODEL= # pin the model in env so it can change without a deploy
Never call the API from the browser. A key in the client bundle is a metered, billable secret handed to anyone who opens devtools. Always proxy through a route handler.
Tool use — the pattern that matters
The reference shape: the user asks about P&L in plain language, Claude calls tools that query Supabase, and answers from real rows.
const tools = [{
name: "get_positions",
description: "Fetch the user's open positions with entry price and current value.",
input_schema: {
type: "object",
properties: { chain: { type: "string", enum: ["evm", "solana", "xrpl"] } },
required: [],
},
}];
let messages = [{ role: "user", content: prompt }];
while (true) {
const res = await anthropic.messages.create({
model: process.env.ANTHROPIC_MODEL!,
max_tokens: 2048,
tools,
messages,
});
if (res.stop_reason !== "tool_use") return res;
const results = await Promise.all(
res.content.filter(c => c.type === "tool_use").map(async (c) => ({
type: "tool_result" as const,
tool_use_id: c.id,
content: JSON.stringify(await runTool(c.name, c.input, userId)), // ← userId from the session
})),
);
messages.push({ role: "assistant", content: res.content },
{ role: "user", content: results });
}
Three rules for that loop:
- Scope every tool to the authenticated user server-side. Pass
userIdfrom the verified session, never from the model's arguments. A tool that accepts auser_idparameter is a data-leak waiting for the right prompt. - Cap the iterations. An unbounded
while(true)with a model that keeps calling tools is an unbounded bill. Ten rounds is generous. - The tool
descriptionis the prompt. Most "the model called the wrong tool" problems are a vague description, not a model problem.
Streaming
const stream = await anthropic.messages.stream({ model, max_tokens, messages });
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
controller.enqueue(encoder.encode(event.delta.text));
}
}
Return it as a ReadableStream from a route handler. Anything over ~2 seconds of
generation needs this — live uses the Vercel AI SDK, which wraps it with React hooks
and is the easier path for a chat UI.
Cost control
- Prompt caching on a long system prompt or a large document reused across turns. This is the single largest saving available and costs one
cache_controlmarker. - Pin the model in env, so switching tiers is a config change.
- Match the model to the job. A classification or extraction step does not need the same tier as multi-step agentic reasoning.
- Set
max_tokensdeliberately. It is a cap on the bill, not just on length. - Log token usage per request —
res.usage— into Supabase. Without it, a cost spike has no explanation.
Gotchas
- Key in the browser. The expensive one.
- Tool args are model-controlled. Never trust them for authorization.
- Unbounded tool loops.
max_tokensis required and truncates silently at the limit — checkstop_reason === "max_tokens".- Rate limits are per-organisation. A batch job and the interactive app share them; queue the batch.
- Vague tool descriptions cause wrong-tool calls far more often than model capability does.