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 codespar.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.create(tenant.id, {
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 carries the metadata.tenant_id you set on session creation, which is what makes a per-tenant view possible at all.
/v1/usage is not an endpoint your billing job can call. It sits in the
service-auth subtree, so a csk_ key answers 401, and the credential that
does open it is CodeSpar's platform secret — it never leaves our
infrastructure. Read the aggregate in the dashboard at
/dashboard/billing.
For your own per-tenant counters, count on the side you control. You already know the tenant at session creation, so record the attribution there rather than trying to reconstruct it from an endpoint you cannot reach:
// At session creation you already hold the tenant id — stamp it on CodeSpar
// for audit AND on your own meter in the same step.
const session = await cs.create({
servers: ["asaas"],
metadata: { tenant_id: tenantId },
});
await meter.increment(tenantId, { sessionId: session.id });
// Tool-call detail for one session stays on the Bearer surface, so a
// reconciliation job can walk what a tenant's session actually did:
// GET /v1/sessions/:id/tool-calls
// GET /v1/tool-calls?since=... (project-scoped, newest first)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-account. 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 commerce.payment.succeeded, automatically issue an NF-e, create shipping, and send WhatsApp. No agent, no LLM, no surprise.
Crypto Pay Agent
Generate a stablecoin payment URL via codespar_crypto_pay (Coinbase Commerce default), share with the user, watch for settlement webhook or paymentStatus.