Guide17 min read

Build a Multi-Tenant AI Answer-Engine-Optimization (AEO/GEO) Platform

A code-first tutorial for building an agentic AEO/GEO product on Naïve — give every brand an AI agent that audits how ChatGPT, Claude, Gemini, and Perplexity answer questions about them, finds the keyword and citation gaps with SEO data, writes answer-optimized content with an OpenRouter model, and publishes to their own connected CMS. Each customer is an isolated tenant user, governed by an Account Kit and a Customer-Billing plan, with usage metered and quota-enforced at execution time and sensitive actions frozen for human approval.

Guide
TL;DR
  • AI answer engines (ChatGPT, Claude, Gemini, Perplexity) are the new search results and brands now need to track and optimize for what those models say about them. That's AEO/GEO (Answer Engine / Generative Engine Optimization).
  • A multi-tenant AEO product is painful to build alone: per-customer AI-visibility data, SEO data, an LLM to write content, per-tenant isolation, metered usage, plan quotas, and a human gate on publishing all before any product logic.
  • Naïve ships those as primitives: the AEO and SEO primitives for the data, the LLM primitive (an OpenRouter wrapper) for the writing, tenant users + Account Kits for isolation and policy, and Customer Billing for plans, metering, and quota enforcement.
  • Every call is scoped with naive.forUser(brandId) and filtered by that brand's Account Kit and plan Brand A can never read Brand B's audits, and an over-quota tenant is stopped with 429 rate_limited at execution time.
  • The agent writes answer-optimized content with naive.llm.chat() using OpenRouter structured outputs (response_format json_schema), then publishes to the brand's own connected CMS and connecting that CMS is frozen for human approval.
  • The full arc define plans, provision a brand, audit AI answers, find gaps, write content, publish with approval, meter and enforce quota — runs against the current @usenaive-sdk/server.

AI answer engines are the new front page. When someone asks ChatGPT, Claude, Gemini, or Perplexity "what's the best project management tool for agencies?", the model returns an answer — and a short list of cited sources. For a growing share of buyers, that answer is the search result. Brands now need to know what those models say about them, and influence it. That discipline has a name: AEO (Answer Engine Optimization), also called GEO (Generative Engine Optimization).

Most "AI SEO" demos are a single prompt against one model. The moment you try to sell AEO as a product to a roster of brands, the model isn't the hard part — the platform is:

  • You need real AI-visibility data across four answer engines, plus classic keyword/SERP/backlink data to ground strategy.
  • One brand's audits and content must never leak into another customer's account.
  • You're a SaaS, so usage must be metered and quota-limited per plan — enforced when the call runs, not reconciled later.
  • Publishing to a customer's live site is high-stakes — it needs a human in the loop, enforced at execution time.

Without a platform you'd build per-tenant isolation, a metering layer, a quota engine, a data pipeline for four LLMs, and an approval workflow before writing a line of product. That plumbing is the product — and it's exactly what Naïve ships as primitives.

This is a developer tutorial. Every code block runs against the current @usenaive-sdk/server, with signatures pulled straight from the docs. By the end you'll have a multi-tenant AEO backend where each brand gets an isolated, metered, governed AI agent.

Per-brand architecture: each brand is an isolated tenant user governed by an Account Kit and a Customer-Billing plan; the agent is scoped with naive.forUser(brand), audits AI answers with the AEO primitive, grounds strategy with the SEO primitive, writes with the LLM primitive over OpenRouter, and publishes through the brand's own connected CMS — with usage metered and quota-enforced at execution time and connecting the CMS frozen for human approval.

What we're building

  • A backend service that provisions an isolated AI agent per brand (the customer of your product).
  • The agent audits how ChatGPT, Claude, Gemini, and Perplexity answer questions about the brand, and tracks mention share vs. competitors.
  • The agent grounds strategy with keyword volume, SERP competitors, and backlink data from the SEO primitive.
  • The agent writes an answer-optimized content brief and draft with an OpenRouter model, using structured outputs.
  • The agent publishes to the brand's own connected CMS — and connecting that CMS is frozen for human approval.
  • Everything is metered per tenant and quota-enforced at execution time by a Customer-Billing plan.

The primitives in play, all from the docs:

