---
title: "Agent loop in TypeScript"
description: Get your first AI agent commerce interaction running in under 5 minutes.
---

import { Callout } from "fumadocs-ui/components/callout";
import { Steps, Step } from "fumadocs-ui/components/steps";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Cards, Card } from "fumadocs-ui/components/card";

<VersionBadge pkg="@codespar/sdk" />

This guide walks you through installing the SDK, creating a session, executing a tool call, and wiring it into a full agent loop -- all in under 5 minutes. By the end, you will have a working commerce agent that can process payments in Brazil.

## Prerequisites

- Node.js 18+ installed
- A CodeSpar API key — mint one at [Dashboard → API Keys](https://codespar.dev/dashboard/api-keys). New accounts get a `test`-environment project plus a `csk_test_*` key auto-created at signup, and the mint modal defaults each new key's prefix to match the active project's environment.
- An LLM provider account (Anthropic, OpenAI, or any Vercel AI SDK provider)

<Callout type="info">
**On Python?** See [Quickstart (Python)](/docs/quickstart-python) for the same flow with `pip install codespar` — sync and async clients are published on PyPI.
</Callout>

<Callout type="info">
This quickstart builds a **seller agent** (your agent collects: checkout, charge, invoice). Building an agent that **buys and pays** instead? Start at the [buyer quickstart](/docs/quickstart-buyer) — MCP-first, no code.
</Callout>

<Steps>

<Step>
### Install the SDK and an adapter

Install the core SDK and the adapter for your preferred framework:

<Tabs items={["Claude", "OpenAI", "Vercel AI SDK"]}>
<Tab value="Claude">
```bash
npm install @codespar/sdk @codespar/claude @anthropic-ai/sdk
```
</Tab>
<Tab value="OpenAI">
```bash
npm install @codespar/sdk @codespar/openai openai
```
</Tab>
<Tab value="Vercel AI SDK">
```bash
npm install @codespar/sdk @codespar/vercel ai @ai-sdk/anthropic
```
</Tab>
</Tabs>

<Callout type="info">
Using pnpm or yarn? Replace `npm install` with `pnpm add` or `yarn add`. All packages are published on the public npm registry.
</Callout></Step>

<Step>
### Set your API keys

You need two environment variables -- one for CodeSpar, one for your LLM provider:

<Tabs items={["Claude", "OpenAI", "Vercel AI SDK"]}>
<Tab value="Claude">
```bash
export CODESPAR_API_KEY="csk_test_your_key_here"
export ANTHROPIC_API_KEY="sk-ant-your_key_here"
```
</Tab>
<Tab value="OpenAI">
```bash
export CODESPAR_API_KEY="csk_test_your_key_here"
export OPENAI_API_KEY="sk-your_key_here"
```
</Tab>
<Tab value="Vercel AI SDK">
```bash
export CODESPAR_API_KEY="csk_test_your_key_here"
export ANTHROPIC_API_KEY="sk-ant-your_key_here"
```
</Tab>
</Tabs>

<Callout type="info">
A `csk_test_*` key authorizes against a `test`-environment project. Provider tool calls go where the catalog's test classification for that provider says: its sandbox host where it publishes one, its own host where that host is already a sandbox or a test credential keeps the call in test, and the host your connection names for a provider you host yourself. A provider with no test environment, or one not yet classified, is refused in a test project with `422 test_venue_unavailable` instead of reaching its production service (see [Sessions](/docs/api/reference/sessions)). For deterministic responses without any upstream call, declare per-session fixtures via `cs.create({ mocks: {...} })` — see [Test Mode](/docs/concepts/test-mode) for the full reference.
</Callout></Step>

<Step>
### Run a session against an inline mock

The fastest way to confirm the SDK is wired correctly — and the pattern most tests use — is to declare a mock on session create. The runtime substitutes the fixture for the upstream call; policy, audit, and commerce-memory still fire on every invocation.

```typescript title="test-mode.ts"
// Requires @codespar/sdk@0.10.0+
import { CodeSpar } from "@codespar/sdk";

const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

const session = await cs.create("user_test", {
  servers: ["asaas"],
  mocks: {
    "asaas/create_payment": { id: "pay_test_42", status: "PENDING" },
  },
});

const result = await session.execute("asaas/create_payment", { value: 100 });

// `execute` returns the envelope; the provider payload is `data`, typed
// `unknown`, so name the shape you expect before reading it.
const payment = result.data as { id: string; status: string };
console.log(payment.id); // "pay_test_42"
```

A non-empty `mocks` field puts the session into strict mode for its lifetime: any tool call whose canonical name is not declared returns a `tool_not_mocked` envelope rather than falling through to the real upstream. See [Test Mode](/docs/concepts/test-mode) for the strict-mode contract, the five `tool_result` envelopes, and how to assert on them in TypeScript and Python.

</Step>

<Step>
### Create a session

A **session** is a scoped connection to one or more MCP servers. It manages authentication, tool routing, and usage tracking. Think of it as a database connection pool -- you create one, use it for the duration of your interaction, and close it when done.

```typescript title="index.ts"
import { CodeSpar } from "@codespar/sdk";

const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

const session = await codespar.create("user_123", {
  servers: ["stripe", "mercado-pago"],
});

console.log("Session ID:", session.id);
console.log("Status:", session.status);
console.log("Servers:", session.servers);
```

```json title="Output"
{
  "id": "ses_a1b2c3d4e5f6",
  "status": "active",
  "servers": ["stripe", "mercado-pago"],
  "created_at": "2026-04-15T14:30:00Z",
  "expires_at": "2026-04-15T15:30:00Z"
}
```

Passing `preset: "brazilian"` instead of a `servers` array bundles the Brazilian rails (payments, fiscal, logistics, messaging, banking, ERP) in one line; the two forms can be combined. The presets are listed under [Sessions → Country presets](/docs/concepts/sessions#country-presets).

<Callout type="warn">
Sessions expire after 30 minutes of inactivity. For long-running agents, implement session renewal or create a new session per interaction. See [Sessions](/docs/concepts/sessions) for lifecycle details.
</Callout></Step>

<Step>
### List available tools

Once your session is active, read the tool surface. `tools(session)` is async and returns the 14 meta-tools plus `codespar_get_started`, the same list on every session:

```typescript title="index.ts"
import { CodeSpar } from "@codespar/sdk";
const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });
const session = await cs.create("user_123", { servers: ["asaas"] });
// ---cut---
import { tools } from "@codespar/sdk";

const available = await tools(session);

console.log(`${available.length} tools available:`);
available.forEach((t) => {
  console.log(`  - ${t.name}: ${t.description}`);
});
```

```json title="Output (first 3 of 15)"
[
  {
    "name": "codespar_discover",
    "description": "Find the right tool for a free-form use case. ...",
    "input_schema": { "type": "object", "properties": { "use_case": { "type": "string" }, "category": { "type": "string" }, "country": { "type": "string" }, "limit": { "type": "number" } }, "required": ["use_case"] }
  },
  {
    "name": "codespar_get_started",
    "description": "Get the recommended happy path for this authenticated CodeSpar workspace. ...",
    "input_schema": { "...": "..." }
  },
  {
    "name": "codespar_manage_connections",
    "description": "List, inspect, or connect the accounts + identity the agent needs. ...",
    "input_schema": { "...": "..." }
  }
]
```

<Callout type="info">
It is the same list on every session: meta-tools resolve the provider through the catalog, so the list does not grow or shrink with the `servers` array. What that array changes is [`connections()`](/docs/api/sdk/connections) and which servers `proxyExecute` may name. To reach a provider's own tools by name, call `<server>/<tool>`, for example `asaas/get_balance`.
</Callout></Step>

<Step>
### Execute a tool call

Call a tool directly against the session to verify everything is wired correctly:

```typescript title="index.ts"
const result = await session.execute("codespar_discover", {
  use_case: "create a pix payment",
});

console.log(JSON.stringify(result.data, null, 2));
```

```json title="Response (related trimmed to 2 entries)"
{
  "use_case": "create a pix payment",
  "search_strategy": "embedding",
  "recommended": null,
  "related": [
    {
      "server_id": "inter-bank",
      "tool_name": "create_pix",
      "description": "Create a PIX payment",
      "http_method": "POST",
      "endpoint_template": "/pix/v2/pix",
      "connection_status": "disconnected",
      "auth_type": "api_key"
    },
    {
      "server_id": "mercado-pago",
      "tool_name": "create_pix_payment",
      "description": "Create a PIX payment",
      "http_method": "POST",
      "endpoint_template": "/v1/payments",
      "connection_status": "connected",
      "auth_type": "api_key"
    }
  ]
}
```

`connection_status` is the useful column: it separates what you can call today from what needs a connection first.

Now try creating a checkout link:

```typescript title="index.ts"
const checkout = await session.execute("codespar_checkout", {
  items: [{ title: "Pro Plan - Monthly", price: 49.9, quantity: 1 }],
  paymentMethod: "pix",
  currency: "BRL",
});

console.log(JSON.stringify(checkout.data, null, 2));
```

```json title="Response (raw trimmed)"
{
  "id": "pay_9ypabpb6jk9v31vy",
  "status": "PENDING",
  "amount": 49.9,
  "currency": "BRL",
  "method": "pix",
  "charge_url": "https://sandbox.asaas.com/i/9ypabpb6jk9v31vy",
  "raw": { "...": "the provider's own payload, verbatim" }
}
```

`price` is in major units (R$ 49.90 is `49.9`) and the total is the sum of `price` times `quantity`. Nothing in the call named a provider: the router picked the one the project has connected for Pix in Brazil, which is why the URL is an Asaas one.
</Step>

<Step>
### Wire it into an agent

Now connect everything to a real LLM. Choose your framework:

<Tabs items={["Claude", "OpenAI", "Vercel AI SDK"]}>
<Tab value="Claude">
```typescript title="agent-claude.ts"
import Anthropic from "@anthropic-ai/sdk";
import { CodeSpar } from "@codespar/sdk";
import { getTools, handleToolUse, toToolResultBlock } from "@codespar/claude";

const anthropic = new Anthropic();
const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

async function run(userMessage: string) {
  // 1. Create session with the servers you need
  const session = await codespar.create("user_123", {
    servers: ["stripe", "mercado-pago"],
  });

  // 2. Get tools in Anthropic format
  const tools = await getTools(session);

  // 3. Start the conversation
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  let response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    system:
      "You are a commerce assistant for a Brazilian e-commerce store. " +
      "Use the available tools to help with payments, invoicing, and shipping. " +
      "Always confirm amounts and details before processing payments.",
    tools,
    messages,
  });

  // 4. Tool-use loop: keep going until Claude stops calling tools
  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter(
      (b) => b.type === "tool_use"
    );

    const toolResults = await Promise.all(
      toolUseBlocks.map(async (block) => {
        try {
          const result = await handleToolUse(session, block);
          return toToolResultBlock(block.id, result);
        } catch (error) {
          return {
            type: "tool_result" as const,
            tool_use_id: block.id,
            is_error: true,
            content: error instanceof Error ? error.message : "Tool call failed",
          };
        }
      })
    );

    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });

    response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      tools,
      messages,
    });
  }

  // 5. Clean up
  await session.close();

  // 6. Return the final text
  const text = response.content.find((b) => b.type === "text");
  return text?.type === "text" ? text.text : "";
}

// Run it
const reply = await run("Create a R$49.90 checkout link for 'Pro Plan' using Stripe");
console.log(reply);
```

See the full [Claude adapter guide](/docs/api/sdk/providers/claude) for streaming, error handling, and advanced patterns.
</Tab>

<Tab value="OpenAI">
```typescript title="agent-openai.ts"
import OpenAI from "openai";
import { CodeSpar } from "@codespar/sdk";
import { getTools, handleToolCall } from "@codespar/openai";

const openai = new OpenAI();
const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

async function run(userMessage: string) {
  const session = await codespar.create("user_123", {
    servers: ["stripe", "mercado-pago"],
  });

  const tools = await getTools(session);

  const messages: OpenAI.ChatCompletionMessageParam[] = [
    {
      role: "system",
      content:
        "You are a commerce assistant for a Brazilian store. " +
        "Use the available tools for payments, invoicing, and shipping.",
    },
    { role: "user", content: userMessage },
  ];

  let response = await openai.chat.completions.create({
    model: "gpt-4o",
    tools,
    messages,
  });

  let message = response.choices[0].message;

  while (message.tool_calls && message.tool_calls.length > 0) {
    messages.push(message);

    for (const toolCall of message.tool_calls) {
      let content: string;
      try {
        content = await handleToolCall(session, toolCall);
      } catch (error) {
        content = JSON.stringify({
          error: error instanceof Error ? error.message : "Tool call failed",
        });
      }
      messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        content,
      });
    }

    response = await openai.chat.completions.create({
      model: "gpt-4o",
      tools,
      messages,
    });
    message = response.choices[0].message;
  }

  await session.close();
  return message.content ?? "";
}

const reply = await run("Generate a Pix QR code for R$250");
console.log(reply);
```

See the full [OpenAI adapter guide](/docs/api/sdk/providers/openai) for streaming and advanced patterns.
</Tab>

<Tab value="Vercel AI SDK">
```typescript title="agent-vercel.ts"
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { CodeSpar } from "@codespar/sdk";
import { getTools } from "@codespar/vercel";

const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

const session = await codespar.create("user_123", {
  servers: ["stripe", "mercado-pago"],
});

const tools = await getTools(session);

const { text } = await generateText({
  model: anthropic("claude-sonnet-4-20250514"),
  tools,
  maxSteps: 5,
  system:
    "You are a commerce assistant for a Brazilian e-commerce store. " +
    "Use the available tools for payments, invoicing, and shipping.",
  prompt: "Create a R$49.90 checkout link for 'Pro Plan' via Stripe",
});

console.log(text);
await session.close();
```

See the full [Vercel AI SDK guide](/docs/api/sdk/providers/vercel) for `streamText`, Next.js API routes, and the `useChat` hook.
</Tab>
</Tabs>
</Step>

</Steps>

## The Complete Loop in action

Here is what happens when you run the Claude example above. The agent performs the [Complete Loop](/docs/index#the-complete-loop) automatically:

1. **Claude receives the prompt** and sees the available tools from `getTools`.
2. **Claude calls `codespar_discover`** (optional) to understand available payment providers.
3. **Claude calls `codespar_checkout`** with the amount, currency, and provider.
4. **CodeSpar routes** the request to the Stripe MCP server, which creates a real checkout session.
5. **Claude receives the result** (checkout URL, status, expiry) and formats a response for the user.

```
User: "Create a R$49.90 checkout link for 'Pro Plan' using Stripe"

Claude thinks: I need to create a checkout link. I have the codespar_checkout tool.

Claude calls: codespar_checkout({
  provider: "stripe",
  amount: 4990,
  currency: "BRL",
  description: "Pro Plan",
  payment_methods: ["pix", "card"]
})

CodeSpar returns: {
  checkout_id: "chk_7f8g9h0i1j2k",
  url: "https://checkout.stripe.com/c/pay/cs_live_...",
  status: "open"
}

Claude responds: "I've created your checkout link for the Pro Plan at R$49.90.
Here it is: https://checkout.stripe.com/c/pay/cs_live_...
The link accepts both Pix and card payments and expires in 24 hours."
```

## Two places to explore without leaving the loop

These are different surfaces — pick the one that matches what you're doing:

- **In-dashboard sandbox** at [`/dashboard/sandbox`](https://codespar.dev/dashboard/sandbox) — a hosted chat interface where you can pick tools and watch them dispatch, without writing any code. Use it to scan what a server exposes before wiring it into your agent.
- **SDK-driven test mode** — the `mocks` field on `cs.create` shown above. Use it inside your test suite to assert deterministic responses for the tools your agent actually calls. See [Test Mode](/docs/concepts/test-mode) for the canonical reference.
## Common errors

| Error | Cause | Fix |
|-------|-------|-----|
| `INVALID_API_KEY` | Missing or malformed API key | Check that `CODESPAR_API_KEY` is set and starts with `csk_test_` (test-environment project) or `csk_live_` (live-environment project) |
| `SESSION_EXPIRED` | Session timed out after 30 minutes of inactivity | Create a new session |
| `SERVER_NOT_FOUND` | Requested a server that does not exist | Check available servers with `codespar_discover` |
| `TOOL_NOT_FOUND` | Called a tool not available in the current session | Verify the tool exists with `tools(session)` |
| `RATE_LIMITED` | Too many requests in a short period | Implement exponential backoff or reduce request frequency |

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "How CodeSpar works", description: "Sessions, meta-tools and the Complete Loop, end to end.", href: "/docs/how-it-works" },
  { label: "CONCEPT", title: "Test Mode", description: "Inline mocks, strict-mode behavior, the five tool-result envelopes, and bidirectional OSS parity.", href: "/docs/concepts/test-mode" },
  { label: "CONCEPT", title: "Authentication", description: "API keys, environments, scopes and provider auth.", href: "/docs/concepts/authentication" },
  { label: "CONCEPT", title: "Sessions", description: "Deep dive into the session lifecycle, server selection, and scoping.", href: "/docs/concepts/sessions" },
  { label: "CONCEPT", title: "Tools & Meta-Tools", description: "Understand the 14 meta-tools and how server routing works.", href: "/docs/concepts/tools" },
  { label: "PROVIDER", title: "Claude Adapter", description: "Full guide for building Claude-powered commerce agents.", href: "/docs/api/sdk/providers/claude" },
  { label: "PROVIDER", title: "OpenAI Adapter", description: "Integrate with GPT-4o and the OpenAI function-calling API.", href: "/docs/api/sdk/providers/openai" },
  { label: "PROVIDER", title: "Vercel AI SDK", description: "Streaming-first agents for Next.js applications.", href: "/docs/api/sdk/providers/vercel" },
  { label: "MCP", title: "Hosted MCP server", description: "Connect Claude Code, Claude Desktop, Cursor or Windsurf to the 14 meta-tools with one URL.", href: "/docs/concepts/meta-tools" },
  { label: "COOKBOOK", title: "E-Commerce Checkout", description: "Full cookbook — discover, checkout, invoice, ship, notify.", href: "/docs/cookbooks/ecommerce-checkout" },
  { label: "COOKBOOK", title: "Pix Payment Agent", description: "The simplest agent you can ship — Pix + WhatsApp in ~50 lines.", href: "/docs/cookbooks/pix-payment-agent" },
  { label: "BLOG", title: "The Complete Loop deep dive", description: "Why the loop closes on a fiscal document, and what each step guarantees.", href: "https://codespar.dev/blog/complete-loop-deep-dive" },
]} />
