Skip to main content

EngenhariaAtualizado em 07/10/20269 min de leituraArtigo em inglês

Session-first: How We Designed the CodeSpar SDK

Sessions as immutable context containers. Meta tools instead of every raw tool. Managed auth that handles OAuth and token refresh. Framework adapters for Claude, OpenAI, and Vercel AI SDK. Here's every design decision and why we made it.
Fabiano Cruz
Engenharia · 14/04/2026demo
import { CodeSpar } from "@codespar/sdk";const session = await new CodeSpar().create("user_0000", { preset: "brazilian" });const run = await session.send("pay the supplier boleto for order 1304");const pay = run.tool_calls.find((c) => c.tool_name === "codespar_pay");const { payment_status } = await session.paymentStatus(pay!.id);

Execução do agente · dados de demonstração

When we started building the CodeSpar SDK, we had dozens of MCP servers published on npm. Developers could install any of them and wire them into their agents manually. But the developer experience was painful. You had to discover which servers existed, figure out which tools to use, manage authentication for each provider, and handle the orchestration yourself.

We needed an SDK that made all of this invisible. One import, one session, one line to send a message to a commerce agent.

agent.ts
import { CodeSpar } from "@codespar/sdk";

const codespar = new CodeSpar({ apiKey: "csk_..." });
const session = await codespar.create("user_123", { preset: "brazilian" });
const messages = await session.send("Charge R$150 via Pix and issue the NF-e");

Five lines. That is the target DX. Here is how we got there.

Sessions as context containers

The core abstraction is the Session. When you call codespar.create(userId, options), the SDK creates an immutable context container that bundles together:

  • A specific user (scoped by userId, isolated per organization)
  • A commerce preset (which MCP servers to connect)
  • The user's provider connections (CodeSpar refreshes OAuth tokens)
  • The organization's policy rules (budgets, rate limits, approval gates, time windows), checked on every tool call
  • An MCP endpoint for framework-agnostic access, when the session has one
  • Usage context (every tool call is logged)

Once created, a session's user and servers do not change. Need different servers? Create a new session. This immutability simplifies debugging, makes sessions safe to pass between functions, and eliminates an entire class of state management bugs.

Sessions are also the unit of record. Every tool call within a session is logged to the session_tool_calls table with input/output payloads, duration, and status. Tool calls are not billed: only a settled money movement is (see Billing integration, below).

session-lifecycle.ts
// Sessions are cheap to create
const session = await codespar.create("user_123", {
  preset: "brazilian",
});

// Session ID is a stable reference
console.log(session.id);  // "ses_a1b2c3..."

// The MCP endpoint (for Claude Desktop, Cursor, etc.), when the session has one
console.log(session.mcp?.url, session.mcp?.headers);

// The provider connections this session can use
console.log(await session.connections());

// Close it when the work is done
await session.close();

Meta tools, not raw tools

Each server in a preset brings its own raw tools. Passing every raw tool definition to an LLM would consume a significant portion of the context window and give the model too many choices.

Instead, the agent sees meta tools that route to the right provider. @codespar/types 0.11.4 publishes fifteen of them:

meta-tools.ts
// The meta tools in @codespar/types 0.11.4 (SHARED_META_TOOL_DEFINITIONS)
// codespar_discover            find the right tool for a use case
// codespar_get_started         the recommended first steps (read-only)
// codespar_manage_connections  list, inspect or connect providers
// codespar_checkout            sell side: a cart and a charge for a shopper
// codespar_charge              an inbound charge: the buyer pays you
// codespar_pay                 an outbound payment or transfer, or its status
// codespar_wallet              the governed wallet: balance, statement, top-up
// codespar_shop                buy side: search a store's catalog and buy
// codespar_invoice             issue, read or amend an NF-e, NFS-e or invoice
// codespar_ship                a shipping label, a quote or tracking
// codespar_notify              WhatsApp, email or SMS
// codespar_crypto_pay          USDC, USDT or BTC payments
// codespar_kyc                 identity checks and account opening
// codespar_ledger              double-entry books and receipts
// codespar_issue               payment cards for agents or end users

When an agent calls codespar_charge with a Pix method, CodeSpar looks up the routes its catalog has for that meta tool, rail, currency and country, keeps the providers you have connected, and calls that provider's tool. The agent sees the meta tools, not the raw tools behind them.

The right number of tools for an agent is the smallest number that covers all the use cases.

Each meta tool definition carries a name, a description and an input_schema. The schema declares the properties, the required ones, and closed vocabularies (actions, methods, channels) as enums, so a conformance check can compare any runtime's tool with the published definition. CodeSpar maps those properties to the parameters each provider expects.

