Skip to main content
All posts
Tutorial

The Complete Loop in 5 Lines of Code

Charge via Pix. Issue NF-e. Generate shipping label. Notify on WhatsApp. Register in ERP. Reconcile in banking. Six APIs, one agent, five lines of TypeScript.

FC
Fabiano Cruz
Co-founder, CodeSpar
2026.04.14
18 min

Selling online in Brazil means touching six different systems for every single order. Payment gateway, tax authority, shipping carrier, messaging platform, ERP, and banking reconciliation. Miss one and you are either breaking the law, losing packages, or burning cash on manual reconciliation.

This is not a convenience problem. NF-e issuance is a legal requirement. SEFAZ authorization must happen before the product ships. Pix confirmations expire. Shipping labels need to be generated before carrier pickup windows close. These are not nice-to-haves you can defer to v2.

The Complete Loop is CodeSpar's orchestration primitive that runs all six steps in sequence, with retry logic, error handling, and rollback built in. This tutorial shows you how to use it.

Before CodeSpar: 6 manual steps, 45 minutes, 3 people. After: 1 agent call, 12 seconds, zero humans.

What the Complete Loop actually does

Every e-commerce order in Brazil requires this exact sequence. Not approximately this sequence. This exact sequence, in this order, with each step depending on the output of the previous one:

  1. Charge via Pix. Generate a Pix QR code, wait for payment confirmation from the PSP. The Pix expires in 30 minutes by default. If the customer does not pay, the entire loop aborts. You cannot issue an invoice for an unpaid order.
  2. Issue NF-e. Submit the Nota Fiscal Eletronica to SEFAZ (the state tax authority). SEFAZ validates the tax calculation, product codes (NCM), and business registration (CNPJ). Authorization can take 1-15 seconds. Without an authorized NF-e, shipping the product is a federal crime under Brazilian tax law.
  3. Generate shipping label. Request a label from the carrier (Correios, Jadlog, or others via Melhor Envio). The label contains the tracking code, delivery estimate, and the NF-e DANFE number. Carriers will not accept packages without a valid NF-e reference.
  4. Notify the customer. Send the order confirmation, tracking code, and NF-e PDF via WhatsApp (or SMS/email as fallback). Brazil has 99% WhatsApp penetration. Customers expect real-time updates.
  5. Register in ERP. Create the sales order in the ERP (Omie, Bling, Tiny) with the payment reference, NF-e number, and shipping tracking. The ERP is the system of record for accounting, tax reporting, and inventory management.
  6. Reconcile in banking. Match the Pix credit in the bank account (via Stark Bank or similar) against the order. This closes the financial loop and ensures no revenue leaks between the PSP settlement and the bank balance.

Skip step 2 and you are evading taxes. Skip step 3 and the carrier rejects the package. Skip step 5 and your accountant cannot close the month. Skip step 6 and you will never know if R$50,000 in Pix payments actually settled in your bank account.

Regulatory context

NF-e issuance is mandatory for all commercial transactions in Brazil (Lei 8.846/94, updated by Ajuste SINIEF 07/05). Operating without NF-e carries fines of 1% of transaction value per day, plus potential criminal liability. The Complete Loop ensures compliance by making NF-e issuance a non-optional step in the orchestration.

1

Install the SDK

The SDK is distributed as five npm packages. For this tutorial, you need the core SDK and the Vercel AI SDK adapter:

terminal
npm install @codespar/sdk @codespar/vercel ai @ai-sdk/anthropic

The packages and their roles:

  • @codespar/sdk -- Core: sessions, tools, execute, loop, managed auth, billing hooks
  • @codespar/vercel -- Vercel AI SDK adapter with execute() functions
  • @codespar/claude -- Claude API adapter (if using Anthropic SDK directly)
  • @codespar/openai -- OpenAI adapter (function calling format)
  • @codespar/mcp -- MCP config generator for IDE integration (Claude Desktop, Cursor)
2

Configure environment variables

.env
# CodeSpar API key (get one at codespar.dev/dashboard/settings)
CODESPAR_API_KEY=csk_live_your_key_here

# Anthropic API key (for the LLM that orchestrates the loop)
ANTHROPIC_API_KEY=sk-ant-your_key_here

