Back to the notebook

The Knowledge Graph Behind Stock Sentiment: One Name, Three Entities

Every name in market news resolves to one typed node in the SentiSense ontology: a company, a person, a product, an organization or a topic. Here is why that matters for sentiment, and how to query the graph with three free calls.

SentiSense Team
SentiSense Team
September 13, 2026 · 8 min read

Ask a sentiment feed about Apple and you get one number for one ticker. Ask a person the same question and they start splitting hairs: Apple the company, or the iPhone? Tim Cook's comments, or the analyst who covers him? The people who split hairs are right. The word "Apple" is at least three different things, and a feed that keys everything to a ticker has already lost that distinction before it counts a single mention.

SentiSense keeps the distinction. Underneath the sentiment numbers sits a knowledge graph, and this post explains what is in it, why it exists, and how to query it.

What is an ontology, concretely?

An ontology is a fixed vocabulary for what things are and how they relate. Ours has eight entity types that matter to a reader: companies, ETFs, people, products, organizations, topics, countries and publishers. Every mention in the tens of thousands of articles, posts, filings and transcripts we read each day is filed against one of those nodes, never against a bare string.

Relationships are typed too. A person leads a company or founded it. A product is a variant of another product. Two companies are peers. A topic is a subtopic of another topic. As of September 2026 the curated graph holds roughly 1,050 companies, a few hundred publishers, several hundred products, a few dozen tracked executives and organizations, about 30 topics, and more than two thousand peer relationships, all reviewed by hand before they ship.

In the SentiSense ontology every name resolves to exactly one typed node, a person, product, organization, topic or company, and that node's handle works on every metrics endpoint that accepts a ticker.

Why is entity resolution the hard part?

Counting mentions is easy. Deciding what was mentioned is not.

"Apple" in a headline about the iPhone 17 is a product mention. "Apple" in a Form 4 filed by an executive is a company mention. "Apple" in a story about a supplier's guidance is neither, it is context for a different ticker. A resolver has to make that call on every sentence, and a wrong call poisons the number downstream: an iPhone review that lands on the company's series makes Apple look more discussed than it was, and a supplier story that lands on Apple makes a peer look quieter than it was.

The rule that makes this tractable is simple to state. A ticker-shaped identifier always means the listed company. Every other entity has exactly one handle, a URL slug like Tim-Cook or Nancy-Pelosi, and the resolver assigns mentions to the most specific node the sentence supports. Ambiguity that the sentence cannot resolve is not guessed; it is left on the company.

This is also why the search endpoint returns the type with every result. When you search for "apple" you get the company and its products in one list, each labeled, and you choose.

How do you go from a name to a time series?

Two calls, one handle.

# 1. Resolve the name
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/kb/entities/search?q=tim%20cook&type=person"

# 2. Read the SentiSense Score for that handle
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v2/metrics/entity/Tim-Cook/metric/sentisense"

The first call returns the handle. The second call takes that handle on the same path a ticker uses, so an executive, a product and the company itself can be read over the same window and compared. A third call returns the graph itself:

# 3. The typed relationships around the ticker
curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v1/stocks/AAPL/graph?depth=1&cap=75"

It answers with every person, product, product family, peer and organization attached to the company, and an edge for each pair: LEADS, FOUNDED, VARIANT_OF, PEER, OWNS, SUBSIDIARY_OF, with a direction and the properties the graph holds for it. Trimmed to two nodes and one edge, the response looks like this:

{
  "ticker": "AAPL",
  "root": "Apple-Inc",
  "depth": 1,
  "cap": 75,
  "truncated": false,
  "counts": { "nodes": 2, "edges": 1, "byType": { "COMPANY": 1, "PERSON": 1 } },
  "groups": {
    "people": ["Tim-Cook"],
    "products": [], "productFamilies": [], "peers": [], "organizations": [], "publishers": [], "topics": []
  },
  "nodes": [
    { "slug": "Apple-Inc", "displayName": "Apple Inc.", "type": "COMPANY" },
    { "slug": "Tim-Cook", "displayName": "Tim Cook", "type": "PERSON" }
  ],
  "edges": [
    { "source": "Tim-Cook", "target": "Apple-Inc", "type": "LEADS", "direction": "DIRECTED",
      "properties": { "role": "Executive Chairman", "since": "2026" } }
  ]
}

