Publish & Share

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.

|View as Markdown
Hunter Hodnett
Hunter HodnettCPTO at Chipp
|1 min read
#embed#alchemist#integration#agents#deployment

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:

TokenDefaultPurpose
--chipp-embed-accent-color#2563EBPrimary action color (buttons, links)
--chipp-embed-background#FFFFFFPanel/widget background
--chipp-embed-foreground#000000Text color
--chipp-embed-muted#6B7280Secondary text, disabled state
--chipp-embed-border-color#D1D5DBDividers, borders
--chipp-embed-radius6pxBorder radius for corners
--chipp-embed-error-color#DC2626Error states
--chipp-embed-font-familysystem-ui, sans-serifFont stack

Widget-Specific Tokens

TokenDefaultPurpose
--chipp-embed-widget-launcher-size60pxLauncher button diameter
--chipp-embed-widget-launcher-offset16pxDistance from screen edge (bottom/left/top/right)
--chipp-embed-widget-z-index10000Stacking order
--chipp-embed-widget-panel-width400pxFloating panel width
--chipp-embed-widget-panel-height600pxFloating 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 <your-api-key>

{
  "anonSessionToken": "eyJhbGciOi...", // The anonymous session token
  "consumerId": "user-123"              // The now-authenticated user's ID
}
  1. Server response includes a new authenticated session token
  2. 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 <div ref={containerRef} style={{ height: "600px" }} />;
}

Vanilla JS (Anonymous + Floating Widget)

html
<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)

html
<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 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