Skip to main content
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:
{
  "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):
{
  "game": "pokemon",
  "cards": [
    { "name": "Charizard ex", "set": "Scarlet & Violet 151", "number": "006/165" },
    { "name": "Pikachu ex", "set": "Surging Sparks", "number": "247/191" }
  ]
}
FieldRequiredMeaning
nameyes (per ref)Card name, e.g. Charizard ex. Min 2 characters.
setnoSet name, substring-matched (Scarlet & Violet 151, or just 151). Narrows ambiguous names.
numbernoCollector number (006/165, 6/165, or 006). The strongest disambiguator.
finishnoFinish / pattern, e.g. Master Ball Pattern, Reverse Holo, Full Art.
cardsnoA list of the above refs for a batch lookup. Provide name or cards, not both.
gamenoGame 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

{
  "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:
FieldMeaning
scopeAlways "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_noteA 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:
statusMeaningproduct_idavailability
resolvedExactly one printing matched.the resolved idpresent if any consented seller stocks it; null if none do (a fallback is returned instead — see below)
candidatesMore than one printing matched — disambiguate first.nullnull
no_matchCard not recognized.nullnull
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:
{
  "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
  }
}
FieldMeaning
nearby_printingsUp 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_unexposedtrue 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.
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.
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).
// "Where can I buy this Charizard?"
hoard.discovery.findAvailability({
  name: "Charizard ex",
  set: "Scarlet & Violet 151",
  number: "006/165"
}); // hoard_read