# The agent marks

> One set of agent marks across every Kismet site, beside share and save. Muse, ChatGPT and Instinct by their own marks, each a real link and a one-click line to paste, from the same served list kismet.travel and the Kismet fixtures use, with AgentMarks and useAgentMarks from @kismet-tech/sdk/react.



A visitor who plans with an AI assistant should be able to hand it the home
or the collection they are looking at, from the page, in one click. Every
Kismet site prints the same three marks beside share and save for that:
Muse, ChatGPT and Instinct, each by its own mark. This guide is the SDK's
copy of what kismet.travel and the Kismet fixtures on WordPress sites
print, so a site built on the SDK looks and behaves the same.

**The division of labor: Kismet decides the agents, the site decides the
pixels.** Which agents are listed, in what order, where each mark links and
what a click copies is data the SDK compiles in and Kismet also serves, so
an agent added in Kismet reaches your site with no release beyond its mark.
The site owns the placement and the styling.

## What a visitor gets

- **A click copies a line to paste** into that agent: the home (on a
  property page) or the collection, the dates and party size when they are
  chosen, and the page the visitor is on. A small note says it was copied,
  and for ChatGPT offers the way in. If the clipboard refuses, the note
  shows the line to select by hand. A click with a modifier key is left to
  the browser.
- **Each mark is a real link.** Muse and Instinct link the brief the page's
  host names for them; ChatGPT links the Kismet plugin. An agent reading the
  markup in a browser takes the address. An agent that fetches the page
  names an icon link by its SVG title, which every mark carries, and the
  name is also text inside the link.
- **No mark links the guest MCP endpoint.** It answers POST only, and a link
  would send a fetching agent to a 405. The endpoint reaches an agent
  through its brief.

## Take it

```tsx
import { AgentMarks } from '@kismet-tech/sdk/react';

// A property page: "Book with" then the three marks.
<AgentMarks
  kind="property"
  collectionSlug="cascadia-getaways"
  collectionName="Cascadia Getaways"
  homeName={home.name}
  checkIn={stay.checkIn}
  checkOut={stay.checkOut}
  guests={stay.guests}
/>

// A results page or a group page: "Plan with" then the marks.
<AgentMarks kind="collection" collectionSlug="cascadia-getaways" collectionName="Cascadia Getaways" />
```

Place it where your share and save controls are. On a property page that
is the listing meta row; on a results or group page, the toolbar. The
marks are drawn in `currentColor`, so they take the text colour of wherever
you put them. On a phone beside share and save, pass `hideLabel` so the
marks stand alone and the words stay in the markup.

`kind` decides the words: `'property'` prints "Book with", `'collection'`
and `'group'` print "Plan with". The dates and guests you pass are only
written into the line that is copied and the served list's request; the
server HTML never names them, so a server render and the first client
render agree.

## Put it in your site: four ways, ranked by what an agent finds

An AI agent that fetches your page reads the server HTML and nothing else.
So the question for each way in is whether the marks are in the HTML your
server sends, or only in the DOM after script runs.

| Way in | In the server HTML | Kismet can add an agent | You write |
|---|---|---|---|
| `<AgentMarks>` in a React or Next page rendered on the server | yes | yes, after the visitor's first intent; the compiled-in three at first paint | one component |
| The served fragment, included by your server at render time | yes | yes, at every render | one include |
| `agentMarksHtml()` at build time | yes | at your next build | one call |
| Client-side mount (no server) | no | yes | one script |

Whatever you choose, keep the four facts an agent measures: every mark is an
`<a href>`; its name is in the markup as the SVG `<title>` and as text inside
the link; no address carries a session id or a quote token; nothing links the
MCP endpoint.

### 1. React or Next, rendered on the server

```tsx
import { AgentMarks } from '@kismet-tech/sdk/react';

export default function Listing({ home }) {
  return (
    <div className="listing-actions">
      <ShareButton /> <SaveButton />
      <AgentMarks kind="property" collectionSlug="cascadia-getaways" collectionName="Cascadia Getaways" homeName={home.name} />
    </div>
  );
}
```

It is a client component (it copies on click and reads the served list), imported
from `@kismet-tech/sdk/react`, never from the main entry: render it from a page or
a client component and Next renders its links on the server as usual, with no
effect and no fetch on that path. The served list is read in the
browser after the visitor's first pointer, focus or touch on the marks.

### 2. Any server: include the served fragment

The served list carries the same marks as one HTML fragment in its `html`
field. Fetch it on your server at render time, cache it for the 300 seconds
the response advertises, and print it where your share and save controls
are. No script, no stylesheet: the marks are `currentColor`, and the class
names (`kismet-agent-marks`, `kismet-agent-marks-label`, `kismet-agent-mark`,
`kismet-agent-mark-name`) are hooks for your CSS.

```
GET https://api.ksmt.app/v1/public/agent-surface/collection/agents
    ?slug=cascadia-getaways&kind=property&host=www.your-site.com
    &home=Riverbend%20Cabin
```

PHP:

```php
$url = 'https://api.ksmt.app/v1/public/agent-surface/collection/agents?' . http_build_query([
  'slug' => 'cascadia-getaways', 'kind' => 'property',
  'host' => $_SERVER['HTTP_HOST'], 'home' => $home->name,
]);
$answer = json_decode(file_get_contents($url), true); // cache this for 300 s
echo $answer['html'] ?? '';
```

Django (a template tag or the view):

```python
answer = requests.get(url, timeout=3).json()   # cache for 300 s
context["agent_marks"] = mark_safe(answer.get("html", ""))
```

Next.js route handler or server component, without the React component:

```ts
const answer = await fetch(url, { next: { revalidate: 300 } }).then((r) => r.json());
return <div dangerouslySetInnerHTML={{ __html: answer.html }} />;
```