# Optional: Override the CodeSpar API endpoint (default: https://api.codespar.dev)
# CODESPAR_BASE_URL=https://api.codespar.dev

# Optional: Enable debug logging for development
# CODESPAR_DEBUG=true
API key scoping

CodeSpar API keys are scoped to an organization. Each key starts with csk_live_ (production) or csk_test_ (sandbox). Test keys hit mock servers that return realistic data without touching real payment gateways or SEFAZ. Always use test keys during development.

3

Create a session

A session is a scoped execution context. It bundles together a user, a set of MCP servers (determined by the preset), auth credentials, and policy rules. Sessions are the billing unit.

agent.ts
import { CodeSpar } from "@codespar/sdk";
import { getTools } from "@codespar/vercel";
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

// Initialize the SDK
const codespar = new CodeSpar({
  apiKey: process.env.CODESPAR_API_KEY!,
  // baseUrl: process.env.CODESPAR_BASE_URL, // optional override
});

// Create a session scoped to this user
const session = await codespar.create("user_123", {
  preset: "brazilian",    // connects 6 MCP servers for Brazil commerce
  budgetLimit: 5000,      // max R$ per session (policy enforcement)
});

// Get tools formatted for Vercel AI SDK
// This returns 6 meta tools with execute() functions built in
const tools = await getTools(session);

console.log(`Session created: ${session.id}`);
console.log(`Loaded ${Object.keys(tools).length} meta tools`);
// Output: Loaded 6 meta tools
// codespar_discover, codespar_checkout, codespar_pay,
// codespar_invoice, codespar_ship, codespar_notify

The brazilian preset connects to six MCP servers: Zoop (payments), Nuvem Fiscal (NF-e), Melhor Envio (shipping), Z-API (WhatsApp), Omie (ERP), and Stark Bank (banking). Each server exposes 10-20 tools. The SDK collapses all of them into 6 meta tools so the LLM sees a clean, unambiguous tool surface.

4

Run the Complete Loop with generateText

The simplest way to run the Complete Loop is to let the LLM orchestrate it. You describe the order in natural language and the model calls the right tools in the right sequence:

loop-simple.ts
// Subscribe to events before sending
codespar.on("payment.completed", (e) => {
  console.log("Payment confirmed:", e.data.amount, e.data.currency);
});
codespar.on("nfe.issued", (e) => {
  console.log("NF-e authorized:", e.data.number, "SEFAZ:", e.data.sefaz);
});
codespar.on("shipping.label_created", (e) => {
  console.log("Label:", e.data.carrier, "Tracking:", e.data.tracking);
});

// Run the Complete Loop
const result = await generateText({
  model: anthropic("claude-sonnet-4-5-20250514"),
  tools,
  maxSteps: 10,
  prompt: `
    Process order #7721 for customer Joao Silva (CPF: 123.456.789-00):
    - Charge R$1,249.00 via Pix
    - Issue NF-e for: 1x MacBook Air M3 (NCM 8471.30.19, CFOP 5102)
    - Generate shipping label (Correios PAC, CEP 01310-100 to 20040-020)
    - Notify customer on WhatsApp (+5511999999999)
    - Register sale in Omie ERP
    - Reconcile payment in Stark Bank
  `,
});

console.log(result.text);

The LLM reads the prompt, identifies the six steps, and calls the meta tools in order. Each meta tool routes to the correct MCP server, executes the real API call, and returns the result. The events fire as each step completes.

5

Run the Complete Loop with session.loop()

For production workloads, you want deterministic orchestration rather than relying on the LLM to figure out the sequence. The session.loop() API gives you explicit control over each step, with built-in retry logic and error handling:

