Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

@codespar/sdk

Commerce SDK for AI agents — sessions, managed auth, Complete Loop orchestration for Latin American commercial APIs.

What shipped in 0.9

The current release line is 0.10; the notes below are the 0.9 changelog, kept for reference.

Eight new typed methods on Session, grouped by capability:

Meta-tool wrappers — neutral input shape, typed payload back, same wire as session.execute(...):

  • session.charge(args) — INBOUND charges (codespar_charge). Buyer pays merchant. Pix BRL via Asaas / MP / iugu / Stone; card USD via Stripe. Distinct from codespar_pay (outbound transfers).
  • session.ship(args) — shipping (codespar_ship). One typed entry into Melhor Envio's 3 rails (action: "label" | "quote" | "track").
  • session.shop(args) — buy-side shopping (codespar_shop). Catalog search → async checkout → Pix mint (action: "search" | "checkout" | "checkout_status"). Discriminated ShopArgs/ShopResult give the action-correct result type. Checkout is async: start, then poll checkout_status until ready_for_payment (carries pix_copia_e_cola) or canceled. Settle the returned Pix via a separate payment tool — settlement and governance are out of this contract. Full spec: docs/codespar-shop-contract.md.

Async settlementcodespar_charge / codespar_pay return synchronously, but real settlement lands via webhook:

  • session.paymentStatus(toolCallId) — poll the latest known status (pending → succeeded / failed / refunded). Correlates via the response idempotency_key ↔ provider external_reference.
  • session.paymentStatusStream(toolCallId, { onUpdate?, signal? }) — SSE variant. Snapshot on open, an envelope per state change, heartbeat every 15s, auto-closes 5s after a terminal state. signal aborts from the caller side.

Async verificationcodespar_kyc returns the inquiry id; the buyer finishes the hosted flow off-platform:

  • session.verificationStatus(toolCallId) — poll the disposition (pending → approved / rejected / review / expired).
  • session.verificationStatusStream(toolCallId, { onUpdate?, signal? }) — SSE variant. Same lifecycle as paymentStatusStream.

Tool discovery + connection wizard (already in 0.4.0, restated for completeness):

  • session.discover(query, options?) — semantic + lexical tool search across the catalog (codespar_discover, pgvector + pg_trgm).
  • session.connectionWizard(options) — connect deep-link backend (codespar_manage_connections). Credentials never travel through this method.

All wrappers call into the same runtime as session.execute(...); you get typed payloads instead of hand-rolling the meta-tool envelope.

The typed meta-tool facades (shop(), charge(), ship(), …) are clients, not implementations. Each is a thin wrapper over execute("codespar_<tool>", args) and requires a runtime that implements that meta-tool (a registered implementation behind the contract). Against a self-hosted runtime with no registered implementation, the call returns Tool not registered. The contract + the typed facade ship here MIT; the implementation is registered by the runtime you point baseUrl at.

Crypto + KYC meta-tools

codespar_crypto_pay (Coinbase Commerce + Bitso + UnblockPay + Foxbit) and codespar_kyc (Persona, Sift, Konduto, Truora) are routable today. The verification half now has a typed wrapper (session.verificationStatus / verificationStatusStream); a session.cryptoPay(...) typed wrapper is not in 0.9.0 — call via raw session.execute("codespar_crypto_pay", {...}) for now.

// Crypto: receive a USDC payment via Coinbase Commerce hosted checkout
const charge = await session.execute("codespar_crypto_pay", {
  amount: 29.9,
  currency: "USDC",
  direction: "receive",
  description: "Order #42",
});
// charge.output.hosted_url → redirect buyer here

// Crypto: send a USDC payout to an external wallet (UnblockPay stablecoin-payout)
const payout = await session.execute("codespar_crypto_pay", {
  rail: "stablecoin-payout",
  amount: 100,
  currency: "USDC",
  direction: "send",
  counterparty: {
    address: "0xRecipientWalletAddress...",
    country: "MX", // ISO 3166-1 alpha-2 — required when direction is "send"
  },
});
// payout.output.id / .status → poll via session.paymentStatus(payout.tool_call_id)

