I Built an AI Agent Platform in One Night — Here's the Architecture

Dev.to / 2026/3/26

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • The article describes the build and architecture of AgentDesk, an AI agent platform for professional services firms that includes pre-built agents for client intake, proposal generation, and reporting.
  • It details the full-stack technology choices—Next.js 16 (App Router/Server Components), React 19, the Anthropic SDK with typed tool use, Stripe for subscriptions via webhooks, and TypeScript for end-to-end type safety.
  • The core design is a reusable “agent engine” where each agent is defined by a configuration object (system prompt, tools, and execution parameters) rather than separate bespoke code paths.
  • It emphasizes how Next.js Server Components reduce client-side JavaScript, while the Anthropic SDK provides first-class TypeScript support for structured tool execution.

Last week I shipped AgentDesk — a platform that gives professional services firms pre-built AI agents for client intake, proposal generation, and reporting. The full stack: Next.js 16, Claude API with tool use, Stripe billing, and a real-time dashboard.

Here is the exact architecture, with code.

The Stack

Next.js 16.2.1         — App Router, React 19, Server Components
@anthropic-ai/sdk      — Claude API with structured tool use
Stripe 21.x            — Subscriptions, checkout, webhooks
Tailwind CSS 4         — Styling
TypeScript 5           — End-to-end type safety
Vercel                 — Deployment + edge functions

Why this stack? Next.js 16 with React 19 gives us Server Components for the landing page (zero JS shipped) and client components for the interactive dashboard. The Anthropic SDK has first-class TypeScript support with proper tool-use typing. Stripe handles billing without us building payment infrastructure.

The Core Pattern: Agent Engine

The key architectural decision was building a generic agent engine that all agents share. Each agent is just a configuration object — a system prompt, a set of tools, and execution parameters.

// src/lib/agent-engine.ts

export interface AgentConfig {
  id: string;
  name: string;
  systemPrompt: string;
  tools: Anthropic.Messages.Tool[];
  maxTokens: number;
  temperature: number;
}

export interface AgentTask {
  id: string;
  agentId: string;
  input: string;
  context?: Record<string, unknown>;
  status: "pending" | "running" | "needs_approval" 
        | "completed" | "failed";
  output?: string;
  actions?: AgentAction[];
  createdAt: string;
  completedAt?: string;
}

This is the pattern that makes the platform extensible. Adding a new agent means defining a new AgentConfig — no engine changes required.

Agent Definitions: Structured Prompts + Tool Schemas

Each agent gets a detailed system prompt and typed tool definitions. Here is a simplified version of the Intake Agent:

export const INTAKE_AGENT: AgentConfig = {
  id: "intake",
  name: "Intake Agent",
  systemPrompt: `You are a professional client intake agent 
  for a consulting firm. Your job is to:

  1. Read incoming inquiries (emails, form submissions)
  2. Qualify the lead: budget, timeline, service fit, size
  3. Generate a personalized professional response
  4. Score the lead on a 1-10 scale with reasoning
  5. Suggest next action: book_call / request_info / decline

  Output structured JSON with: score, qualification 
  (hot/warm/cold/spam), summary, suggestedResponse, 
  suggestedAction, reasoning.`,

  tools: [{
    name: "qualify_lead",
    description: "Analyze an inquiry and produce a 
      qualification score, suggested response, and action.",
    input_schema: {
      type: "object",
      properties: {
        inquiry_text: {
          type: "string",
          description: "The full text of the incoming inquiry"
        },
        source: {
          type: "string",
          description: "Where the inquiry came from"
        },
        sender_info: {
          type: "string",
          description: "Available info about the sender"
        }
      },
      required: ["inquiry_text"]
    }
  }],
  maxTokens: 2048,
  temperature: 0.3,
};

The tool schema is critical. It gives Claude structured output — the agent does not just return prose, it returns typed data the platform can act on (send emails, book calendar slots, update CRM).

The Proposal Agent follows the same pattern but with higher maxTokens (4096) for longer output and a generate_proposal tool that accepts call_notes, client_name, and service_type. The Report Agent takes project_name, period, metrics, and milestones.

The Execution Engine

The runAgent function is where it all comes together. It is intentionally simple:

export async function runAgent(
  agentId: string,
  input: string,
  context?: Record<string, unknown>
): Promise<AgentTask> {
  const agent = getAgent(agentId);
  if (!agent) throw new Error(`Agent not found`);

  const task: AgentTask = {
    id: `task_${Date.now()}_${Math.random()
      .toString(36).slice(2, 8)}`,
    agentId,
    input,
    context,
    status: "running",
    createdAt: new Date().toISOString(),
  };

  const client = new Anthropic({ 
    apiKey: process.env.ANTHROPIC_API_KEY 
  });

  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: agent.maxTokens,
    temperature: agent.temperature,
    system: agent.systemPrompt,
    messages: [{
      role: "user",
      content: `${input}${contextStr}`,
    }],
  });

  const textBlocks = response.content.filter(
    (block): block is Anthropic.Messages.TextBlock => 
      block.type === "text"
  );
  task.output = textBlocks.map((b) => b.text).join("

");
  task.status = "completed";
  return task;
}

