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" }
]
}
| 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
{
"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:
{
"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.
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.
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).
// "Where can I buy this Charizard?"
hoard.discovery.findAvailability({
name: "Charizard ex",
set: "Scarlet & Violet 151",
number: "006/165"
}); // hoard_read