meta-tool-definition.ts
// The published definition of codespar_pay (outbound money).
// Charging a customer is codespar_charge, not an action of codespar_pay.
import { SHARED_META_TOOL_DEFINITIONS } from "@codespar/sdk";

const pay = SHARED_META_TOOL_DEFINITIONS.codespar_pay;
console.log(pay.input_schema.required); // ["action"]
console.log(pay.contract.enums);
// { action: ["pay", "status"], method: ["pix", "card", "boleto", "wire"] }

Managed auth

Each provider requires different authentication. Zoop and NFe.io take an API key. Melhor Envio uses OAuth. Z-API needs an instance ID, an instance token and a Client-Token. Stark Bank signs every request with a key pair. Managing credentials for each provider is a pain developers should not have to deal with.

The session hands this to CodeSpar:

auth.ts
// An OAuth provider (Melhor Envio): a consent link for the user
const { authorizeUrl } = await session.authorize("melhor-envio", {
  redirectUri: "https://yourapp.com/connected/melhor-envio",
});

// API-key and multi-credential providers (Zoop, NFe.io, Z-API,
// Stark Bank): the connection wizard says what to provide
const wizard = await session.connectionWizard({
  action: "initiate",
  server_id: "nfe-io",
});
console.log(wizard.initiate?.connect_url, wizard.initiate?.required_secrets);

Credentials are stored per user, and CodeSpar refreshes OAuth tokens when they expire. Your code never stores them.

The key design decision: auth is per-user, not per-session. When a user authorizes Melhor Envio once, every subsequent session for that user inherits the credential. This means the OAuth flow happens once during onboarding, and every session after that just works.

auth-flow.ts
// Typical production flow:
// 1. During onboarding, send the user to the provider's consent page
const session = await codespar.create("user_123", { preset: "brazilian" });
const { authorizeUrl } = await session.authorize("melhor-envio", {
  redirectUri: "https://yourapp.com/connected/melhor-envio",
});
// Redirect the user to authorizeUrl...

// 2. Every later session for the same user sees the connection
const next = await codespar.create("user_123", { preset: "brazilian" });
const connections = await next.connections();
console.log(connections.filter((c) => c.connected).map((c) => c.id));
// No authorize() call needed once Melhor Envio is connected

Framework adapters

The SDK works with any AI framework through adapter packages. Each package converts session tools into the format the framework expects. The adapters are thin: they depend only on @codespar/sdk for types and the session interface. No heavy dependencies.

adapters.ts
// Vercel AI SDK - includes execute() functions
import { getTools as vercelTools } from "@codespar/vercel";
const forVercel = await vercelTools(session);
// Returns: Record<string, Tool> with execute() wired to session

// Claude API (Anthropic SDK) - native Tool format
import { getTools as claudeTools } from "@codespar/claude";
const forClaude = await claudeTools(session);
// Returns: Tool[] with { name, description, input_schema }

// OpenAI - function calling format
import { getTools as openaiTools } from "@codespar/openai";
const forOpenAI = await openaiTools(session);
// Returns: { type: "function", function: { name, description, parameters } }[]

// MCP - no adapter needed, use the session endpoint directly
const endpoint = session.mcp;
// Works with Claude Desktop, Cursor, VS Code, any MCP client

The await getTools(session) call is async because the session fetches its tool definitions from the CodeSpar API the first time they are asked for, and keeps them for its lifetime. Tools can change between sessions (different presets, different servers), so a new session fetches its own.

Note: @codespar/claude is for the Anthropic API directly. @codespar/vercel is for the Vercel AI SDK (which can use Anthropic, OpenAI, or any provider underneath). @codespar/mcp is an MCP server that gives Claude, Codex, Cursor and ChatGPT a CodeSpar commerce agent over MCP.

Real-time streaming with session.sendStream()

For user-facing applications, waiting for a whole Complete Loop to finish before showing any UI update is unacceptable. session.sendStream() returns a Server-Sent Events stream that emits events as each tool call starts, progresses, and completes:

streaming.ts
const stream = session.sendStream(
  "Process order #7721: charge R$1,249, issue NF-e, ship via PAC."
);