PrimitiveRole in the AEO platformDocs
UsersOne tenant user per brand — the isolation boundarynaive.users
Account KitsThe brand's policy: enabled primitives, allowed apps, approval rulesnaive.accountKits
Customer BillingPlans, subscriptions, metering, and per-primitive quotasnaive.plans, forUser(id).billing
AEO / GEOWhat LLMs say about the brand + mention metricsforUser(id).aeo
SEOKeyword volume, SERP competitors, backlinksforUser(id).seo
LLMOpenRouter-backed writing across 300+ modelsforUser(id).llm
ConnectionsPer-brand access to their CMS (Webflow, WordPress, …)forUser(id).connections
ApprovalsHuman-in-the-loop on connecting the CMSforUser(id).approvals

The answer engines and data sources the platform reads from:

The four AI answer engines the AEO primitive audits — ChatGPT, Claude, Gemini, and Perplexity — plus the OpenRouter model router behind the LLM primitive.

Step 0: Install and authenticate

You need one Naïve workspace key (nv_sk_...) from Studio → Settings → API keys. The SDK is server-only — keys are server secrets, never shipped to the browser.

npm install @usenaive-sdk/server
import { Naive } from "@usenaive-sdk/server";
 
const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
  • naive.* (the root client) acts on your default tenant user and exposes the control plane (naive.users, naive.accountKits, naive.plans).
  • naive.forUser(id).* returns the same data-plane surface (aeo, seo, llm, connections, billing) bound to a specific brand.

That forUser scoping is the whole game. Read it as: "do this, as this brand, under this brand's policy and plan."

Step 1: Define the policy and the plans once

A brand's capabilities come from two things working together:

  • An Account Kit — a reusable policy template that declares which primitives are enabled and which third-party apps the brand may connect.
  • A Customer-Billing plan — maps a plan key to that Account Kit plus per-primitive quotas.

Define a "Starter" and a "Growth" tier a single time. Each kit:

  • Enables the primitives an AEO agent needs (llm for writing).
  • Allowlists only the CMS apps a brand may connect (try anything else and the API returns forbidden).
  • Leaves approval on for connecting a third-party service (the default), so granting the agent write access to a live site is gated.
// One Account Kit per tier. Connecting a CMS is gated by default for tenant users.
const starterKit = await naive.accountKits.create({
  name: "AEO Starter",
  primitives_config: {
    llm: { enabled: true },
  },
  connections_config: {
    mode: "allowlist",
    toolkits: ["webflow", "wordpress"], // the only CMSs a brand may connect
    // requiresApproval defaults on for connecting — we keep it on
  },
});
 
const growthKit = await naive.accountKits.create({
  name: "AEO Growth",
  primitives_config: { llm: { enabled: true } },
  connections_config: { mode: "allowlist", toolkits: ["webflow", "wordpress", "contentful"] },
});

Discover valid app slugs with GET /v1/toolkits?search= — the same catalog the dashboard's Account Kit editor uses. Never hardcode a slug you haven't confirmed exists.

Now map each kit to a plan with quotas. Metered primitives (aeo, seo, email) record one usage event per successful call, and the API returns 429 rate_limited the moment a tenant exceeds the quota:

await naive.plans.upsert({
  key: "starter",
  name: "AEO Starter",
  accountKitId: starterKit.id,      // assigned to the tenant on subscribe
  stripePriceId: "price_starter",   // your Stripe price (optional)
  quotas: { aeo: 100, seo: 500 },   // calls per period
  period: "month",
});
 
await naive.plans.upsert({
  key: "growth",
  name: "AEO Growth",
  accountKitId: growthKit.id,
  stripePriceId: "price_growth",
  quotas: { aeo: 1000, seo: 5000 },
  period: "month",
});
  • The plan's accountKitId is what ties billing tier → permissions. Subscribe a brand to growth and they get the Growth kit's policy automatically.
  • quotas are the per-primitive call budgets enforced at execution time. No quota engine to build.

Step 2: Provision a brand and subscribe them

