loading
loading
Running an agent locally on your laptop is a start. Shipping it as a hosted service others can call — that's the move. Here's the complete deployment playbook.
Your agent works perfectly on your machine. You've tested it with 20 inputs. The evals are green. Now what?
Deploying an agent isn't like deploying a static site. Agents are stateful, long-running, and unpredictable in ways a simple API endpoint isn't. You need to think about:
Let's walk through each.
Synchronous agents complete in one HTTP request. The caller waits. Good for: fast tasks (< 30 seconds), simple tools, single-shot tasks.
Asynchronous agents accept a job, run in the background, and notify when done. The caller gets a job ID immediately and polls or receives a webhook on completion. Good for: anything that takes >30 seconds, multi-step workflows, tasks with retries.
For your first deployed agent, start sync. Switch to async when you hit timeout limits.
Railway is the fastest path from local to hosted for a Node.js agent.
# Create your agent as an Express/Hono server
npm install hono @anthropic-ai/sdk zod
src/server.ts:
import { Hono } from "hono";
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const app = new Hono();
const client = new Anthropic();
const RunSchema = z.object({
task: z.string().min(1).max(4000),
context: z.string().optional(),
});
app.post("/run", async (c) => {
const body = RunSchema.safeParse(await c.req.json());
if (!body.success) {
return c.json({ error: "Invalid input" }, 400);
}
const { task, context } = body.data;
const message = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 4096,
system: `You are a task execution agent. Complete the task given by the user.
${context ? `Context: ${context}` : ""}`,
messages: [{ role: "user", content: task }],
});
const content = message.content[0];
if (content.type !== "text") {
return c.json({ error: "Unexpected response type" }, 500);
}
return c.json({ result: content.text, usage: message.usage });
});
app.get("/health", (c) => c.json({ ok: true }));
export default { port: process.env.PORT ?? 3000, fetch: app.fetch };
Add railway.json:
{ "build": { "builder": "NIXPACKS" }, "deploy": { "startCommand": "npx tsx src/server.ts" } }
Deploy:
railway login && railway up
Your agent is live at the Railway URL Railway gives you. Add your ANTHROPIC_API_KEY as an environment variable in Railway's dashboard.
If you're building a web product on Next.js, use Vercel's AI SDK for streaming responses:
app/api/agent/route.ts:
import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
export async function POST(req: Request) {
const { task } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-5"),
system: "You are a helpful task execution agent.",
prompt: task,
maxSteps: 10,
tools: {
// your tools here
},
});
return result.toDataStreamResponse();
}
The Vercel AI SDK handles streaming, tool calls, and multi-step execution. For tool-using agents on the web, this is the fastest path.
Never expose your agent without authentication. At minimum, use a static API key:
app.use("*", async (c, next) => {
const key = c.req.header("x-api-key");
if (key !== process.env.AGENT_API_KEY) {
return c.json({ error: "Unauthorized" }, 401);
}
return next();
});
For production multi-tenant agents, use a proper auth service (Clerk, Auth.js, or Supabase Auth).
Add Langfuse before you launch. Seriously.
npm install langfuse
import { Langfuse } from "langfuse";
const langfuse = new Langfuse({
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
});
// Wrap your agent call
const trace = langfuse.trace({ name: "agent-run", input: { task } });
const span = trace.span({ name: "claude-call" });
const message = await client.messages.create({ ... });
span.end({ output: message.content[0] });
await langfuse.flushAsync();
With this in place, every production run is observable: you can see inputs, outputs, latency, and cost for each call. When something breaks in prod, you have the context to debug it.
If your agent is public or multi-tenant, add rate limiting:
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"), // 10 requests per minute
});
app.use("/run", async (c, next) => {
const ip = c.req.header("cf-connecting-ip") ?? "unknown";
const { success } = await ratelimit.limit(ip);
if (!success) return c.json({ error: "Rate limit exceeded" }, 429);
return next();
});
Before you call something "deployed":
Watch the first 100 real runs. Read the traces. Note where the agent succeeds and where it doesn't. Use that data to update your prompt (check Guide 02). Run your evals against the behavior you actually saw. Ship the improved version.
This is the loop: deploy → observe → eval → improve → repeat. There's no finish line, only better versions.
Congratulations on completing the Boostor learn path. You went from "what is an agent" to running one in production. That's the foundation everything else builds on.