for await (const event of stream) {
  switch (event.type) {
    case "assistant_text":
      // The model's text, as it is produced
      process.stdout.write(event.content);
      break;
    case "tool_use":
      // A meta tool is about to execute
      console.log(`Starting: ${event.name}`);
      // event: { id, name, input }
      break;
    case "tool_result":
      // A meta tool finished
      console.log(`Done: ${event.toolCall.tool_name} (${event.toolCall.duration_ms}ms)`);
      // event.toolCall: { id, tool_name, server_id, status, duration_ms, input, output, error_code }
      break;
    case "error":
      // The run failed
      console.error(`Error: ${event.error}`);
      break;
    case "done":
      // All tool calls complete, final response ready
      console.log("Session finished:", event.result.tool_calls.length, "tool calls");
      break;
  }
}

The stream protocol is compatible with the standard SSE format, so it works with any HTTP client. The CodeSpar dashboard sandbox reads the same streaming endpoint (POST /v1/sessions/:id/send) to show tool execution as it happens.

One important detail: sendStream() handles the full agentic loop internally. The model receives the tools, reasons about which to call, executes them via MCP, reads the results, and continues until the model stops calling tools, up to an iteration limit. You do not need to implement a tool execution loop on the client side -- the SDK manages it server-side and streams events back.

Policy enforcement built in

Policy rules belong to the organization, and every tool call in every session is checked against them. You create them once, through the API: rate limits, time windows and approval gates.

policies.ts
// Policy rules belong to the organization: every tool call in every
// session is checked against them before it runs ("*" matches all)
await codespar.api.post("/v1/policies", {
  body: {
    name: "rate-limit", type: "rate-limit", agents: ["*"], tools: ["*"],
    config: { maxPerMinute: 30 }, enabled: true,
  },
});

await codespar.api.post("/v1/policies", {
  body: {
    name: "business-hours", type: "time-window", agents: ["*"], tools: ["*"],
    config: { startHour: 8, endHour: 18 }, enabled: true,
  },
});

// A person decides each payout before it dispatches
// (the rule matches agents and tools, never an amount)
await codespar.api.post("/v1/policies", {
  body: {
    name: "payouts-need-approval", type: "approval-required",
    agents: ["*"], tools: ["codespar_pay"], enabled: true,
  },
});

Policy checks happen before every tool execution. A denied call comes back as a policy_denied tool result, and a call held for a person as approval_required; isPolicyDenied() and isApprovalRequired() tell them apart, so your application can handle both gracefully.

The approval gate is particularly important for high-value commerce operations. An AI agent processing a R$50,000 wholesale order should pause and wait for human confirmation. An approval-required rule matches agents and tools, never an amount: the matched call does not dispatch, it waits in the approvals queue in the Dashboard for an admin, and if nobody decides within 24 hours by default it expires and never runs. What holds a payment by its amount is the mandate's per-transaction cap: above it, a person approves a boleto the agent pays or a Pix through the consumer-payments REST routes; other rails refuse, and the periodic and total caps always refuse.

Commerce events as triggers

Triggers deliver commerce events to your endpoint. You subscribe once, through the API:

triggers.ts
// Subscribe your endpoint to commerce events (exact event names)
await codespar.api.post("/v1/triggers", {
  body: {
    name: "payment-succeeded",
    event: "commerce.payment.succeeded",
    webhook_url: "https://yourapp.com/hooks/payment-succeeded",
  },
});

// CodeSpar POSTs each matching event to the webhook URL;
// your handler decides what follows, such as issuing the NF-e

A trigger matches the event name exactly: commerce.payment.succeeded does not receive commerce.payment.received. Each matching event is posted to the webhook URL, whatever language the agent spoke in.

Triggers run server-side: CodeSpar watches the provider connections in your project and its own runtime, and posts the event; your code decides what follows, such as issuing the NF-e when a payment succeeds or notifying the customer on WhatsApp. This turns the SDK from a tool execution layer into the start of a commerce automation engine.

Billing integration

At v0.2.0, every tool call was a billing unit. This was a deliberate design choice: billing at the tool call level gave precise cost attribution per user, per session, per order. Today tool calls are free under the rate card, and the example below reads what is billed now: the fee lanes.

billing.ts
// Tool calls are not billed; settled money movement is.
// The fee lanes measured and charged for your organization:
const fees = await codespar.api.get("/v1/fees");

console.log(fees.period);
console.log(fees.lanes.movimentar.charging);         // money leaving under a mandate
console.log(fees.lanes.movimentar.latest?.feeMinor); // what is owed, in BRL minor units

The v0.2.0 billing model mapped directly to Stripe metered billing: at the end of each billing cycle, the session_tool_calls table was aggregated per organization and reported as metered usage.

Why not bill per session or per API call? Because sessions vary wildly in complexity. A simple "check my balance" query is one tool call. A Complete Loop is four, one per step that has a meta tool. A customer support agent handling a return might make twelve. That was the case for per-tool-call billing; the update below says where pricing went.