loop-production.ts
const loop = await session.loop({
  steps: [
    {
      server: "mcp-zoop",
      tool: "create_transaction",
      params: {
        amount: 124900,  // centavos
        currency: "BRL",
        payment_type: "pix",
        customer: {
          name: "Joao Silva",
          cpf: "12345678900",
          email: "joao@example.com",
        },
        description: "Order #7721 - MacBook Air M3",
        expiration_seconds: 1800,  // 30 min Pix expiration
      },
    },
    {
      server: "mcp-nuvem-fiscal",
      tool: "create_nfe",
      params: {
        natureza_operacao: "Venda de mercadoria",
        items: [{
          descricao: "MacBook Air M3",
          ncm: "84713019",
          cfop: "5102",
          quantidade: 1,
          valor_unitario: 124900,
          icms: { cst: "00", aliquota: 18 },
          pis: { cst: "01", aliquota: 1.65 },
          cofins: { cst: "01", aliquota: 7.6 },
        }],
        destinatario: {
          cpf: "12345678900",
          nome: "Joao Silva",
          endereco: { cep: "20040020", uf: "RJ" },
        },
      },
      // Use output from step 0 (payment) as input
      mapInput: (prevResults) => ({
        payment_id: prevResults[0].data.transaction_id,
      }),
    },
    {
      server: "mcp-melhor-envio",
      tool: "create_shipment",
      params: {
        service: "PAC",
        from: { postal_code: "01310100" },
        to: { postal_code: "20040020" },
        package: { weight: 1.5, width: 35, height: 5, length: 25 },
      },
      mapInput: (prevResults) => ({
        invoice_key: prevResults[1].data.chave_nfe,
      }),
    },
    {
      server: "mcp-z-api",
      tool: "send_message",
      params: { phone: "5511999999999" },
      mapInput: (prevResults) => ({
        message: [
          "Pedido #7721 confirmado!",
          `Pagamento: R$ 1.249,00 via Pix`,
          `NF-e: ${prevResults[1].data.number}`,
          `Rastreio: ${prevResults[2].data.tracking_code}`,
          `Previsao de entrega: ${prevResults[2].data.estimated_delivery}`,
        ].join("\n"),
      }),
    },
    {
      server: "mcp-omie",
      tool: "create_order",
      mapInput: (prevResults) => ({
        order_number: "7721",
        customer_cpf: "12345678900",
        payment_id: prevResults[0].data.transaction_id,
        nfe_number: prevResults[1].data.number,
        tracking_code: prevResults[2].data.tracking_code,
        total: 124900,
      }),
    },
    {
      server: "mcp-stark-bank",
      tool: "get_balance",
      mapInput: (prevResults) => ({
        reference: prevResults[0].data.transaction_id,
        expected_amount: 124900,
      }),
    },
  ],

  // Retry policy for transient failures
  retryPolicy: {
    maxRetries: 3,
    backoff: "exponential",   // 1s, 2s, 4s
    retryableErrors: ["TIMEOUT", "RATE_LIMITED", "SERVER_ERROR"],
  },

  // Called after each step completes
  onStepComplete: (step, result) => {
    console.log(`[${step.server}] ${step.tool}: ${result.status}`);
  },

  // Called if a step fails after all retries
  onStepFailed: (step, error) => {
    console.error(`[${step.server}] ${step.tool} FAILED:`, error.code);
  },
});

console.log(`Complete Loop finished in ${loop.durationMs}ms`);
console.log(`Steps: ${loop.results.length}/6 succeeded`);

Error handling: what actually goes wrong

In production, every step can fail. Here is what goes wrong at each stage and how to handle it.

Pix payment timeout

Pix QR codes expire. The default expiration is 30 minutes, but some PSPs allow as little as 5 minutes. If the customer does not scan and confirm within the window, the payment times out.

error-pix.ts
try {
  const payment = await session.execute("ZOOP_CREATE_CHARGE", {
    amount: 124900,
    payment_type: "pix",
    expiration_seconds: 1800,
  });
} catch (error) {
  if (error.code === "PIX_EXPIRED") {
    // Generate a new Pix QR code with extended expiration
    const retry = await session.execute("ZOOP_CREATE_CHARGE", {
      amount: 124900,
      payment_type: "pix",
      expiration_seconds: 3600,  // extend to 1 hour
    });

    // Notify the customer that a new QR code is available
    await session.execute("ZAPI_SEND_MESSAGE", {
      phone: "5511999999999",
      message: "Seu Pix expirou. Geramos um novo QR code. Escaneie para pagar.",
    });
  }

  if (error.code === "PSP_UNAVAILABLE") {
    // Zoop is down - fall back to Asaas or queue for retry
    console.error("PSP unavailable. Queuing order for retry.");
    await queueForRetry("payment", orderData, {
      maxAttempts: 5,
      backoffMs: 30000,  // retry every 30 seconds
    });
  }
}
Production tip

