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

# Discovery

> Find which sellers across the consented network have a card in stock, with the lowest price and an affiliate buy link.

The discovery endpoint answers a buyer-side question: **which sellers have this
card in stock, and where can I buy it?** Give it a card reference (or a list),
and it returns — per card — the sellers across Hoard's *consented* network who
currently stock it, with the lowest price, total quantity, store name, and an
affiliate-wrapped buy link.

This is the demand-layer surface: an AI assistant grounds a buyer's fuzzy card
guess to an exact catalog printing, then looks up live availability. Two
properties make it safe to expose:

* **Network-wide, consented-only.** Results come from the whole seller network
  that has opted in to discovery and published its binder — never the calling
  bearer's own inventory. The token identifies *who is asking*, not *what is
  searched*. A seller who has not opted in never appears.
* **No silent wrong pick.** An underspecified reference (e.g. just `Charizard`,
  which has dozens of printings at wildly different prices) returns **ranked
  candidates**, not a guess. The assistant disambiguates the exact printing
  before any buy link is produced.

The endpoint is mounted at `/api/discovery/availability` (not `/api/v1/...`) and
uses Bearer authentication (`Authorization: Bearer YOUR_API_KEY`). It is
read-only — `mcp:read` is sufficient.

## Find availability

```
POST /api/discovery/availability
```

Provide a single card reference:

```json theme={null}
{
  "name": "Charizard ex",
  "set": "Scarlet & Violet 151",
  "number": "006/165",
  "finish": "Master Ball Pattern",
  "game": "pokemon"
}
```

…or a batch (a decklist) under `cards` (max 25 per request):

```json theme={null}
{
  "game": "pokemon",
  "cards": [
    { "name": "Charizard ex", "set": "Scarlet & Violet 151", "number": "006/165" },
    { "name": "Pikachu ex", "set": "Surging Sparks", "number": "247/191" }
  ]
}
```

| Field    | Required      | Meaning                                                                                       |
| -------- | ------------- | --------------------------------------------------------------------------------------------- |
| `name`   | yes (per ref) | Card name, e.g. `Charizard ex`. Min 2 characters.                                             |
| `set`    | no            | Set name, substring-matched (`Scarlet & Violet 151`, or just `151`). Narrows ambiguous names. |
| `number` | no            | Collector number (`006/165`, `6/165`, or `006`). The strongest disambiguator.                 |
| `finish` | no            | Finish / pattern, e.g. `Master Ball Pattern`, `Reverse Holo`, `Full Art`.                     |
| `cards`  | no            | A list of the above refs for a batch lookup. Provide `name` **or** `cards`, not both.         |
| `game`   | no            | Game scope. Defaults to `pokemon`.                                                            |

Resolved product ids are batched into a single availability lookup, so a
decklist costs one supply query regardless of length.

### Response

```json theme={null}
{
  "game": "pokemon",
  "scope": "network_discovery",
  "scope_note": "Availability across the consented seller network — not your own inventory.",
  "results": [
    {
      "query": { "name": "Charizard ex", "set": "Scarlet & Violet 151", "number": "006/165" },
      "status": "resolved",
      "product_id": 502558,
      "candidates": [
        { "product_id": 502558, "name": "Charizard ex - 006/165", "set": "SV: Scarlet & Violet 151", "number": "006/165", "finish": null, "game": "pokemon" }
      ],
      "total": 1,
      "truncated": false,
      "owned_by_caller": false,
      "availability": {
        "min_price": 8.0,
        "total_quantity": 3,
        "as_of": "2026-06-25T17:00:00Z",
        "sellers": [
          { "store_name": "Cheap Cards", "price": 8.0, "quantity": 2, "buy_url": "https://partner.tcgplayer.com/..." },
          { "store_name": "Cardstack Co", "price": 12.5, "quantity": 1, "buy_url": "https://partner.tcgplayer.com/..." }
        ]
      }
    }
  ]
}
```

Every response is explicitly tagged as discovery, not own-inventory:

