Storefront Bot (AI chat widget)

Add Brainerce's AI shopping assistant to any storefront with one line of code. The bot answers product questions, shows recommendation cards, handles cart adds, and escalates to the merchant team when needed.

Add the store's AI shopping assistant to any page with one line, with no props and no config. Everything (name, avatar, colors, greeting, starter questions, capabilities, guardrails) is configured by the merchant in the dashboard under Customers → Storefront Bot, and the widget renders nothing until the bot is switched Live there. Those same settings are also readable and writable over the admin API. See Configure programmatically.

Zero-code embed (any site)

<script src="https://cdn.brainerce.com/bot.js" data-connection-id="vc_abc123" defer></script>

Keep the tag exactly this bare, and do not add integrity or crossorigin (the bootstrap is intentionally mutable so merchants never re-paste the tag on releases; it is origin-pinned by your CSP script-src instead).

SDK mount (React / Next.js / any bundler)

import { BrainerceBot } from 'brainerce/bot';

// e.g. in a useEffect or client entry — mounts a floating chat bubble
const bot = await BrainerceBot.mount({ connectionId: 'vc_abc123' });

// later, if needed:
bot?.destroy();

mount resolves to null when the bot is switched off or unconfigured for the connection, so it is safe to call unconditionally. If the store's AI credits run out, the widget still mounts but replies with a short "taking a break" message until credits reset.

Mount options

