> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentvault.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Page hints

> Point Agent Vault at a checkout total it cannot find, so the account holder's limits can be checked, and store an authenticator secret the agent never sees.

Point Agent Vault at a checkout total it cannot read, so a card fill that failed with `checkout_total_unknown` goes through. The same call stores a site's authenticator secret without the agent seeing it. Each takes about 5 minutes to add.

Before a card fill, the vault reads the total off the page to check the account holder's limits. It looks for names like `order-summary`, `total` or `cart-total`. On a shop that names things differently, it finds nothing:

* A card with spending limits is refused with `checkout_total_unknown`.
* A card with a `confirmAbove` threshold asks the account holder to approve an amount nobody can read.

A hint is a **location**, never a value.

## Before you start

* A proxy session on the checkout page, before your first token fill if you can.
* For a 2FA secret: the login's label from the token catalog, and the site's setup page open.

## Point at the total

<Steps>
  <Step title="Declare the hint when you reach the checkout page">
    ```ts theme={null}
    const r = await page.hint("order.total", { selector: ".summary__grand-total" });
    // { accepted: true, checked: true, resolved: true, amount: 49.99, currency: "USD" }
    ```

    Over MCP, `declare_page_hint` takes the same fields flat. Declare **before the first token fill** (see [what changes after the first fill](#what-changes-after-the-first-fill)).
  </Step>

  <Step title="Check that it resolved">
    `resolved: true` with an `amount` means a usable figure. `untrusted` says why a figure was refused. If `resolved` is false, point somewhere else before you fill. `accepted: false` means the hint was dropped, so declare it again. With `checked: false` there is no `resolved` to read: see below.
  </Step>

  <Step title="Declare again after every navigation">
    A hint describes the page you declared it on. A new page or a route change needs a new hint.
  </Step>

  <Step title="Fill the card">
    Continue with [the card fill order](/guides/first-checkout#the-card-fill-order). The vault uses your hint at fill time.
  </Step>
</Steps>

The vault can now check this checkout against the account holder's limits. A hint can resolve or raise a total, never lower one.

### What changes after the first fill

Once any token is filled in the session, the rules tighten. Selectors that compare an attribute **value** (in `selector` or `frame`) are no longer allowed:

* **Declared after the fill**, such a hint is refused with `invalid_hint` and nothing is recorded. You never get `{ accepted: true }` for it.
* **Declared before the fill**, such a hint stays on record, but the vault skips it when it reads the total at the card fill. It does not help the fill.

A hint that uses only ids, classes, element names or presence-only attribute tests (`[data-total]`) is still accepted after a fill. The reply is `{ accepted: true, checked: false }` with no amount. `checked: false` is not a rejection: the hint is kept and used at the card fill.

| Selector | Before any fill | After any fill |
| - | - | - |
| `#order-total`, `.summary__total`, `[data-total]` | Accepted and checked | Accepted, `checked: false` |
| `[data-testid="total"]`, `[attr*=value]` | Accepted and checked | Refused with `invalid_hint`. One declared before the fill is skipped at the card fill |

### Totals inside an iframe

Overlay checkouts (Paddle, Lemon Squeezy, embedded Stripe) show the total in a child frame the vault never reads. Name the frame:

```ts theme={null}
await page.hint("order.total", { selector: "#total", frame: "#paddle-checkout iframe" });
```

<Warning>
  Name the frame by id, class or a bare attribute. `iframe[src*='paddle.com']` compares a value. After any fill it is refused with `invalid_hint`, and one declared before the fill is skipped at the card fill. An overlay usually opens after a fill.
</Warning>

With `frame`, the vault reads the frame's whole text along with the element you name, so a larger figure beside it cannot hide. That read has a ceiling:

* A frame holding more than 20,000 characters of text is too big. The hint is dropped.
* The vault parses at most 16,384 characters for one look at a checkout: the page's own text plus the hint's text, the frame's whole text included. A hint that would take it past that is dropped too.
* The vault also reads every other frame on the page, so a cheaper frame cannot stand in for the real one. With more than 32 frames, a frame that does not answer, any frame over 20,000 characters, or more than 24,000 characters across them, the total is unknown.

A dropped hint answers `accepted: false`, even though your selector matched, and declaring it again gives the same answer. After a token fill the reply is still `{ accepted: true, checked: false }`, but the hint is dropped when the card fill reads the total. Either way the total stays unknown. Point at a total outside the frame (a hint without `frame`) if the page shows one. Otherwise a card with an approval threshold asks the account holder to approve the purchase, and a card with spending limits is refused with `checkout_total_unknown`.

### Reading an attribute

```ts theme={null}
await page.hint("order.total", {
  selector: "#order-total",
  read: "attribute",
  attribute: "aria-label",   // also: title, content, data-total, data-price, href
});
```

The attribute must name a currency: `aria-label="Total: $49.99"` resolves, `content="49.99"` does not. Without `frame`, the page must also show some amount as text, or the hint is dropped.

A selector should name the total, not a region. An `order.total` hint that matches more than 8 elements, or an element whose text or attribute is over 4,000 characters, adds nothing and answers `accepted: false`.

Selectors are plain CSS only. Playwright extensions (`text=`, `:has-text()`, `>>`, XPath) are refused with `invalid_hint`. A `selector` or `frame` may be at most 512 characters, counted after surrounding spaces are trimmed. A longer one is refused with `invalid_hint` and nothing is recorded. List current hint kinds with `page.hintKinds()` (MCP: `list_page_hint_kinds`).

A page holds at most 4 hints of each kind. A fifth distinct hint of the same kind on that page is refused with `invalid_hint`. These do not use a place:

* Declaring the same hint again, with the same kind, `selector`, `frame`, `read` and `attribute`.
* A malformed hint, which is refused before anything is recorded.
* A hint that was dropped (`accepted: false`), or a 2FA secret attempt that failed with `hint_unresolved`.

Any navigation of the page, a route change included, clears its hints and the count with them.

## Storing a 2FA secret

When the agent turns on authenticator two-factor for a site, it points at the `otpauth://` link the site shows, and the vault stores the secret.

<Steps>
  <Step title="Sign in as that login in this session">
    Fill the login's password (or its current code) first. Filling only its username does not count.
  </Step>

  <Step title="Reveal the setup key as a link">
    Use the site's "can't scan the QR code?" option so the `otpauth://` link is visible on the page.
  </Step>

  <Step title="Point at the link">
    ```ts theme={null}
    await page.hint(
      "login.totp_secret",
      { selector: ".otpauth-link", read: "attribute", attribute: "href" },
      { login: "github" },   // the label from the token catalog
    );
    // -> { stored: true, digits: 6, period: 30 }
    ```
  </Step>

  <Step title="Finish the setup on the site">
    Fetch the catalog again, and fill the login's new `totp` token into the site's verify field.

    ```ts theme={null}
    const login = (await listTokens()).logins.find((l) => l.label === "github");
    await page.fill("#verify-code", login.tokens.totp);
    ```

    <Warning>
      Do not skip this. An unconfirmed site has no two-factor, while the vault holds a secret it will not overwrite. The account holder then has to clean it up by hand.
    </Warning>
  </Step>
</Steps>

The login now has two-factor, and your agent can answer its authenticator step at every [sign-in](/guides/sign-in).

Three preconditions are checked first, before the page is read. Each fails with its own code and a message that says what to do:

* The page is served over `https://` (or `http://` on a loopback address, for a local demo). Otherwise you get `insecure_page`.
* This session has already filled a token. Otherwise you get `sensitive_data_blocked`.
* This session signed in as that login: its password (or two-factor code) token landed in a field. Otherwise you get `sensitive_data_blocked`.

Past those, a secret is stored only when all of these hold. Otherwise you get one `hint_unresolved`, without saying which failed:

* The account holder has only one login for this site, and it has no two-factor yet.
* The `otpauth://` link names an account matching the stored username exactly. `ada` does not match `ada@example.com`.
* Exactly one element matches, visible, on the top-level page. It is a link read through `href`, not a QR image or `<canvas>`.
* The page stays on the same site, still on `https://` (or loopback `http://`), while the vault reads it.

When a condition cannot be met, the account holder enrolls from the dashboard. See [TOTP](/security/totp).

## What the account holder sees

When the total cannot be read on a card with an approval threshold, they get a purchase approval with no amount, and a denial is reasonable. A hint that resolves the total removes that prompt when the amount is under the threshold.

<Frame caption="A purchase approval for a total that could not be read.">
  <img className="block dark:hidden" src="https://mintcdn.com/self-labs/VeDWRFlNWGO5jQN6/images/guides/approval-unknown-total-light.webp?fit=max&auto=format&n=VeDWRFlNWGO5jQN6&q=85&s=dbee967abc147cfdbafc84a99f6bc02d" alt="Approval page explaining that the agent card is trying to pay at a merchant site but the checkout total could not be established, with Approve and Deny buttons." width="2880" height="1800" data-path="images/guides/approval-unknown-total-light.webp" />

  <img className="hidden dark:block" src="https://mintcdn.com/self-labs/VeDWRFlNWGO5jQN6/images/guides/approval-unknown-total-dark.webp?fit=max&auto=format&n=VeDWRFlNWGO5jQN6&q=85&s=60fc2219a389fa404cf5350d26bc0a3d" alt="Approval page explaining that the agent card is trying to pay at a merchant site but the checkout total could not be established, with Approve and Deny buttons." width="2880" height="1800" data-path="images/guides/approval-unknown-total-dark.webp" />
</Frame>

When an agent stores a secret, Agent Vault tries to send the account holder a `totp_enrolled` notification. It is best effort: the secret stays stored even if the notification fails, so the login's **2FA** chip on **Logins** is the reliable record.

<Frame caption="The notification an account holder gets when an agent enrolls a secret.">
  <img className="block dark:hidden" src="https://mintcdn.com/self-labs/VeDWRFlNWGO5jQN6/images/guides/totp-enrolled-light.webp?fit=max&auto=format&n=VeDWRFlNWGO5jQN6&q=85&s=7e8b78bc5c3c7c9ce718526192356fab" alt="Activity entry stating that an agent added an authenticator secret to a named login." width="2368" height="426" data-path="images/guides/totp-enrolled-light.webp" />

  <img className="hidden dark:block" src="https://mintcdn.com/self-labs/VeDWRFlNWGO5jQN6/images/guides/totp-enrolled-dark.webp?fit=max&auto=format&n=VeDWRFlNWGO5jQN6&q=85&s=ba019123e6ee1abe0f70624d8fa8e2fe" alt="Activity entry stating that an agent added an authenticator secret to a named login." width="2368" height="426" data-path="images/guides/totp-enrolled-dark.webp" />
</Frame>

## If something goes wrong

| Symptom | Cause | Fix |
| - | - | - |
| `checkout_total_unknown` on a card fill | The vault could not read the total | Declare a hint (with `frame` for an overlay) and retry once |
| `limit_reached` | A limit really was reached | Shrink the cart or tell the account holder. A hint cannot lower a total |
| `accepted: false` | The page navigated while the vault read, the hint added nothing, or a framed hint's frame holds too much text (over 20,000 characters, or over 16,384 with the page's own text) | Declare again on the current page. A total still calculating is the usual cause. For a frame that is too big, point at a total outside it. See [Totals inside an iframe](#totals-inside-an-iframe) |
| `checked: false` | A token was already filled, so the vault will not say what it read | The hint is still used. Declare on arrival next time |
| `invalid_hint` | Malformed, not plain CSS, a `selector` or `frame` over 512 characters, a selector that compares an attribute value declared after a token fill, or a fifth distinct hint of one kind on the page | Fix what the message names. After a fill, use an id, class or `[attr]` presence test. Over the cap of 4, the hints already declared stay in use until the page navigates |
| `hint_unresolved` | A secret hint found nothing usable, or an opaque enrollment check failed | Recheck the conditions after the preconditions above |
| `insecure_page` | The 2FA setup page is not on `https://`, and is not a loopback `http://` page | Enroll only on the site's `https://` page |
| `hint_uncertain` | The write reached the vault and the reply was lost | **Do not retry**: a retry reads as a second failure. Ask the account holder to check **Logins** |
| `sensitive_data_blocked` | This session has filled no token yet, or has not signed in as the named login | Sign in with that login's tokens, then declare |

Full codes: [Error codes](/sdk/error-codes#page-hints).

**Next:** [Run a checkout end to end](/guides/first-checkout) to see where a hint fits in the full checkout.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.