# Embed Chipp Agents into Alchemist Projects Bind and embed your Chipp AI agents into Alchemist projects with inline panels, floating widgets, and both authenticated and anonymous visitor support. --- Embed your Chipp AI agents directly into Alchemist projects with a flexible, composable runtime that supports both authenticated users and anonymous visitors. This guide walks you through binding an agent, configuring identity modes, choosing your surface (inline panel, floating widget, or both), theming, and handling the anonymous-to-authenticated upgrade path. ## Overview Alchemist projects can bind and embed **existing Chipp agents from the same organization**. The embedded agent runs in a composable runtime that: - Supports **authenticated SaaS users** (primary mode) and **anonymous visitors** (secondary mode) - Offers **two surfaces**: inline chat panel and native floating widget (both share the same runtime) - Uses **server-policy-controlled host context** so arbitrary browser claims never become trusted instructions - Tracks usage and billing normally to the organization owning the agent - Allows **deterministic upgrade** of anonymous visitors to authenticated users without losing conversation history ## Prerequisites - A Chipp organization with at least one published Chipp agent/application - Access to the Alchemist project where you want to embed the agent - Both the Chipp agent and Alchemist project must belong to the **same organization** ## Step 1: Bind an Existing Chipp Agent Only agents from the same organization can be bound. This ensures billing, data isolation, and access control remain straightforward. ### Via the Alchemist Dashboard 1. Open your Alchemist project and go to **Integrations > Embed Chipp Agent** 2. Select the Chipp agent you want to embed from the dropdown (only same-org agents appear) 3. Choose your **identity mode**: - **Authenticated only** – Only signed-in SaaS users can access - **Authenticated + Anonymous** – Both modes enabled; visitors without a session can chat anonymously 4. Configure **conversation retention**: - **Ephemeral** – Anonymous conversations are not retained after the session ends - **Retained** – Anonymous conversations are stored and can be resumed if the visitor returns 5. Click **Create Binding** Your binding now appears on the **Embed Settings** page with a unique binding ID. You'll use this ID to initialize the embedded runtime on your host. ## Step 2: Choose Your Surface The embedded Chipp agent can appear as: - **Inline Panel** – A chat panel embedded inline in your page layout - **Floating Widget** – A launcher button with a floating chat overlay (launcher + open/close/minimize + unread badge) - **Both** – Host your page with both surfaces reading/writing to the exact same shared runtime Both surfaces are implemented on the same composable package (`embed-client`), so they share conversation history, message state, and all runtime behavior. Choosing "both" means your page can have an inline chat section and a floating widget side-by-side, all against one underlying agent connection. ## Step 3: Install the Client Library The `@chipp/embed-client` package provides the composable runtime and UI components. ```bash npm install @chipp/embed-client # or yarn add @chipp/embed-client # or pnpm add @chipp/embed-client ``` ## Step 4: Set Up the Runtime You'll initialize the runtime with a session token and optional host context. Here's the basic pattern: ### Authenticated Mode For authenticated users, issue a session token server-side via the Chipp API, then pass it to the client: ```typescript // On your backend import { createEmbedSessionToken } from "@chipp/services"; // Issue a token for an authenticated user const token = await createEmbedSessionToken({ applicationId: "your-agent-id", organizationId: "your-org-id", consumerId: "user-database-id", mode: "authenticated", }); // Send the token to your frontend response.json({ token }); ``` ```typescript // On your frontend import { createHttpEmbedTransport, createEmbedChatController, mountChippEmbedPanel, } from "@chipp/embed-client"; // Create transport with the authenticated token const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", token: authToken, // from your server }); // Create the controller (the shared state machine) const controller = createEmbedChatController(transport); // Mount the inline panel (or floating widget) mountChippEmbedPanel(document.getElementById("chat-container"), controller); ``` ### Anonymous Mode For anonymous visitors, request a new anonymous session directly from the client: ```typescript // On your frontend const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", // No token – the client will request an anonymous session }); const controller = createEmbedChatController(transport); mountChippEmbedPanel(document.getElementById("chat-container"), controller); ``` The client automatically requests an anonymous session on first use. The session is scoped to a single visitor + agent pair, and isolation is enforced server-side: a visitor cannot read or resume another visitor's conversation. ## Step 5: Mount Your Surface(s) ### Inline Panel Only ```typescript import { createHttpEmbedTransport, createEmbedChatController, mountChippEmbedPanel, } from "@chipp/embed-client"; const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", token, // optional; omit for anonymous }); const controller = createEmbedChatController(transport); const handle = mountChippEmbedPanel( document.getElementById("chat-panel"), controller, { autoLoadHistory: true, // Load previous messages on mount } ); // Later, if you need to unmount: handle.destroy(); ``` ### Floating Widget Only ```typescript import { createHttpEmbedTransport, createEmbedChatController, mountChippEmbedWidget, } from "@chipp/embed-client"; const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", token, }); const controller = createEmbedChatController(transport); const handle = mountChippEmbedWidget(controller, { launcherPosition: "bottom-right", // "bottom-left", "top-left", "top-right" launcherSize: "large", // "small", "medium", "large" zIndex: 10000, }); handle.destroy(); // Unmount when needed ``` ### Both Inline Panel + Floating Widget Both surfaces read and write to the exact same controller, so they stay in sync: ```typescript import { createHttpEmbedTransport, createEmbedChatController, mountChippEmbedSurfaces, } from "@chipp/embed-client"; const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", token, }); const controller = createEmbedChatController(transport); const { panelHandle, widgetHandle } = mountChippEmbedSurfaces( document.getElementById("chat-panel"), controller, { surfaces: "both", // or "inline" | "floating" widget: { launcherPosition: "bottom-right", }, } ); ``` ## Theming & Design Tokens Both surfaces use CSS custom properties (design tokens) so you can match your host site's styling. The available theme variables are: | Token | Default | Purpose | | --- | --- | --- | | `--chipp-embed-accent-color` | `#2563EB` | Primary action color (buttons, links) | | `--chipp-embed-background` | `#FFFFFF` | Panel/widget background | | `--chipp-embed-foreground` | `#000000` | Text color | | `--chipp-embed-muted` | `#6B7280` | Secondary text, disabled state | | `--chipp-embed-border-color` | `#D1D5DB` | Dividers, borders | | `--chipp-embed-radius` | `6px` | Border radius for corners | | `--chipp-embed-error-color` | `#DC2626` | Error states | | `--chipp-embed-font-family` | `system-ui, sans-serif` | Font stack | ### Widget-Specific Tokens | Token | Default | Purpose | | --- | --- | --- | | `--chipp-embed-widget-launcher-size` | `60px` | Launcher button diameter | | `--chipp-embed-widget-launcher-offset` | `16px` | Distance from screen edge (bottom/left/top/right) | | `--chipp-embed-widget-z-index` | `10000` | Stacking order | | `--chipp-embed-widget-panel-width` | `400px` | Floating panel width | | `--chipp-embed-widget-panel-height` | `600px` | Floating panel height | ### Applying Theme Overrides ```typescript import { applyChippEmbedThemeOverrides } from "@chipp/embed-client"; const container = document.getElementById("chat-container"); applyChippEmbedThemeOverrides(container, { "--chipp-embed-accent-color": "#FF6600", "--chipp-embed-background": "#FAFAFA", "--chipp-embed-foreground": "#1F2937", "--chipp-embed-widget-launcher-offset": "24px", }); ``` Or set them directly in CSS: ```css #chat-container { --chipp-embed-accent-color: #ff6600; --chipp-embed-background: #fafafa; --chipp-embed-foreground: #1f2937; --chipp-embed-widget-launcher-offset: 24px; } ``` ## Host-Provided Context You can supply optional page/record/workspace context to the agent without it becoming a trusted instruction or authorization. Context is **advisory data only** — it flows through a server-side policy contract and is never used for access control. ### Sending Context from Your Host ```typescript // On your frontend const transport = createHttpEmbedTransport({ baseUrl: "https://your-api.example.com", applicationId: "your-agent-id", token, context: { pageUrl: window.location.href, pageTitle: document.title, // Add any context your agent might find useful customMetadata: { projectId: "proj-123", environment: "staging", featureFlags: ["feature-a", "feature-b"], }, }, }); ``` ### Policy Validation (Server-Side) The server validates all context against the binding's policy before allowing it to influence the agent. The policy can: - **Allow** specific context keys and types - **Deny** context that looks like authorization - **Transform** context (e.g., only accept hashes of sensitive values) - **Log** suspicious claims for audit Example: a context key named `userId` is always rejected (prevented from becoming an authorization claim), but `projectId` or `recordName` can pass through if the policy permits it. See your Alchemist binding settings to configure which context keys are allowed. ## Anonymous → Authenticated Upgrade When an anonymous visitor signs in, their in-flight conversation is deterministically attached to their authenticated consumer account **without leaking it to any other identity**. ### User Experience Flow 1. **Visitor chats anonymously** – No authentication required; session is pseudonymous 2. **Visitor sees sign-in prompt** or explicitly signs in on your host app 3. **Your backend calls the upgrade endpoint**: ```bash POST /api/v1/alchemist/projects/{projectId}/embed/upgrade-anonymous Content-Type: application/json Authorization: Bearer { "anonSessionToken": "eyJhbGciOi...", // The anonymous session token "consumerId": "user-123" // The now-authenticated user's ID } ``` 4. **Server response** includes a new authenticated session token 5. **Your frontend swaps the token** in the transport, and the conversation seamlessly continues ```typescript // After sign-in, replace the old transport const newAuthenticatedToken = upgradeResponse.token; transport.setToken(newAuthenticatedToken); // Controller continues running; messages are now tied to the authenticated user ``` The conversation history remains intact, visible to the now-authenticated user. No cross-identity leakage occurs because: - The upgrade is server-initiated and server-validated - The anonymous session is revoked after upgrade - The authenticated session is bound to a specific consumer ID (not just a browser cookie) ## Rate Limits & Abuse Prevention Anonymous sessions are subject to per-session and per-IP rate limits (using Redis): - **Session creation**: Max 10 anonymous sessions per IP per hour - **Message volume**: Max 100 messages per session per hour - **Concurrent sessions**: Max 5 per IP Authenticated sessions have higher limits and are tracked per user, not IP. If a limit is exceeded, the client receives a `429 Too Many Requests` response and should display a friendly error message. ## Isolation & Security Guarantees ### An Anonymous Session Cannot: - Read or enumerate another visitor's conversation - Resume another visitor's messages - Access other sessions' metadata or context - Make requests outside its scoped agent ### Every Read/Mutate Operation is Scoped By: 1. **Session identity** – The pseudonymous visitor identifier 2. **Application ID** – The bound agent 3. **Organization ID** – Cross-org access is denied at the binding level This is enforced server-side, not browser-side. The client cannot override or spoof these boundaries. ## Billing & Attribution Usage (messages, tokens, API calls) from both authenticated and anonymous visitors is attributed to the **organization that owns the embedded agent**. If Org A embeds one of their agents into Org B's Alchemist project: - Messages from Org B's users (both anonymous and authenticated) bill to **Org A** - Org B is using Org A's compute and quota This keeps billing and capacity planning simple: each organization pays for the agents they own, regardless of where they're embedded. ## Mobile & Responsive Behavior Both the inline panel and floating widget are fully responsive: - **Desktop**: Panel at full width or widget floating in a corner - **Mobile**: Panel scales to viewport; widget uses full-height overlay with safe-area insets - **Notch/Home Indicator**: Safe-area env() variables ensure the launcher and widget don't hide behind notches or home indicators The floating widget automatically adapts `--chipp-embed-widget-launcher-offset` when the device has a notch or home indicator. ## Accessibility Both surfaces support: - **Keyboard navigation**: Tab/Shift+Tab to move focus, Enter to send, Escape to close widget - **Focus trap**: When the widget opens, focus is trapped inside; Escape restores focus to the launcher - **ARIA live regions**: New messages and unread state are announced to screen readers (`aria-live="polite"`) - **Reduced motion**: Respects `prefers-reduced-motion` media query - **High contrast**: Text meets WCAG AA contrast ratios ## Conversation Retention & History ### Ephemeral Sessions (Anonymous) If you choose **ephemeral** retention when binding the agent: - Visitor chats anonymously; conversation is stored during the session - Browser closes or visitor navigates away - **Conversation is permanently deleted** (no persistence) - Next time the visitor returns, they start a new, empty conversation ### Retained Sessions (Anonymous or Authenticated) If you choose **retained** retention: - Conversation persists in the database - Authenticated users can resume their conversation across devices and sessions - Anonymous visitors can resume **if they return within the TTL** (default 30 days); after that, the session expires and a new one is issued ### Loading History When initializing the controller: ```typescript const controller = createEmbedChatController(transport); // Load previous messages (if this session has history) await controller.loadHistory(); // Or select a specific thread if the agent supports multi-thread conversations await controller.selectThread(threadId); ``` ## Troubleshooting ### Anonymous Session Not Being Created 1. **Check the binding is active**: Open the Alchemist project's **Embed Settings** and verify the binding status is "Active" 2. **Check network errors**: Open browser DevTools → Network tab and look for `POST /api/v1/embed/token` requests; any `4xx` or `5xx` indicates an issue 3. **Check rate limits**: If you see `429 Too Many Requests`, you've hit the per-IP session creation limit; wait an hour ### Conversation Not Persisting 1. **Check retention setting**: Go to **Embed Settings** and verify the binding is set to "Retained" (not "Ephemeral") 2. **Check session expiration**: Anonymous sessions expire after 30 days by default; if the visitor returns after that, a new session is issued ### Theme Not Applying 1. **Ensure CSS is loaded**: Verify `applyChippEmbedThemeOverrides()` is called *after* the container element exists in the DOM 2. **Check specificity**: Custom properties follow normal CSS specificity; ensure no other rules are overriding them 3. **Check browser support**: CSS custom properties are supported in all modern browsers (Chrome 49+, Firefox 31+, Safari 9.1+) ### Widget Not Appearing on Mobile 1. **Check for overflow**: Ensure the page body and parent elements don't have `overflow: hidden` 2. **Check z-index**: If other modals or floating elements have higher z-index, the widget may be behind them; increase `--chipp-embed-widget-z-index` 3. **Check safe-area**: On devices with notches, the widget should respect `env(safe-area-inset-*)` automatically ### Upgrade Failing 1. **Check the API key**: Ensure your backend has a valid Chipp API key with the correct scopes 2. **Check token format**: The `anonSessionToken` must be the exact JWT from the anonymous session (not modified or decoded) 3. **Check project ID**: Verify the project ID in the upgrade endpoint matches the Alchemist project ## Examples ### React Component (Authenticated) ```jsx import { useEffect, useRef } from "react"; import { createHttpEmbedTransport, createEmbedChatController, mountChippEmbedPanel, } from "@chipp/embed-client"; export function ChatWidget({ authToken, agentId }) { const containerRef = useRef(null); const handleRef = useRef(null); useEffect(() => { if (!containerRef.current) return; const transport = createHttpEmbedTransport({ baseUrl: process.env.REACT_APP_API_URL, applicationId: agentId, token: authToken, }); const controller = createEmbedChatController(transport); handleRef.current = mountChippEmbedPanel(containerRef.current, controller); return () => { handleRef.current?.destroy(); }; }, [authToken, agentId]); return
; } ``` ### Vanilla JS (Anonymous + Floating Widget) ```html ``` ### Upgrade Flow (Vanilla JS) ```html ``` ## API Reference ### `createHttpEmbedTransport(config)` Creates a transport for communicating with the embedded agent API. **Parameters:** - `baseUrl` (string) – API base URL - `applicationId` (string) – Embedded agent ID - `token` (string, optional) – Session token for authenticated mode - `context` (object, optional) – Host-provided context (validated server-side) - `fetchImpl` (function, optional) – Custom fetch implementation (defaults to global `fetch`) **Returns:** `EmbedTransport` ### `createEmbedChatController(transport)` Creates the shared state machine for all surfaces. **Methods:** - `getState()` – Current state (status, threads, messages, etc.) - `subscribe(callback)` – Listen for state changes - `loadHistory(options?)` – Load previous messages - `selectThread(threadId)` – Switch threads - `sendMessage(text)` – Send a message - `retry()` – Retry the last failed message - `cancel()` – Cancel the current streaming response **Returns:** `EmbedChatController` ### `mountChippEmbedPanel(container, controller, options?)` Mounts the inline chat panel. **Parameters:** - `container` (HTMLElement) – DOM node to mount into - `controller` (EmbedChatController) – Shared controller - `options` (object, optional): - `placeholder` (string) – Greeting text - `autoLoadHistory` (boolean) – Load history on mount **Returns:** `{ destroy() }` ### `mountChippEmbedWidget(controller, options?)` Mounts the floating widget. **Parameters:** - `controller` (EmbedChatController) – Shared controller - `options` (object, optional): - `launcherPosition` (string) – `"bottom-right" | "bottom-left" | "top-left" | "top-right"` - `launcherSize` (string) – `"small" | "medium" | "large"` - `zIndex` (number) – CSS z-index **Returns:** `{ widgetHandle, destroy() }` ### `mountChippEmbedSurfaces(inlineContainer, controller, options?)` Mounts both inline panel and floating widget. **Parameters:** - `inlineContainer` (HTMLElement) – DOM node for the panel - `controller` (EmbedChatController) – Shared controller - `options` (object, optional): - `surfaces` (string) – `"inline" | "floating" | "both"` - `widget` (object) – Widget options (launcherPosition, zIndex, etc.) **Returns:** `{ panelHandle, widgetHandle, destroy() }` ### `applyChippEmbedThemeOverrides(element, overrides)` Apply CSS custom properties to override theme. **Parameters:** - `element` (HTMLElement) – Element to apply theme to - `overrides` (object) – `{ "--custom-property": "value" }` **Returns:** void ## Support & Next Steps - **Questions?** Reach out to support@chipp.ai - **API docs**: See the [Builder API reference](/docs/builder-api) for programmatic agent management - **Monetization**: Learn how to [sell access to your agents](/docs/guides/selling-access)