Pass `host` so Muse and Instinct link the brief your host names; pass
`kind`, `home`, `checkIn`, `checkOut`, `guests` and `page` (origin and path
only) so the paste line on each mark's `data-paste` is written for the page.
If the read fails, print `agentMarksHtml(...)` from the SDK instead, or
nothing; never a hand-written brief address.

The fragment carries the paste line on `data-paste` but no click handler.
To copy on click, attach one to `.kismet-agent-mark` in your own script, or
leave the marks as links: each already opens the agent's brief or the
plugin.

### 3. Build time: `agentMarksHtml`

For a static site (Astro, Hugo, Eleventy, a Rails view with no request-time
fetch), the compiled-in list as a string, from `@kismet-tech/sdk/server`
(edge safe, no React):

```ts
import { agentMarksHtml } from '@kismet-tech/sdk/server';

const html = agentMarksHtml({
  kind: 'collection',
  collectionSlug: 'cascadia-getaways',
  collectionName: 'Cascadia Getaways',
  briefs, // optional: the briefs your host names, if your build already knows them
});
```

It is byte-compatible with the served `html`, so you can render it at build
time and swap in the served fragment at request time with no markup change.
`renderAgentMarksHtml({ label, kind, agents })` renders a served list you
already fetched. A new agent reaches a build-time site at its next build.

### 4. No server at all: mount in the browser

```tsx
import { createRoot } from 'react-dom/client';
import { AgentMarks } from '@kismet-tech/sdk/react';

createRoot(document.getElementById('agent-marks')!).render(
  <AgentMarks kind="collection" collectionSlug="cascadia-getaways" />,
);
```

This is the fallback. A person sees the marks; an agent that fetches the
page does not. Use it only where the page has no server, and say so in your
own notes.

## The briefs

Muse and Instinct connect through a **brief**: a short page an agent reads
to wire itself to the Kismet guest MCP. Which brief a host names, the
collection's own brand brief or the shared Kismet one, is decided by Kismet,
never by a site. With nothing handed in, the marks link the collection's
For AI agents page (`https://kismet.travel/c/<slug>/for-agents`), which
names the briefs, until the served list arrives with the exact ones.

If your server already knows the briefs for your host (a site that prints
them in its footer or `llms.txt`), hand them in so the server HTML carries
them from the first byte:

```tsx
<AgentMarks kind="property" collectionSlug="cascadia-getaways" briefs={{ muse: museBriefUrl, instinct: instinctBriefUrl }} />
```

Only `https://` addresses are accepted. Never write a brief address by
hand: ask [`GET /v1/public/agent-surface/connector?host=<your host>&agent=muse`](https://developers.kismet.travel/api.md)
(and `agent=instinct`) and print what it returns.

## The served list

On the visitor's first intent toward the marks (pointer, focus or touch),
never on the render path, the component reads

```
GET https://api.ksmt.app/v1/public/agent-surface/collection/agents?slug=<collection>&kind=<kind>&host=<your host>
```

and shows that list in its order, with the exact briefs the host names and
the line Kismet writes for the stay. The read is anonymous and public; it
carries no cookie. An agent in the served list that the installed SDK has
no mark for is left out until a release carries it. A served list that
breaks a rule (a mark that would link an MCP endpoint, an address that
carries a session id, a token or a marked endpoint) is refused whole, and
the compiled-in list stays.

Point `apiBaseUrl` at a different Kismet API base when you are told to, or
pass `served={false}` to show only the compiled-in agents.

## Restyle it

`className` styles the group, `markClassName` each mark's link, and
`noteClassName` the note. The note hangs from the marks' right edge by
default (`noteAlign="end"`, for marks at the right of a row such as beside
share and save); pass `noteAlign="start"` for marks at the left of a row,
such as a toolbar, so the note stays on a phone's screen. The note is drawn on `currentColor` with white
text; set `--kismet-agent-marks-note-text` on the group to change the
text colour.

## Build your own on `useAgentMarks`

```tsx
import { useAgentMarks, AgentMark } from '@kismet-tech/sdk/react';

function MyMarks() {
  const { label, agents, wantServed, hand, note } = useAgentMarks({
    kind: 'collection',
    collectionSlug: 'cascadia-getaways',
    collectionName: 'Cascadia Getaways',
  });
  return (
    <div onPointerEnter={wantServed} onFocus={wantServed}>
      <span>{label}</span>
      {agents.map((agent) => (
        <a key={agent.id} href={agent.href} title={`${label} ${agent.name}`} onClick={(e) => { e.preventDefault(); void hand(agent.id); }}>
          <AgentMark id={agent.id} title={agent.name} />
          <span className="sr-only">{agent.name}</span>
        </a>
      ))}
      {note && <span role="status">{note.text}</span>}
    </div>
  );
}
```

Whatever you render, keep the four facts a reader measures: every mark is
an `<a href>`; its name is in the markup as the SVG `<title>` and as text
inside the link; no address carries a session id or a quote token; and
nothing links the MCP endpoint.

## The data functions

- `agentMarksList(input)`: the compiled-in list for a page, or `null` for a
  slug that is not shaped like one.
- `agentMarksRequest(input)`: the line in the visitor's voice.
- `acceptServedAgents(json)`: the served list reduced to what the SDK can
  render, or `null` when it breaks a rule.
- `servedAgentsUrl(apiBaseUrl, input, host)`: the address of the served list.
- `agentMarksHtml(input)` and `renderAgentMarksHtml({ label, kind, agents })`: the marks as an HTML string.

All of these are pure and exported from `@kismet-tech/sdk/react` and, without the React component, from `@kismet-tech/sdk/server`.