Update, July 2026

This section describes the metering design as we shipped it in v0.2.0. Pricing has since moved up one level of abstraction, and tool calls are no longer billed: plans are now priced per settled money movement (the payment is the billable event; the invoicing, shipping, messaging, and ERP calls around it are included). Current plans live on the pricing page.

What we shipped

Five packages. All typed. All building via Turborepo. Published on npm at v0.2.0.

PackagePurpose
@codespar/sdkCore: sessions, tools, execute, send, sendStream, loop, managed auth
@codespar/claudeClaude API adapter (getTools, handleToolUse, toToolResultBlock)
@codespar/openaiOpenAI function calling adapter
@codespar/vercelVercel AI SDK adapter with execute() functions
@codespar/mcpMCP transport: connects Claude Desktop, Cursor and VS Code to CodeSpar sessions

The architecture decision that made shipping fast: all adapters depend on @codespar/sdk for types and the session interface, but they do not depend on each other. Installing @codespar/openai does not pull in Vercel AI SDK. Installing @codespar/vercel does not pull in the Anthropic SDK. You only pay for the dependencies you actually use.

What is next

The SDK at v0.2.0 covers the core surface: sessions, tools, send, sendStream, execute, loop, managed auth, and billing tracking. Here is what is coming next, in order of priority:

  1. Stripe billing integration (Marco 3). Wire metered billing from session_tool_calls to Stripe. Quota enforcement in the execute and send endpoints. Self-serve plan upgrades from the dashboard. This is the monetization milestone.
  2. Real MCP routing. Currently, the meta tools execute against mock servers. The next step connects them to the live MCP servers via Streamable HTTP transport. Tool calls will hit real Zoop, the fiscal provider, Melhor Envio, and Omie APIs.
  3. Managed connection UI. An OAuth connection flow in the dashboard where users click "Connect Zoop" and complete the authorization without writing code. Tokens stored server-side, auto-refreshed, never exposed to the client.
  4. Python SDK. Same API surface, same session model, targeting LangChain and CrewAI adapters. Python is essential for reaching the ML/data science agent community.
  5. Server-side triggers. Define automation rules in the dashboard: "When payment.completed, auto-issue NF-e" or "When shipping.label_created, notify customer on WhatsApp." This turns CodeSpar from a tool execution layer into a commerce automation engine that runs with no code at all.
  6. Mexico and Colombia presets. The MCP server catalog already covers 4 countries. Adding presets for Mexico (SAT/CFDI, Conekta, Envia) and Colombia (DIAN, Wompi, Envia) unlocks the rest of LatAm.

The SDK is the product. Everything else -- dashboard, servers, docs -- exists to make the SDK easier to adopt.

The SDK is open source. The code is at github.com/codespar/codespar-core. Install it with npm install @codespar/sdk and tell us what breaks.

This post was written at @codespar/sdk@0.2.0; its examples compile against 0.16.11. For a hands-on tutorial, see the Complete Loop tutorial. The enterprise packages (PolicyEngine, MandateGenerator, PaymentGateway) are covered in the thesis post.

Atualizado em 07/10/2026: o link do código do SDK aponta para o repositório dele, codespar/codespar-core.

Atualizado em 07/10/2026: os exemplos agora compilam com o @codespar/sdk 0.16.11. Os de antes estavam marcados como 0.2.0, mas usavam APIs que nenhuma versão publicada teve (budgetLimit, AuthManager, codespar.on, session.usage). As políticas são regras da organização que casam agente e ferramenta, nunca valor; acima do teto por transação do mandato, uma pessoa aprova um boleto pago pelo agente ou um Pix pelas rotas REST de consumer payments, e os outros trilhos recusam. Chamadas de ferramenta não são cobradas; só a movimentação liquidada é. As meta tools são as quinze que o @codespar/types 0.11.4 publica, roteadas pelo catálogo da CodeSpar; o exemplo de OAuth usa a Melhor Envio, o provedor OAuth do catálogo; e saíram as contagens e os tempos que não foram medidos.

Publicado em 14/04/2026 · Fabiano Cruz#engineering

Mais para ler

Próximos em Engenharia.

Todos os posts

Construa agentes que movimentam dinheiro.

Docs, cookbooks e um sandbox com chaves de teste.

As chaves de teste são gratuitas.

terminal · modo de testedemo
$ npm install @codespar/sdk$ export CODESPAR_API_KEY=csk_test_…
Session-first: How We Designed the CodeSpar SDK