# Agent-owned inference trial

Try the agent-embed assistant on your own dummy site with no signup, email, card or model API key.
You, the coding agent, are the model: each assistant turn the page needs is queued here, you read its
prompt and post the reply. This service does not call or bill a model for trial turns.

Limits: one trial per anonymous identity, one page origin plus up to 4 API origins,
24 hours, 100 turns in total, one outstanding turn at a time, 240 s per turn,
20 turns per browser run. Use dummy data only: page controls, messages and data the assistant reads
are sent to you through this service.

## 1. Read the public config

```sh
curl -s https://api.bunkhouse.ai/v1/trials/config
# {"authUrl":"https://<project>.supabase.co/auth/v1","publishableKey":"sb_publishable_...",
#  "embedOrigin":"https://...","lifetimeSeconds":86400,"turnLimit":100,"pollAfterMs":2000}
```

A 503 with code `agent_unavailable` ("Agent trials are disabled.") means this deployment has trials
switched off; nothing below will work there.

## 2. Create a native anonymous identity

```sh
AUTH_URL=...        # config.authUrl
PK=sb_publishable_... # config.publishableKey
curl -s -X POST "$AUTH_URL/signup" -H "apikey: $PK" -H "Content-Type: application/json" -d '{"data":{}}'
# {"access_token":"...","refresh_token":"...","expires_in":3600,"user":{"id":"...","is_anonymous":true,...}}
```

No email, password or existing provider credential is involved. Keep `access_token` and
`refresh_token` only in your tool process or a temporary file readable by you alone (`chmod 600`);
never put them in page JavaScript, URLs, logs, screenshots or commits. They are your trial's only
credential. Refresh before `expires_in` runs out:

```sh
curl -s -X POST "$AUTH_URL/token?grant_type=refresh_token" -H "apikey: $PK" -H "Content-Type: application/json" \
  -d '{"refresh_token":"..."}'
```

## 3. Create the trial

```sh
TOKEN=...  # access_token
curl -s -X POST https://api.bunkhouse.ai/v1/trials -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"origin":"http://localhost:3000"}'
# 201 {"trial":{"id":"...","siteId":"...","publicKey":"pk_...","origin":"http://localhost:3000","apiOrigins":[],
#      "hintsPath":null,"expiresAt":"...","revokedAt":null,"turnLimit":100,"turnsUsed":0,"turnsRemaining":100},
#      "embedSnippet":"<script src=\"https://.../embed.js\" data-public-key=\"pk_...\"></script>"}
```

- Optional fields: `"apiOrigins": ["http://localhost:8080"]` (at most 4) for the app's own API, and
  `"hintsPath": "/agent-hints.json"` (same-origin path).
- Origins are exact: HTTPS, or HTTP only for `localhost`, `127.0.0.1` and `[::1]`. No paths, wildcards,
  LAN addresses or `file://`.
- Repeating the identical request returns the same trial (200); different settings return 409. After a
  hints PATCH (below), the original POST body differs and also returns 409.
- Never send an `Origin` header to `/v1/trials*`: browser requests are refused.

## 4. Install the snippet on a dummy page

- Put `embedSnippet` in the page's `<head>` before the app's entry script.
- Serve the page over HTTP(S) at exactly the registered origin, not from `file://`. `localhost` and
  `127.0.0.1` are different origins, and so are different ports.
- If the page sets a Content-Security-Policy: `script-src` and `frame-src` must allow the embed origin,
  `connect-src` must allow https://api.bunkhouse.ai.
- Use a page of your own with disposable data (for example a static list of products with an add-to-list
  form). The hosted demo storefront has no backend API.

## 5. Serve one task (single agent, no deadlock)

1. Open the page in a browser you control, open the assistant, read the disclosure and click
   "Accept and continue".
2. Type a message and send it. Do not wait for the reply: the reply is you.
3. Fetch the pending turn:
   ```sh
   curl -s -i https://api.bunkhouse.ai/v1/trials/current/turn -H "Authorization: Bearer $TOKEN"
   ```
   - `204` with `Retry-After: 2`: nothing pending yet; try again.
   - `200`: `{"turnId","requestId","runId","deadlineAt","purpose","prompt":{"messages":[...],"tools":[...]},"maxCompletionTokens":4096}`.
     `prompt` is the exact prompt a hosted model would receive. For `"purpose":"chat"`: system prompt with
     the capability declarations, page summary, transcript and any tool results.
   - `"purpose":"discovery"` comes once, before the first chat turn of a page's first request: a one-off,
     tool-less review of the page evidence (`"tools":[]`). Answer it with the JSON object the system prompt
     describes as `content` and `"toolCalls":[]`, e.g.
     `{"status":"completed","content":"{\"version\":1,\"skills\":[],\"hypotheses\":[]}","toolCalls":[]}`.
     Never answer it with a codemode call; the chat turn is queued only after this one ends.