A tenant user is one of your end-users — here, a brand. They never sign into Naïve; you manage them through the SDK. Create one per brand from your own signup flow, then set their subscription (which applies the plan's Account Kit in the same call):

async function onboardBrand(dbBrand: { id: string; email: string; name: string; planKey: "starter" | "growth" }) {
  // 1. Provision the isolation boundary
  const brand = await naive.users.create({
    external_id: dbBrand.id,        // your DB row id — stable, your source of truth
    email: dbBrand.email,
    label: dbBrand.name,
  });
 
  // 2. Subscribe — assignKit (default true) applies the plan's Account Kit to the brand
  await naive.forUser(brand.id).billing.setSubscription({
    planKey: dbBrand.planKey,
    status: "active",
    stripeCustomerId: "cus_...",        // from your Stripe webhook
    stripeSubscriptionId: "sub_...",
    currentPeriodEnd: "2026-07-01T00:00:00Z",
    assignKit: true,
  });
 
  return brand; // brand.id → the handle you scope every future call with
}

From here on, everything the brand's agent does goes through their scoped client:

const brand = naive.forUser(brandId);

naive.forUser("brand_a") and naive.forUser("brand_b") see entirely separate audits, content, connections, and usage counters. There is no shared state, no cross-tenant read path to get wrong.

Step 3: Audit how AI answer engines describe the brand

This is the core of AEO. The AEO primitive asks the real answer engines a question and returns the response plus the domains and URLs they cited — so you can see whether the brand shows up, and who's winning the answer.

const brand = naive.forUser(brandId);
 
// What does ChatGPT actually say for a high-intent buyer query?
const chatgpt = await brand.aeo.llmResponses("chatgpt", {
  user_prompt: "best project management software for agencies 2026",
  model_name: "gpt-4o-mini",
});
 
const answer = chatgpt.results[0];
console.log(answer.response);        // the model's full answer
console.log(answer.cited_domains);   // e.g. ["asana.com", "linear.app", "notion.so"]
console.log(answer.cited_urls);      // the exact pages it cited

Run the same prompt across every engine to see where the brand is present or absent. ChatGPT, Claude, and Gemini support async tasks for batch audits; Perplexity is live-only:

const engines = ["chatgpt", "claude", "gemini", "perplexity"] as const;
const modelByEngine = {
  chatgpt: "gpt-4o-mini",
  claude: "claude-3-5-haiku-latest",
  gemini: "gemini-2.0-flash",
  perplexity: "sonar",
} as const;
 
const audits = await Promise.all(
  engines.map((engine) =>
    brand.aeo.llmResponses(engine, {
      user_prompt: "best project management software for agencies 2026",
      model_name: modelByEngine[engine], // each engine expects its own model id — list with GET .../{engine}/models
    }),
  ),
);

Then quantify share of voice — how often the brand (and its competitors) are mentioned and cited across LLMs:

const mentions = await brand.aeo.llmMentions("search", {
  keyword: "project management software for agencies",
  target: [{ domain: "yourbrand.com" }, { domain: "asana.com" }, { domain: "notion.so" }],
});
 
const me = mentions.results.find((r) => r.target === "yourbrand.com");
console.log(me?.mention_count);   // how often we appear in LLM answers
console.log(me?.llm_platforms);   // which engines mention us
console.log(me?.cited_urls);      // which of our pages get cited

And size the opportunity with AI-search keyword volume:

const volume = await brand.aeo.aiKeywords({
  keywords: ["project management for agencies", "agency resource planning tool"],
});
// volume.results → [{ keyword, ai_search_volume, competition }, ...]

Each of these calls is metered against the brand's plan. The brand's agent literally cannot run more audits than their tier allows — more on that enforcement in Step 6.

Step 4: Ground the strategy with SEO data

AI answers are heavily influenced by the classic web — the pages models retrieve and cite. The SEO primitive wraps keyword, SERP, and backlink data so the agent builds on facts, not vibes.

const brand = naive.forUser(brandId);
 
// Who ranks for the query today? These are the pages LLMs are likely to cite.
const serp = await brand.seo.serpCompetitors(["project management software for agencies"]);
 
// Real search demand + difficulty for the topic cluster
const overview = await brand.seo.labs("google", "keyword-overview", {
  keywords: ["project management for agencies"],
  location_code: 2840, // US
});
 
// Why do the cited domains win? Audit their backlink authority.
const authority = await brand.seo.backlinksSummary("asana.com");
console.log(authority.results[0].total_referring_domains, authority.results[0].rank);

Now the agent has a complete picture per brand: which engines mention them, who's being cited instead, the demand behind the query, and the authority of the incumbents. That's the input to the writing step.

Step 5: Write answer-optimized content with an OpenRouter model

The LLM primitive is a full wrapper over OpenRouter — one OpenAI-compatible endpoint routing to 300+ models, billed in Naïve credits from the exact cost OpenRouter reports. You don't hold an OpenRouter key; Naïve injects it server-side. Because the request/response bodies are exactly OpenRouter's, you get provider-prefixed model ids, a models[] fallback chain, a provider{} routing object, and response_format for structured outputs — all for free.

Use structured outputs so the agent returns a machine-usable content plan, not prose you have to parse:

const brand = naive.forUser(brandId);
 
const plan = await brand.llm.chat({
  model: "anthropic/claude-sonnet-4.6",
  models: ["anthropic/claude-sonnet-4.6", "openai/gpt-5.2"], // OpenRouter fallback chain
  provider: { sort: "throughput", data_collection: "deny" },  // OpenRouter routing
  messages: [
    {
      role: "system",
      content:
        "You are an AEO strategist. Given how AI answer engines respond and the SERP/backlink data, " +
        "produce an answer-optimized article plan that maximizes the chance of being cited by LLMs.",
    },
    {
      role: "user",
      content: JSON.stringify({
        query: "best project management software for agencies 2026",
        chatgpt_answer: answer.response,
        cited_domains: answer.cited_domains,
        serp_competitors: serp.results,
        our_mentions: me,
      }),
    },
  ],
  // OpenRouter structured outputs — the body is forwarded as-is
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "aeo_brief",
      strict: true,
      schema: {
        type: "object",
        additionalProperties: false,
        properties: {
          title: { type: "string" },
          target_question: { type: "string" },
          tl_dr_answer: { type: "string", description: "A direct 2-sentence answer LLMs can lift verbatim" },
          headings: { type: "array", items: { type: "string" } },
          faqs: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              properties: { q: { type: "string" }, a: { type: "string" } },
              required: ["q", "a"],
            },
          },
        },
        required: ["title", "target_question", "tl_dr_answer", "headings", "faqs"],
      },
    },
  },
});
 