| Field        | Meaning                                                                                                                                                                            |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scope`      | Always `"network_discovery"`. A constant marker that these results are NETWORK-WIDE availability across the consented seller network — **not** the calling bearer's own inventory. |
| `scope_note` | A human-readable restatement of `scope`, safe to surface verbatim.                                                                                                                 |

This matters for a *seller's own* store assistant: discovery results are the whole
consented network, so an assistant must never present them as "your stock." Use
the per-result `owned_by_caller` flag (below), not the presence of results, to
tell whether the caller's own listing appears.

There is one `results` entry per input reference, in input order. The `status`
field drives how to read it:

| `status`     | Meaning                                              | `product_id`    | `availability`                                                                                              |
| ------------ | ---------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------- |
| `resolved`   | Exactly one printing matched.                        | the resolved id | present if any consented seller stocks it; `null` if none do (a `fallback` is returned instead — see below) |
| `candidates` | More than one printing matched — disambiguate first. | `null`          | `null`                                                                                                      |
| `no_match`   | Card not recognized.                                 | `null`          | `null`                                                                                                      |

On `candidates`, the ranked `candidates` array (lowest-surprise printing first)
lets the assistant ask the buyer which one they mean, or re-query with a `set` /
`number`. `total` is how many printings matched and `truncated` is `true` when
that list was capped — a signal to narrow the query rather than assume the list
is complete.

`availability.min_price` and `total_quantity` aggregate across the consented
sellers; each `sellers` entry is one store with its own price, quantity, and
buy link. `as_of` is when the snapshot was computed (availability may be a few
minutes stale, but the staleness is always visible, never silent).

### `owned_by_caller`: is this *your* listing in the network?

Each resolved result carries `owned_by_caller`. It is `true` only when the
calling bearer is itself a consented, published seller whose **own** listing is
part of these network results for that product — so a seller's assistant can tell
which of the network results are the seller's own stock.

It is a pure annotation. It **never** changes what's returned: a consented seller
who owns the card and a stranger querying the same card get **identical**
`availability` — the same sellers, the same `min_price`, the same buy links — and
differ only in this flag. Discovery always returns the whole consented network;
`owned_by_caller` just labels the caller's own row within it.

`owned_by_caller` is `false` for `candidates` and `no_match` results (there is no
single product to own) and for a resolved miss (no network listing exists to own).

### No consented seller? A useful fallback, never a dead end

When a card resolves but **no consented seller is currently exposing it**,
`availability` is `null` and a `fallback` object takes its place. It keeps the
buyer moving instead of returning an empty result:

```json theme={null}
{
  "status": "resolved",
  "product_id": 502558,
  "availability": null,
  "fallback": {
    "nearby_printings": [
      { "product_id": 502999, "name": "Charizard ex - 199/165", "set": "SV: Scarlet & Violet 151", "number": "199/165", "finish": null, "game": "pokemon" }
    ],
    "in_network_unexposed": true
  }
}
```

| Field                  | Meaning                                                                                                                                                                                                                                |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nearby_printings`     | Up to 5 other printings of the same card (different set/number/finish) the buyer can pivot to — so "no consented seller has *this* Charizard" still surfaces the other Charizards. Catalog rows only, no seller data.                  |
| `in_network_unexposed` | `true` when a seller in the network holds this exact product but has not opted in to discovery yet — the card exists in-network, just isn't consented. `false` when no seller holds it at all (honestly not currently in the network). |

`fallback` is **only** present on a resolved miss. A `candidates` or `no_match`
result never carries it.

<Note>
  **`in_network_unexposed` is a boolean and nothing more.** It tells you whether
  the product exists somewhere in the network, never *who* holds it, their price,
  their quantity, or their store. A seller who has not opted in to discovery is
  never identifiable through this flag.
</Note>

### Surface the buy link verbatim

Every `buy_url` is wrapped through the TCGplayer affiliate program
(`partner.tcgplayer.com`). Surface it as-is — rewriting it through a different
URL shortener or stripping the wrapper removes the seller's affiliate revenue
from any purchase the assistant drives.

## Over the MCP

The hosted MCP exposes this as `discovery.findAvailability` — a read tool, so it
runs in `hoard_read`. The argument shape mirrors the REST body (a single
`{ name, set?, number?, finish? }` or a `cards` list, with `game` defaulting to
`pokemon`).

```js theme={null}
// "Where can I buy this Charizard?"
hoard.discovery.findAvailability({
  name: "Charizard ex",
  set: "Scarlet & Violet 151",
  number: "006/165"
}); // hoard_read
```
