Multi-Tenant Agent
Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.
Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.
Prerequisites
npm install @codespar/sdk @codespar/openai openai nextYou need one CodeSpar API key for your SaaS — not one per tenant. Tenant isolation happens inside CodeSpar via session metadata.
Session factory
Wrap sessions.create() in a helper that takes a tenant config and injects metadata.tenant_id:
import { CodeSpar } from "@codespar/sdk";
const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });
interface TenantConfig {
id: string;
servers: string[];
metadata: Record<string, string>;
}
export async function createTenantSession(tenant: TenantConfig) {
return codespar.sessions.create({
servers: tenant.servers,
metadata: {
tenant_id: tenant.id,
...tenant.metadata,
},
});
}API route per tenant
Use a dynamic route segment [tenantId] to scope every request to a tenant. The tenant config (which servers, which metadata) comes from your database:
import { createTenantSession } from "@/lib/commerce";
import { getTools, handleToolCall } from "@codespar/openai";
import OpenAI from "openai";
import { NextResponse } from "next/server";
const openai = new OpenAI();
// In production, fetch from DB
const TENANTS: Record<string, { servers: string[] }> = {
tenant_acme: { servers: ["stripe", "correios", "nuvem-fiscal"] },
tenant_loja: { servers: ["asaas", "melhor-envio", "z-api"] },
};
export async function POST(req: Request, { params }: { params: { tenantId: string } }) {
const tenant = TENANTS[params.tenantId];
if (!tenant) return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
const { message } = await req.json();
const session = await createTenantSession({
id: params.tenantId,
servers: tenant.servers,
metadata: { source: "api" },
});
try {
const tools = await getTools(session);
// ... run agent loop with tools, return response ...
} finally {
await session.close();
}
}Billing per tenant
Every tool call includes the metadata.tenant_id you set on session creation. Query CodeSpar's usage API and aggregate by tenant:
// In your billing cron job
const response = await fetch("https://api.codespar.dev/v1/usage", {
headers: { Authorization: `Bearer ${process.env.CODESPAR_API_KEY}` },
});
const data = await response.json();
// Group by tenant_id metadata
const perTenant = data.tool_calls.reduce((acc, call) => {
const tenantId = call.metadata.tenant_id;
acc[tenantId] = (acc[tenantId] || 0) + 1;
return acc;
}, {});
// Bill each tenant based on usage
for (const [tenantId, callCount] of Object.entries(perTenant)) {
await billTenant(tenantId, callCount);
}The metadata field you set on a session is forwarded to every tool call log, making tenant-level billing and audit trails straightforward.
Security considerations
- Session isolation. Each session has access only to the servers listed at creation time. Tenant A cannot reach Tenant B's providers — the runtime blocks it.
- API key scoping. Use separate CodeSpar keys per environment (test/prod), not per tenant. Tenant isolation is enforced by session scoping, not by key fragmentation.
- Credential vault. Each tenant's provider credentials are stored in CodeSpar's vault, encrypted per-organization. Tenants never see each other's secrets.
- Rate limiting. Apply per-tenant rate limits in your API route — CodeSpar won't stop tenant A from exhausting tenant B's budget unless you cap it upstream.
Next steps
Webhook Listener
React to payment webhooks with a deterministic loop. On payment.confirmed, automatically issue an NF-e, create shipping, and send WhatsApp — no agent, no LLM, no surprise.
Sessions
Sessions are scoped connections to MCP servers that manage tool access, authentication, and usage tracking for AI agent commerce operations.