const brief = JSON.parse(plan.choices[0].message.content);
console.log("credits used:", plan.credits_used);
  • model takes a provider-prefixed id — anthropic/claude-sonnet-4.6, openai/gpt-5.2. Browse them free with brand.llm.models("claude").
  • response_format: { type: "json_schema", strict: true } is OpenRouter's structured-output contract — the model returns JSON matching your schema, so JSON.parse is safe.
  • The tl_dr_answer and faqs matter for AEO specifically: answer engines preferentially lift concise, self-contained answers and Q&A blocks.

Already have OpenAI/OpenRouter client code? Point its baseURL at https://api.usenaive.ai/v1/proxy/openrouter and keep your code as-is — Naïve injects the key and bills your credits. The drop-in proxy is not Account-Kit gated, so use the typed forUser(id).llm routes (above) when you want per-brand enforcement and metering.

Step 6: Metering and quota enforcement — at execution time

Because every aeo and seo call is metered against the brand's plan, you get usage tracking and quota enforcement for free. Read the current period's usage at any time:

const usage = await naive.forUser(brandId).billing.usage();
// { plan: "starter", status: "active", period: "month",
//   quotas: { aeo: 100, seo: 500 }, usage: { aeo: 100, seo: 213 } }

When a tenant exceeds a primitive's quota, the next call returns 429 rate_limited — server-side, the instant the call runs. Your agent loop just has to handle it:

Execution-time enforcement: a metered AEO call is checked against the brand's plan quota before it runs; under quota it executes and increments usage, over quota it returns 429 rate_limited; and connecting the brand's CMS is frozen as 202 pending_approval until a human approves, after which the API replays the action.

import { NaiveError } from "@usenaive-sdk/server";
 
async function auditWithinQuota(brandId: string, prompt: string) {
  try {
    return await naive.forUser(brandId).aeo.llmResponses("chatgpt", {
      user_prompt: prompt,
      model_name: "gpt-4o-mini",
    });
  } catch (err) {
    if (err instanceof NaiveError && err.code === "rate_limited") {
      // The brand hit their plan quota. Stop the agent; prompt an upgrade.
      return { quotaExceeded: true };
    }
    throw err;
  }
}