// KYC: kick off a Persona identity verification, then poll typed
const inquiry = await session.execute("codespar_kyc", {
  buyer: { email: "alice@example.com", first_name: "Alice", last_name: "Smith" },
  check_type: "identity",
});
const v = await session.verificationStatus(inquiry.tool_call_id);
//        ^^ approved | rejected | review | expired | pending

Install

npm install @codespar/sdk

Usage

import { CodeSpar } from "@codespar/sdk";

const cs = new CodeSpar({ apiKey: "ak_..." });

const session = await cs.create("user_123", {
  preset: "brazilian",
  manageConnections: { waitForConnections: true },
  // projectId: "prj_a1b2c3d4e5f6g7h8", // optional — overrides client default, falls back to org's default project
});

// Natural language
const result = await session.send("Charge R$150 via Pix and issue the NF-e");

// Direct tool execution
const charge = await session.execute("asaas/create_payment", {
  customer: "cus_xxx",
  billingType: "PIX",
  value: 150,
});

// Complete Loop — tools, findTools, and loop are free functions
import { tools, findTools, loop } from "@codespar/sdk";

const available = await tools(session);
const payments = await findTools(session, "payment");

const result = await loop(session, {
  steps: [
    { tool: "codespar_charge", params: { amount: 150, currency: "BRL", method: "pix", buyer: { name, document: cpf } } },
    { tool: "codespar_invoice", params: (prev) => ({ rail: "nfe", company_id, payment_id: prev[0].data.id }) },
    { tool: "codespar_ship", params: { action: "label", origin, destination, items } },
    { tool: "codespar_notify", params: { recipient: phone, message: "Your order is on the way!" } },
  ],
  onStepComplete: (step, r) => console.log(`✓ ${step.tool}`),
  retryPolicy: { maxRetries: 3, backoff: "exponential" },
});

API

new CodeSpar(config)

Option Type Default Description
apiKey string CODESPAR_API_KEY env Your API key
baseUrl string CODESPAR_BASE_URL env, else https://api.codespar.dev API base URL. Set CODESPAR_BASE_URL=http://localhost:8000 to point the SDK at a local OSS runtime; the managed backend is the default.
managed boolean true Enable managed billing/logging
projectId string Optional prj_<16alphanum>. Client-wide default project; sent as x-codespar-project. Falls back to the org's default project when omitted.

cs.create(userId, config)

Option Type Description
servers string[] MCP servers to connect
preset string "brazilian", "mexican", "argentinian", "colombian", "all"
manageConnections.waitForConnections boolean Block until all servers connected
projectId string Optional prj_<16alphanum>. Overrides the client-level projectId; falls back to the org's default project when both are unset.
mocks Record<string, MockValue> Optional test-mode mocks. See Test-mode mocks.

Session methods

Method Description
session.execute(tool, params) Execute a specific tool
session.send(message) Send natural language message
session.sendStream(message) Stream events from a natural language message
session.authorize(serverId) Start OAuth flow for a server
session.proxyExecute(request) Proxy a raw HTTP call through the session
session.connections() List connected servers
session.discover(query, options?) Tool search via codespar_discover (typed)
session.connectionWizard(options) Connect deep-link via codespar_manage_connections (typed)
session.charge(args) Inbound charge via codespar_charge (typed) — buyer pays merchant
session.ship(args) Shipping via codespar_ship (typed) — action: "label" | "quote" | "track"
session.paymentStatus(toolCallId) Async settlement status for a codespar_charge / codespar_pay call
session.paymentStatusStream(toolCallId, opts) SSE variant of paymentStatus; resolves on terminal
session.verificationStatus(toolCallId) Async KYC disposition for a codespar_kyc call
session.verificationStatusStream(toolCallId, opts) SSE variant of verificationStatus; resolves on terminal
session.mcp MCP transport URL and headers (when using managed runtime)
session.close() Close session

