The graph's vocabulary — and the three properties that will bite you if you ignore them. ~10 minutes.
In Lesson 1 you learned the graph is the output: events in, edges out. Now we open the box. By the end you'll know what a node actually is, the eight families the edges fall into, and the three node properties every contributor must respect — get one wrong and your query silently returns garbage or scans the whole DB.
:EntityHere's the first surprising thing. Almost every node in the graph carries the same
primary label: :Entity. The kind of thing it is (token, vault, multisig…)
lives in a property, and is also mirrored as a secondary label.
The node's identity is its address (id). There are 19 node
types — the full list is in pkg/types/schema.go as NodeType
constants and lives in your glossary. A few you'll meet constantly:
| Node type | What it is | Secondary label |
|---|---|---|
token | An ERC-20 / focus token | :Token |
eoa | An externally-owned account (a wallet) | :EOA |
contract | A generic smart contract | :Contract |
multisig | A Gnosis-Safe-style multisig | :Multisig |
vault | An ERC-4626 / yield vault | :Vault |
lending_market | A money market (Aave aToken, Compound cToken…) | :LendingMarket |
oracle | A price feed (Chainlink etc.) | :Oracle |
Source: pkg/types/schema.go — NodeType constants + NodeTypeToLabel map.
graph_id — the partition keyOne Memgraph instance holds multiple independent graphs side by side, separated
only by the graph_id property on every node and every edge. The live one might be
risk-graph-rt; a shadow/test one might be test_carlos. They share the
database but must never mix in a query.
graph_id. Forget it and you'll union two graphs
together and get nonsense — or accidentally read another partition's data. It's a property, not a
label, so the DB won't stop you. This is the #1 way new contributors get silently-wrong results.
category / subcategory — with a legacy trapCurrent code stores a node's kind in category + subcategory. But
older nodes carry type + subtype instead. The repo's own
CLAUDE.md mandates the defensive read:
// Reading a node's kind safely across old + new nodes (from CLAUDE.md): coalesce(n.type, n.subcategory, n.category)
Source: risk-graph-indexer/CLAUDE.md § Sanity checks. coalesce returns
the first non-null — so it works whether the node is legacy (type) or current (category).
pending_enrichment — the lifecycle flagFrom Lesson 1: the indexer creates bare nodes with pending_enrichment=true,
and the enrichment-worker flips it to false once it's classified. So this boolean tells you
whether a node's metadata (symbol, decimals, labels) can be trusted yet.
There are ~19+ edge types, but you don't memorise them flat — they group into eight
parent categories (the EdgeCategory map in schema.go).
Learn the categories and the types slot in underneath:
| Category | Meaning | Edge types in it |
|---|---|---|
| CONTAINS | X holds/contains a balance of Y | HOLDS, POOL_ASSET, VAULT_ASSET, RESERVE_BACKING |
| CONTROLLED_BY | X is controlled/approved/owned | ADMIN_CTRL, ADMIN_OF, OWNS, OWNS_ADMIN, CURATES, APPROVES, CUSTODY_VIA |
| COLLATERALISED_BY | X is backed by collateral Y | LENDING_COLLATERAL, VAULT_ALLOCATION |
| DEPENDS_ON | X structurally depends on Y | ORACLE_DEP, BRIDGE_BACKED_BY, DVN_VERIFIES, SUBORDINATE_TO |
| DERIVED_FROM | X is a derivative/receipt of Y | RECEIPT_FOR, DEBT_FOR, WRAP_UNWRAP |
| OPERATED_BY | X is operated/deployed by Y | DEPLOYED_BY, SERVICE_FOR |
| AUDIT_TRAIL | A historical/governance record | AFFECTS (governance actions) |
| ANALYTICAL | Derived risk projection, not real topology | AT_RISK |
Source: pkg/types/schema.go — the EdgeCategory map. The full edge list with
directions + properties lives in your glossary.
AT_RISK are computed by the risk engine and layered under the true graph —
"if this admin key is compromised, these tokens are at risk." They're projections, not structure, and the
graph-viz architecture view deliberately hides them (FORTA-2986).
aToken's underlying reserve is a LENDING_COLLATERAL edge. A proxy's
admin is an ADMIN_CTRL edge. A Safe's signers are OWNS edges. Same facts you'd
pull from contract storage — now they're traversable.
Cypher is "ASCII-art SQL for graphs." Nodes are (parens), edges are
-[brackets]-> with a direction. Here's a real-shaped read — "what tokens does this wallet hold?":
MATCH (w:Entity {id: $wallet, graph_id: $g})-[r:HOLDS]->(t:Entity {graph_id: $g}) RETURN t.symbol, r.quantity_raw, t.usd_price
Read it left to right: start at the wallet node w (anchored by its id),
follow an outgoing HOLDS edge r, to a token node t.
Return the token's symbol, the raw balance on the edge, and its price.
Notice three things, all of which are rules, not style:
:Entity and both carry graph_id: $g — partition-scoped, per Rule #1.id — it starts from one node, not the whole graph.->). HOLDS goes wallet→token.MATCH (n) with no anchor. The graph
is huge; an unanchored scan = OOM + timeout. Always start from a known id or a label+index,
bound depth, and LIMIT.-[r]- without an arrow) when you're unsure.
Source: risk-graph-indexer/CLAUDE.md § "Graph DB queries — no brute-force" and § "Before writing code". Real query shapes verified against pkg/ (e.g. the :HOLDS reads in the graphwrite/risk packages).
Instant feedback — these target the exact things that trip up new contributors.
MATCH (t:Entity)-[:HOLDS]->(x) RETURN x and get results from two unrelated graphs mixed together. What did you forget?type: "token" but no category. A newer node has category: "token" but no type. How do you read the kind safely for both?AT_RISK edge is in the ANALYTICAL category. What does that tell you?MATCH (n) RETURN n a fireable offense in this codebase?:Entity keyed by address id, scoped by graph_id.graph_id, category/type, pending_enrichment — and the coalesce read.MATCH, and recite the three query rules.Grounded in: pkg/types/schema.go (NodeType, EdgeType, EdgeCategory, NodeTypeToLabel),
CLAUDE.md (graph-id partition, coalesce rule, no-scan / both-directions rules),
docs/architecture.md. Verify against source — the code is the truth.