This is execution-time permission enforcement, and it matters that the API does it, not your code:

  • The quota is checked server-side the instant the agent calls — there's no way for a tenant to run more audits than their tier by hammering your endpoint.
  • It's policy, not plumbing: change a plan's quotas and every brand on that plan is re-limited, with no deploy.
  • Tenants without a subscription aren't quota-limited — seed a free-plan subscription on signup if you want every brand metered.

Upgrading a brand is one call — setSubscription swaps the plan and the Account Kit together:

await naive.forUser(brandId).billing.setSubscription({ planKey: "growth", status: "active", assignKit: true });
// Higher quotas + the Growth kit's larger CMS allowlist, atomically.

Step 7: Publish to the brand's CMS — with a human gate

Generating content is safe; pushing it to a customer's live website is not. The agent publishes through the brand's own connected CMS via the Connections primitive — and connecting a third-party service is approval-gated by default for tenant users. The agent's connect call freezes instead of starting OAuth:

import { isPendingApproval } from "@usenaive-sdk/server";
 
const brand = naive.forUser(brandId);
 
// Agent attempts to connect the brand's Webflow — gated by default
const res = await brand.connections.connect("webflow", {
  callbackUrl: "https://yourapp.com/oauth/done",
});
 
if (isPendingApproval(res)) {
  // Nothing happened yet. Surface res.approval_id to the brand's admin.
  console.log("CMS connect awaiting approval:", res.approval_id);
 
  // A human approves (from your dashboard, or the operator key):
  await brand.approvals.approve(res.approval_id);
 
  // The API replays the frozen connect and returns the hosted redirect
  const resolved = await brand.approvals.get(res.approval_id);
  // resolved.status === "executed" → resolved.result holds the redirectUrl
} else {
  // Un-gated path: send the brand admin to res.redirectUrl to finish OAuth
}

What's gated by default for agent calls on tenant users, straight from the Approvals docs:

Actionaction_type
Connect / sign up for a 3rd-party serviceconnections.connect
Issue or top up a virtual cardcards.create, cards.topup
Purchase a domaindomains.purchase
Start KYCverification.start
Form / file a companyformation.create, formation.submit

Once the brand finishes OAuth, the connection flips to ACTIVE and the agent publishes. Don't guess a tool's name or arguments — discover them at runtime for whatever the brand connected:

const tools = await brand.connections.tools("webflow");
// → the exact tool slugs + arg schemas Webflow exposes
 
// Then execute the publish tool the catalog reports, with the structured brief
await brand.connections.execute("webflow", "<PUBLISH_TOOL_SLUG>", {
  title: brief.title,
  body: renderArticle(brief), // your renderer turns the brief into HTML/markdown
});

Two brands publishing concurrently never collide: each execute is bound to its own scoped client and its own connected account.

Step 8: Put it together — onboard a brand, run an AEO cycle

The whole backend, end to end:

import { Naive, isPendingApproval, NaiveError } from "@usenaive-sdk/server";
 
const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
 
// One-time: policy + plans
const starterKit = await naive.accountKits.create({
  name: "AEO Starter",
  primitives_config: { llm: { enabled: true } },
  connections_config: { mode: "allowlist", toolkits: ["webflow", "wordpress"] },
});
await naive.plans.upsert({
  key: "starter",
  name: "AEO Starter",
  accountKitId: starterKit.id,
  quotas: { aeo: 100, seo: 500 },
  period: "month",
});
 
// Per brand: provision + subscribe (applies the plan's Account Kit)
export async function onboardBrand(b: { id: string; email: string; name: string }) {
  const brand = await naive.users.create({ external_id: b.id, email: b.email, label: b.name });
  await naive.forUser(brand.id).billing.setSubscription({
    planKey: "starter",
    status: "active",
    assignKit: true,
  });
  return brand.id;
}
 