Free functions

tools, findTools, and loop are free functions that accept any SessionBase — they work with the managed runtime, Managed Agents sessions, and custom runtimes alike.

Function Description
tools(session) Get all available tools from the session
findTools(session, query) Search tools by name or description
loop(session, config) Run a Complete Loop workflow

Migrating from 0.2.x

tools, findTools, and loop moved from session instance methods to free functions in 0.3.0.

// 0.2.x
const tools = await session.tools();
const found = await session.findTools("payment");
const result = await session.loop({ steps: [...] });

// 0.3.0
import { tools, findTools, loop } from "@codespar/sdk";
const available = await tools(session);
const found = await findTools(session, "payment");
const result = await loop(session, { steps: [...] });

Multi-environment (projects)

CodeSpar orgs have a second tenancy tier — projects — so dev / staging / prod each get isolated API keys, connected accounts, triggers, and sessions while billing stays at the org level. Every request accepts an optional x-codespar-project: prj_<16 hex> header; omit it and the backend resolves the org's default project (self-healed on first read).

Pin the whole client to one environment:

const cs = new CodeSpar({
  apiKey: process.env.CODESPAR_API_KEY!,
  projectId: "prj_staging0123abcd", // every session this client spawns scopes here
});

Or override per session:

const session = await cs.create("user_123", {
  preset: "brazilian",
  projectId: "prj_prod0123abcd", // overrides the client default
});

Precedence: sessionConfig.projectId > clientConfig.projectId > backend's org default. Format is validated at construction via Zod (/^prj_[A-Za-z0-9]{16}$/) so typos fail fast. See the Projects concept doc for the full tenancy model.

Test-mode mocks

Skip live providers in tests by passing a mocks map to cs.create. Keys are canonical tool names in slash form (asaas/create_payment, melhor-envio/calculate_shipping, …). Values are either a single object — used as the response on every matching call — or an array of objects consumed in order, returning mocks_exhausted once the list drains.

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", status: "PENDING" },
  },
});

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

Pass an array for stateful mocks:

mocks: {
  "asaas/create_payment": [
    { id: "pay_1", status: "PENDING" },
    { id: "pay_1", status: "RECEIVED" },
  ],
}

Mocks live behind the managed backend's test-mode gate — a csk_test_* API key against a test-environment project. Live keys against the same map return mocks_not_permitted. The SDK forwards keys verbatim; if you send the OSS double-underscore form (asaas__create_payment) the backend rejects with mocks_invalid rather than the SDK silently rewriting.

