chain-insights 0.45.0 → 0.46.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,18 @@
2
2
 
3
3
  Chain Insights Graph accepts ISO GQL through `graph_query` and
4
4
  `graph_query_batch`. A query is routed by its leading `USE <graph>` clause to
5
- one of two backends, each with its own accepted surface:
5
+ one of three backends, each with its own accepted surface:
6
6
 
7
- | Graph | Backend | Query surface |
8
- | ---------- | --------------------------- | ------------------------------------------ |
9
- | `topology` | DozerDB, directly over Bolt | **ISO GQL**, bounded (see topology bounds) |
10
- | `facts` | StarRocks warehouse | Corpus-scoped GQL subset, compiled to SQL |
7
+ | Graph | Backend | Query surface |
8
+ | ---------- | --------------------------- | ----------------------------------------------------------------------- |
9
+ | `topology` | DozerDB, directly over Bolt | **ISO GQL**, bounded (see topology bounds) |
10
+ | `facts` | StarRocks warehouse | Corpus-scoped GQL subset, compiled to SQL |
11
+ | `chain` | The chain node | One keyed lookup: `Transaction`, `Block`, `Address` or `Head`, by a key |
12
+
13
+ The same thing has the same name on all three: `address`, `tx_id`,
14
+ `block_height` and `block_hash`, with time in epoch milliseconds and `network`
15
+ from the query. The `chain-insights-cypher` skill holds the table, and
16
+ `meta_schema` reads the fields of each from the live graph.
11
17
 
12
18
  Two consequences drive everything below:
13
19
 
@@ -26,7 +32,7 @@ Two consequences drive everything below:
26
32
  > removed in the 2026-09 Robinhood Dozer cutover. Docs and skills that teach
27
33
  > those path operators or client merge are historical.
28
34
 
29
- ## The shared-graph model — what `network` actually selects
35
+ ## The `network` argument and the `network` property
30
36
 
31
37
  Read this before writing any query that matches `:Address` without an exact
32
38
  address.
@@ -36,49 +42,44 @@ space over ONE address-grain topology graph. There is no SS58/H160 split and
36
42
  no second query network — `network=robinhood` selects the one public graph and
37
43
  there is no `network=robinhood_evm` argument to pass.
38
44
 
39
- The consequence is exact and easy to get wrong:
40
-
41
- > **The `network` argument selects the GRAPH, not the subset of addresses
42
- > inside it.**
45
+ `network` is also a property of every node and every relationship on
46
+ `USE topology`, `USE facts` and `USE chain`. Its value is the query's `network`
47
+ argument. It is computed and never stored, with one exception: on a `:Chain`
48
+ node (the far side of a bridge) it names the remote chain, and it is stored and
49
+ read as it is.
43
50
 
44
- A `USE topology` query that matches `:Address` without a network predicate
45
- scans the whole public H160 space; scope by the node property when you want an
46
- explicit subset:
51
+ > **A filter on `network` never narrows a scan.** `WHERE a.network = "robinhood"`
52
+ > keeps every row, and a filter on another value gives no rows. Narrow a query
53
+ > with an address anchor.
47
54
 
48
55
  ```cypher
49
- -- WRONG: unbounded sweep — returns every H160 address in the public space
50
- USE topology MATCH (a:Address) RETURN a.address AS address LIMIT 100
56
+ -- A probe: one node, no WHERE, a LIMIT of 100 or less
57
+ USE topology MATCH (a:Address) RETURN a.address AS address, a.network AS network LIMIT 20
51
58
 
52
- -- RIGHT: scope by the node property
53
- USE topology MATCH (a:Address) WHERE a.network = "robinhood"
54
- RETURN a.address AS address LIMIT 100
59
+ -- Anchored: an address in the pattern
60
+ USE topology MATCH (a:Address {address: "0x…"})-[f:FLOWS_TO]->(b:Address)
61
+ WHERE NOT a:Pool
62
+ RETURN b.address AS address LIMIT 100
55
63
  ```
56
64
 
57
- Exact-address lookups (`MATCH (a:Address {address: "0x…"})`) need no predicate:
58
- the address is already a unique key, and adding a network predicate there fails
59
- closed on an H160 address screened under the chain's primary network name.
60
-
61
- ### `USE facts` is the opposite case
65
+ ### `USE facts` serves it too
62
66
 
63
- `facts` is the one place each network _does_ get its own backing database —
64
- the routing metadata on a result reports it as
65
- `facts.routing.starrocks_database`. Because the database already scopes the
66
- network, the facts `Address` label has **no mapped `network` property at all**:
67
+ Facts rows and the facts `Address` endpoint carry `network` as well. The server
68
+ computes it from the query's `network` argument. No view holds a column for it:
67
69
 
68
70
  ```text
69
71
  USE facts MATCH (a:Address {address:"0x…"})-[t:TRANSFER]->(b:Address {address:"0x…"})
70
72
  WHERE t.block_date = "2026-07-11"
71
- RETURN a.network AS from_network
72
- → unknown graph identifier: property "network" is not mapped on label "Address"
73
+ RETURN a.network AS from_network, t.network AS row_network
74
+ → from_network = robinhood, row_network = robinhood
73
75
  ```
74
76
 