There is one identifier in the whole response, and it is the slug: it keys the nodes, it is both ends of every edge, it fills the groups buckets so an agent does not have to classify anything, and it is what the metrics endpoint accepts. truncated says whether the cap cut the neighbourhood short. That is how an agent goes from "who is around this stock" to a table, and from the table to "how are they connected", without knowing any names in advance.

We use the Score rather than raw sentiment for these comparisons because the Score weights tone by how actively a name is discussed. A single post about a quiet executive cannot swing it, and each point carries the count of directional mentions behind it, so a thin sample is visible instead of hidden.

What does the graph know that a list does not?

A list of the entities near a ticker answers "who". The graph answers "how".

Take the people around a company. The list says there are six. The graph says one of them leads it, since a given year, and one founded it. Take the products. The list says twenty-three. The graph says which eight are variants of each other, so a launch's reception can be read against its own family rather than against the whole catalog. Take the peers. The list says nothing, because peers are not "attached" to a ticker in the way a product is. The graph carries them as edges, more than two thousand of them.

That relationship layer is what makes the ontology different from a tag cloud, and it is where the interesting questions live: whether an executive's coverage is separating from the company's, whether a new product generation is landing differently from the last one, whether a story about one company is moving its peers.

Reading it is short. This prints who leads a company and since when:

const headers = { "X-SentiSense-API-Key": process.env.SENTISENSE_API_KEY };
const graph = await fetch("https://app.sentisense.ai/api/v1/stocks/AAPL/graph", { headers }).then((r) => r.json());
const node = Object.fromEntries(graph.nodes.map((n) => [n.slug, n]));

for (const e of graph.edges.filter((e) => e.type === "LEADS")) {
  console.log(node[e.source].displayName, e.properties.role, "since", e.properties.since);
}
// Tim Cook Executive Chairman since 2026
// John Ternus CEO since 2026

The groups arrays list slugs, and a slug is already the handle the metrics endpoint accepts, so going from a person on the graph to that person's Score is a lookup, not a second resolution call:

curl -H "X-SentiSense-API-Key: $SENTISENSE_API_KEY" \
  "https://app.sentisense.ai/api/v2/metrics/entity/John-Ternus/metric/sentisense"

Fetch the same series with AAPL in place of the handle and the two lines can be read over the same window.

What does it cost?

Nothing beyond a free API key. Name resolution, the entities behind a ticker, the graph with its typed edges, and each node's Score and mention history all work on the free tier. The only ceiling is the per-minute rate limit, and PRO raises that ceiling rather than unlocking a hidden layer of the graph.

Two rules apply. The graph is served one ticker at a time, so a call is an answer to a question, not a download, and there is no endpoint that lists everything.

The two per-ticker calls are worth telling apart. /entities is the flat list: the people and products attached to a ticker, each with a name, a type and a handle. /graph returns the same nodes plus the edge between each pair, and it is the only one of the two that carries peers, because a peer is a relationship between two companies rather than something attached to one ticker.

Entity What /entities returns What /graph adds
Tim Cook person, handle Tim-Cook LEADS Apple Inc., role Executive Chairman, since 2026
iPhone 17 product, handle VARIANT_OF the iPhone family, so it can be read against its siblings
Microsoft not returned PEER of Apple Inc., with the sector the pairing was curated under

The list is enough to build a watchlist. The graph is enough to build an analysis, and both are one free call.

What can you build with it?

  • Executive versus company. Pull the CEO's Score and the ticker's Score over the same thirty days. When they separate, the coverage is about the person, not the business, and that is worth knowing before the next filing.
  • Product launch monitoring. Read a new model against its variant family rather than against the whole company, so a launch week does not disappear into the parent's noise.
  • Peer contagion. When a story breaks on one name, read its peers over the same window and see which ones the market actually moved with it.

Every one of these is a few calls on a free key, and the stock-ontology skill packages them as ready-made workflows: resolve a name, walk one ticker's graph, compare Scores over the same window. Claude Code, Cursor, Codex and Gemini pick it up with npx skills add SentiSenseApp/skills; OpenClaw agents install it with npx clawhub install stock-ontology.

SentiSense provides market data and analytics for research and educational purposes. Nothing here is investment advice.

Walk the graph for any covered ticker on a free key. No card required.

Get a free API key