Embedding the Chat
You can put a loop’s chat on your own web page with one <iframe>. The loop must be published,
and its record must declare the embed delivery and the sites the chat may load on. Your
visitors never sign in to plutonium.io, and they never see a price, a model or a word about the
platform — only the conversation.
1. Add your site
On Loops, open the loop and its On your site tab. Under Add a site, type your site’s
address, such as https://www.example.com, and press Add and review. This saves a waiting
change and opens the Publish card, which names the site the change adds and checks the change
against the loop’s test questions first: “Each is a real conversation and costs a few cents from
your wallet.” Press publish to make it serve. If you leave the card, the loop shows Change
waiting until you publish or discard it. To put the chat on another site, add that one the same
way.
The tab lists every site the chat can load on. Remove… first asks “Take it off? The chat stops working on that site once you publish.”, with Take it off and review and Keep it; taking it off opens the Publish card too. A loop that embeds the chat needs at least one site, so the last one has no Remove…: add another first.
A site is its https:// address without a path: type a page’s address and the tab adds the site
it is on. https://www.example.com and https://example.com are two sites, so add both if your
site answers on both. The frame tells plutonium.io which page it is on. On a site the loop does
not list, the chat says it is not set up for that page, and no run starts.
The list is the loop’s delivery.origins, so you can also write it yourself with Change it
yourself:
delivery:
kinds: [embed]
origins: [https://www.example.com]
api_key_scopes: []
2. Copy the snippet
On the same tab, under Paste this into your site, press Copy. The snippet has this shape:
<iframe
src="https://plutonium.io/embed?loop=pl_…"
style="width:100%;height:520px;border:0"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
title="Your loop's name"></iframe>
The same snippet works on every site the loop declares. Keep all five sandbox flags:
allow-scripts runs the chat, allow-forms lets the visitor send, and allow-same-origin lets
the frame load its own code. allow-popups lets a search result or a link in an answer open in
its own tab, and allow-popups-to-escape-sandbox lets that page work normally there. Without
them, a click on a link does nothing. The snippet does not grant allow-top-navigation, so the
frame cannot move your page to another address. The frame stays on plutonium.io, a different
origin from your site, so it reaches nothing on your page: not its content, its cookies or its
storage.
The tab shows the snippet only for a published loop that declares embed and at least one site.
A search box
If the loop uses a search index, the loop’s On your site tab asks What the frame shows:
The chat; The chat, with a search box over what this loop searches, whose snippet address
ends in &search=1 and whose frame shows a search box above the chat; or Search only, with no
chat, whose address ends in &search=only and whose frame shows the search box alone. Choose
before you copy. A visitor’s search never reaches a collection index; see
Letting a Loop Search.
Your own accent colour
Under Accent colour, pick the colour of the frame’s buttons and links, for this snippet only.
Leave it empty for the platform’s. The colour must read in both the light and the dark theme:
at least 3:1 against the page in each, with white or dark text that reads on it at 4.5:1. A
colour that fails is refused in words, and the code keeps the platform’s colour. A colour that
passes is added to the address as &accent=%23…, and the frame works out the shade it uses for
links. The frame checks the colour again when it loads, so an address edited by hand to a
colour that fails shows the platform’s colour; your browser’s console says why.
Light or dark
By default the frame follows each visitor’s own light or dark setting, so a visitor whose
device is set to dark sees a dark chat on any site. If your site is always light or always
dark, choose Always light or Always dark under Light or dark. The choice is added to
the address as &theme=light or &theme=dark, and the frame keeps that theme for every
visitor.
3. Decide who the visitor is
The loop’s identity decides whether the chat knows who your visitor is.
Anonymous visitors
With identity.kind: anonymous, each browser gets its own identity and nobody is asked to sign
in. The snippet alone is enough.
Users your site signs in
With identity.kind: signed_assertion, your server vouches for the user. The frame asks your
page for an assertion, and your page answers. The tab shows the listener to paste beside the
snippet:
<script>
window.addEventListener('message', function (e) {
if (e.origin !== 'https://plutonium.io') return;
if (e.data && e.data.type === 'pio:need-assertion') {
// Mint a fresh assertion for the signed-in visitor — each is single-use.
mintAssertion().then(function (assertion) {
e.source.postMessage({ type: 'pio:assertion', assertion: assertion },
'https://plutonium.io');
});
}
});
</script>
mintAssertion is yours: it asks your own server to sign a JWT for the user with the
loop’s secret. Its sub becomes the user’s id in every run.
An owner finds the secret on the loop’s On your site tab, under For a server of yours: keys and secrets, then Signing secret, once the loop is published: press Show the secret, then Copy the secret. Keep it on your server: anyone who holds it can sign in as any of your visitors. Identity and Keys says how to rotate it.
Mint a new assertion each time the frame asks, because each one can be used only once and the frame asks again when it needs another. Identity in the API reference describes the assertion.
4. Let the loop act on your page
A loop can declare client tools: actions that your own page performs for the user, such as opening a page of your app or turning on a setting. When the loop declares any, the tab shows a second listener with one empty handler per tool. Fill in each handler:
- A handler runs with the signed-in user’s own authority on your site. Check what it does exactly as if the user had clicked.
- What a handler returns, the loop reads as untrusted.
- A tool your page has no handler for, or a handler that throws, tells the loop that this page does not handle it.
Before the loop acts on your page, the chat shows the user a card that describes the action
and its details, and the user approves or declines. Each detail is labelled with its property’s
title in the tool’s input schema, so give every property one the user can read; a property
with no title is labelled with its name. A tool that only moves the page to
another of your pages shows no card, unless the loop’s record asks for one. If the user
declines, the loop reads that as the answer and the conversation goes on. The user can also
write a note on the card: it travels with Decline, or on its own with Reply, which
answers neither way. The page is not asked to act, and the assistant reads the note and may ask
again.
Keep the chat on topic
Every message a visitor sends starts a run that your budget pays for, and a chat with no
limits answers anything it is asked. Declare on_topic in the loop’s record to say which
messages it is for:
on_topic:
about: questions about booking a class at our studio - classes, times, prices, what to bring and getting here
reply: I can only help with booking a class at our studio.
refuses: true
about is one sentence that names everything the loop answers, and reply is what a visitor
reads when their message is not. A question about something about leaves out reads as off
topic, so name the whole scope: “booking a class” alone would leave out parking and prices.
LoopAssistant writes both for every loop that a stranger can reach, and publish refuses either
one when it is blank. The cards you approve a version on show both,
under What it talks about. Visitors never read about: it is for the topic check below and
for you on those cards, and the empty chat says only Say something to start.
While the loop answers a message, the topic check reads the message and the last few turns of
the conversation, and decides whether the message is within about. The check runs
beside the answer, never before it, so it adds at most a moment before the first words. Short
follow-ups such as “thanks” or “and then?” count as on topic. Each check’s small cost is part
of what the run spent.
With refuses: true, a visitor whose message is off topic reads reply and never sees the
loop’s answer: the loop holds its first words until the check is done, shows reply the moment
it is (the time under it counts to that moment), and runs none of the answer’s actions. The run spends the check and that one answer’s
model call, and the visitor can type again. Their next message is checked on its own, and the loop
answers it without turning back to the refused one. A refused visitor reads nothing but reply, so
put a way to reach you in it, such as a phone number or an email address. Without refuses, the check only records what it decided, and every message is
still answered. Start there to see what the check decides for your visitors’ real messages,
and turn refuses on when its decisions look right. If the check itself fails, the message is answered.
What your visitor sees
- The conversation, with formatted answers, and a short line such as “Thinking” or “Searching the docs” with three moving dots while the loop works. From the first second the line also shows how long the turn has taken: “Searching the docs · 4 s”.
- If the loop declares
chat: {show_activity: true}, each step the loop takes, in words (“Searched the docs”), as it takes it; when the answer arrives, the steps fold into one line under it that the visitor can open. A loop that does not declare it shows none of this, because a step says what the loop does inside and that is your decision. A step is only ever a tool’s own words (a custom tool’sworking_label), never the model’s text: what the model writes is its answer. - Links in an answer, which open in a new tab. A link to another site shows that site’s address beside it, so the visitor sees where it goes. A link with no web address, such as an uploaded file’s name, shows as its words: nothing on the visitor’s page could open it.
- When the loop searched or read a web page, a Sources line under the answer, linking to each
page the search found and each page the loop read with
web_fetch(the address it ended at, after any redirect). A link the model wrote is kept only when it points to one of those pages; any other, including any link in a run that found no page, shows as its words alone, because a model can write an address that does not exist. To give a fixed link, such as a booking page, put it on your page or say it in words. - Answers that never say how the loop works: no tool, collection, page it fetches, update or schedule, and never that one has not run yet. A loop whose sources have not been read yet answers from the rest of its instructions, or says it does not know. Citing a page it read is allowed.
- Any card the loop asks them to answer: a form, a choice, an email check or an approval of their own. It shows in the conversation, under the message that asked for it. The action the card asks for is the filled button; the other choice is outlined. The visitor can always refuse: Don’t send on a card that asks for details they type, Skip on a card of choices. One line under the buttons says what each does, and one says who reads what they type. After the visitor sends a form, the conversation shows their turn as “Sent.” A form of one or two details names them instead (“Sent: Work email”). It never shows what they typed. An approval your team decides, such as a refund, never reaches the visitor; it waits for your team in the console.
- One box to write in. Enter sends; Shift+Enter starts a new line. While the loop works, a Stop button takes the place of Send, so a visitor can end a run that goes wrong. Enter still sends while the loop works: when the loop then waits for the visitor, what they sent is their answer, in the order they sent it, with nothing to send again. When the loop waits for the visitor’s own answer, Send is back. Send is also back when a member of your team takes the conversation over and writes to the visitor, so the visitor can answer them.
- New conversation, at all times once the visitor has said something. While the loop is still working or waiting for the visitor, it asks first, because it stops that run; the stopped run stays in Runs. When the job is done, the loop’s own closing line and New conversation in place of the box to write in.
- If the visitor reloads the page, the chat comes back as it was, in the same tab, with each answer’s folded steps when the loop shows them. A visitor never sees how long an answer took. A new tab, or New conversation, starts fresh.
- When the frame cannot hold the chat, for example because the snippet names no loop, the frame says why in its middle instead of showing an empty chat. Opened on its own, it says the chat lives on a website and to open that website.
Nothing about cost, budgets, models or tools reaches the visitor.
The React SDK
A loop can also declare the sdk delivery, for a React application that you build and render
yourself. The package, embed-sdk-pio, is not published yet, so there is no install line
to give you. The tab says the same, and will carry the line when there is one.
Never put a key in a page
An API key (rk_…) is for your server. A key in a page can be read by anyone who opens it, and
it spends your account’s money. The embed and the SDK never carry a key: they get the visitor’s
own credential through the loop’s public id.