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). 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.
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.
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 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 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=localgrounds through a model server you point it at. The default is ollama onhttp://localhost:11434— nothing leaves your machine.UNOTEST_GROUNDER_TRANSPORT=openai, withUNOTEST_GROUNDER_BASE_URLandUNOTEST_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
cd unotest && npx @unotest/web box picker oncd unotest && npx @unotest/web box picker offon asks the cloud to switch the Picker on for the box, mints the box’s
tokens the way 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 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:
cd unotest && npx @unotest/web picker oncd unotest && npx @unotest/web picker offpicker on 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_elementtool; - an intent locator inside
explore_steporfind_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 offfor 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.