Always set Pix expiration to at least 30 minutes for desktop users. Mobile users who are already in their banking app can pay in seconds, but desktop users need to switch devices. Monitor your timeout rate: anything above 15% means your expiration window is too short.

NF-e SEFAZ rejection

SEFAZ can reject an NF-e for dozens of reasons: invalid NCM code, incorrect ICMS calculation, expired digital certificate, server timeout, or the state's SEFAZ being offline entirely. Each scenario requires different handling.

error-nfe.ts
try {
  const nfe = await session.execute("NUVEM_FISCAL_CREATE_NFE", {
    natureza_operacao: "Venda de mercadoria",
    items: [/* ... */],
  });
} catch (error) {
  if (error.code === "SEFAZ_REJECTION") {
    // SEFAZ returned a specific rejection code
    // Common: 301 (IE destinatario), 539 (NCM), 694 (CFOP/CST)
    console.error(`SEFAZ rejection ${error.data.cStat}: ${error.data.xMotivo}`);

    // Log for manual review - do NOT auto-retry rejected NF-e
    // Rejections are typically data errors that require human correction
    await alertOps("nfe_rejected", {
      order: "7721",
      rejection_code: error.data.cStat,
      reason: error.data.xMotivo,
    });
  }

  if (error.code === "SEFAZ_TIMEOUT" || error.code === "SEFAZ_UNAVAILABLE") {
    // SEFAZ is down - switch to contingencia mode (NFC-e offline)
    console.warn("SEFAZ unavailable. Issuing in contingencia mode.");
    const nfeContingencia = await session.execute("NUVEM_FISCAL_CREATE_NFE", {
      // Same params, but with contingencia flag
      contingencia: true,
      justificativa: "SEFAZ indisponivel",
      // The NF-e will be transmitted when SEFAZ is back online
    });

    // Schedule automatic retransmission check
    await scheduleRetransmission(nfeContingencia.data.chave_nfe, {
      checkIntervalMs: 300000,  // check every 5 minutes
      maxDurationHours: 24,     // give up after 24 hours
    });
  }
}
Critical

Never auto-retry a SEFAZ rejection. Rejections mean the data is wrong (bad NCM, invalid CFOP, tax math error). Retrying the same payload will produce the same rejection. SEFAZ timeouts are different and can be retried safely. Always distinguish between rejection (data error) and timeout (infrastructure error).

Shipping label generation failure

Melhor Envio aggregates multiple carriers. If the primary carrier (Correios PAC) is unavailable, you can fall back to alternatives:

error-shipping.ts
const CARRIER_FALLBACK = ["PAC", "SEDEX", "jadlog_package", "azul_amanha"];

async function createShipmentWithFallback(
  session: Session,
  params: ShipmentParams
): Promise<ShipmentResult> {
  for (const service of CARRIER_FALLBACK) {
    try {
      return await session.execute("MELHOR_ENVIO_CREATE_SHIPMENT", {
        ...params,
        service,
      });
    } catch (error) {
      if (error.code === "CARRIER_UNAVAILABLE" || error.code === "SERVICE_AREA") {
        console.warn(`${service} unavailable, trying next carrier...`);
        continue;  // try next carrier
      }
      throw error;  // unexpected error - don't swallow it
    }
  }
  throw new Error("All carriers unavailable. Manual intervention required.");
}

WhatsApp delivery failure

WhatsApp delivery can fail if the number is not registered, the Z-API session has expired, or Meta's infrastructure is having issues. Always have a fallback channel:

error-whatsapp.ts
async function notifyCustomer(
  session: Session,
  customer: { phone: string; email: string },
  message: string
): Promise<void> {
  try {
    // Primary: WhatsApp via Z-API
    await session.execute("ZAPI_SEND_MESSAGE", {
      phone: customer.phone,
      message,
    });
    return;
  } catch (error) {
    console.warn("WhatsApp failed:", error.code);
  }

  try {
    // Fallback 1: SMS via Twilio (or similar)
    await session.execute("TWILIO_SEND_SMS", {
      to: `+${customer.phone}`,
      body: message.substring(0, 160),  // SMS character limit
    });
    return;
  } catch (error) {
    console.warn("SMS failed:", error.code);
  }

  // Fallback 2: Email (always available)
  await session.execute("SENDGRID_SEND_EMAIL", {
    to: customer.email,
    subject: "Pedido #7721 confirmado",
    body: message,
  });
}

ERP sync failure

ERP systems go down. Omie has maintenance windows. Bling rate-limits aggressively. The correct pattern is to queue and retry, not to block the order:

error-erp.ts
async function syncToERP(session: Session, orderData: OrderData): Promise<void> {
  try {
    await session.execute("OMIE_CREATE_ORDER", {
      order_number: orderData.orderId,
      customer_cpf: orderData.customerCpf,
      total: orderData.total,
      nfe_number: orderData.nfeNumber,
    });
  } catch (error) {
    if (error.code === "RATE_LIMITED" || error.code === "ERP_UNAVAILABLE") {
      // Queue for async retry - the order is already paid and shipped
      // ERP sync is important but not blocking
      await enqueueERPSync(orderData, {
        maxRetries: 10,
        backoff: "exponential",
        initialDelayMs: 60000,  // first retry in 1 minute
      });

      console.warn(`ERP sync queued for order ${orderData.orderId}`);
      return;
    }

    // Data errors (invalid CPF, duplicate order) need human review
    await alertOps("erp_sync_failed", {
      order: orderData.orderId,
      error: error.code,
      message: error.message,
    });
  }
}
Design principle

Steps 1-3 (payment, NF-e, shipping) are blocking. If they fail, the order cannot proceed. Steps 4-6 (notification, ERP, reconciliation) are non-blocking. They should be queued and retried asynchronously. Never let an ERP timeout prevent a customer from receiving their order confirmation.

Real-time streaming with session.sendStream()

For user-facing applications where you need to show progress as the loop executes, use session.sendStream() instead of generateText. This returns a Server-Sent Events stream that emits events as each tool call starts and completes:

stream.ts
// SSE stream for real-time UI updates
const stream = await session.sendStream(
  "Process order #7721: charge R$1,249 via Pix, issue NF-e, ship, notify."
);

for await (const event of stream) {
  switch (event.type) {
    case "tool_call_start":
      console.log(`Starting: ${event.data.tool_name}...`);
      // UI: show spinner on current step
      break;
    case "tool_call_complete":
      console.log(`Done: ${event.data.tool_name} (${event.data.duration_ms}ms)`);
      // UI: check mark on completed step
      break;
    case "text_delta":
      process.stdout.write(event.data.text);
      // UI: stream the agent's reasoning
      break;
    case "error":
      console.error(`Error: ${event.data.code} - ${event.data.message}`);
      break;
    case "done":
      console.log("Complete Loop finished.");
      break;
  }
}

What happened under the hood

When you call generateText or session.loop() with the CodeSpar tools, here is what the SDK does at each layer:

  1. Policy check. The PolicyBridge verifies the R$1,249 charge is within the session's budget limit. If the charge would exceed the budget, a typed BudgetExceededError is thrown before any API call is made. Rate limits and time-window policies are also checked.
  2. Meta tool routing. The MetaToolExecutor receives codespar_pay and resolves it to the Zoop MCP server. It scores all tools on that server using keyword matching against the input parameters and selects ZOOP_CREATE_CHARGE as the best match.
  3. Auth injection. The AuthManager retrieves stored OAuth2 tokens for Zoop, checks expiration, refreshes if needed, and injects the bearer token into the MCP request headers. Your code never touches a raw token.
  4. MCP execution. The tool call is sent to the Zoop MCP server via Streamable HTTP transport. The server validates the request, calls the Zoop REST API, and returns the result.
  5. Event detection. The EventDetector analyzes the tool call name and response payload to fire typed commerce events (payment.completed, nfe.issued). Detection works with both English and Portuguese response text.
  6. Billing. Every tool call is logged to the session_tool_calls table with input/output payloads, duration, and status. These records drive metered billing via Stripe. One tool call equals one billing unit.
  7. Budget tracking. The PolicyBridge records the R$1,249 charge against the session budget. Remaining budget is available via session.budget().