// Per cycle: audit → ground → write → (publish via approved CMS)
export async function runAeoCycle(brandId: string, query: string) {
  const brand = naive.forUser(brandId);
 
  try {
    const chatgpt = await brand.aeo.llmResponses("chatgpt", { user_prompt: query, model_name: "gpt-4o-mini" });
    const answer = chatgpt.results[0];
 
    const mentions = await brand.aeo.llmMentions("search", {
      keyword: query,
      target: [{ domain: "yourbrand.com" }],
    });
    const serp = await brand.seo.serpCompetitors([query]);
 
    const plan = await brand.llm.chat({
      model: "anthropic/claude-sonnet-4.6",
      models: ["anthropic/claude-sonnet-4.6", "openai/gpt-5.2"],
      messages: [
        { role: "system", content: "You are an AEO strategist. Output an answer-optimized article plan." },
        { role: "user", content: JSON.stringify({ query, answer: answer.response, cited: answer.cited_domains, serp: serp.results, mentions: mentions.results }) },
      ],
      response_format: { type: "json_schema", json_schema: { name: "aeo_brief", strict: true, schema: BRIEF_SCHEMA } },
    });
 
    return { brief: JSON.parse(plan.choices[0].message.content), creditsUsed: plan.credits_used };
  } catch (err) {
    if (err instanceof NaiveError && err.code === "rate_limited") return { quotaExceeded: true };
    throw err;
  }
}
 
// On demand: connect the brand's CMS (freezes for approval)
export async function connectCms(brandId: string, cms: string) {
  const brand = naive.forUser(brandId);
  const res = await brand.connections.connect(cms, { callbackUrl: "https://yourapp.com/oauth/done" });
  if (isPendingApproval(res)) return { status: "pending", approvalId: res.approval_id };
  return { status: "ready", redirectUrl: res.redirectUrl };
}

That's a complete, multi-tenant agentic AEO backend. The product logic is small; the platform carries identity, isolation, metering, quotas, and authority.

Step 9: Put the cycle on autopilot

AEO is a tracking discipline — answer engines change their responses constantly, so audits should run on a schedule. Set up a recurring job that re-audits each brand and flags movement. With the CLI:

naive cron create "0 9 * * 1" \
  "Re-audit AI answer-engine visibility for every active brand. Compare mention share vs last week. Draft an answer-optimized brief for any query where we lost a citation." \
  --name "Weekly AEO audit"

Or hand the whole toolset to an LLM and let it drive the loop — agentTools() returns a ready meta-toolset bounded by the brand's Account Kit and plan, so the model discovers and runs the aeo, seo, llm, and connected-app tools itself, and gated methods still resolve to a pending_approval payload it relays to the human:

const kit = naive.forUser(brandId).agentTools();
// kit.tools  → tool-use definitions, filtered by the brand's kit + plan
// kit.handle(name, input) → dispatcher, AccountKit-gated and quota-metered server-side

Why this can't exist without the moat

Strip Naïve out and rebuild Steps 1–7 yourself:

  • A four-engine AI-visibility pipeline — query ChatGPT, Claude, Gemini, and Perplexity, parse answers, and extract cited domains/URLs reliably, per tenant.
  • Real SEO data — keyword volume, SERP competitors, and backlink authority from a maintained provider.
  • Hard tenant isolation — a scoping layer that is designed to block cross-tenant reads server-side, not just "we remembered the WHERE brand_id =."
  • A metering + quota engine — count every call per tenant per primitive and reject over-quota requests at execution time, mapped to billing tiers.
  • An approval queue with replay — freeze connecting a customer's live CMS, surface it to a human, and replay the exact call on approval.

Each of those is a project. Together they're the reason a serious multi-tenant AEO product is hard. Naïve makes them forUser(id), an Account Kit, a plan with quotas, and isPendingApproval(res) — so you ship the product, not the plumbing.

Ship it

  1. Get a key at studio.usenaive.ai → API keys.
  2. Define the kits and plans once — allowlist the CMSs, set per-primitive quotas, keep connect-approval on.
  3. Provision a brand with naive.users.create() and billing.setSubscription({ assignKit: true }).
  4. Audit AI answers across the four engines and quantify mention share.
  5. Ground and write with SEO data and an OpenRouter model using structured outputs.
  6. Connect the CMS and watch the approval gate freeze and replay it; publish.
