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.
- package/README.md +8 -7
- package/dist/capabilities-Cur8x6c7.mjs.map +1 -1
- package/dist/mcp-proxy.cjs +31 -18
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +31 -18
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/public-tools-CZyeptJQ.mjs.map +1 -1
- package/docs/graph-query-compatibility.md +56 -46
- package/docs/graph-tools.md +78 -34
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +101 -40
|
@@ -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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
--
|
|
50
|
-
USE topology MATCH (a:Address) RETURN a.address AS address LIMIT
|
|
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
|
-
--
|
|
53
|
-
USE topology MATCH (a:Address
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
the
|
|
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
|
-
→
|
|
73
|
+
RETURN a.network AS from_network, t.network AS row_network
|
|
74
|
+
→ from_network = robinhood, row_network = robinhood
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
151
|
-
|
|
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.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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**:
|
package/docs/graph-tools.md
CHANGED
|
@@ -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
|
|
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
|
|
41
|
-
2. I know
|
|
42
|
-
3. I want the chain's own record of a transaction by its
|
|
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`
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
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 `
|
|
97
|
-
by `
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
are
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
284
|
-
|
|
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
|
|
522
|
-
the
|
|
523
|
-
infer support for unadvertised networks from
|
|
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-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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
compare one with an ISO string. `block_date` is the
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
43
|
-
2. I know
|
|
44
|
-
3. I want the chain's own record of a transaction by its
|
|
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`
|
|
47
|
-
|
|
48
|
-
The layers hand each other keys. Topology gives the pair and the first and last seen time.
|
|
49
|
-
A list, a range or a whole-chain question
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
208
|
-
MATCH (
|
|
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
|
-
|
|
274
|
+
```cypher
|
|
275
|
+
USE chain
|
|
276
|
+
MATCH (b:Block {block_height: 79841521}) RETURN b.block_hash, b.block_date
|
|
277
|
+
```
|
|
213
278
|
|
|
214
|
-
|
|
279
|
+
An address at the newest block, and at a past block:
|
|
215
280
|
|
|
216
281
|
```cypher
|
|
217
282
|
USE chain
|
|
218
|
-
MATCH (
|
|
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 (
|
|
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.
|
|
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
|
|
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`.
|