# The Picker

> For the complete documentation index, see [llms.txt](/llms.txt)

An **intent locator** names what an element means rather than where it sits:
"the checkout button", not `.btn.btn-primary`. Resolving one takes a model
that reads the page — that is the **Picker**.

Without it your tests still run. Locators that name what they click —
`getByRole`, `getByTestId`, `getByLabel` — need no model at all, and they are
the ones you should be writing anyway (see [Stable
selectors](/concepts/selectors/)). The Picker is for the part before that:
your agent exploring a page it has never seen, and turning what it finds into
those locators.

The grounder a box gives you is the one **your MCP server** uses while your
agent writes tests. A suite running on the box is handed the same address and
token, but it resolves no intents of its own: a scenario carries the locator it
was written with, and grounding belongs to the side that writes the test.

Most of this page is the Picker **on a box**. An account can also have one of
its own, with no box rented at all — the same service, a different switch and
a different way of being charged: [Without a box](#without-a-box).

## Which model reads your page

Grounding takes two steps, and only the second one is what the mode is about.
First the page is turned into a structured capture — the visible text, the
roles and the accessible names of its elements — and indexed on hardware we
operate; nothing from that step leaves us, in either mode. Then a shortlist of
candidate elements is put to a model together with your intent phrase, and
the model picks between them.

That split is how the boxes we run are configured, not a property of the
software: the indexing model is ours in both modes and only the picker
changes. A grounder you run yourself can be pointed at a provider for both
steps, and then this promise is not the one that applies — see
[Your own endpoint](#your-own-endpoint-your-own-key).

There are two modes, and they are not the same deal:

| Mode | What it means |
|---|---|
| **Our GPU** | The model that reads the shortlist runs on hardware we operate as well. The shortlist reaches no third party. |
| **External provider (Gemini)** | The shortlist and your intent phrase go to Google's Gemini API — never the whole capture. We hold the key and pay the bill: you never bring one, and there is no on-prem grounder to install. |

The mode is how the service is configured, not a per-account setting and not a
switch in the dashboard. <!-- The mode a rented box gets is deliberately not
named here: naming it in prose makes this page wrong on the day it changes, and
the dashboard's Picker card is where a customer reads what applies to theirs. -->

### The straight answer

If your app draws secrets on the page itself — one-time codes, tokens, card
numbers — **Our GPU** is the mode to ask for.

Masking works by value: it hides a secret the run was told about, written the
same way. A value your application renders on its own, that the run never knew,
is text on the page like any other — and it reaches the model inside the
shortlist like any other. That is how grounding works, not a defect about to be
fixed, and it is the real reason the perimeter is worth paying for.

The [privacy notice](https://unotest.com/privacy) is the long version: what is
sent in the provider mode, what is masked before it goes, and what we keep.

## Your own endpoint, your own key

Everything above is the Picker **on a box**, where we run the grounder and
hold the key. `@unotest/web` also grounds locally, and there the backend is
yours:

- `UNOTEST_GROUNDER_MODE=local` grounds through a model server you point it
  at. The default is ollama on `http://localhost:11434` — nothing leaves your
  machine.
- `UNOTEST_GROUNDER_TRANSPORT=openai`, with `UNOTEST_GROUNDER_BASE_URL` and
  `UNOTEST_GROUNDER_API_KEY`, sends the same requests to any
  OpenAI-compatible endpoint using **your** key.

The second one is outside everything this page promises. We do not hold that
key, we never see those requests, and no ceiling of ours applies to them. Where
the page content goes, under whose terms it is processed, and who pays for it
are decided by the endpoint you named — read its terms before you point a
test suite at it.

## Turning it on and off

```sh
cd unotest && npx @unotest/web box picker on
cd unotest && npx @unotest/web box picker off
```

`on` asks the cloud to switch the Picker on for the box, mints the box's
tokens the way [`box access`](/reference/cli-web/#box-access) does, and writes
the Picker's three keys — `UNOTEST_GROUNDER_MODE=remote` and
`UNOTEST_GROUNDER_REMOTE_URL` into `unotest/.env`,
`UNOTEST_GROUNDER_REMOTE_TOKEN` into `unotest/.secrets`.

The server reads the token back from the same places, in this order: the
environment first (an `export`, a CI secret), then `unotest/.secrets`, then
`unotest/.env`. Nothing needs to be pasted into your editor's MCP
configuration — and nothing should be, since that file is committed.

The keys are read when the MCP server **starts**, so a Picker switched on
under a running editor changes nothing until that editor reconnects. When
grounding refuses, the message names every place the token was looked for:
an editor that was not restarted is the first thing to rule out.

`off` switches it back and clears those three again — but only when they name
**this** box's grounder. A project pointed at a grounder of its own, or at
another box's, keeps its settings and is told so.

Neither command re-creates the box or re-pushes the suite, and asking for the
state a box is already in is not an error.

[`box create`](/reference/cli-web/#box-create) asks the same question once, on
a terminal, quoting what the Picker costs for the size you are creating. `--picker`
answers yes up front; `--yes`, `--json` and a run with no terminal take the
safe answer, which is no. You can connect it later with `box picker on`.

## Without a box

The Picker is also an account's own, and that one needs no box. It is switched
on in the dashboard, on the **Picker** tab — the spend decision lives beside
the price, not in a terminal — and each project is connected with one command:

```sh
cd unotest && npx @unotest/web picker on
cd unotest && npx @unotest/web picker off
```

[`picker on`](/reference/cli-web/#picker) fetches this project's own access
and writes the same three keys `box picker on` writes: `UNOTEST_GROUNDER_MODE=remote` and
`UNOTEST_GROUNDER_REMOTE_URL` into `unotest/.env`,
`UNOTEST_GROUNDER_REMOTE_TOKEN` into `unotest/.secrets`, kept out of the
repository. The editor has to be restarted afterwards for the same reason as
above. Run it in a second project — a laptop, a CI job, another suite — and
that project gets access of its own; the dashboard lists them and can
disconnect any one of them alone.

`off` disconnects **this project** and clears those three again, and only when
they name the account's service: a project pointed at a model server of its
own keeps it and is told so. The Picker stays on for the account and the other
projects go on resolving intents. Access ends within the time it takes the
withdrawn grant to reach the service, not at once — the command says so. What
switches it off for everything, and stops it counting against the monthly
ceiling, is the switch in the dashboard.

There is **no hourly charge** for a Picker without a box. What bounds the
spend is the account's monthly ceiling, and `picker on` says what it is before
anything is spent.

So the two commands answer two different questions:

| Command | What it switches | What it costs |
|---|---|---|
| `box picker on\|off` | the Picker on one box | an hourly surcharge on that box |
| `picker on\|off` | nothing — it connects or disconnects one project | nothing per hour; the monthly ceiling is the limit |

The account's Picker is switched on and off in the dashboard, never by
`picker on|off`.

## What it costs

The Picker on a box has a **list price**: an hourly amount on top of the box rate, one
per box size, held in the cloud's own price list.

**Right now it is included in the box price.** Nothing is added to your hourly
rate, and the dashboard shows the list price struck through beside it. When
that ends, the list price is what starts being charged — and it will have been
on the page all along rather than appearing on the day.

This page does not repeat the numbers. A price copied into prose is a price
that goes stale: `box create` quotes what applies to the size you are
creating, and the dashboard shows it beside each size and on the box itself.

Whatever applies, it starts and stops at the next hourly tick and not at the
command: a started hour is charged at the rate it started with.

## The monthly ceiling

On **External provider (Gemini)**, the estimated spend on your account has
a monthly ceiling, so a runaway suite cannot turn into somebody else's
bill.

- At **80 %** you get an email, and the dashboard shows how much of the
  ceiling the month's estimate has reached.
- At **100 %** the grounder stops resolving intents.

What stops is **writing** tests, not running them — only the MCP server asks
the grounder:

- the `ground_element` tool;
- an intent locator inside `explore_step` or `find_element`.

They fail with **`grounder-budget-exhausted`** — HTTP **402**, never 401. The
token is perfectly good and re-minting it changes nothing: this is money, not
permission. The refusal quotes no amount and says nothing about any other
account.

What keeps working: your scenarios, their runs on the box, and the viewer. An
intent locator in a test file was resolved when the test was written and is a
plain locator from then on — a suite that is already written does not touch
the grounder at all.

There are two ways out:

- **top up the balance** to raise the ceiling, or
- **stop the Picker** — `cd unotest && npx @unotest/web box picker off` for a box's,
  the switch in the dashboard for an account's own. Either way the surcharge
  or the counting stops and intents ground locally again.

The ceiling turns over with the month either way.

The figure you see is an **estimate**, not an invoice: it is what we expect the
provider to charge for what your account used, rounded up. We show it as a
percentage of the ceiling rather than in euros, because it is not a number we
own — you would rightly check ours against theirs.