4. Answer before `deadlineAt`:
   ```sh
   curl -s -X POST https://api.bunkhouse.ai/v1/trials/current/turns/$TURN_ID -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"status":"completed","content":"","toolCalls":[{"id":"call-1","name":"codemode","arguments":{"code":"return await tools.inspect({ query: \"Products\" });"}}]}'
   # {"accepted":true}
   ```
   - `toolCalls`: at most 8, unique ids, name `codemode`, `code` at most 32 KiB of plain async JavaScript.
   - `content`: at most 64 Ki characters. A completed turn needs content or at least one tool call.
   - `{"status":"failed","message":"..."}` ends the turn with an error in the page.
   - Body at most 256 KiB; no other fields (no usage, credentials or callback URLs).
   - Repeating the identical body is accepted again; a different body, or answering after Stop, a
     disconnect or the deadline, returns 409.
5. The page runs your tool call in its sandbox. Writes (`tools.batch`) open an approval dialog in the
   page: approve only disposable fixture changes. The page then sends the tool results as the next turn;
   go back to step 3.
6. Reply with text only (`"toolCalls":[]`) to finish. Only now wait for the page to show "Done".

A background worker on your host may loop steps 3-4 with the same calls; there is no webhook or callback.

## Beyond the trial: what a production install is

The trial runs the same embed and sandbox as production; only the model source and the account differ.

- One script tag on your app. The embed discovers what the page can do (observed same-origin and allowed
  API requests, visible DOM controls, an optional same-origin hints file, WebMCP tools the page declares)
  and lets a hosted model drive it through Code Mode: model-written JavaScript in a sandboxed QuickJS
  worker that can only call the discovered capabilities. Writes always go through an approval dialog.
- Identity: signed mode (recommended) has your backend sign a short-lived HS256 identity JWT for the
  current app user with a per-site `ss_...` secret (`window.AgentEmbedConfig.getIdentityToken`); mint
  mode has your backend exchange a site secret key for a session instead. Anonymous mode is opt-in per site.
  App API calls the agent makes use the user's own browser session or a `getAuthHeaders` hook.
- Billing: a base subscription plus prepaid credit at per-token rates; every model call reserves the
  worst case against the balance first. Anonymous traffic has its own token and spend caps.
  Usage is reported per site and per app user.
- Owners manage sites, origins, keys, billing and an optional shared discovery cache in a dashboard.
  Trial identities cannot use any of that; a trial does not convert into an account.

## Status and revocation

```sh
curl -s https://api.bunkhouse.ai/v1/trials/current -H "Authorization: Bearer $TOKEN"            # status, also after expiry
curl -s -X DELETE https://api.bunkhouse.ai/v1/trials/current -H "Authorization: Bearer $TOKEN"  # revoke now (idempotent)
curl -s -X PATCH https://api.bunkhouse.ai/v1/trials/current -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"hintsPath":"/agent-hints.json"}'  # change hints path (or null); applies on the next page reload
```

Revocation ends the page's sessions and cancels a pending turn. Expired and revoked trials are deleted
about an hour later together with the anonymous identity.

## Errors

Every error is `{"error":{"code":"...","message":"...","requestId":"..."}}`.

| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Body, origin or schema rejected |
| 401 | unauthorized | Missing or expired token: refresh it |
| 403 | forbidden | An Origin header was sent, or the identity is not a native anonymous one |
| 403 | trial_exhausted | All 100 turns are used |
| 404 | not_found | No trial for this identity, or not your turn |
| 409 | conflict | Turn no longer pending, another turn outstanding, or different trial settings |
| 410 | trial_expired | The trial expired or was revoked |
| 429 | rate_limited | Wait for `Retry-After` seconds |
| 503 | agent_unavailable | Trials are disabled on this deployment |

In the page, a failed reply shows "Connected agent failed: ...", a missed deadline shows "The connected agent did not complete this turn before its deadline.",
and an expired or exhausted trial says so. The trial never asks for a login, card or subscription.

## After the trial

Nothing converts automatically. The paid plan is $49/month and includes $20 of usage credit each month;
hosted model usage is billed at $1 per million input tokens and $3 per million output tokens.

- Human guide: https://docs.bunkhouse.ai/guides/agent-trial/
- Billing terms: https://docs.bunkhouse.ai/guides/billing/