Key decisions:

  1. Claude Sonnet 4 — best balance of speed and quality for business tasks. Fast enough for real-time use, smart enough for nuanced proposals.
  2. Low temperature (0.3-0.4) — consistency matters more than creativity for business documents.
  3. Typed text block extraction — the type guard ensures we only process text content, properly handling tool-use response blocks.
  4. Context injection — additional structured context (client data, firm preferences) gets appended as JSON, keeping the main input clean.

API Routes: Clean and Minimal

Each agent gets a REST endpoint via Next.js App Router:

// src/app/api/agents/[agentId]/route.ts

export async function POST(
  request: Request,
  { params }: { params: { agentId: string } }
) {
  const apiKey = request.headers.get("x-api-key");
  if (!apiKey) {
    return NextResponse.json(
      { error: "API key required" }, 
      { status: 401 }
    );
  }

  const { input, context } = await request.json();
  const task = await runAgent(params.agentId, input, context);
  return NextResponse.json({ task });
}

The API is intentionally RESTful. Any tool that can make HTTP requests can use these agents — Zapier, Make, n8n, custom scripts. No SDK required.

# Run the intake agent from any HTTP client
curl -X POST https://agentdesk-inky.vercel.app/api/agents/intake \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "New email from CEO of Acme Corp..."}'

Stripe Integration: Subscription Tiers

Billing maps directly to agent access:

// src/lib/stripe.ts

export const PLANS = {
  starter: {
    name: "Starter",
    priceId: process.env.STRIPE_PRICE_STARTER!,
    agents: ["intake"],
    monthlyPrice: 99,
    taskLimit: 100,
  },
  professional: {
    name: "Professional",
    priceId: process.env.STRIPE_PRICE_PROFESSIONAL!,
    agents: ["intake", "proposal", "report"],
    monthlyPrice: 349,
    taskLimit: 500,
  },
  agency: {
    name: "Agency",
    priceId: process.env.STRIPE_PRICE_AGENCY!,
    agents: ["intake", "proposal", "report"],
    monthlyPrice: 799,
    taskLimit: -1, // unlimited
  },
} as const;

The agents array controls which agents a subscriber can access. The taskLimit is checked before each agent run. Stripe webhooks handle subscription lifecycle (created, updated, canceled) via /api/billing/webhook.

The Dashboard: Client-Side Interactivity

The dashboard is a client component (needs useState for the quick-run feature) while the landing page is entirely server-rendered:

// Dashboard quick-run feature
async function handleQuickRun() {
  const res = await fetch(`/api/agents/${selectedAgent}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": apiKey,
    },
    body: JSON.stringify({ input: quickInput }),
  });
  const data = await res.json();
  setResult(data.task?.output || data.error);
}

Users pick an agent from a dropdown, paste their input (email, call notes, project data), and hit Run. The agent output appears in real-time.

Architecture Decisions Worth Noting

Why not LangChain? Too much abstraction for what we need. The Anthropic SDK with tool use is already well-structured. LangChain would add complexity without value for this use case.

Why configuration-based agents instead of code-based? Extensibility. A new agent is a JSON-like config object — no deployment needed for the engine. Future: users will create custom agents from the dashboard.

Why Claude over GPT-4? Tool use reliability. Claude's structured output with tool definitions produces more consistent results for business documents. The TypeScript SDK typing is also superior.

Why Vercel? Zero-config deployment for Next.js, edge functions for API routes, and preview deployments for every PR. Shipping speed matters more than infra optimization at this stage.

What I Would Do Differently

  1. Add a queue — Right now agent runs are synchronous. For production scale, I would add a job queue (BullMQ or Inngest) so long-running agents do not timeout.
  2. Persistent storage — Tasks are currently ephemeral. Adding Postgres (via Supabase or Neon) for task history and analytics is the next step.
  3. Streaming — The Claude API supports streaming responses. Showing the agent thinking in real-time would improve the UX dramatically.

Ship It

The total build: ~15 files, ~1,200 lines of code, deployed on Vercel. The agent engine pattern means adding new agents scales linearly — each new agent is just a config object with a system prompt and tool definitions.

If you are building AI agent products, the key insight is: keep the engine generic and the configuration specific. The smarts live in the prompts and tool schemas, not in custom code per agent.

Try AgentDesk or check out the architecture yourself.

I Built an AI Agent Platform in One Night — Here's the Architecture | AI Navigate