75
- `Address` on facts is served only as a `TRANSFER` relationship endpoint, so a
76
- single-node `MATCH (a:Address)` is refused there as well. Read address-grain
77
- node properties — including `network` — on `USE topology`.
78
-
79
- Getting these two rules backwards is not a stylistic problem. An unscoped
80
- topology sweep publishes wrong-network results at double the metered cost, and
81
- a facts query projecting `network` hard-fails rather than degrading.
77
+ A filter on `network` is no anchor: a facts read still names an address pair
78
+ with one day, or one `tx_id`. `Address` on facts is served only as a `TRANSFER`
79
+ relationship endpoint, so a single-node `MATCH (a:Address)` is refused there as
80
+ well. Facts serves no kind label: a read that names `:Account` or `:Contract` is
81
+ refused, so read the kind on `USE topology`, or on `USE chain` with
82
+ `is_contract` and `nonce`.
82
83
 
83
84
  ## `topology` — ISO GQL
84
85
 
@@ -147,8 +148,9 @@ not valued, and sits in none of the four reason counters.
147
148
 
148
149
  The canonical probe is point-anchored and sub-second on the hosted endpoint:
149
150
  given one operator address, its top owners. Scope comes from the tool's
150
- `network=robinhood` argument (it selects the graph — see the shared-graph
151
- model above), so no in-query network predicate is needed:
151
+ `network=robinhood` argument (it selects the graph — see
152
+ [the `network` argument](#the-network-argument-and-the-network-property)), so no
153
+ in-query network predicate is needed:
152
154
 
153
155
  ```cypher
154
156
  USE topology
@@ -172,18 +174,15 @@ acting.
172
174
  The whole-graph high-fan-in sweep — every operator grouped by distinct owner
173
175
  count — is a valid shape but a heavy one: at millions of edges it can exceed
174
176
  the per-query limit (60 seconds by default), and a sweep that times out can
175
- still consume the metered seconds. Scope both endpoints by the network
176
- property (this match has no exact-address key, so the shared-graph rule
177
- applies), bound it by a recent `last_seen_timestamp` window — recompute the
178
- cutoff rather than copying a literal, for example now minus 7 days in Unix
179
- milliseconds — and prefer the point-anchored probe on metered endpoints:
177
+ still consume the metered seconds. A `network` filter does not narrow it. Bound
178
+ it by a recent `last_seen_timestamp` window — recompute the cutoff rather than
179
+ copying a literal, for example now minus 7 days in Unix milliseconds — and
180
+ prefer the point-anchored probe on metered endpoints:
180
181
 
181
182
  ```cypher
182
183
  USE topology
183
184
  MATCH (owner:Address)-[operation:OPERATED_BY]->(operator:Address)
184
- WHERE owner.network = "robinhood"
185
- AND operator.network = "robinhood"
186
- AND operation.last_seen_timestamp >= 1787631843154 -- now minus 7 days, ms
185
+ WHERE operation.last_seen_timestamp >= 1787631843154 -- now minus 7 days, ms
187
186
  WITH operator,
188
187
  count(DISTINCT owner) AS owner_count,
189
188
  sum(operation.tx_count) AS transfer_count
@@ -382,6 +381,7 @@ StarRocks. Use the topology graph, or restructure:
382
381
  | Self-joins / node-to-node comparison `WHERE a <> b` | Compare key properties: `a.address <> b.address` |
383
382
  | Untyped relationship `-[r]->` | Name the relationship type |
384
383
  | Metadata functions `keys(n)`, `labels(n)`, `type(r)` | Project known properties explicitly |
384
+ | `:Account` and `:Contract` kind labels | `USE topology` for the label, or `USE chain` for `is_contract` and `nonce`. Facts serves no kind |
385
385
 
386
386
  There is no longer a local pinned conformance suite; verify supported and
387
387
  rejected shapes against a live Chain Insights Graph endpoint.
@@ -408,6 +408,16 @@ Caveats: facts carries only its mapped labels, so role-label patterns are
408
408
  topology-only. The property-flag form (`is_exchange`) remains the canonical
409
409
  filter.
410
410
 
411
+ ## Kind labels (topology graph)
412
+
413
+ A kind is a second label on an `Address`, set from a chain fact: `:Account` for
414
+ a key holder and `:Contract` for a contract (`:Contract:SmartAccount` for a
415
+ contract wallet). `is_contract` stays beside `:Contract`. An address that only
416
+ received has no kind, and no address carries both kinds. A role label stays
417
+ beside the kind. Facts serves no kind, and a kind is never a `USE chain` lookup
418
+ label: `USE chain MATCH (a:Address {address: "0x…"}) RETURN a.is_contract, a.nonce`
419
+ reads it at one block.
420
+
411
421
  ## Practical guidance
412
422
 
413
423
  - **Prefer inline property maps for equality lookups**:
@@ -35,35 +35,61 @@ primitive-backend status instead.
35
35
  A graph query goes to one of three layers. Name the layer at the start of the
36
36
  query: `USE topology`, `USE facts` or `USE chain`.
37
37
 
38
- Route by what you know: topology searches, while facts and chain look up one known thing.
38
+ Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.
39
39
 
40
- 1. I do not know the thing yet: `USE topology`.
41
- 2. I know the pair and the day, or the transaction hash, and want the indexed rows: `USE facts`.
42
- 3. I want the chain's own record of a transaction by its hash, a block by its number or hash, or the head: `USE chain`.
40
+ 1. I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.
41
+ 2. I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.
42
+ 3. I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`.
43
43
 
44
- `USE chain` has no address lookup. Find the address on `USE topology`, then read the pair and one day on `USE facts`.
44
+ `USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.
45
45
 
46
- When two fit, as with a hash: ask `USE chain` first for the record and the result, then `USE facts` for the transfers it caused.
46
+ A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.
47
47
 
48
- The layers hand each other keys. Topology gives the pair and the first and last seen time. Chain gives the `block_date` of a hash or a height. Facts gives the `tx_id`.
48
+ The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.
49
49
 
50
- A list, a range or a whole-chain question is served on no layer. Say so, and go back to an anchored `USE topology` search.
50
+ A list, a range or a whole-chain question names no address, pair, transaction or block, so no layer serves it today. Say so in one line, ask the user for one of those, and send no workaround query.
51
51
 
52
- | The question | Layer | First query |
53
- | ----------------------------------------------------------- | ---------- | ------------------------------------------------------------ |
54
- | Who is connected to this address? Where did the money go? | `topology` | One anchored hop on `FLOWS_TO`, with a `LIMIT`. |
55
- | What moved between A and B on one day? | `facts` | `TRANSFER` with both addresses and `block_date`. |
56
- | Which days did A and B trade? | `topology` | The link's `first_seen_timestamp` and `last_seen_timestamp`. |
57
- | Did transaction `0x…` succeed? Which block, which day? | `chain` | `Transaction {hash}`. |
58
- | Which day is block N? | `chain` | `Block {height}`, and return `block_date`. |
59
- | How far behind are the graph and the warehouse? | `chain` | `Head`. |
60
- | Every transaction of block N, or every address with label X | none | A list or a scan. Say so. Do not retry. |
52
+ | The question | Layer | First query |
53
+ | ---------------------------------------------------------- | ---------- | --------------------------------------------------------------------------- |
54
+ | Recent activity or newest links of one address? | `topology` | `FLOWS_TO` out of the address, `ORDER BY last_seen_timestamp`. |
55
+ | Who sent money to an address? Who received from it? | `topology` | `FLOWS_TO` into or out of the address, with a `LIMIT`. |
56
+ | Where did the money go, or come from, in N hops? | `topology` | A guarded quantified walk, at most 4 hops, with a `LIMIT`. |
57
+ | Is there a route between A and B? | `topology` | `SHORTEST 1` with both addresses. |
58
+ | How many, how much, first or last active, for one address? | `topology` | The address node: `degree_in`, `tx_total_count`, `last_activity_timestamp`. |
59
+ | What moved between A and B on one day? | `facts` | `TRANSFER` with both addresses and `block_date`. |
60
+ | Which days did A and B trade? | `topology` | The link's `first_seen_timestamp` and `last_seen_timestamp`. |
61
+ | Did transaction `0x…` succeed? Which block, which day? | `chain` | `Transaction {tx_id}`. |
62
+ | Which day is block N? | `chain` | `Block {block_height}`, and return `block_date`. |
63
+ | Does this address hold a balance? Is it a contract? | `chain` | `Address {address}`, with `at_block` for a past block. |
64
+ | How far behind are the graph and the warehouse? | `chain` | `Head`. |
65
+ | Recent addresses, biggest senders, a top list of the chain | none | A list of the whole chain. Say so. Do not retry. |
61
66
 
62
67
  The server publishes the limits of a layer in `meta_network_capabilities`
63
68
  (`cia network robinhood --json` prints the same reply): the `chain_admission`
64
69
  block today, and `topology_admission` and `facts_admission` when the server
65
70
  sends them. Read a limit there. Do not write one down.
66
71
 
72
+ ## One name on every layer
73
+
74
+ A thing has one name on topology, facts and chain. Copy a value from one layer
75
+ into the next as it is. The layers differ by properties only.
76
+
77
+ | Thing | `USE topology` | `USE facts` | `USE chain` |
78
+ | ---------- | --------------------------------------------- | ----------------------------- | --------------------------------------------------- |
79
+ | An address | `:Address {address}`, and a kind label | `:Address {address}`, no kind | `Address {address}`, optional `at_block` |
80
+ | A kind | `:Account` or `:Contract`, `is_contract` kept | none: refused with a hint | `is_contract`, `nonce` and `delegated_to`, no label |
81
+ | A tx | `tx_id` on `DEPLOYED_CONTRACT` only | `tx_id` on every row | `Transaction {tx_id}` |
82
+ | A block | `block_height` on `DEPLOYED_CONTRACT` only | `block_height` on every row | `Block {block_height}` or `Block {block_hash}` |
83
+ | Time | epoch milliseconds | epoch milliseconds | epoch milliseconds |
84
+ | `network` | the query's `network` | the query's `network` | the query's `network` |
85
+
86
+ `network` is a property of every node and every relationship, and its value is
87
+ the query's `network`. It is never stored, except on a `:Chain` node, where it
88
+ names the remote chain. A filter on another value gives no rows. A kind is a
89
+ second label that topology takes from chain facts: `:Account` for a key holder,
90
+ `:Contract` for a contract. An address that only received has none. Facts
91
+ serves no kind.
92
+
67
93
  ## When a query is refused
68
94
 
69
95
  A refusal is an answer, not an outage: the endpoint is up.
@@ -93,19 +119,32 @@ lists every code with its layer, its class and the next move.
93
119
  ## Chain lookups
94
120
 
95
121
  `USE chain` asks the chain node for one known thing by its key. It serves the
96
- lookups that `chain_admission.lookups` lists: `Transaction` by `hash`, `Block`
97
- by `height` or `hash`, and `Head`, which takes no key. No lookup takes an
98
- address: `:Address` is a topology and a facts label, never a chain one. A lookup
99
- is one node with literal keys in braces and a `RETURN` of `var.property` items.
100
- It takes no `WHERE`, no relationship and no range. The properties of each lookup
101
- are named in the refusal of an unknown property.
122
+ lookups that `chain_admission.lookups` lists: `Transaction` by `tx_id`, `Block`
123
+ by `block_height` or `block_hash` (exactly one), `Address` by `address` (with an
124
+ optional `at_block`), and `Head`, which takes no key. A lookup is one node with
125
+ literal keys in braces and a `RETURN` of `var.property` items. It takes no
126
+ `WHERE`, no relationship and no range. The names `hash` and `height` are no key
127
+ and no property of any label: they are refused like any name a label lacks. The
128
+ properties of each lookup are named in the refusal of an unknown property, and
129
+ `meta_schema` lists the lookups the network serves.
102
130
 
103
131
  A transaction, with its result and the day it was mined:
104
132
 
105
133
  ```bash
106
134
  cia mcp call graph_query \
107
135
  network=robinhood \
108
- 'query=USE chain MATCH (t:Transaction {hash: "0x..."}) RETURN t.status, t.block_height, t.block_date'
136
+ 'query=USE chain MATCH (t:Transaction {tx_id: "0x..."}) RETURN t.status, t.block_height, t.block_date'
137
+ ```
138
+
139
+ An address at the newest block, and at a past block:
140
+
141
+ ```bash
142
+ cia mcp call graph_query \
143
+ network=robinhood \
144
+ 'query=USE chain MATCH (a:Address {address: "0x..."}) RETURN a.balance, a.nonce, a.is_contract'
145
+ cia mcp call graph_query \
146
+ network=robinhood \
147
+ 'query=USE chain MATCH (a:Address {address: "0x...", at_block: 79000000}) RETURN a.balance, a.nonce, a.is_contract'
109
148
  ```
110
149
 
111
150
  The head of the chain, and how far the graph and the warehouse are behind it:
@@ -113,11 +152,16 @@ The head of the chain, and how far the graph and the warehouse are behind it:
113
152
  ```bash
114
153
  cia mcp call graph_query \
115
154
  network=robinhood \
116
- 'query=USE chain MATCH (h:Head) RETURN h.height, h.age_seconds, h.warehouse_blocks_behind, h.graph_blocks_behind'
155
+ 'query=USE chain MATCH (h:Head) RETURN h.block_height, h.age_seconds, h.warehouse_blocks_behind, h.graph_blocks_behind'
117
156
  ```
118
157
 
119
- A chain lookup that comes back `chain_unavailable` means the chain layer is off
120
- or behind: read `chain_admission.status`. Topology and facts still work.
158
+ `block_timestamp` is an integer number of epoch milliseconds on every layer,
159
+ chain included. `network` is served on every label, from the query's `network`.
160
+ An `Address` lookup reads the kind from `is_contract`, `nonce` and
161
+ `delegated_to`, and a past block must be at least
162
+ `chain_admission.at_block_min_depth` blocks below the tip. A chain lookup that
163
+ comes back `chain_unavailable` means the chain layer is off or behind: read
164
+ `chain_admission.status`. Topology and facts still work.
121
165
 
122
166
  ## Swaps, liquidity pools and bridges
123
167
 
@@ -279,9 +323,9 @@ and `:Sanctioned`. Each role also has a flag on the node.
279
323
  labels, risk, lifetime metrics, and `FLOWS_TO`/`LINKED` relationships
280
324
  belong to `USE topology`.
281
325
  - Use `USE chain` for the chain node's own record of one known transaction,
282
- block or the head. It looks up one key and never searches.
283
- - A facts read names an address pair with one day, or one `tx_id` (the `0x`
284
- transaction hash on EVM networks). The pair is both endpoint addresses, from
326
+ block, address or the head. It looks up one key and never searches.
327
+ - A facts read names an address pair with one day. A transaction is read on
328
+ `USE chain` by its `tx_id`, not on facts. The pair is both endpoint addresses, from
285
329
  then to, and the day is a `block_date` equality. `block_timestamp` bounds in
286
330
  epoch milliseconds may narrow the day to a time window. One address, a day
287
331
  alone, a window of days, a block range and a bare `LIMIT` are not enough. A
@@ -518,10 +562,10 @@ statements of the graph (`CALL db.labels()`, `CALL db.relationshipTypes()`,
518
562
  each main link, through one session, and keeps the answer for 24 hours. Fields
519
563
  come from a sample, so a rare field may be missing. The probes below are the
520
564
  fallback. The current public Chain Insights Graph network is
521
- the single robinhood network; the network argument selects the graph, and
522
- the address-space split lives on the `:Address.network` node property. Do not
523
- infer support for unadvertised networks from internal database names or
524
- historical examples.
565
+ the single robinhood network; the network argument selects the graph.
566
+ `network` is the query's network on every node and relationship, never stored,
567
+ except on `:Chain`. Do not infer support for unadvertised networks from
568
+ internal database names or historical examples.
525
569
 
526
570
  Useful schema probes:
527
571
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chain-insights",
3
- "version": "0.45.0",
3
+ "version": "0.46.2",
4
4
  "description": "AML investigation CLI and MCP proxy for blockchain risk screening and Chain Insights Graph access",
5
5
  "homepage": "https://chain-insights.ai",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: chain-insights-cypher
3
- description: Use when answering any question about addresses, money flows, transfers, transactions or blocks with Chain Insights, when tracing where money went or came from, when writing or reviewing graph_query or graph_query_batch ISO GQL, when choosing between USE topology, USE facts and USE chain, or when a graph query is refused. Address, unit, layer, graph search and refusal rules, with one query for each kind of search.
3
+ description: Load this before the first graph_query or graph_query_batch call: without it, most queries are refused. Use when answering any question about addresses, money flows, transfers, transactions or blocks with Chain Insights, when tracing where money went or came from, when writing or reviewing graph_query or graph_query_batch ISO GQL, when choosing between USE topology, USE facts and USE chain, or when a graph query is refused. Address, unit, layer, graph search and refusal rules, with one query for each kind of search.
4
4
  ---
5
5
 
6
6
  # Chain Insights Cypher
@@ -8,7 +8,8 @@ description: Use when answering any question about addresses, money flows, trans
8
8
  Standard ISO GQL for `graph_query` and `graph_query_batch`. Read-only. No
9
9
  `CALL`, no procedures, no vendor syntax.
10
10
 
11
- Run a query with `cia mcp call graph_query network=robinhood "query=<query>"`.
11
+ In an MCP host, such as Claude Desktop, call the `graph_query` tool with `network` and `query`; `graph_query_batch` takes `queries`. There is no shell. In a shell, run a query with
12
+ `cia mcp call graph_query network=robinhood "query=<query>"`.
12
13
  Send related reads in one call with `cia mcp call graph_query_batch network=robinhood 'queries=[{"id":"a","query":"<query>"}]'`.
13
14
  Always pass `network` and your own `LIMIT`. These two commands, and
14
15
  `cia network robinhood --schema` for field names, are all you need. Do not run
@@ -19,12 +20,11 @@ Always pass `network` and your own `LIMIT`. These two commands, and
19
20
  1. Write every address in full, in lowercase: `0x` plus 40 hex characters, 42
20
21
  in all. Never shorten one with `...` or `…`, in a query or in an answer.
21
22
  Copy addresses from results exactly.
22
- 2. Timestamps on topology and facts are integer milliseconds since the epoch,
23
- UTC. Compute them from the current date: a day is 86,400,000 ms. Never
24
- compare one with an ISO string. `block_date` is the string `"YYYY-MM-DD"`.
25
- `USE chain` returns `block_timestamp` as an ISO string: read it, never
26
- filter on it. Every `*_usd` field is US dollars. `*_raw` is the token's
27
- smallest unit, not dollars.
23
+ 2. Timestamps on every layer are integer milliseconds since the epoch, UTC,
24
+ and `USE chain` is no exception. Compute them from the current date: a day
25
+ is 86,400,000 ms. Never compare one with an ISO string. `block_date` is the
26
+ string `"YYYY-MM-DD"`. Every `*_usd` field is US dollars. `*_raw` is the
27
+ token's smallest unit, not dollars.
28
28
  3. A shortened address, a mixed-case address, or an ISO string against a
29
29
  millisecond column gives zero rows with no error. Check these first. Zero
30
30
  rows is not proof of safety.
@@ -37,20 +37,56 @@ Always pass `network` and your own `LIMIT`. These two commands, and
37
37
 
38
38
  ## Pick the layer first
39
39
 
40
- Route by what you know: topology searches, while facts and chain look up one known thing.
40
+ Route by what the question names: an address, a pair with a day, a transaction or a block. Topology searches outward from an address, while facts and chain look up one known thing.
41
41
 
42
- 1. I do not know the thing yet: `USE topology`.
43
- 2. I know the pair and the day, or the transaction hash, and want the indexed rows: `USE facts`.
44
- 3. I want the chain's own record of a transaction by its hash, a block by its number or hash, or the head: `USE chain`.
42
+ 1. I know an address and want its links, senders, receivers, hops or a route: `USE topology`, anchored on that address.
43
+ 2. I know both addresses of a pair and one day, and want the rows of that day: `USE facts`.
44
+ 3. I want the chain's own record of a transaction by its `tx_id`, a block by its `block_height` or `block_hash`, an address at one block (`Address`), or the head: `USE chain`.
45
45
 
46
- `USE chain` has no address lookup. Find the address on `USE topology`, then read the pair and one day on `USE facts`.
47
- When two fit, as with a hash: ask `USE chain` first for the record and the result, then `USE facts` for the transfers it caused.
48
- The layers hand each other keys. Topology gives the pair and the first and last seen time. Chain gives the `block_date` of a hash or a height. Facts gives the `tx_id`.
49
- A list, a range or a whole-chain question is served on no layer. Say so, and go back to an anchored `USE topology` search.
46
+ `USE chain` with `Address` reads the balance, the nonce or the kind of one address, now or at a past block with `at_block`. A past block must be at least `chain_admission.at_block_min_depth` blocks below the tip. To find the counterparties of an address, search `USE topology`, then read the pair and one day on `USE facts`.
47
+ A transaction is a chain question: read it on `USE chain` by its `tx_id`. Chain gives its `block_date`; the transfers between a pair on that day are a `USE facts` read.
48
+ The layers hand each other keys, and each key keeps its name. Topology gives the pair and the first and last seen time. Facts gives the `tx_id` and the `block_height`. Chain takes an `address`, a `tx_id`, a `block_height` or a `block_hash`, and gives the `block_date` of a transaction or a block.
49
+ A list, a range or a whole-chain question names no address, pair, transaction or block, so no layer serves it today. Say so in one line, ask the user for one of those, and send no workaround query.
50
50
 
51
- A top list, a count, a scan of a label or the recent active addresses of the
52
- whole chain are such questions. Do not search for a workaround. Say: "The graph
53
- cannot list those. Name an address, a pair with a day, or a transaction hash."
51
+ Find the question in this table. Send its first query, once, with the addresses the user gave.
52
+
53
+ | The user asks | Layer | First query |
54
+ | ------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------- |
55
+ | Recent activity or newest links of one address | `topology` | Newest links of one address (see Graph searches) |
56
+ | Who sent money to an address? Who received from it? | `topology` | Newest links of one address, the arrow flipped for senders |
57
+ | Where did the money go, or come from, in N hops? | `topology` | Where the money went, or came from (`{0,N-1}`, N up to 5) |
58
+ | Is there a route between two addresses? | `topology` | Route between two addresses, `SHORTEST 1` |
59
+ | What do two addresses have in common? | `topology` | Counterparties that two addresses share |
60
+ | Biggest senders or receivers of one address | `topology` | Newest links of one address, `ORDER BY f.amount_usd_sum DESC LIMIT 10` |
61
+ | How many, how much, first or last active, for one address | `topology` | Profile of one address |
62
+ | The transfers of a pair on one day | `facts` | `TRANSFER` of the pair and the day, see Facts |
63
+ | What happened in this transaction hash? | `chain` | `Transaction {tx_id}`, see Chain |
64
+ | The latest block, block N, how far behind the graph is | `chain` | `Head`, or `Block {block_height}` |
65
+ | What fields does an Address have? | none | Call `meta_schema {network}`. Send no query. |
66
+ | Recent addresses, biggest senders, any top list of the chain | none today | Send no query. Say the line below. |
67
+
68
+ For a question of the last row, do not probe, scan or try another layer: a probe returns arbitrary rows, never the newest or the biggest. Say: "The graph cannot list the whole chain yet. Give me an address, a pair with a day, or a transaction hash, and I will show its recent links and flows."
69
+
70
+ ## One name on every layer
71
+
72
+ A thing has one name on topology, facts and chain. Copy a value from one layer into the next as it is. The layers differ by properties only.
73
+
74
+ | Thing | `USE topology` | `USE facts` | `USE chain` |
75
+ | ---------- | --------------------------------------------- | ----------------------------- | ---------------------------------------------- |
76
+ | An address | `:Address {address}`, and a kind label | `:Address {address}`, no kind | `Address {address}`, optional `at_block` |
77
+ | A kind | `:Account` or `:Contract`, `is_contract` kept | none: refused with a hint | `is_contract`, `nonce` and `delegated_to` |
78
+ | A tx | `tx_id` on `DEPLOYED_CONTRACT` only | `tx_id` on every row | `Transaction {tx_id}`, see Chain |
79
+ | A block | `block_height` on `DEPLOYED_CONTRACT` only | `block_height` on every row | `Block {block_height}` or `Block {block_hash}` |
80
+ | Time | epoch milliseconds | epoch milliseconds | epoch milliseconds |
81
+ | `network` | the query's `network` | the query's `network` | the query's `network` |
82
+
83
+ `block_timestamp` is an integer number of epoch milliseconds on every layer, chain included. `block_date` is a UTC day, `YYYY-MM-DD`. The old chain names `hash` and `height` are gone: they are refused like any name a label lacks.
84
+
85
+ `network` is a property of every node and every relationship, and its value is the query's `network`. It is never stored, except on a `:Chain` node, where it names the remote chain. A filter `x.network = "robinhood"` keeps every row, and a filter on another value gives no rows. It never narrows a scan: anchor on an address instead.
86
+
87
+ A kind is a second label that topology takes from a chain fact: `:Account` for a key holder, `:Contract` for a contract (`:Contract:SmartAccount` for a contract wallet). Never both on one address. An address that only received has none, and so does a Nitro precompile on topology (chain reads it as a contract). A role label such as `:Pool` stays beside the kind. `is_contract` stays beside `:Contract`. Facts serves no kind: a facts read that names `:Account` or `:Contract` is refused, so ask `USE topology`, or `USE chain` for `is_contract` and `nonce`.
88
+
89
+ A topology link is a lifetime summary of a pair, and a facts row is one event. They are two things, so they keep two names: `SWAPPED`, `ADDED_LIQUIDITY` and `REMOVED_LIQUIDITY` on topology, `SWAP`, `LIQUIDITY_ADD` and `LIQUIDITY_REMOVE` on facts. Read the summary on topology, anchored on an address, then the events behind it on facts, by the pair and one day.
54
90
 
55
91
  ## Topology
56
92
 
@@ -58,16 +94,39 @@ cannot list those. Name an address, a pair with a day, or a transaction hash."
58
94
  bridges and the `LINKED` overlay. The server checks these before it runs:
59
95
 
60
96
  - Anchor every pattern on a full address: `{address: "<address>"}` in the node, or `WHERE a.address = "<address>"`. A list of at most 25 addresses is an anchor. An address in `RETURN`, in an `OR` or in a `$parameter` is no anchor.
61
- - The one read with no anchor is a probe: one node or one hop, no `WHERE`, no `ORDER BY`, no aggregate, and a small `LIMIT`. Its rows are arbitrary.
97
+ - The one read with no anchor is a probe: one node or one hop, no `WHERE`, no `ORDER BY`, no aggregate, and a small `LIMIT`. Its rows are arbitrary: never call them recent, top or biggest.
62
98
  - A sort, an aggregate, `DISTINCT`, `collect()` or a filter on a link property needs an anchor on every pattern.
63
99
  - End every read with a literal `LIMIT`: at most 5,000 rows, 100 for a probe. A path has at most 5 hops and a query at most 8. The live numbers are in `topology_admission` (`cia network robinhood --json`).
64
100
  - Paths use bounded quantifiers such as `-[:FLOWS_TO]-{1,5}`. A `*` range, the legacy path functions, `PROFILE`, writes and `CALL` are refused.
65
101
  - Role flags `is_exchange`, `is_scam`, `is_victim` and `is_sanctioned` are absent unless true: use `IS NULL` or `IS NOT NULL`, never `= false`.
102
+ - A kind label sits beside `:Address`: `WHERE a:Contract` or `WHERE a:Account` tests it, as `:Pool` is tested. It is no anchor.
66
103
 
67
104
  ## Graph searches
68
105
 
69
106
  Each query below is admitted by the live server. Change only the addresses and the `RETURN`. Keep the guards inside the pattern: they apply the [pool trace rule](#pool-trace-rule), and a `WHERE` after a `SHORTEST` pattern runs too late. A walk follows links, not coins: confirm a hop with a pair and a day on `USE facts`.
70
107
 
108
+ Newest links of one address, its receivers. Its newest senders are the same read with the arrow flipped:
109
+
110
+ ```cypher
111
+ USE topology
112
+ MATCH (a:Address {address: "0x04911a118f11c75667e4d0dfb8e640af5a353550"})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool
113
+ RETURN a.address AS from_address, b.address AS to_address, f.amount_usd_sum AS amount_usd_sum, f.tx_count AS tx_count, f.last_seen_timestamp AS last_seen_timestamp ORDER BY f.last_seen_timestamp DESC LIMIT 25
114
+ ```
115
+
116
+ ```cypher
117
+ USE topology
118
+ MATCH (b:Address)-[f:FLOWS_TO]->(a:Address {address: "0x7e3702e9dfaa847f9829a258f1e26fa431160662"}) WHERE NOT a:Pool
119
+ RETURN b.address AS from_address, a.address AS to_address, f.amount_usd_sum AS amount_usd_sum, f.tx_count AS tx_count, f.last_seen_timestamp AS last_seen_timestamp ORDER BY f.last_seen_timestamp DESC LIMIT 25
120
+ ```
121
+
122
+ Profile of one address: counterparties, transactions, volume and the active window, all from the node. Timestamps are milliseconds.
123
+
124
+ ```cypher
125
+ USE topology
126
+ MATCH (a:Address {address: "0x04911a118f11c75667e4d0dfb8e640af5a353550"})
127
+ RETURN a.degree_in AS senders, a.degree_out AS receivers, a.tx_total_count AS tx_count, a.total_volume_usd AS volume_usd, a.first_activity_timestamp AS first_active, a.last_activity_timestamp AS last_active LIMIT 1
128
+ ```
129
+
71
130
  Route between two addresses, one path. Only `MATCH SHORTEST 1` and `MATCH ANY SHORTEST` are served: a count above 1 is `route_search_refused`.
72
131
 
73
132
  ```cypher
@@ -180,17 +239,19 @@ USE topology
180
239
  MATCH ()-[r:FLOWS_TO]->() RETURN keys(r) AS keys LIMIT 5
181
240
  ```
182
241
 
183
- Name another link type to read its keys: `SWAPPED`, `LINKED`, `OPERATED_BY`, `BRIDGED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY`. `synced_through_height` and `pair_key` are bookkeeping: never filter or sort on them.
242
+ Name another link type to read its keys: `SWAPPED`, `LINKED`, `OPERATED_BY`, `BRIDGED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY`. The keys of an address and of a link include `network`: it is the query's `network`, computed on every node and link. `synced_through_height` and `pair_key` are bookkeeping: never filter or sort on them.
184
243
 
185
244
  The server serves five read-only catalog calls: `CALL db.labels()`, `CALL db.relationshipTypes()`, `CALL db.propertyKeys()`, `CALL db.schema.visualization()` and `SHOW INDEXES`. `meta_schema` sends four of them for you through one session and keeps the answer, so do not send them yourself.
186
245
 
187
246
  ## Facts: an address pair with one day
188
247
 
189
- `USE facts` looks up rows you already know. A facts read names an address pair with one day, or one `tx_id`. A pair is both addresses, from then to, each as `{address: "<address>"}`, and the day is `t.block_date = "YYYY-MM-DD"`. One address, a day alone, a window of days, a block range and a bare `LIMIT` are not enough. It serves `TRANSFER`, `SWAP`, `LIQUIDITY_ADD`, `LIQUIDITY_REMOVE` and `BRIDGE_CROSSING` rows.
248
+ `USE facts` looks up rows you already know. A facts read names an address pair with one day. A transaction is read on `USE chain`, not here. A pair is both addresses, from then to, each as `{address: "<address>"}`, and the day is `t.block_date = "YYYY-MM-DD"`. One address, a day alone, a window of days, a block range and a bare `LIMIT` are not enough. It serves `TRANSFER`, `SWAP`, `LIQUIDITY_ADD`, `LIQUIDITY_REMOVE` and `BRIDGE_CROSSING` rows.
190
249
 
191
250
  - A reply holds at most 200 rows, whatever the `LIMIT`.
192
251
  - A facts read has one relationship and no hop. For a walk, go to topology.
193
252
  - A facts read takes no `ORDER BY`. Rows come in the server's own order. Sort the page yourself.
253
+ - Every row and both endpoints carry `network`, the query's `network`. A filter on it is no anchor: it never replaces the pair and the day.
254
+ - Facts serves no kind. A read that names `:Account` or `:Contract` is refused with `facts_no_anchor`: ask `USE topology` for the kind, or `USE chain` for `is_contract` and `nonce`.
194
255
  - A facts read covers one day. For more days, send one read for each day.
195
256
  - Amounts and USD values come back as text, such as `"5.23661492"`. Convert them before you add them.
196
257
  - `TRANSFER` columns: `tx_id`, `block_date`, `block_height`, `block_timestamp`, `event_index`, `edge_index`, `kind`, `asset_contract`, `asset_symbol`, `amount`, `amount_usd`, `price_usd`, `price_missing`, `token_id`, `token_standard`, `operator_address`, `raw_amount` and `decimals`. `kind` is `token`, `native` or `internal`: an `internal` row is ETH a contract sends while it runs a call.
@@ -201,33 +262,41 @@ MATCH (a:Address {address: "0x31a817802ee183eb8b13167ffe24bd28dcc6f30c"})-[t:TRA
201
262
  RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.asset_symbol AS asset_symbol, t.amount AS amount, t.amount_usd AS amount_usd, t.kind AS kind LIMIT 50
202
263
  ```
203
264
 
204
- One transaction hash needs no day. The server finds it:
265
+ ## Chain: one key, one node
266
+
267
+ `USE chain` asks the chain node for one known thing: `Transaction` by `tx_id`, `Block` by `block_height` or `block_hash` (exactly one), `Address` by `address` (with an optional `at_block`), or `Head`. One node, literal keys in braces, a `RETURN` of `var.property` items, no `WHERE`, no range. Read its limits in `chain_admission` (`cia network robinhood --json`).
205
268
 
206
269
  ```cypher
207
- USE facts
208
- MATCH (a:Address)-[t:TRANSFER]->(b:Address) WHERE t.tx_id = "0x044587122970de1e3c377a8ed7ab56a49c777a2b7441a2f5d9ca32dbab9fbe71"
209
- RETURN a.address AS from_address, b.address AS to_address, t.amount AS amount, t.kind AS kind LIMIT 50
270
+ USE chain
271
+ MATCH (t:Transaction {tx_id: "0x044587122970de1e3c377a8ed7ab56a49c777a2b7441a2f5d9ca32dbab9fbe71"}) RETURN t.status, t.block_height, t.block_date
210
272
  ```
211
273
 
212
- ## Chain: one key, one node
274
+ ```cypher
275
+ USE chain
276
+ MATCH (b:Block {block_height: 79841521}) RETURN b.block_hash, b.block_date
277
+ ```
213
278
 
214
- `USE chain` asks the chain node for one known thing: `Transaction` by `hash`, `Block` by `height` or `hash`, or `Head`. One node, literal keys in braces, a `RETURN` of `var.property` items, no `WHERE`, no range. Read its limits in `chain_admission` (`cia network robinhood --json`).
279
+ An address at the newest block, and at a past block:
215
280
 
216
281
  ```cypher
217
282
  USE chain
218
- MATCH (t:Transaction {hash: "0x044587122970de1e3c377a8ed7ab56a49c777a2b7441a2f5d9ca32dbab9fbe71"}) RETURN t.status, t.block_height, t.block_date
283
+ MATCH (a:Address {address: "0x04911a118f11c75667e4d0dfb8e640af5a353550"}) RETURN a.balance, a.nonce, a.is_contract
219
284
  ```
220
285
 
221
286
  ```cypher
222
287
  USE chain
223
- MATCH (b:Block {height: 79841521}) RETURN b.hash, b.block_date
288
+ MATCH (a:Address {address: "0x04911a118f11c75667e4d0dfb8e640af5a353550", at_block: 79000000}) RETURN a.balance, a.nonce, a.is_contract
224
289
  ```
225
290
 
226
291
  ```cypher
227
292
  USE chain
228
- MATCH (h:Head) RETURN h.height, h.age_seconds, h.warehouse_blocks_behind, h.graph_blocks_behind
293
+ MATCH (h:Head) RETURN h.block_height, h.age_seconds, h.warehouse_blocks_behind, h.graph_blocks_behind
229
294
  ```
230
295
 
296
+ An `Address` lookup always answers one row, because every address has a state. `a.balance` is decimal text. Read the kind from `a.is_contract`, `a.nonce` and `a.delegated_to`: `a.is_contract` is `true` for a contract and null for any other address, a `nonce` above 0 with `a.is_contract` null is an account, and `a.delegated_to` names the target of an EIP-7702 delegation, which is an account. `a.code_size` is the size of the code, and the code itself is never returned. `:Account` and `:Contract` are never a chain lookup label.
297
+
298
+ `at_block` must be at least `chain_admission.at_block_min_depth` blocks below the tip. A nearer block is refused with `chain_block_out_of_range` and the rule `at_block_near_tip`: leave `at_block` out to read the newest block. The names `hash` and `height` are no key and no property of any label.
299
+
231
300
  ## When a query is refused
232
301
 
233
302
  A refusal is an answer, not an outage. Act on its class.
@@ -257,12 +326,4 @@ Claude Desktop draws a `graph_query` answer from its column names. Alias the
257
326
 
258
327
  A graph also reads these optional columns: `amount_usd_sum`, `tx_count`, `first_seen_timestamp`, `last_seen_timestamp`, `link_kind`, `from_labels` and `to_labels`. In `graph_query_batch`, each query draws its own tab. Claude Code draws nothing and shows the rows as text.
259
328
 
260
- A graph, the newest receivers of one address. Flip the arrow for its senders:
261
-
262
- ```cypher
263
- USE topology
264
- MATCH (a:Address {address: "0x04911a118f11c75667e4d0dfb8e640af5a353550"})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool
265
- RETURN a.address AS from_address, b.address AS to_address, f.amount_usd_sum AS amount_usd_sum, f.tx_count AS tx_count, f.last_seen_timestamp AS last_seen_timestamp ORDER BY f.last_seen_timestamp DESC LIMIT 25
266
- ```
267
-
268
- A chart is the facts read of one pair over one day above, with `block_timestamp` and `amount_usd` in the `RETURN`.
329
+ A graph is the newest-links read of one address, receivers or senders, in Graph searches. A chart is the facts read of one pair over one day above, with `block_timestamp` and `amount_usd` in the `RETURN`.