Latency breakdown: where does the time go?

A Complete Loop takes 8-15 seconds end-to-end in production, depending on carrier response times and SEFAZ load. Here is the typical latency breakdown for each step:

StepAPIP50P99
Pix chargeZoop1.2s3.5s
NF-e issuanceSEFAZ2.1s8.0s
Shipping labelMelhor Envio1.8s4.2s
WhatsApp msgZ-API0.8s2.0s
ERP registrationOmie1.5s3.8s
Bank reconciliationStark Bank0.6s1.5s
Total (sequential)8.0s23.0s

SEFAZ is the bottleneck. NF-e authorization involves cryptographic signature verification and cross-state validation, which is why P99 can spike to 8 seconds. During SEFAZ maintenance windows (typically Sunday nights), the contingencia path adds 200-500ms of overhead but avoids the outage entirely.

Steps 4-6 (notify, ERP, reconcile) can optionally run in parallel since they do not depend on each other's output. Running them concurrently cuts 2-4 seconds from the total:

parallel-optimization.ts
const loop = await session.loop({
  steps: [
    { server: "mcp-zoop", tool: "create_transaction", params: { /* ... */ } },
    { server: "mcp-nuvem-fiscal", tool: "create_nfe", params: { /* ... */ } },
    { server: "mcp-melhor-envio", tool: "create_shipment", params: { /* ... */ } },
    // Steps 4-6 run in parallel (no dependencies between them)
    {
      parallel: [
        { server: "mcp-z-api", tool: "send_message", params: { /* ... */ } },
        { server: "mcp-omie", tool: "create_order", params: { /* ... */ } },
        { server: "mcp-stark-bank", tool: "get_balance", params: { /* ... */ } },
      ],
    },
  ],
  retryPolicy: { maxRetries: 3, backoff: "exponential" },
});

// With parallel optimization: ~6s P50 vs ~8s sequential

Billing: what a Complete Loop costs

Every tool call in a Complete Loop is a billing unit. A standard 6-step loop consumes 6 tool calls. With the LLM-orchestrated approach (generateText), the model may make additional discovery calls, so expect 8-10 tool calls per loop.

PlanTool calls/moLoops/moPrice
Hobby20K~2,500$0
Starter200K~25,000$29
Growth2M~250,000$229
EnterpriseCustomCustomCustom

The Hobby tier gives you 20,000 tool calls per month for free. That is roughly 2,500 Complete Loops, enough for most small businesses processing a few dozen orders per day.

Production deployment checklist

Before you deploy the Complete Loop to production, make sure you have:

  1. Production API keys. Switch from csk_test_ to csk_live_. Test keys hit sandbox MCP servers. Live keys hit real APIs. Do not discover this in production.
  2. Provider credentials. Authorize each MCP server via session.authorize() with production OAuth tokens or API keys from Zoop, Nuvem Fiscal, Melhor Envio, Z-API, Omie, and Stark Bank.
  3. Digital certificate. NF-e requires an A1 digital certificate (PFX file) issued by a Brazilian certificate authority. Upload it via the dashboard or pass it in the session config. Certificates expire annually.
  4. Webhook endpoints. Set up codespar.handleWebhook() to receive async events from providers (Pix confirmation, shipping status updates, SEFAZ retransmission results).
  5. Budget limits. Set realistic budget limits per session. A single order for R$50,000 will blow through a R$1,000 budget limit and throw BudgetExceededError before the Pix charge is created.
  6. Monitoring. Track loop completion rate, step-level latency, and failure reasons. The SDK emits structured logs that work with any observability stack (Datadog, Grafana, CloudWatch).
  7. Dead letter queue. Set up a queue for failed non-blocking steps (notification, ERP, reconciliation) so they can be retried without losing the order.
  8. Idempotency. Always pass an order ID or idempotency key in your tool calls. If a loop is retried after a partial failure, idempotent calls ensure you do not double-charge the customer or issue duplicate NF-e.
