# Answers

> A shop's search tells an agent what it understood, why each product qualifies, and what it turned away.

Source: https://storeware.ai/docs/agents/answers

A Storeware shop answers a question rather than returning a list. A search says what it understood the shopper to want, which of their constraints each product meets and on what evidence, what it found and turned away, and how sure it is.

An agent handed that can correct a misreading in one turn, cite the merchant rather than assert on their behalf, and say "the cheapest waterproof pair is £30 over your budget, shall I show you it" instead of "nothing matched".

## Where to get it

| Surface | What arrives |
| --- | --- |
| `search_products` on [`/mcp`](https://storeware.ai/docs/agents/mcp) | Everything, inline: `verdicts` on each result, the rest beside them. |
| `POST /api/ucp/v1/catalog/search` | `messages[]` always. The structured object under `ai.storeware.shopping.answer` when your profile declares it. |
| `search_catalog` on `/ucp/mcp` | `messages[]`. The structured object is REST only, because the MCP binding's shapes come from the protocol's own method list. |

## Over MCP

Nothing to negotiate. Call `search_products` and read the extra fields, or ignore them.

```jsonc
{
  "understood": {
    "query": "waterproof walking boots",
    "max_price_minor": 15000,
    "min_price_minor": null,
    "required_attributes": [{ "key": "waterproof", "value": "true" }],
    "context": ["for a wet week in Iceland"]
  },
  "degraded": false,
  "as_of": "2026-08-23T14:31:02.118Z",
  "currency": "GBP",
  "ruled_out": [
    {
      "handle": "alpine-summit-gtx",
      "title": "Alpine Summit GTX",
      "price_minor": 18000,
      "verdicts": [
        { "constraint": "max_price", "outcome": "failed", "wanted": 15000, "found": 18000, "source": "live_price" }
      ]
    }
  ],
  "ruled_out_count": 4,
  "cheapest_ruled_out_minor": 18000,
  "unrelated_count": 0,
  "relevance_judged": true,
  "passes": 1,
  "results": [
    {
      "handle": "fjell-trail-gtx",
      "price_minor": 12000,
      "verdicts": [
        { "constraint": "attribute", "outcome": "satisfied", "key": "waterproof", "wanted": "true", "found": ["true"], "source": "enrichment" }
      ]
    }
  ]
}
```

On a host that draws MCP Apps, `search_products` and `get_product` also name a view of the answer in `_meta.ui.resourceUri`, and under `_meta["openai/outputTemplate"]` for ChatGPT. The view draws only what the tool result held and fetches nothing. A host that reads neither key gets the same tools and results.

## Over UCP

Declare `ai.storeware.shopping.answer` in your own profile, at the version the shop publishes, and the structured object arrives under that key. The shop declares it in its profile at `/.well-known/ucp`, with the version and the address of its JSON Schema. The version moves only when the shape does, and each move so far has added to it.

Without it, the response-level facts still arrive as `messages[]` on the base search response:

```jsonc
{
  "messages": [
    {
      "type": "info",
      "code": "ai.storeware.understood",
      "content": "Read as \"waterproof walking boots\", filtered by at most £150.00, waterproof=true."
    },
    {
      "type": "info",
      "code": "ai.storeware.ruled_out",
      "content": "4 further products matched the description but failed a stated constraint. The cheapest of them is £180.00."
    }
  ]
}
```

## Reading the fields

| Field | What it says |
| --- | --- |
| `understood` | How the shop read the question: the query, price bounds, required attributes, and context. |
| `understood.remembered` | Present only when the shop applied a constraint this message did not state. Each entry has a `scope`: `conversation` for earlier in this conversation, or `standing` for something a named shopper told the shop before. |
| `results` | The products that meet every stated constraint, each with its `verdicts`. |
| `ruled_out`, `ruled_out_count` | Products the shop has that a stated constraint turned away, with the reason. |
| `cheapest_ruled_out_minor` | The price of the nearest of them. |
| `unrelated_count` | Products the search found that were not the kind of thing asked for. |
| `relevance_judged` | False when the shop could not judge relevance this time, and then `unrelated_count` is zero because nothing was assessed. |
| `passes` | How many searches the answer is made of. Above 1, the shop read the question as asking for more than one thing. |
| `degraded` | True when the shop could not read constraints from the question, so the filters in `understood` were not applied. Ask again rather than trusting them. |
| `as_of`, `currency` | When prices and stock were read, and the currency they are in. |

What the shopper says now always beats this conversation, which beats what they told the shop before. A remembered constraint only fills a gap. A shopper can remove a standing one from their account page at the shop, so say so when one narrowed their answer. Standing memory is off unless a merchant turns it on.

### The shop does not stock it

`ruled_out_count` and `unrelated_count` tell two different stories:

- **`results` empty, `ruled_out_count` above zero:** the shop has something close. Ask one question: "the cheapest is £30 over your budget, shall I show you it".
- **`results` empty, `unrelated_count` above zero:** the shop does not sell anything of the kind. Say so plainly and stop asking this shop. Over UCP this also arrives as a message with code `ai.storeware.not_stocked`.
- **Both zero:** the search found nothing at all.

### Two things at once

When `passes` is above 1, `results` mixes products from each search. A budget stated for the whole request is held per item, because the shop cannot know how the shopper would split it, so ask before building a basket that has to fit one total. A requirement applies to what it was said of: in "a waterproof jacket and warm gloves", only the jacket is held to waterproof.

## Reading a verdict

`outcome` is `satisfied`, `failed` or `not_assessed`. `not_assessed` means nobody established whether the product has the property, which is a different answer from no. The product is still left out, because it cannot be shown to meet the requirement.

`source` says where the answer came from:

| `source` | Means |
| --- | --- |
| `live_price` | Read from the shop's database at `as_of`, not from a search index. |
| `inventory` | Stock across variants and locations, read live. |
| `enrichment` | The merchant's recorded knowledge about the product. |
| `enrichment_image` | The same, read off a product photograph. Only what a photograph can show, such as colour, pattern or shape, and never a claim, a measurement or a certification. Confirm it rather than assert it. |
| `unenriched` | Nothing has been recorded about this product. Always `not_assessed`. |
| `not_recorded` | The shop has recorded facts about this product, and this one is not among them. Always `not_assessed`. |

A `not_assessed` verdict that carries a `found` value means the shop recorded something less specific than you asked for: you asked for `rust orange` and it holds `rust`. "The shop records this one as rust, is that the colour you meant" is the turn that gets an answer.

Money is always in minor units of `currency`, never formatted and never a float. `15000` in GBP is £150.00; `15000` in JPY is ¥15,000.

## Verifying what you were told

Every response from the UCP REST surface and the three MCP servers is signed with RFC 9421 HTTP Message Signatures, ES256:

```text
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@status" "@authority" "@path" "content-digest" "content-type");keyid="..."
Signature: sig1=:MEUCIQD...:
```

Resolve `keyid` against the `keys` in the shop's `/.well-known/ucp` profile. The signature covers the endpoint as well as the body, so an answer from one endpoint cannot be replayed as another's. The digest is over the exact bytes sent: hash the body before you parse it. A response that arrives unsigned should be treated as unsigned.

Prices and stock are never read from the search index: every figure is read live at `as_of`. And an answer can only name products the search actually returned.
