Guides/Research a Concept

    Guides

    Research a Concept

    Some questions aren't about price at all. Does this company have anything to do with Greenland? Which stocks are exposed to GLP-1 drugs? Concept research answers them from what a company says in its SEC filings, what the news says about it and its company profile. Every "yes" comes with the passage it rests on and a link to the source.

    There are two questions, and an endpoint for each:

    QuestionEndpointReturns
    Does this ticker relate to the concept?GET /api/v2/stocks/concept-research/relateOne answer: related, not_related or unknown, with its citation.
    Which tickers relate to the concept?GET /api/v2/stocks/concept-research/findThe related tickers ranked by probability, each with its citation.

    A concept is free text: a theme, place, product, technology or event ("rare earths", "Strait of Hormuz", "GLP-1"). There is no list to pick from. Pass the phrase as a person would say it rather than expanding it into keywords. Generic market vocabulary ("valuation", "growth") is refused.

    You'll need: a Power User or Business token. The examples use the made-up tickers XMPL and XMPM.

    Rolling out

    Concept research opens account by account. Until it's on for yours, both endpoints answer 404 and the MCP tool says it isn't available yet. Once it's on, a 503 with code: "not_available_yet" means the concept index itself hasn't opened.

    GET /api/v2/stocks/search-securities?q=lithium&query_description=true finds companies whose description contains a keyword. It is fast and cheap, but it can't tell a lithium miner from a company that mentions lithium batteries once, and it never says why it matched.

    Concept research judges the relation. A classifier reads the company's filings, news and profile and decides whether it relates to the concept, and a positive answer always carries the passage that convinced it. Use search to look a company up; use concept research when the question is "is X exposed to / connected to Y?".

    1. Ask about one ticker

    python
    import osimport requests
    API = "https://scanr.ai/api/v2"HEADERS = {"Authorization": f"Bearer {os.environ['SCANR_API_TOKEN']}"}
    answer = requests.get(    f"{API}/stocks/concept-research/relate",    headers=HEADERS,    params={"ticker": "XMPL", "concept": "rare earths"},    timeout=60,).json()print(answer["answer"], "-", answer["meaning"])
    json
    {  "ok": true,  "mode": "relate",  "ticker": "XMPL",  "concept": "Rare earths",  "conceptSlug": "rare-earths",  "conceptStatus": "active",  "answer": "related",  "meaning": "XMPL relates to \"Rare earths\". Say so and cite the evidence: link its url as a markdown link naming the source and date, and quote the passage briefly.",  "coverage": "index",  "provenance": "From the concept index: a sweep asked this pair against the ticker's current digest (its filings, news and company profile).",  "probability": 0.91,  "threshold": 0.62,  "confidence": 0.88,  "citation": {    "source": "SEC filing",    "quote": "Our Halvorsen project hosts a rare earth deposit, and in 2024 we completed a pilot plant producing a mixed rare earth carbonate.",    "url": "https://example.com/filings/xmpl-10k-2024",    "publishedOn": "2025-03-14",    "docId": "0001234567-25-000012:14"  },  "relatedSince": "2025-03-14",  "askedAsOf": "2026-09-30",  "modelVersion": "concept-clf/2026-09",  "resolvedBy": "hot"}

    concept comes back as the label it resolved to, so "rare earth metals" and "REEs" both answer as Rare earths. resolvedBy says how: hot for a known concept or alias, warm for a close match to one, cold for a phrase nothing matched. A cold phrase is answered live and comes back with conceptStatus: "provisional".

    2. Read the answer

    There are three ways to answer, and only one of them is a yes:

    answerMeaningWhat to say
    relatedThe classifier found evidence, and citation holds it.It relates, citing the source.
    not_relatedThe pair was asked and the classifier found no evidence.No evidence in its filings, news or profile as of askedAsOf. That is not proof of no connection.
    unknownNothing had asked this pair.Unknown. Not a no.

    coverage says where the answer came from:

    coverageSource
    indexA sweep asked this pair against the ticker's current filings, news and profile.
    liveNobody had asked yet, so it was classified just now. The answer is stored, so the next ask is a read.
    partialPast dates only: a stored relation exists, but no sweep had asked this pair by that date.
    noneNothing had asked the pair by that date. answer is unknown.

    meaning and provenance put the same thing in plain language, worded for an AI model to relay: pass them to your model verbatim, or build your own text from answer and coverage. Don't collapse unknown into not_related; they mean different things, and the distinction is the point.

    probability is the classifier's probability that the ticker relates, and threshold is the bar it was compared against. probability is null when it fell below the storage floor (so not_related, exact value not kept) or the pair was never asked. relatedSince is when the cited evidence became public, which is when the relation starts.

    3. Find the tickers that relate

    python
    found = requests.get(    f"{API}/stocks/concept-research/find",    headers=HEADERS,    params={"concept": "rare earths", "limit": 15},    timeout=60,).json()
    print(found["meaning"])for hit in found["results"]:    c = hit["citation"]    print(f"{hit['ticker']:6} p={hit['probability']:.2f}  {c['source']}, {c['publishedOn']}: {c['url']}")
    json
    {  "ok": true,  "mode": "find",  "concept": "Rare earths",  "conceptSlug": "rare-earths",  "conceptStatus": "active",  "coverage": "partial",  "meaning": "Partial, not exhaustive: what the index holds plus one live pass over 200 keyword-matched candidates, as of 2026-09-30. Other tickers may relate too; say this is not a complete list. Cite each ticker's evidence as a markdown link naming the source and date. A background sweep of the whole universe was requested, so asking again later may find more.",  "count": 2,  "candidatesAsked": 200,  "limitReached": false,  "sweepRequested": true,  "askedAsOf": "2026-09-30",  "resolvedBy": "hot",  "results": [    {      "ticker": "XMPL",      "probability": 0.91,      "citation": {        "source": "SEC filing",        "quote": "Our Halvorsen project hosts a rare earth deposit, and in 2024 we completed a pilot plant producing a mixed rare earth carbonate.",        "url": "https://example.com/filings/xmpl-10k-2024",        "publishedOn": "2025-03-14",        "docId": "0001234567-25-000012:14"      },      "live": false    },    {      "ticker": "XMPM",      "probability": 0.77,      "citation": {        "source": "news article",        "quote": "The motor maker signed a five-year supply agreement for neodymium magnets.",        "url": "https://example.com/news/n-88231",        "publishedOn": "2026-05-02",        "docId": "n-88231"      },      "live": true    }  ]}

    Results are ranked by probability, highest first. limit is 1–25 (default 15).

    Read coverage before you treat the list as complete:

    • index: the concept is fully indexed, so this is the whole market's answer. If limitReached is true the list was cut at limit and more tickers relate. An empty index list really does mean no ticker relates.
    • partial: the concept isn't fully indexed yet. The list is what the index holds plus one live pass over candidatesAsked keyword-matched candidates; live: true marks a ticker that pass found. Other tickers may relate too.

    An empty partial list means "none among the 200 candidates asked", not "no ticker relates":

    json
    {  "ok": true,  "mode": "find",  "concept": "Lunar mining",  "conceptSlug": "lunar-mining",  "conceptStatus": "provisional",  "coverage": "partial",  "meaning": "None among the 200 candidates asked, as of 2026-09-30. This is NOT \"no ticker relates to Lunar mining\": only those candidates were checked. Say exactly that.",  "count": 0,  "candidatesAsked": 200,  "limitReached": false,  "sweepRequested": false,  "askedAsOf": "2026-09-30",  "resolvedBy": "cold",  "results": []}

    When sweepRequested is true, the answer was thin and a sweep of the whole market was queued for the concept. Asking again later can return more.

    Asking about the past

    Both endpoints take as_of (YYYY-MM-DD) to answer as of an earlier date. A past date is answered from the index only and never classified live: a live pass would read today's filings into the past. So a past date can come back unknown (relate) or as an empty partial list (find) where today's answer would not:

    json
    {  "ok": true,  "mode": "relate",  "ticker": "XMPL",  "concept": "Lunar mining",  "conceptSlug": "lunar-mining",  "conceptStatus": "provisional",  "answer": "unknown",  "meaning": "Unknown: there is no answer for XMPL and \"Lunar mining\" as of 2026-01-02. This is NOT a no. Say it is unknown; asking without a date gets today's answer, which may be classified live.",  "coverage": "none",  "provenance": "Nothing had asked this pair as of 2026-01-02, and a past date is never classified live.",  "probability": null,  "threshold": null,  "confidence": null,  "citation": null,  "relatedSince": null,  "askedAsOf": "2026-01-02",  "modelVersion": null,  "resolvedBy": "cold"}

    askedAsOf is the date the question was answered for. relatedSince (relate) and each citation's publishedOn say when the evidence became public: a relation exists from that date on.

    Follow the citations

    Every related answer and every find result carries a citation:

    FieldMeaning
    sourceSEC filing, news article or company profile.
    quoteThe passage the classifier read, trimmed at a word boundary (relate keeps up to 600 characters, find up to 240).
    urlA link to the source document. null when none is known.
    publishedOnWhen the passage became public.
    docIdThe passage's id in its corpus, stable across calls.

    Show the citation with the answer, and link url rather than paraphrasing it: the quote is the evidence, and a reader should be able to check it. The quote is stored with the answer, so it outlives the source (old news is pruned). A related answer without a citation is never returned; if the classifier ever said yes without evidence, the answer comes back unknown.

    Refusals

    A refusal is an answer, not a failure. It has the usual error body plus a stable code to branch on and retryable, whether the same request can succeed later:

    json
    {  "error": {    "statusCode": 422,    "message": "\"valuation\" is generic market vocabulary, which concept research refuses rather than answers.",    "code": "generic_concept",    "retryable": false  }}
    StatuscodeMeaningWhat to do
    503not_available_yetThe concept index hasn't opened, so nothing is answered yet. It says nothing about the ticker or concept.Don't retry in a loop. Fall back to search-securities with query_description=true, and call it a keyword match.
    503classifier_unavailableThe answer needs a live classification and the classifier is down. Not a no.Retry later (retryable: true).
    422generic_conceptThe concept is generic market vocabulary ("valuation", "growth").Ask about something specific.
    422empty_conceptNothing is left of the concept once normalized.Ask which theme, place, product or event is meant.
    404no_source_documentsrelate only: the ticker has no filings, news or profile to judge from. Not a no.Report that it can't be answered for this ticker.
    504timeoutAnswering took too long.Safe to retry once (retryable: true).

    Two 503s mean different things, so branch on code, not only on the status:

    python
    def relate(ticker, concept):    res = requests.get(        f"{API}/stocks/concept-research/relate",        headers=HEADERS,        params={"ticker": ticker, "concept": concept},        timeout=60,    )    if res.ok:        return res.json()    error = res.json()["error"]    if error.get("code") == "not_available_yet":        return None  # the feature isn't open yet; don't hammer it    if error.get("retryable"):        raise RuntimeError(f"try again later: {error['message']}")    raise ValueError(error["message"])  # rephrase the question

    A 404 without a code means concept research isn't enabled for your account yet. The usual 400, 401, 403 (a Starter token) and 429 apply as on every endpoint.

    Over MCP

    The MCP server has the same research as one tool, concept_research: with ticker it answers one pair (relate), without it it finds the tickers that relate (find). It returns the same JSON as the endpoints, and a refusal comes back as a result with ok: false, its code, a message to relay and retryable, rather than as a tool error. It needs a Power User or Business token.

    A prompt like this is enough:

    text
    Which of my watchlist relates to rare earths? My watchlist: XMPL, XMPM, AAPL, NVDA.Cite the source for every yes, and say which ones are unknown rather than no.

    A well-behaved assistant calls concept_research once per ticker with ticker set, because find returns only the top results and can be partial, while relate gives every watchlist name a definitive related, not_related or unknown. It then reports the yes answers with their links and keeps the unknowns separate from the noes.

    Limits

    Each request counts once against the endpoint's normal rate limits, whether it was answered from the index or classified live. A live answer is stored, so asking the same pair again is a fast read. Concept research spends no AI credits.

    Next