production.ts
// Production configuration example
const codespar = new CodeSpar({
  apiKey: process.env.CODESPAR_API_KEY!,  // csk_live_...
});

const session = await codespar.create(userId, {
  preset: "brazilian",
  budgetLimit: 100000,  // R$100K - set based on your AOV
  policies: [
    { name: "rate-limit", type: "rate-limit", config: { maxPerMinute: 60 } },
    { name: "business-hours", type: "time-window", config: { startHour: 6, endHour: 23 } },
  ],
});

// Connect provider credentials (typically done once per org)
await session.authorize("zoop", { token: process.env.ZOOP_API_KEY! });
await session.authorize("nuvem_fiscal", { token: process.env.NUVEM_FISCAL_KEY! });
await session.authorize("melhor_envio", { token: process.env.MELHOR_ENVIO_TOKEN! });
await session.authorize("z_api", { token: process.env.ZAPI_TOKEN! });
await session.authorize("omie", {
  appKey: process.env.OMIE_APP_KEY!,
  appSecret: process.env.OMIE_APP_SECRET!,
});
await session.authorize("stark_bank", {
  privateKey: process.env.STARK_BANK_PRIVATE_KEY!,
  projectId: process.env.STARK_BANK_PROJECT_ID!,
});

The business case

We built the Complete Loop because we watched e-commerce operations teams in Brazil do the same six steps manually for every order, every day.

ManualCodeSpar
Steps6 (across 6 different UIs)1 (single SDK call)
Time per order35-45 minutes8-15 seconds
People involved2-3 (ops, finance, shipping)0 (fully automated)
Error rate5-8% (data entry errors)<0.1% (validated inputs)
NF-e complianceManual, often delayedImmediate, with contingencia
ReconciliationEnd of month, manualReal-time, per-transaction
Cost at 100 orders/day~R$15K/mo (labor)~$29/mo (Starter plan)

The math is simple. A mid-size e-commerce operation processing 100 orders per day spends R$15,000/month on operations staff to do these six steps manually. The Complete Loop replaces that with a $29/month Starter plan and 12 seconds of compute time per order.

That is not a 10% improvement. That is a 500x cost reduction with better accuracy, faster fulfillment, and real-time compliance.

Check the results

output
[mcp-zoop] create_transaction: completed (1,247ms)
  Payment: { amount: 1249, currency: "BRL", method: "pix", status: "confirmed" }

[mcp-nuvem-fiscal] create_nfe: completed (2,103ms)
  NF-e: { number: "NF-4421", status: "authorized", sefaz: "SP", chave: "3526..." }

[mcp-melhor-envio] create_shipment: completed (1,812ms)
  Shipping: { carrier: "Correios PAC", tracking: "BR123456789", eta: "2026-04-21" }

[mcp-z-api] send_message: completed (834ms)
  WhatsApp: { to: "+5511999999999", status: "delivered" }

[mcp-omie] create_order: completed (1,523ms)
  ERP: { orderId: "7721", status: "registered", nfe: "NF-4421" }

[mcp-stark-bank] get_balance: completed (612ms)
  Banking: { transaction: "matched", settled: true, balance: "updated" }

Complete Loop finished in 8,131ms. 6 APIs. 6 tool calls. 0 human steps.

Next steps

Now that you have the Complete Loop running:

  • Add real provider credentials via session.authorize()
  • Create mandates for recurring payments with session.createMandate()
  • Set up webhook handling with codespar.handleWebhook() for async provider events
  • Use session.sendStream() for real-time UI updates in customer-facing apps
  • Monitor loop performance in the dashboard (completion rate, latency, failure reasons)
  • Explore the full server catalog across 4 countries (Brazil, Mexico, Colombia, Argentina)
  • Read the SDK design post for architecture details on sessions, meta tools, and managed auth

All MCP servers are MIT licensed and available on npm. The SDK is at @codespar/sdk@0.2.0. The source is at github.com/codespar/codespar-core.

Full source code for this tutorial is available on GitHub. Questions? Open an issue or reach us at team@codespar.dev.