# Trust

Every kwery result ships with a **confidence score** — a machine-readable answer to the
question *"how much should I trust this data point?"*. You don't have to guess whether a
crawl landed on the right product, whether the offer list looks contaminated, or whether the
data is stale: the result tells you, per result, on every result, at no extra cost.

> **Rolling out.** Trust scoring is being enabled per source over the coming weeks, so the
`trust` object may not yet appear on your results. Build your pipeline to treat it as
**optional** for now — when it is absent, nothing else about the result changes, and no
action is needed on your side to start receiving it once your sources are live.


## Where the score appears 

The `trust` object is attached to **every** result, in every delivery channel:

- batch downloads (`GET /job/{id}/download`),
- stream webhook deliveries,
- polled stream results (`GET /stream/{id}/results`).


There is nothing to enable and no extra credits are consumed — trust scoring is part of
producing the result.

## The `trust` object 

```json
"trust": {
  "score": 0.81,
  "components": {
    "identity": 1.0,
    "completeness": 0.85,
    "price_integrity": 0.70,
    "offer_contamination": 0.90,
    "freshness": 0.55
  },
  "flagged_offers": [2, 5]
}
```

| Field | Meaning |
|  --- | --- |
| `score` | Overall confidence, **0** (don't trust) to **1** (fully trusted). A weighted average of the five components — the exact arithmetic is shown below. |
| `components` | The five sub-scores that make up `score`, each also 0–1. They tell you *why* a score is what it is. |
| `flagged_offers` | Indices (into the result's `offers[]` array) of offers flagged as price outliers. Empty when nothing looked suspicious. |


## The five components 

Each component measures one independent dimension of result quality.

### `identity` — is this the product you asked for? (weight 0.25)

High when the returned product's name and brand match what you requested — and, for Google
Shopping's structured lookups, when the match scorer agrees the selected product corresponds
to your GTIN + title + brand record. Low when the crawl landed on the wrong product, a
variant you didn't ask for, or the wrong marketplace locale.

**When it's low:** treat the whole result with suspicion — price data about the wrong
product is worse than no data. Re-submit with more identifying data (on Google, use
`key: product` with title and deeplink filled in — see
[Google Shopping](/sources/google#gtin-plus-data)).

### `completeness` — are the expected fields there? (weight 0.20)

Scored against the set of fields we expect for that source (each source page documents its
fields). Missing *critical* fields — price and offers — are penalised extra; missing
nice-to-have metadata (reviews, category names) costs less.

**When it's low:** the marketplace page may have changed layout or served a reduced
variant. The data present is still real; what's missing is missing, not zero.

### `price_integrity` — is the pricing believable? (weight 0.25)

Checks that the pricing is internally consistent (min ≤ avg ≤ max, offers clustered
sensibly) and in line with what we've seen historically for this product. Drops when the
offer set contains price outliers, or the average price jumps far from the previous
snapshot.

**When it's low:** look at `flagged_offers` first — usually one or two contaminated offers
(wrong variant, accessory listed as the product, currency glitch on the seller side) explain
it. A low `price_integrity` with *no* flagged offers means the whole price level moved —
which may be a genuine market move (a sale) rather than an error.

### `offer_contamination` — what fraction of the offers are clean? (weight 0.15)

Defined as `1 − (flagged offers / total offers)`. **1.0** means no offer was flagged; a
result with 10 offers and 2 flagged scores 0.8.

**When it's low:** the aggregates (`price_min` in particular) may be dragged by junk
offers. Recompute your own minimum over the non-flagged offers — the indices in
`flagged_offers` tell you exactly which to exclude.

### `freshness` — how recent is the data? (weight 0.15)

Highest for a just-completed crawl, decaying as the data ages. Results assembled with
cached data (within your `max_age` window) carry the age of the *original* observation —
`freshness` is honest about that recency. A cached "not found" also decays: the longer
since we last confirmed a product is absent, the less certain that answer is.

**When it's low:** you asked for (or accepted) cached data. If you need current data,
lower `max_age` on the request to force a fresh crawl.

## How `score` is computed 

`score` is the weighted average of the five components. The weights sum to 1.0, so the
score always lands in 0–1. For the example above:

```
score = identity·0.25 + completeness·0.20 + price_integrity·0.25
      + offer_contamination·0.15 + freshness·0.15

      = 1.0·0.25 + 0.85·0.20 + 0.70·0.25 + 0.90·0.15 + 0.55·0.15
      = 0.25 + 0.17 + 0.175 + 0.135 + 0.0825
      = 0.81
```

The weights are fixed per API version and published here — if we ever rebalance them, that
is an announced, versioned change.

## Using the score 

Treat `score` as a **routing signal**, not a hard gate:

| Score | Reading | Suggested handling |
|  --- | --- | --- |
| ≥ 0.8 | High confidence | Ingest directly. |
| 0.5 – 0.8 | Usable with caveats | Ingest, but spot-check high-value SKUs. Check which component drags the score. |
| < 0.5 | Low confidence | Review before trusting; consider re-crawling with `max_age: 0` or more identifying data. |


And when a specific component is the problem:

| Low component | Likely cause | What to do |
|  --- | --- | --- |
| `identity` | Wrong product matched | Re-submit with richer input (Google: `key: product`). Don't use the prices. |
| `completeness` | Layout change / reduced page | Use what's present; expect the gap, don't infer zero. |
| `price_integrity` | Outlier offers or a price jump | Check `flagged_offers`; if empty, verify against a second source before repricing. |
| `offer_contamination` | Junk offers in the list | Exclude the flagged indices, recompute your aggregates. |
| `freshness` | Cached / aged data | Lower `max_age` if you need current data (a fresh crawl is billed as usual). |


## `flagged_offers` in practice 

When `flagged_offers` is non-empty, those offers are priced far enough from the median of
the offer set to look like data errors or contamination — a €4.90 "offer" among €200
listings is almost never the product you're pricing against. The offers are **still
included** in `offers[]` (we report, you decide); the flags plus `offer_contamination`
let you exclude them deterministically:

```
usable_offers = offers.filter((o, i) => !trust.flagged_offers.includes(i))
```

Automated repricing against `price_min` should always use the flag-filtered minimum.

## Questions we get 

- **Does trust cost credits?** No. It's computed on every result as part of the standard
lookup — one result, one credit, trust included.
- **Does a low score mean the result failed?** No — `success` and `trust` are independent.
A result can be successful (the lookup completed) and still warrant review. Failed
lookups are never billed and carry no trust object.
- **Can I filter by score in the API?** Filtering is client-side by design: you always get
the data plus the score, and your pipeline decides. This keeps the billing model simple
(you pay for the lookup, not for our opinion of it).
- **Is the score comparable across sources?** Yes — the components and weights are
identical for every source, so 0.8 means the same thing on Idealo and on Walmart.


## Next steps

- [Audit & provenance](/guides/audit-and-provenance) — the full crawl trail behind each result.
- [Change events](/guides/change-events) — get notified when watched products change.
- [Self-serve → Cached re-serves](/guides/self-serve#cached-re-serves-and-freshness) — how
`max_age` interacts with `freshness`.