The OSS runtime accepts the same mocks shape on its session API (see codespar/codespar#113), so the same test fixtures work whether you point at api.codespar.dev or a self-hosted instance via CODESPAR_BASE_URL. Self-hosted runtimes must additionally set CODESPAR_TEST_MODE_ENABLED=true on the server process; without it, the SDK receives mocks_not_permitted / HTTP 501 instead of fixture responses.

Storage shape differs by runtime — the wire contract does not. The managed backend persists mocks and per-tool consume counters; sessions and their fixtures survive restarts and multi-replica deployments. The OSS runtime holds both in process memory; they are scoped to the HTTP-session process and are lost on restart, and channel-bridge sessions (WhatsApp, Slack, Telegram, Discord) cannot carry mocks under the OSS shape. Response envelopes, status codes, sibling fields, and gate ordering are byte-identical between runtimes regardless. See the test-mode concept doc for the full per-runtime split.

Test mode is a property of the runtime, not the session. On the managed backend it's project.environment === 'test'; on a self-hosted OSS runtime it's CODESPAR_TEST_MODE_ENABLED=true on the server process. When the runtime is in test mode, every external tool call your code or LLM dispatches must match a declared mock — unmatched calls return tool_not_mocked (HTTP 422 on the catalog-routed /execute path; a tool_result block on the chat-loop) and no upstream provider runs. The envelope covers three failure modes: the mocks map has no entry for the canonical name, the session was created with no mocks field, or the canonical name has an unknown server prefix. A session that doesn't declare mocks can't dispatch any tools in test mode; declare the mocks the test will exercise, or run the same code against a live-mode runtime where the real providers handle dispatch. Built-in metadata tools — codespar_list_tools on OSS, codespar_discover and codespar_manage_connections on the managed backend — bypass this gate.

Type aliases

MockObject (Record<string, unknown>) and MockValue (MockObject | MockObject[]) ship from @codespar/types and re-export through @codespar/sdk. Use them when you want to define mock fixtures separately from the create call site.

import type { MockValue } from "@codespar/sdk";

const fixtures: Record<string, MockValue> = {
  "asaas/create_payment": { id: "pay_test", status: "PENDING" },
};

Typed errors

Every transport failure from createSession, proxyExecute, send, sendStream, paymentStatus(Stream), verificationStatus(Stream), and authorize throws a CodesparApiError with a structured code field. The old e.message.includes("foo") pattern is gone — branch on e.code instead.

import { CodesparApiError } from "@codespar/sdk";

try {
  await cs.create("user_test", { mocks: { "asaas/create_payment": {} } });
} catch (err) {
  if (err instanceof CodesparApiError) {
    if (err.code === "mocks_not_permitted") {
      // Live key against a mocks map. Swap to csk_test_*.
    } else if (err.code === "mocks_invalid") {
      // Backend rejected a tool-name key. Check the slash form.
    } else if (err.status === 0) {
      // Network never reached the backend; err.cause has the fetch rejection.
    }
    throw err;
  }
}

session.execute keeps its returns-vs-throws asymmetry: tool failures come back as ToolResult.success === false with the body on error. Only transport-level failures throw.

Tool-result guards

The five reserved tool-result codes (policy_denied, approval_required, mocks_exhausted, mocks_engine_error, tool_not_mocked) ship typed guards plus an exhaustive-match helper. Guards run against any unknown payload — both ToolResult.data from session.execute and ToolCallRecord.output from send / sendStream. Each guard checks the code discriminant AND the variant's required sibling fields, so a malformed payload returns false rather than narrowing positive on the code alone.

import {
  isApprovalRequired,
  isMocksEngineError,
  isMocksExhausted,
  isPolicyDenied,
  isToolNotMocked,
  assertExhaustiveToolResult,
  ToolResultCode,
} from "@codespar/sdk";

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

if (isPolicyDenied(result.data)) {
  console.warn(`blocked by ${result.data.rule_id}: ${result.data.message}`);
} else if (isApprovalRequired(result.data)) {
  console.log(`needs approval ${result.data.approval_id} by ${result.data.expires_at}`);
} else if (isMocksExhausted(result.data)) {
  // Stateful mock array drained — pad it or extend the test.
} else if (isMocksEngineError(result.data)) {
  // Backend-side mocks engine failure; usually a malformed fixture.
} else if (isToolNotMocked(result.data)) {
  console.warn(`no mock for ${result.data.tool_name}`);
}

The same guards apply inside a sendStream loop against event.toolCall.output.

When a switch over ToolResultCode covers every variant, call assertExhaustiveToolResult in the default branch. TypeScript fails to compile if a sixth code lands without a matching arm.

function handle(outcome: ToolResultOutcome): string {
  switch (outcome.code) {
    case ToolResultCode.PolicyDenied: return outcome.rule_id;
    case ToolResultCode.ApprovalRequired: return outcome.approval_id;
    case ToolResultCode.MocksExhausted: return "exhausted";
    case ToolResultCode.MocksEngineError: return "engine";
    case ToolResultCode.ToolNotMocked: return outcome.tool_name;
    default: return assertExhaustiveToolResult(outcome);
  }
}

Need more?

Need governance, budget limits, and audit trails for agent payments? CodeSpar Enterprise adds policy engine, payment routing, and compliance templates on top of these MCP servers.

License

MIT — codespar.dev