Add it to your web app or site with one line. Your users stay signed in, and their roles carry over.
<script src="https://embed.lotsagent.com/v1/a/AGENT_ID/widget.js" async></script>
<lots-agent token-url="/lotsagent-token"></lots-agent>AGENT_ID. Use the id, not the handle. The id never changes.https://app.example.com. Exact https://host (with a port if you use one), or https://*.example.com for any one-level subdomain. Up to 20. http://localhost works only while Test mode is on.token-url. Examples in Node, Python and PHP are in section 4.token-url is a path on your own site. It must be the same origin as the page. A URL on another origin is ignored, and the person is treated as not signed in.GET, with the person's cookies, and Accept: text/plain.200 with the token as the whole body, as plain text, and Cache-Control: no-store.401 (or any status that is not 2xx) when nobody is signed in. The sidebar then says "You are signed out of the app. Sign in again to keep chatting."jti works once.<lots-agent> by itself and uses the Token URL saved on the Embed page. Leave out token-url for an agent where anyone can chat (section 4).Nothing extra to do. Side, colour, title and button text are set on the Embed page. Use side to change the side for one page. On screens narrower than 640 px the sidebar opens as a full-screen sheet.
<!-- The default: a round button in the corner opens a sidebar -->
<lots-agent token-url="/lotsagent-token" side="left"></lots-agent>Use launcher="none", or pick "No floating button" on the Embed page. The agent then opens only from your own triggers below.
<!-- No floating button. Only your own triggers open it. -->
<lots-agent token-url="/lotsagent-token" launcher="none"></lots-agent>Add an attribute to any element: in your navbar, sidebar, header or help menu. It also works for elements you add later, because one listener on the page handles every click.
<!-- Any element with data-lotsagent-open opens the agent -->
<a href="#" data-lotsagent-open>Ask AI</a>
<!-- Open with a question typed in for them (not sent) -->
<button type="button" data-lotsagent-open data-lotsagent-prompt="How do I export my data?">Help with export</button>
<!-- Open and send the question at once -->
<button type="button" data-lotsagent-open data-lotsagent-prompt="Show my last invoice" data-lotsagent-send>Last invoice</button>
<!-- Open if closed, close if open -->
<button type="button" data-lotsagent-toggle>AI</button>data-lotsagent-open opens it. data-lotsagent-toggle opens it, or closes it if open.data-lotsagent-prompt types a question into the box. Add data-lotsagent-send to send it at once.// LotsAgent exists once widget.js has run. Use ?. in case it has not.
window.LotsAgent?.open(); // open it
window.LotsAgent?.open({ prompt: "Summarise my week" }); // open with a question typed in
window.LotsAgent?.open({ prompt: "Show my orders", send: true }); // open and send it
window.LotsAgent?.close();
window.LotsAgent?.toggle();Events fire on the <lots-agent> element and bubble up, so you can listen on document. For example, a dot on your own menu item:
// Events bubble up from <lots-agent>, so you can listen on document.
const help = document.querySelector("#help-menu-item"); // your own menu item
document.addEventListener("lotsagent:unread", (event) => {
// A reply arrived while the sidebar was closed. Show a dot on your own menu item.
help.dataset.unread = String(event.detail.count);
});
document.addEventListener("lotsagent:open", () => {
delete help.dataset.unread;
});
document.addEventListener("lotsagent:disabled", () => {
// The owner switched the agent off. Hide your own entry point.
help.hidden = true;
});| Event | When | event.detail |
|---|---|---|
lotsagent:loaded | The element started and read its settings. | { agentId } |
lotsagent:ready | The chat inside the sidebar has loaded. This happens on the first open, or at page load for inline, full screen and "Load the chat early". | { agentId } |
lotsagent:open | It opened. | { agentId } |
lotsagent:close | It closed (sidebar only). | { agentId } |
lotsagent:unread | A reply arrived while the sidebar was closed. | { agentId, count } |
lotsagent:disabled | The owner switched the agent off while the page was open, and again on each open call after that. | { agentId } |
widget.js is an empty script. Nothing appears, window.LotsAgent is not defined, and data-lotsagent-open elements do nothing. To hide your own menu item in that case, check window.LotsAgent after the page has loaded. close() does nothing in inline and full-screen modes.<!-- Open by itself once per visit, 5 seconds after load, only on /help and /billing pages -->
<lots-agent
token-url="/lotsagent-token"
auto-open="once-per-session"
auto-open-delay="5"
auto-open-paths="/help,/billing"
></lots-agent>| auto-open | Opens |
|---|---|
off | Never. The default. |
always | On every page load. |
once-per-session | Once per visit (until the browser tab is closed). |
once-per-person | Once per person, remembered in that browser. |
auto-open-delay: seconds to wait after load, 0 to 600.auto-open-paths: opens only when the page path starts with one of them. Empty means every page.open attribute opens it at once instead, and then auto-open is ignored.For a full help page or a dashboard panel. The chat fills the element, with no close button. Give the element a height. It is at least 420 px tall.
<!-- The chat fills this element. Give it a height in your own CSS. -->
<lots-agent token-url="/lotsagent-token" mode="inline" class="help-chat"></lots-agent>/* In your own stylesheet file */
.help-chat {
height: 600px;
}For a page of its own, like "Ask AI". The chat covers the window at once, with no close button.
<!-- A dedicated "Ask AI" page: the chat covers the whole window -->
<lots-agent token-url="/lotsagent-token" mode="fullscreen"></lots-agent>Off by default. Up to three of mod, ctrl, alt, shift, meta, then one key: a letter, a digit, /, ., ,, ;, enter or space. For example mod+k or ctrl+/. The shortcut toggles the sidebar.
<!-- mod is Cmd on a Mac and Ctrl elsewhere. Write it in lower case. -->
<lots-agent token-url="/lotsagent-token" shortcut="mod+k"></lots-agent>Esc closes the sidebar, unless the person is typing in a field on your page. Focus goes back to what opened it.
Set getToken on the element instead of token-url. It wins over token-url. Set it before the chat loads: as soon as your page starts. If it throws, the person is treated as signed out.
// In your app's own code. The page has <lots-agent></lots-agent> with no token-url.
// Called each time the sidebar needs a token: when it starts, and every 4 minutes.
// Return a new token every time. Return null for a visitor who is not signed in.
document.querySelector("lots-agent").getToken = async () => {
return await myAuth.getLotsAgentToken(); // your own code
};Load each agent's script. Name the agent with agent on the element, data-lotsagent-agent on a trigger, or LotsAgent.open({ agent: "AGENT_ID_2" }) in code. Without a name, the first agent loaded on the page is used.
<script src="https://embed.lotsagent.com/v1/a/AGENT_ID_1/widget.js" async></script>
<script src="https://embed.lotsagent.com/v1/a/AGENT_ID_2/widget.js" async></script>
<lots-agent agent="AGENT_ID_1" token-url="/lotsagent-token"></lots-agent>
<lots-agent agent="AGENT_ID_2" token-url="/lotsagent-token" launcher="none"></lots-agent>
<button type="button" data-lotsagent-open data-lotsagent-agent="AGENT_ID_2">Billing help</button>The Embed page sets these for every page. An attribute changes them for one element. Attributes are read once, when the element is added to the page.
| Attribute | Values | Default |
|---|---|---|
agent | The agent id. | The first agent loaded on the page |
token-url | A path on your own site, like /lotsagent-token. Same origin only. | Token URL on the Embed page |
side | right or left | right |
launcher | button or none | button |
mode | sidebar, inline or fullscreen | sidebar |
open | No value. Starts open, without taking focus. | Closed |
prompt | Text typed into the box the first time it opens. Not sent. | None |
auto-open | off, always, once-per-session or once-per-person | off |
auto-open-delay | Seconds, 0 to 600. | 0 |
auto-open-paths | Comma-separated paths, like /help,/billing. | Every page |
shortcut | Like mod+k, in lower case. | None |
| Trigger attribute | What it does |
|---|---|
data-lotsagent-open | Opens the agent. |
data-lotsagent-toggle | Opens it, or closes it if open. |
data-lotsagent-prompt | Types this question into the box. |
data-lotsagent-send | With a prompt: sends it at once. |
data-lotsagent-agent | Which agent, when the page has more than one. |
Set only on the Embed page: colour (text colour is picked for contrast), title, button text (empty for an icon only), up to 4 starter questions, and "Load the chat early", which loads the chat when the browser is idle so the first click opens at once.
script-src https://embed.lotsagent.com
frame-src https://embed.lotsagent.comA full example header
Content-Security-Policy: default-src 'self'; script-src 'self' https://embed.lotsagent.com; frame-src https://embed.lotsagent.com; style-src 'self'; require-trusted-types-for 'script'connect-src entry. Your page never calls LotsAgent: the loader has its settings built in, and the token request goes to your own server.style-src entry. The loader styles itself with constructed stylesheets inside a closed shadow root. It creates no HTML from strings, so pages that require Trusted Types work.frame-ancestors lists only your allowed sites, so no other site can frame your agent.style="" attribute on <lots-agent> is blocked by a strict style-src. Set its height in your own stylesheet (see inline mode).| Browser | Result |
|---|---|
| Chrome (headless), 27 Oct 2026 | Page with only the two sources above, plus style-src 'self' and require-trusted-types-for 'script'. The button, sidebar and chat work. No violations. |
| Safari | Not tested yet. |
| Firefox | Not tested yet. |
A browser too old for constructed stylesheets falls back to a style element, which a strict style-src blocks.
The person becomes a record inside this agent only, keyed by your issuer and their sub. They never get a LotsAgent account, and an email in the token is never matched to one.
| Claim | Rule |
|---|---|
Header alg | HS256 with a key from the Embed page, or ES256, RS256 or EdDSA with your JWKS URL. Anything else is refused, none too. The algorithm must match the kind of key. |
Header kid | Required. Picks the key: the key id from the Embed page (it starts with k_), or a kid in your JWKS. |
iss | Required. Exactly the issuer saved on the Embed page, like https://app.example.com. |
aud | Required. Exactly lotsagent:AGENT_ID. A token for one agent never works on another. |
sub | Required. Your stable user id, 1 to 255 characters. Use an id that never changes. Not an email, unless emails never change in your app. |
email, name | Optional, recommended. Shown to the agent and in your activity view. |
workspace | Optional. { "id": "...", "name": "..." }, the workspace the person is in. id is required when it is there. |
role | Optional. Shown to the model only, so it does not offer what the person cannot do. Never used to allow anything. Your own tools decide (section 5). |
locale | Optional, like ta-IN. A hint for the reply language when the message does not show one. |
iat, exp | Required. At most 600 seconds apart. We recommend 300. Clock skew allowed: 60 seconds. |
jti | Required, up to 128 characters, unique per token. Each one is accepted once. |
| Whole token | At most 8 KB. Unknown claims are ignored. |
An example payload
{
"iss": "https://app.example.com",
"aud": "lotsagent:AGENT_ID",
"sub": "user_8412",
"email": "priya@example.com",
"name": "Priya",
"workspace": { "id": "ws_31", "name": "Acme Exports" },
"role": "viewer",
"locale": "ta-IN",
"iat": 1793080800,
"exp": 1793081100,
"jti": "5f0c6c1e-2b7a-4f7e-9d0a-3c1b8e6f4a21"
}Each example answers GET /lotsagent-token for the signed-in person with a 5-minute token and Cache-Control: no-store. Put the key id and secret from the Embed page in LOTSAGENT_EMBED_KID and LOTSAGENT_EMBED_SECRET. Use the secret exactly as shown. Never put it in a page.
Node (jose)
import { SignJWT } from "jose";
import { randomUUID } from "node:crypto";
const key = new TextEncoder().encode(process.env.LOTSAGENT_EMBED_SECRET);
// Express. requireLogin is your own sign-in check; req.user is the signed-in person.
app.get("/lotsagent-token", requireLogin, async (req, res) => {
const token = await new SignJWT({ email: req.user.email, name: req.user.name })
.setProtectedHeader({ alg: "HS256", kid: process.env.LOTSAGENT_EMBED_KID })
.setIssuer("https://app.example.com")
.setAudience("lotsagent:AGENT_ID")
.setSubject(String(req.user.id))
.setJti(randomUUID())
.setIssuedAt()
.setExpirationTime("5m")
.sign(key);
res.set("Cache-Control", "no-store").type("text/plain").send(token);
});Python (PyJWT)
import os, time, uuid
import jwt # pip install PyJWT
from flask import Response
from flask_login import current_user, login_required
# Flask. app is your Flask app; current_user is the signed-in person.
@app.get("/lotsagent-token")
@login_required
def lotsagent_token():
now = int(time.time())
token = jwt.encode(
{
"iss": "https://app.example.com",
"aud": "lotsagent:AGENT_ID",
"sub": str(current_user.id),
"email": current_user.email,
"name": current_user.name,
"iat": now,
"exp": now + 300,
"jti": str(uuid.uuid4()),
},
os.environ["LOTSAGENT_EMBED_SECRET"],
algorithm="HS256",
headers={"kid": os.environ["LOTSAGENT_EMBED_KID"]},
)
return Response(token, mimetype="text/plain", headers={"Cache-Control": "no-store"})PHP (firebase/php-jwt)
<?php
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
// Run this only for a signed-in person. $user is your own signed-in user.
$now = time();
$token = JWT::encode([
'iss' => 'https://app.example.com',
'aud' => 'lotsagent:AGENT_ID',
'sub' => (string) $user->id,
'email' => $user->email,
'name' => $user->name,
'iat' => $now,
'exp' => $now + 300,
'jti' => bin2hex(random_bytes(16)),
], getenv('LOTSAGENT_EMBED_SECRET'), 'HS256', getenv('LOTSAGENT_EMBED_KID'));
header('Content-Type: text/plain');
header('Cache-Control: no-store');
echo $token;Save a JWKS URL on the Embed page instead of making a key. Sign with ES256, RS256 or EdDSA. Use this when your own OAuth server must check the same token (section 5).
Node (jose), ES256
import { SignJWT, importPKCS8 } from "jose";
import { randomUUID } from "node:crypto";
// Your private key (PEM, PKCS#8). The public half is in your JWKS under kid "2026-10".
// user is the signed-in person.
const key = await importPKCS8(process.env.LOTSAGENT_EMBED_PRIVATE_KEY, "ES256");
const token = await new SignJWT({ email: user.email })
.setProtectedHeader({ alg: "ES256", kid: "2026-10" })
.setIssuer("https://app.example.com")
.setAudience("lotsagent:AGENT_ID")
.setSubject(String(user.id))
.setJti(randomUUID())
.setIssuedAt()
.setExpirationTime("5m")
.sign(key);Your JWKS
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "2026-10",
"use": "sig",
"alg": "ES256",
"x": "...",
"y": "..."
}
]
}use other than sig, and symmetric (oct) keys, are ignored.Turn on Anyone can chat on the Embed page and leave out token-url. Each visitor gets a random id inside the chat frame. Use it only for an agent with no tools that need a person's sign-in, like a help agent that answers from your docs. It cannot know who anyone is. The Embed page refuses it while the agent has tools that act for a signed-in person. When a token is sent, it is always used.
For an embedded chat, LotsAgent never uses your own connection, a stored connection, a site key or any shared key. It uses a token for that person, issued by your server, through OAuth token exchange (RFC 8693).
aud = lotsagent:AGENT_ID. It authenticates with private_key_jwt using LotsAgent's keys at https://lotsagent.com/.well-known/jwks.json.LotsAgent sends this to your token endpoint when the agent calls one of your tools:
POST {your token endpoint} for example https://api.example.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<the identity token your server signed, as received>
subject_token_type=urn:ietf:params:oauth:token-type:jwt
resource=https://api.example.com/mcp
scope=mcp:read mcp:write mcp:send mcp:money mcp:delete
client_id=<the client id you registered for this agent>
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion=<a JWT signed by LotsAgent with ES256>client_assertion is a JWT signed by LotsAgent with ES256: iss and sub are the client id, aud is your token endpoint, a unique jti, and exp 60 seconds away. Answer within 10 seconds.
iss is you, aud is the agent bound to this client_id, not expired, jti used once.workspace is present, still a member.HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 900,
"scope": "mcp:read mcp:write"
}act in the access token, so your audit log shows the call came through LotsAgent:{
"sub": "user_8412",
"scope": "mcp:read mcp:write",
"act": { "sub": "<client id>" }
}| Scope | Lets the agent |
|---|---|
mcp:read | Read. |
mcp:write | Create and change. |
mcp:send | Send to other people, like an email or a message. |
mcp:money | Move money. |
mcp:delete | Delete. |
LotsAgent keeps the access token, encrypted, until 30 seconds before it expires. Calls to your MCP server carry only Authorization: Bearer with that token. No other header names anyone. If your MCP server answers 401, LotsAgent exchanges once more and tries again once.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "error": "invalid_grant", "error_description": "The person no longer has access." }| Your token endpoint answers | The tool fails with |
|---|---|
invalid_grant: the subject token is bad or expired, or the person lost access | identity_rejected |
| HTTP 429 | rate_limited |
| HTTP 500 or higher | upstream_error |
| No answer within 10 seconds | timeout |
Anything else, like invalid_client, unauthorized_client, invalid_target or invalid_scope | tool_auth_misconfigured: your setup needs fixing |
The sidebar sends a fresh identity token every 4 minutes. If the person's latest one has run out (the sidebar was closed for a while), the tool fails with identity_expired and the person sees "Reconnecting to the app. Try again in a moment."
| LotsAgent reads | Rule |
|---|---|
tools.json at your MCP server's origin | Each tool's permission_max, or permission when there is no max. send, money or delete asks. read and write do not. The highest the tool can need wins. Our own gateway publishes this file. |
| MCP tool annotations | destructiveHint: true asks as delete. readOnlyHint: true never asks. openWorldHint: true asks as send. |
| Neither | Asks. Safe by default. |
tools.json (optional)
{
"tools": [
{ "name": "list_invoices", "permission": "read" },
{ "name": "update_invoice", "permission": "write", "permission_max": "delete" },
{ "name": "send_invoice", "permission": "send" }
]
}When a tool has both, the one that asks more wins: a write tool marked destructiveHint: true asks as delete.
Both are read when the integration's tools are synced. Tools synced before 27 Oct 2026 ask until you refresh them. If you run an MCP server, mark read-only tools with readOnlyHint: true.
A tool that can delete may be harmless for a given call. Before showing a card, LotsAgent asks POST /api/tools/classify on your token endpoint's origin, with the person's access token. If it answers within 3 seconds, that class is used for this call, but never below what the tool's annotations ask: a destructive tool always asks. Any other answer keeps the stored class.
POST /api/tools/classify
Authorization: Bearer <the person's access token>
Content-Type: application/json
{ "tool": "update_invoice", "arguments": { "id": "inv_204", "note": "Paid" } }
200 OK
{ "class": "write" }The owner can set each tool to Default, Always ask or Never ask. Never ask is allowed for send tools and tools that do not ask. It is never allowed for money or delete. Always ask also wins over the call-time answer. This setting is not on the Embed page yet.
Text inside an email or a document is read as data, never as an order.
| Setting on the Embed page | Default | Range |
|---|---|---|
| Messages per person per day | 50 | 1 to 1000 |
| Spend cap per day (USD) | $20 | $0.01 to $10,000 |
| Email me at (percent of the cap) | 80% | 1 to 100 |
| Time zone | UTC | Any time zone name, like Asia/Kolkata |
| Keep chats for (days) | 30 | 1 to 365 |
| Notice | None | Up to 1,000 characters |
Only the person's own messages count. Over the limit, the agent replies "You have used today's 50 messages. They reset at midnight." (with your number). That reply calls no model and costs nothing. Limits reset at midnight in the time zone you pick.
With Anyone can chat on, a visitor's id is chosen by their browser, so it cannot stand for one person. On top of the per-person limit, each address that opens chats gets twice that number of messages a day, and all anonymous visitors together get 500 messages a day per agent. Signed-in people are never counted against these. Over them, the agent replies "This agent has answered as many visitors as it can today. Sign in, or try again tomorrow."
The cap counts what you are charged. At 80% (or your percent) you get one email that day from notifications@lotsagent.com. At the cap the agent replies "This agent is resting until tomorrow." until the day turns. Replies already running can finish, so a day can end slightly over.
Turn off Show the agent on your site. New chats are refused at once, and the next message in an open chat within about 10 seconds. Within 60 seconds, pages that load show no button, and an open sidebar says "This agent is resting. Try again later." and removes the button. Your page gets lotsagent:disabled. On a page that was already loaded and never opened the sidebar, the button goes the next time someone clicks it.
Shown before the first message. The person clicks OK once. Changing the text raises its version, and everyone sees it again. It is shown as plain text, with your line breaks.
Chats are kept 30 days by default, then deleted. Every night, chats with no activity for longer than your setting are deleted: the conversation, its tool logs, confirmations and stored tokens. When a person has no chat left, their memory and their name, email and workspace go too. Billing records and events are kept. They hold no message text. Events are kept 13 months.
Embedded chats run on Gemini, through Google Vertex AI. There is no fallback to another provider. If Vertex cannot serve, the chat does not start, and the person sees "This agent is not available right now." Chats are not used to train AI.
| Event | Written when |
|---|---|
conversation_started | A new conversation starts. |
message | The person sends a message that is let through. |
limit_reached | A message is refused by the daily message limit. |
spend_capped | A message is refused by the spend cap. |
tool_call, tool_error | The agent calls a tool. tool_error carries an error code (below). |
confirm_shown, confirm_accepted, confirm_cancelled | A confirmation card is shown, confirmed or cancelled (section 6). |
confirm_expired | Nightly: a card nobody answered in 15 minutes. |
feedback_up, feedback_down | Thumbs on a reply. |
gave_up | Nightly: a conversation idle for 30 minutes where tools were tried and none worked, or the agent has tools and the person sent 3 or more messages with no tool working. |
The Activity tab on the Embed page shows these per day, with spend against your cap, and the 50 latest conversations as event timelines. It never shows message text.
| Code | Meaning | The person sees |
|---|---|---|
identity_required | The chat has no identity token: an anonymous visitor. | Sign in to the app to let the agent work in it. |
identity_expired | The person's latest identity token ran out. The sidebar sends a new one within 4 minutes. | Reconnecting to the app. Try again in a moment. |
identity_rejected | Your token endpoint answered invalid_grant, or your MCP server answered 401 twice. | The app did not accept your sign-in. |
permission_denied | Your MCP server answered 403 for this person. | Your role in the app does not allow this. |
not_found | Your MCP server answered 404. | The app could not find that. |
invalid_input | Your tool returned an error result. | Your tool's own error text. |
rate_limited | Your server answered 429. | The app is busy. Try again in a minute. |
timeout | Your server did not answer in time. | The app's tool did not answer in time. |
upstream_error | Your server failed. | The app's tool failed. |
tool_auth_misconfigured | Your token endpoint refused LotsAgent's client, resource or scopes (section 5). | Token exchange refused (and the error). |
tool_not_ready | The integration is not linked to token exchange for this agent, or the tool is gone. | This tool is not available in the sidebar. |
When the sidebar cannot start, the chat frame's request to embed.lotsagent.com/api/v1/embed/sessions answers with a code, and for sign-in problems a detail. To see them, open your browser's developer tools, then Network, and find the sessions request.
| code | HTTP | Meaning |
|---|---|---|
not_found | 404 | No agent with this id, or its Embed page was never saved. |
disabled | 403 | The agent is switched off. |
origin_not_allowed | 403 | This site is not in Allowed sites. |
identity_required | 401 or 400 | No token was sent, and Anyone can chat is off. |
identity_invalid | 401 | The token failed a check. See detail below. |
identity_replayed | 401 | This token's jti was already used. |
identity_mismatch | 401 | A fresh token names a different person than the open chat. |
session_not_found | 404 | The chat being resumed no longer exists. |
rate_limited | 429 | Too many chats are starting right now. Try again in a few minutes. |
unavailable | 503 | The chat could not start. Often the owner is out of credits. |
| detail (with identity_invalid) | Fix |
|---|---|
no_issuer | Save an issuer on the Embed page. |
malformed | The body of your token URL must be the token alone: a compact JWT, nothing else. |
too_large | Keep the token under 8 KB. Drop large claims. |
bad_alg | Use HS256 with an Embed page key, or ES256, RS256 or EdDSA with a saved JWKS URL. |
unknown_kid | Set kid to an active key id from the Embed page, or a kid in your JWKS. |
bad_signature | Sign with the secret that belongs to that kid, exactly as shown. |
wrong_issuer | Make iss exactly equal to the saved issuer. Watch for a trailing slash. |
wrong_audience | Set aud to lotsagent:AGENT_ID, with the agent id, not the handle. |
expired | Sign a new token for each request. Check your server's clock. |
not_yet_valid | Your server's clock is ahead. Fix the clock. |
too_long_lived | Keep exp at most 600 seconds after iat. |
bad_claims | A claim is missing or too long: sub, jti, iat, exp, or workspace.id. |
jwks_unavailable | LotsAgent could not read your JWKS URL. Check it answers over https. |
| code | The agent replies |
|---|---|
limit_reached | You have used today's 50 messages. They reset at midnight. |
spend_capped | This agent is resting until tomorrow. |
disabled | This agent is resting. Try again later. |
anonymous_limit | This agent has answered as many visitors as it can today. Sign in, or try again tomorrow. |
unavailable | Something went wrong. Try again in a minute. |
| The sidebar shows | Why | Fix |
|---|---|---|
| Connecting... | The chat is waiting for your token URL or for LotsAgent. | If it stays, check your token URL answers quickly. |
| Open this agent from the app it belongs to. | The chat address was opened on its own, not inside your page. | Use the script tag. The chat only runs inside your page. |
| This agent is not set up for this site. | Your page's origin is not in Allowed sites. | Add the exact origin. http://localhost needs Test mode. |
| This agent is resting. Try again later. | The agent is switched off. | Turn on Show the agent on your site. |
| You are signed out of the app. Sign in again to keep chatting. | Your token URL answered with a status that is not 2xx, or getToken threw. | Expected when signed out. Otherwise check the token URL answers 200. |
| Sign in to the app to use this agent. | No token was sent, and Anyone can chat is off. | Set token-url (same origin), or the Token URL on the Embed page. Make sure it returns a token. |
| Your sign-in could not be checked. Reload the page. | The token failed a check, or its jti was used before. | Read detail in the sessions response (section 8). Sign a new token for every request. |
| This agent is not available here. | Wrong agent id, or the Embed page was never saved. | Copy the id from the Embed page. |
| This agent is not available right now. | The chat could not start: the owner is out of credits, the model cannot serve, too many starts in 10 minutes, or a network error. | Check your credits. Then wait a minute and click Try again. |
| You are signed in as someone else now. Reload the page. | A fresh token names a different person than the open chat. | Reload the page. A new chat starts for the new person. |
| That chat has ended. | The chat being resumed was deleted. | Click Try again. A new chat starts. |
| The notice changed. Reload to read it. | The owner changed the notice while it was open. | Reload the page. |
| Bad request | The chat frame's request was changed or cut. | Reload the page. |
| The chat shows | Why | Fix |
|---|---|---|
| You have used today's 50 messages. They reset at midnight. | The daily message limit (your number). | Wait for midnight in your time zone, or raise the limit. |
| This agent is resting until tomorrow. | The daily spend cap was reached. | Raise the cap, or wait for the day to turn. |
| This agent is resting. Try again later. | The agent was switched off. | Turn it back on. |
| Something went wrong. Try again in a minute. | LotsAgent could not check the limits, so the message was refused. | Send it again in a minute. |
| The model could not complete this turn. | The reply failed partway. | Send the message again. |
| This reply didn't finish loading. Reload the conversation. | The reply could not be shown. | Reload the page. |
| A red Test badge | Test mode is on. | Turn off Test mode before you go live. |
When one of your tools fails, the agent tells the person in its own words, based on the text in the tool error table (section 8).
| The card shows | Why | Fix |
|---|---|---|
| Done. | Confirmed, and it ran. | Nothing to fix. |
| Cancelled. | The person clicked Cancel. | Nothing to fix. |
| Working... | Confirmed, and it is running. | Wait. |
| It did not work. | Confirmed, but your tool failed. The reason shows under it. | See the tool error table in section 8. |
| This request expired. Ask again. | Nobody answered in 15 minutes. | Ask the agent again. A new card appears. |
| This request has expired. Ask again. | Confirm or Cancel was clicked after 15 minutes. | Ask the agent again. |
| This request was already answered. | It was confirmed or cancelled already, for example in another tab. | Nothing to fix. Nothing ran twice. |
| Already answered. | Same as above. | Nothing to fix. |
| This request is not available. | The card belongs to another chat, or was deleted. | Ask the agent again. |
launcher="none" is set, and your page has no trigger.agent attribute does not match the agent id in the script URL.The chat frame only lets your allowed sites frame it. Add your page's origin to Allowed sites, check the agent is on, and wait up to 30 seconds.