Frequently Asked Questions
What is AEO / GEO, and how is it different from SEO?+
AEO (Answer Engine Optimization), also called GEO (Generative Engine Optimization), is optimizing for how AI answer engines — ChatGPT, Claude, Gemini, Perplexity — describe and cite your brand when a user asks them a question. SEO optimizes for ranked blue links on Google and Bing; AEO optimizes for being the answer (and the cited source) inside an LLM's response. They're complementary: Naïve exposes both as primitives, so the agent can audit AI answers with the AEO primitive and ground its strategy with classic keyword, SERP, and backlink data from the SEO primitive.
Why can't I just build a multi-tenant AEO tool with the OpenAI API and a scraper?+
You can prompt a model, but the product is everything around it. You'd need reliable AI-visibility data across four answer engines, real keyword/SERP/backlink data, an LLM to write content, hard isolation so one brand's audits never leak into another's account, per-tenant metering, plan-based quotas enforced when the call runs, and a human in the loop before the agent publishes to a customer's live site. Naïve ships those as primitives — the AEO and SEO primitives, the LLM (OpenRouter) wrapper, tenant users, Account Kits, Customer Billing, and approvals — so you write the product, not the plumbing.
How does Naïve isolate one brand (tenant) from another?+
Every primitive — aeo, seo, llm, connections, billing — is scoped to a tenant user. You create one tenant user per customer with naive.users.create(), then make every call through naive.forUser(brandId). That scoped client can only see and act on that brand's resources, and each call is additionally filtered by the brand's Account Kit. There is no path for one brand's scoped client to reach another brand's audits, content, or connected CMS.
How is usage metered and limited per customer?+
With the Customer Billing primitive. You define plans that map a key to an Account Kit plus per-primitive quotas (e.g. { aeo: 250, seo: 1000 } calls per period), then give each tenant a subscription. Naïve meters every successful aeo, seo, and email call against the tenant's plan and returns 429 rate_limited the moment a tenant exceeds its quota — enforced server-side at execution time, not in your application code.
Which models can the agent use to write content, and how is it billed?+
The llm primitive is a full wrapper over OpenRouter, so you get 300+ models across Anthropic, OpenAI, Google, Meta, and Mistral through one OpenAI-compatible endpoint — e.g. anthropic/claude-sonnet-4.6 or openai/gpt-5.2, with an optional models[] fallback chain, a provider{} routing object, and response_format for structured outputs. The request/response bodies are exactly OpenRouter's. Naïve holds the OpenRouter key and bills the exact cost OpenRouter reports, converted to credits ($0.50 = 1 credit).
Where does human approval come in?+
Publishing to a customer's live website is high-stakes, so the agent goes through the brand's own connected CMS via the Connections primitive — and connecting a third-party service is approval-gated by default. When the agent calls connections.connect, the API returns 202 pending_approval instead of starting the OAuth flow; a human approves, and the API replays the action. This is execution-time permission enforcement: the gate is in the API, not your UI.
NT
Naïve Team

Building the autonomous company infrastructure.

Keep reading
Introducing /seo and /aeo: rank in Google and show up in AI answers

Keyword volume, backlinks, SERP competitors, and ranked-keyword data for classic search — plus AEO/GEO visibility into whether ChatGPT, Claude, Gemini, and Perplexity actually mention you. The full visibility stack behind one key.

Introducing /llm: 300+ models behind one key, billed in credits

OpenAI-compatible chat completions across 300+ models with provider routing and fallbacks — streaming included, no per-provider keys, billed in Naïve credits at the exact upstream cost. Plus a drop-in OpenRouter proxy so your existing code just changes a base URL.

Introducing /billing: meter and monetize your own agent users

Define plans that map to an Account Kit, a Stripe price, and usage quotas — then reflect each customer's subscription and read their metered usage. The monetization layer for building a multi-tenant product on Naïve.

Introducing the governance layer: approvals, logs, sessions, and jobs

Human-in-the-loop approvals that freeze sensitive actions until you say yes, a per-user audit trail of everything an agent did, short-lived scoped MCP sessions, and unified async job tracking — the controls that make an autonomous fleet safe to run.

Launching the Naïve Developer Platform: existence infrastructure for AI agents

The Naïve Developer Platform turns every Naïve primitive into a multi-tenant building block for your own products. 38 primitives across 8 groups, four surfaces (SDK, CLI, MCP, REST), per-customer isolation, governance, and metered billing — so you can give every AI agent a real existence.