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
- Open your Alchemist project and go to Integrations > Embed Chipp Agent
- Select the Chipp agent you want to embed from the dropdown (only same-org agents appear)
- 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
- 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
- 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.
npm install @chipp/embed-client
# or
yarn add @chipp/embed-client
# or
pnpm add @chipp/embed-clientStep 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:
// 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 });// 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:
// 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
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
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 neededBoth Inline Panel + Floating Widget
Both surfaces read and write to the exact same controller, so they stay in sync:
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
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:
#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
// 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
- Visitor chats anonymously – No authentication required; session is pseudonymous
- Visitor sees sign-in prompt or explicitly signs in on your host app
- Your backend calls the upgrade endpoint:
POST /api/v1/alchemist/projects/{projectId}/embed/upgrade-anonymous
Content-Type: application/json
Authorization: Bearer <your-api-key>
{
"anonSessionToken": "eyJhbGciOi...", // The anonymous session token
"consumerId": "user-123" // The now-authenticated user's ID
}- Server response includes a new authenticated session token
- Your frontend swaps the token in the transport, and the conversation seamlessly continues
// After sign-in, replace the old transport
const newAuthenticatedToken = upgradeResponse.token;
transport.setToken(newAuthenticatedToken);
// Controller continues running; messages are now tied to the authenticated userThe 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:
- Session identity – The pseudonymous visitor identifier
- Application ID – The bound agent
- 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-motionmedia 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:
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
- Check the binding is active: Open the Alchemist project’s Embed Settings and verify the binding status is “Active”
- Check network errors: Open browser DevTools → Network tab and look for
POST /api/v1/embed/tokenrequests; any4xxor5xxindicates an issue - 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
- Check retention setting: Go to Embed Settings and verify the binding is set to “Retained” (not “Ephemeral”)
- 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
- Ensure CSS is loaded: Verify
applyChippEmbedThemeOverrides()is called after the container element exists in the DOM - Check specificity: Custom properties follow normal CSS specificity; ensure no other rules are overriding them
- Check browser support: CSS custom properties are supported in all modern browsers (Chrome 49+, Firefox 31+, Safari 9.1+)
Widget Not Appearing on Mobile
- Check for overflow: Ensure the page body and parent elements don’t have
overflow: hidden - 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 - Check safe-area: On devices with notches, the widget should respect
env(safe-area-inset-*)automatically
Upgrade Failing
- Check the API key: Ensure your backend has a valid Chipp API key with the correct scopes
- Check token format: The
anonSessionTokenmust be the exact JWT from the anonymous session (not modified or decoded) - Check project ID: Verify the project ID in the upgrade endpoint matches the Alchemist project
Examples
React Component (Authenticated)
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 <div ref={containerRef} style={{ height: "600px" }} />;
}Vanilla JS (Anonymous + Floating Widget)
<body>
<script type="module">
import {
createHttpEmbedTransport,
createEmbedChatController,
mountChippEmbedWidget,
applyChippEmbedThemeOverrides,
} from "https://cdn.example.com/embed-client.js";
const container = document.body;
// Apply theme overrides
applyChippEmbedThemeOverrides(container, {
"--chipp-embed-accent-color": "#FF6600",
"--chipp-embed-widget-launcher-offset": "20px",
});
// Create transport and controller
const transport = createHttpEmbedTransport({
baseUrl: "https://api.example.com",
applicationId: "my-agent",
// No token – anonymous
});
const controller = createEmbedChatController(transport);
// Mount floating widget
const { widgetHandle } = mountChippEmbedWidget(controller, {
launcherPosition: "bottom-right",
zIndex: 9999,
});
</script>
</body>Upgrade Flow (Vanilla JS)
<button id="sign-in">Sign In</button>
<script type="module">
import {
createHttpEmbedTransport,
createEmbedChatController,
mountChippEmbedPanel,
} from "https://cdn.example.com/embed-client.js";
let transport, controller;
let anonToken = null;
// Initialize with anonymous session
async function initAnonymous() {
transport = createHttpEmbedTransport({
baseUrl: "https://api.example.com",
applicationId: "my-agent",
});
controller = createEmbedChatController(transport);
mountChippEmbedPanel(document.getElementById("chat"), controller);
// Store the anonymous token for later upgrade
anonToken = transport.getToken();
}
// After user signs in
async function upgradeToAuthenticated(userId, authToken) {
const response = await fetch(
"https://api.example.com/api/v1/alchemist/projects/my-project/embed/upgrade-anonymous",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.API_KEY}`,
},
body: JSON.stringify({
anonSessionToken: anonToken,
consumerId: userId,
}),
}
);
const { token } = await response.json();
// Update the transport with the new authenticated token
transport.setToken(token);
// Controller continues running; conversation is now authenticated
}
document.getElementById("sign-in").addEventListener("click", async () => {
// Your sign-in flow...
const { userId, authToken } = await yourSignInFlow();
await upgradeToAuthenticated(userId, authToken);
});
initAnonymous();
</script>API Reference
createHttpEmbedTransport(config)
Creates a transport for communicating with the embedded agent API.
Parameters:
baseUrl(string) – API base URLapplicationId(string) – Embedded agent IDtoken(string, optional) – Session token for authenticated modecontext(object, optional) – Host-provided context (validated server-side)fetchImpl(function, optional) – Custom fetch implementation (defaults to globalfetch)
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 changesloadHistory(options?)– Load previous messagesselectThread(threadId)– Switch threadssendMessage(text)– Send a messageretry()– Retry the last failed messagecancel()– Cancel the current streaming response
Returns: EmbedChatController
mountChippEmbedPanel(container, controller, options?)
Mounts the inline chat panel.
Parameters:
container(HTMLElement) – DOM node to mount intocontroller(EmbedChatController) – Shared controlleroptions(object, optional):placeholder(string) – Greeting textautoLoadHistory(boolean) – Load history on mount
Returns: { destroy() }
mountChippEmbedWidget(controller, options?)
Mounts the floating widget.
Parameters:
controller(EmbedChatController) – Shared controlleroptions(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 panelcontroller(EmbedChatController) – Shared controlleroptions(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 tooverrides(object) –{ "--custom-property": "value" }
Returns: void
Support & Next Steps
- Questions? Reach out to support@chipp.ai
- API docs: See the Builder API reference for programmatic agent management
- Monetization: Learn how to sell access to your agents