OptionRequiredDescription
connectionIdyesYour vc_* connection id
baseUrlnoAPI origin override (self-hosted / staging)
targetnoMount element (defaults to document.body)
onAddToCartno({ productId, variantId, quantity }) => boolean | Promise<boolean>. Route the widget's cart adds through your own cart so your header count stays in sync. variantId is null for simple products. Return false to make the widget fall back to the product page.
customerSessionProxyPathnoRarely needed. A same-origin proxy path the client does not know about (the Next.js template's /api/store is found automatically). It is the last source tried. See below.
getBotSessionnoRarely needed. () => client.getBotSession(): wire the shopper's session yourself instead of relying on the automatic lookup. Takes precedence over it.

Order tracking and the signed-in shopper: automatic

There is nothing to configure. When the page uses BrainerceClient (SDK 3.11.0 or later), the widget finds the shopper's session through the client by itself, so "where is my order?" works for a signed-in shopper with the one-line <script> embed as much as with BrainerceBot.mount.

How it works:

  • The client registers a single method on the page, getBotSession(), keyed by the connection id. The widget looks it up when a message is sent, so it does not matter which of the two loads first.
  • The client calls GET /api/storefront-bot/:connectionId/customer-session itself, with its own credentials: the customer token as a Bearer header in token mode (client.setCustomerToken(token)), or through your same-origin proxy in proxy mode (an httpOnly cookie the SDK cannot read; the server decides, because the brainerce_logged_in indicator cookie is only a hint a custom proxy may never set).
  • The widget receives only a 5-minute, bot-only token: audience storefront-bot-session, bound to this store, revoked when the customer changes their password. It never receives the shopper's real session token, and the registry exposes one method, never the client.

What the widget guarantees:

  • No stale identity. It re-checks before every message. A "signed in" answer is trusted for at most 15 seconds, a guest answer is never cached, and a source it cannot reach drops the token (the shopper is treated as a guest rather than as whoever they were last).
  • Sign-out and account switches clear the chat. client.clearCustomerToken(), client.onLogout() and client.setCustomerToken(other) tell the widget immediately; the transcript, the stored conversation ids and the greeting reset. A flaky connection does not clear a conversation: only a real "not signed in" answer does.
  • The server enforces it too. A conversation that served a signed-in shopper belongs to that shopper. A guest, or another shopper, who presents the same anonymous session id (it sits in localStorage, so it survives a logout on a shared device) gets a fresh conversation on /chat and a 404 on the history route; the model is never given the earlier shopper's turns. Both routes answer Cache-Control: private, no-store.

A shopper the bot cannot identify is told what the bot can see ("I can't see a signed-in account on this page right now. If you just logged in, send your message again...") instead of being ordered to log in. The bot never offers an order-number or email workaround.

When it does not work, the client says so. If the shopper looks signed in but the server cannot verify the session, BrainerceClient logs one console warning. The usual causes: the token belongs to another store, the proxy does not forward the cookie as a Bearer header to /api/storefront-bot/*, or the site is not an allowed domain for the sales channel.

Older storefronts. A storefront on a brainerce client older than 3.11.0 has nothing to register, so the bot treats every shopper as a guest. Upgrade the package. As a stopgap, customerSessionProxyPath still works exactly as before, and BrainerceBot.mount({ getBotSession: () => client.getBotSession() }) wires the client explicitly.

Scripts on the same page can call the registered method and obtain a 5-minute order-lookup token for the signed-in shopper. They already share the page with your proxy cookie or in-memory token, so this is no wider than what exists, but keep third-party scripts out of order and account pages as you would anyway.

Order lookups are additionally scoped to the sales channel the bot is mounted on: a shopper only ever sees orders placed through this connection.

What the bot does

  • Answers product questions: searches the catalog, describes items, compares options
  • Exact answers over the whole catalog: "what is the cheapest", "the most expensive", a budget ("up to 60"), "what do you have in the coffee category" and "how many products do you have" are answered by reading the whole published catalog of the channel (the newest 300 products at most, and the bot says so when a catalog is larger), using the price a shopper actually pays: the channel price, a sale, the cheapest option of a product with options. A product priced 0 is treated as having no price yet, so it is never "the cheapest" or "within budget". A search is for a specific item and is not used for these
  • The catalog's own language: the bot learns which language your product names are written in (from the names themselves) and searches in it, so an English shopper on a Hebrew catalog still finds the product. When a search finds nothing it says "I couldn't find it", never "we don't carry it", and it retries once with simpler words first. By default it answers in the language the shopper wrote; personaJson.languages (any of en, he, ar, es, fr) then only picks the chat window's labels. With personaJson.restrictLanguages: true the bot answers only in languages: the shopper's own language when it is listed, otherwise the widget's language when listed, otherwise the first one listed, with one sentence saying which languages it can help in. The sentences the server writes itself (spend-ceiling pause, sign-in notice, refusal) follow the same rule, and the widget does not switch to a language outside the list. restrictLanguages is off by default, so a list on its own never limits the bot
  • Recommendation cards: image, price, add-to-cart button, in-card variant picker (attribute chips with live price update)
  • Live stock: the bot reads availability for the storefront it is mounted on (the same figure the cart enforces, after reservations and any units held back for other channels). It says plainly when something is out of stock, tells the shopper which options of a product are available, offers in-stock alternatives, and never sends an out-of-stock add to the cart. A product card can carry availability ('in_stock' | 'low_stock' | 'out_of_stock' | 'unknown'); the widget badges a sold-out card and disables its cart button. The field is optional, so an older widget or server simply shows no badge. A count of units left is only ever spoken when the merchant's low-stock warning is on
  • Cart adds: shopper can say "add the blue one to my cart"; resolves through your onAddToCart hook → brainerce:bot:add-to-cart CustomEvent → product page fallback. The bot says it is adding the item, not that it is already in the cart: the cart makes the final call and the shopper sees the result
  • Order cards. When a signed-in shopper asks about their orders, the widget shows one card per order (up to three, newest first) from an order frame: order number, a four-step progress line (placed, preparing, shipped, delivered; a cancelled or refunded order shows a status pill instead), the dates it has, up to three item lines with photo, options and quantity (itemCount is the real count), the total, and a "Track shipment" button when the order has a tracking link. Only http(s) tracking links and photos reach the widget. The bot's own sentence does not repeat what the card shows. An older widget ignores the order frame and the shopper simply gets the bot's text
  • The shopper's language: the widget starts in the page language when the merchant enabled it, then follows the script the shopper writes in (Hebrew and Arabic switch the whole chat to right-to-left, with its own labels, buttons and form). Replies the server writes itself (sign-in notice, refusals) follow the shopper's language too, not the store's default
  • Failure handling: a request that drops before anything arrives is retried once, quietly, under the same turn id (the server replays a stored answer instead of answering twice). If it still fails the shopper gets a "Try again" button that asks the same question under a new turn id
  • Order tracking. "Where is my order?" is the most common question a storefront bot gets. The bot looks up the shopper's own recent orders and their shipment status, identifying them from their verified session, and it never asks for an order number or email. On by default, and works automatically for a signed-in shopper (see above)
  • Escalation. When the question can't be answered from the catalog or store information, the bot opens a "leave a message" contact form that lands in the merchant's Inquiries inbox (unless the merchant switched the contact form off)
  • Unmet demand analytics: searches that match nothing are logged for the merchant to see which products shoppers want but can't find

The bot is read-only by design: aside from cart adds the shopper explicitly initiates, it never modifies store data.

Capabilities the merchant can switch off

Five toggles in the dashboard (Customers → Storefront Bot → Studio → Abilities & limits) decide what the bot is allowed to do. They change which tools the model is handed, so a disabled capability is not merely hidden: the bot becomes incapable of it.

CapabilityDashboard labelDefaultWhat switching it off removes
productQaProduct Q&AOnCatalog search, product lookup and cart adds. The bot can no longer discuss or show products at all
recommendationsRecommendationsOnPopular-product suggestions (needs productQa too, since without product lookup there is no way to show a result)
orderTrackingOrder trackingOncheckOrderStatus, covering order and shipment lookups
returnsReturns & refundsOnReturn-policy and refund guidance from the bot's instructions
bundleNudgesBundle nudgesOffAdd-on and bundle suggestions (needs productQa too)

The contact form is on by default and is not one of the capability switches: even with every capability disabled, the bot can still hand the shopper to the store team. The merchant can switch the contact form off on its own (escalationJson.enabled); the bot then never offers it, the widget hides its message button and POST /escalate answers 403.

Your embed code does not set these and cannot override them; they are read from the merchant's saved settings on every conversation.

Add-to-cart integration

const bot = await BrainerceBot.mount({
  connectionId: 'vc_abc123',
  onAddToCart: ({ productId, variantId, quantity }) => {
    // return true to confirm add, false to fall back to product page
    return myCart.add(productId, variantId, quantity);
  },
});

As an alternative, listen for the CustomEvent anywhere on the page:

window.addEventListener('brainerce:bot:add-to-cart', (e) => {
  const { productId, variantId, quantity } = e.detail;
  myCart.add(productId, variantId, quantity);
  e.preventDefault(); // prevents the product page fallback
});

Scaffolds from create-brainerce-store ≥ 1.45 wire onAddToCart for you automatically.

Display & configuration

Display behavior (compact / full-screen / shopper-resizable, position, auto-open, accent color, avatar, greeting, starter questions) is controlled by the merchant in the dashboard under Customers → Storefront Bot. The embed tag never decides display behavior; it only provides the connectionId.

Dashboard configuration (merchant side)

Merchants configure the bot at Customers → Storefront Bot in the Brainerce dashboard:

  • Status strip: on every Studio page, whether the bot is on and what is still missing (greeting, starter questions, store knowledge, a test chat), each a button to the setting. The Studio has five pages: Look & greeting, Store knowledge, Style & language, Abilities & limits, Privacy & cost
  • Test mode: chat with the bot using the settings on screen, saved or not. Nothing is stored (no conversation, no analytics), it runs as a guest, and each test message uses AI credits like a shopper's chat
  • Shipping, payment and contact details (off by default): the bot reads shipping zones and rates, the free-shipping threshold, payment methods and contact details from the store's own settings, scoped to the channel and cached for a few minutes
  • Knowledge pages: up to five published content pages (returns policy, size guide) whose text the bot may answer from
  • Live / Off toggle: the bot renders nothing until switched Live. It saves the moment it is flipped
  • Name and avatar: preset avatars or an AI-generated / uploaded image
  • Greeting and starter questions: the opening message and quick-tap chips
  • Accent color: matches the store brand
  • Custom instructions and FAQs: guide the bot's personality and pre-load answers
  • Daily spend ceiling: an optional cap on what the bot may spend per day across the whole store. Left empty, there is no ceiling. Once reached, the bot tells shoppers it is taking a short break until the next UTC day
  • Keep conversations for N days: retentionJson.days, 7 to 3650, empty keeps them forever. A daily worker deletes conversations (and their messages) with no activity for longer than that. Contact inquiries a chat created and anonymous analytics counters are kept. The dashboard shows how many chats a period would delete before saving
  • Analytics funnel: conversations, shown a recommendation, opened a product, added to cart, and orders. Orders are those placed by signed-in shoppers within 7 days of a click or a cart add (guests cannot be followed to checkout, so it is a floor)
  • Rules: up to 20 plain-language rules ("always", "never", "hand over to the team when"), compiled into the bot's instructions
  • Customer memory: off by default. Switch, what may be remembered, how many days (30 to 365), and whether the shopper is asked first. See Customer memory
  • Contact form: on by default. The merchant can switch it off, set the subject of the inquiry, add up to five more email recipients and a line shown above the form
  • Topics to avoid, discount ceiling, minimum recommendations: personaJson.avoidTopics (up to 500 characters), maxDiscountPct (the bot never mentions a discount above it; it cannot create one) and minRecommend (capped at 8 when the prompt is built)
  • Voice and look: personaJson.tone (friendly, formal, playful, concise), responseLength (concise, balanced, detailed), emoji, bubbleShape (rounded, square), position (start, end), displayMode (floating, auto_open, full_screen; side_rail is accepted, and inline is accepted for old rows but behaves like floating), and allowExpand

Field limits on personaJson: greeting 500 characters, customInstructions 2,000, up to 30 faqs (q 300, a 1,000), up to 20 rules (200 characters each), up to 5 knowledgePageIds, and starterQuestions up to 20 of 150 characters each (the dashboard offers six). The bot name is displayName, up to 40 characters.

Customer memory

Optional and off by default. Set personaJson.memory (updateBotSettings, the dashboard, or the update_bot_settings MCP tool):

await admin.updateBotSettings({
  salesChannelId: 'vc_abc123',
  personaJson: {
    memory: { enabled: true, kinds: ['size', 'taste'], days: 90, consentMode: 'ask' },
  },
});
FieldValuesMeaning
enabledtrue / false (default)Master switch. With no kinds left it is treated as off
kindssize, taste, constraint, languageWhat the bot may remember. Default: all four
days30 to 365, default 90How long a memory lives. A repeated preference renews it. A daily worker deletes expired rows
consentModeask (default), implicitask: the widget asks once and nothing is saved until the shopper agrees. implicit: no question, you are responsible

How it works:

  • Signed-in shoppers only. The shopper is the one the widget's bot-session token proves (see Order tracking and the signed-in shopper). A guest, or a shopper whose session cannot be verified, has no memory in or out. The customer id is never taken from a URL or body.
  • Consent is stored per store and shopper with the wording version. A shopper's "no" or a withdrawal wins in both modes. The shopper also has two switches: remember new things, and use what is remembered.
  • What is stored: short statements the shopper made, 200 characters at most, up to 20 per shopper and store. Anything that looks like an email, a phone number or a long number is refused, and the prompt tells the model never to save contact details, addresses, payment, order contents, health or anything about children.
  • In the prompt: at most 10 memories and 1,200 characters, inside the per-request half of the prompt (the cached shared half is identical for every shopper), fenced as data and never as instructions.
  • Test mode never reads or writes memory.

The widget calls these public routes with Authorization: Bearer <bot-session token>, all Cache-Control: private, no-store: GET /storefront-bot/:connectionId/memory, PUT .../memory/consent ({ status: 'granted' | 'declined' | 'revoked' }), PUT .../memory/switches, PATCH .../memory/items/:id, DELETE .../memory/items and DELETE .../memory/items/:id. A storefront does not call them itself. Deleting works even after a merchant switches memory off. For the dashboard, GET|DELETE /storefront-bot/customer-memory (store-scoped, customer permissions) powers the customer profile.

The public settings carry memoryEnabled so the widget only offers memory when the store switched it on.

Configure programmatically (admin API)

The bot's settings and its conversation inbox are not dashboard-only. With an admin-mode client (apiKey: 'brainerce_*') the SDK exposes five methods over /v1/storefront-bot/*, useful for agencies configuring many stores, or for pulling conversations into your own analytics.

MethodWhat it does
getBotSettings()Settings for every connection on the store: name, avatar, persona, starter questions, guardrails, capabilities
updateBotSettings(data)Create or update one connection's settings. PATCH semantics: only the fields present in data change
listBotConversations({ salesChannelId, page, limit })Paginated conversation inbox, optionally filtered to one connection
getBotConversation(conversationId)Full transcript for one conversation, including its summary if one was generated
summarizeBotConversation(conversationId)Summarize a conversation on demand and persist it. Threads of 10 messages or fewer are a graceful no-op, with no credits charged
const admin = new BrainerceClient({ apiKey: 'brainerce_...' });

await admin.updateBotSettings({ salesChannelId: 'vc_abc123', enabled: true });
const { data } = await admin.listBotConversations({ limit: 20 });

All five require Admin mode, so a vc_* or storeId client cannot call them. The raw operations are listed under external API v1.

Where the bot is allowed to load

The bot answers only requests that come from the domain configured on the connection. This is checked on every widget call (bootstrap, chat, escalation) against the browser's Origin, and it is the same rule the rest of the storefront API uses.

ConnectionRule
LiveThe page's origin must match the connection's domain (exact host or a subdomain of it), or be listed in the connection's additional allowed origins. A domain is required, and a Live connection without one serves nothing.
Test, no domain setAny origin. This is what makes localhost, preview URLs and staging hosts work while you build.
Test, domain setSame rule as Live.

A blocked origin does not produce an error. The bot simply does not appear. The bootstrap always answers "bot is off" rather than explaining why, so that nobody can probe which connection ids exist. That means the widget silently not rendering has four possible causes and they look identical from the page: the bot is switched off, the plan/credits are exhausted, the connectionId is wrong, or the page's domain is not on the connection. Check the connection's domain in Sales channels first, because it is the one people hit when moving a working storefront to a new host.

Add staging, preview or secondary domains to the connection's allowed origins rather than switching it back to Test.

Server-rendered calls send no Origin at all. A Test connection without a domain accepts them; a Live connection rejects them. Mount the widget client-side, because it is a browser widget and always sends one.

CSP requirements

If your storefront has a Content Security Policy, add:

script-src https://cdn.brainerce.com;
connect-src https://api.brainerce.com;

Plan requirement

The Storefront Bot is available on every plan. Usage draws from the store's shared AI credit pool (FREE: 5/day, 30/month). Once exhausted, the widget stays mounted but replies with a short "taking a break" message instead of calling the model, and resumes automatically once credits reset.