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.

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’s working_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.