chain-insights 0.61.0 → 0.63.0

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.
@@ -222,7 +222,7 @@ const ROUTING_HANDOFF = "The layers hand each other keys, and each key keeps its
222
222
  * A whole-chain question is a time-bounded topology search, sent only when the
223
223
  * family of its link type is close to the head; a refusal names the family.
224
224
  */
225
- const ROUTING_NOT_SERVED = "A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. Read `Head` first (`USE chain MATCH (h:Head) RETURN h.behind_money, h.behind_dex`, under a second): a `FLOWS_TO` search is sent when `behind_money` is at most 100000; a `SWAPPED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY` search when `behind_dex` is at most 100000 (100000 blocks is a few hours: a robinhood block is about a tenth of a second apart from the next). When the family is further behind, the search cannot finish, so do not send it: say which family, how many blocks, and ask for an address. On a server whose Head has no `behind_money`, read `h.graph_blocks_behind` and hold it to the same limit. Never turn the block count into days or hours by head. To say how fresh the data is, give the newest `_utc` time you read on the layer you used: a family's lag counts its slowest layer, and the links of an address and the facts rows are often newer than it. If the server refuses the search with `anchor_missing` or `family_behind`, or stops it with `query_timeout`, it cannot search the whole chain now: say so in one line, name the family the refusal names, and ask for an address or drop the link type that is behind. A narrower window times out the same way, so do not send it again.";
225
+ const ROUTING_NOT_SERVED = "A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. Read `Head` first (`USE chain MATCH (h:Head) RETURN h.behind_money, h.behind_dex, h.behind_transfers`, under a second): a `FLOWS_TO` search is sent when `behind_money` is at most 100000; a `SWAPPED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY` search when `behind_dex` is at most 100000; a `TRANSFERRED` search when `behind_transfers` is at most 100000 (100000 blocks is a few hours: a robinhood block is about a tenth of a second apart from the next). When the family is further behind, the search cannot finish, so do not send it: say which family, how many blocks, and ask for an address. On a server whose Head has no `behind_money`, read `h.graph_blocks_behind` and hold it to the same limit. On a server whose Head has no `behind_transfers`, it serves no `TRANSFERRED` link: send no `TRANSFERRED` search. Never turn the block count into days or hours by head. To say how fresh the data is, give the newest `_utc` time you read on the layer you used: a family's lag counts its slowest layer, and the links of an address and the facts rows are often newer than it. If the server refuses the search with `anchor_missing` or `family_behind`, or stops it with `query_timeout`, it cannot search the whole chain now: say so in one line, name the family the refusal names, and ask for an address or drop the link type that is behind. A narrower window times out the same way, so do not send it again.";
226
226
  /** What a refusal carries, and which part of it decides the move. */
227
227
  const MOVE_INTRO = "A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.";
228
228
  /** The move for each class of the refusal envelope. */
@@ -830,7 +830,7 @@ function resolveMcpProxyMode(env = process.env) {
830
830
  if (raw === "no-workspace" || raw === "workspace-less") return "stateless";
831
831
  throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got "${raw}"`);
832
832
  }
833
- const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
833
+ const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool, Token and NFT labels, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
834
834
  const TOOL_RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once before this tool. For a graph, alias the columns from_address and to_address; the host draws the picture from the answer itself. Never draw it again with another tool, such as a visualize or widget tool.";
835
835
  const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
836
836
  meta_network_capabilities: "Return the current Chain Insights network and tool support matrix. Takes no arguments: send {}.",
@@ -894,7 +894,8 @@ const GRAPH_SCHEMA_HINTS = [
894
894
  "- (:Address)-[:OPERATED_BY]->(:Address) is the directed owner-to-operator edge: the transaction sender that moved the owner's tokens (ERC-20/721), or the event operator (ERC-1155); not the approved spender. Aggregate properties: tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp, and optional token_standard (ERC20/ERC721/ERC1155; absent when the pair is mixed-standard). Valuation fields: valuation_tracked_count, valued_count and the reason counters missing_valuation_price_count, unknown_quantity_count, unrepresentable_quantity_count and usd_range_count. valuation_complete is true when valued_count equals valuation_tracked_count, valuation_coverage_ratio is valued_count divided by valuation_tracked_count, and amount_usd_sum counts a transfer with no USD value as 0, so it is a floor unless valuation_complete is true. One edge per owner/operator pair; direct transfers with no operator create none; an owner never operates for itself. Topology-only, and a topology fact — not a risk label. Probe: MATCH (owner:Address)-[operation:OPERATED_BY]->(operator:Address {address: $addr}) RETURN owner.address AS owner, operation.tx_count AS tx_count ORDER BY operation.tx_count DESC LIMIT 10.",
895
895
  "- FLOWS_TO properties are tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp. pair_key and synced_through_height are internal sync bookkeeping, not to be queried. synced_through_height is also on the other summed links (SWAPPED, OPERATED_BY, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED, APPROVED, SPONSORED, BUNDLED, SIGNED_FOR) and pair_key on SWAPPED: bookkeeping there too. Tx ids of every transfer, internal ones included, come from USE facts TRANSFER. Averages come from inline arithmetic. FLOWS_TO into and out of pools is kept; the pool trace rule below governs a trace that reaches one.",
896
896
  "- Swaps, liquidity and bridges on USE topology are lifetime totals per pair; single events are USE facts rows. (:Address)-[:SWAPPED]->(:Address) joins a swap payer to a different recipient, one edge per payer, recipient, sold_asset and bought_asset, with swap_count, sold_amount_raw, bought_amount_raw, sold_usd, bought_usd, pools, families, strength, first_seen_height and last_seen_height. sold_usd and bought_usd sum the routes on the edge at the day price: a route side with no price adds 0, so 0 can mean no price. Do not read 0 as worth nothing. The USE facts SWAP row says which side had no price (sold_price_missing, bought_price_missing). strength is swap (the whole route is proven: payer, recipient, assets, exact raw amounts and conservation) or swap_like (the shape is a swap but the pool code is not proven). Today no route is swap: the swap reader reads transaction receipts only, with no execution trace, so every served route is swap_like, with reason unknown_pool_code and families unknown. A Uniswap V2 or V3 swap reads this way, and unknown does not mean the protocol is unsupported. A filter on strength = \"swap\" or on a known family matches nothing. A route that could not be paired (swap_unsplit) is never served: it has no payer, recipient or pool, makes no SWAPPED edge and no SWAP row, and exists in the warehouse only. Every Uniswap v4 swap is swap_unsplit today, by design, so v4 swaps are hidden. A self swap makes no edge, and a missing SWAPPED edge is not proof that no swap happened. Swap attribution is read from SWAPPED, the aggregate (strength, pools, families), or from the USE facts SWAP row, one route. FLOWS_TO carries value only.",
897
- "- Pool is a second label on an Address: the pool of a swap route or liquidity event, with liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.",
897
+ "- Pool is a second label on an Address: a pool contract the DEX indexer knows, traded or not (a bonding curve or an empty pair is a Pool too; a singleton pool manager never is). Once there is liquidity, it also carries liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count; a pool known only to the DEX indexer has none of them. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.",
898
+ "- Token and NFT are second labels on an Address that mark a token contract: Token for every ERC20, ERC721, ERC1155 and ERC6909 contract the asset scan knows, NFT for the ERC721 and ERC1155 ones, and token_standard on the node says which. A token name from an explorer gives Token, not Protocol. They are marks, not stops: the pool trace rule and the guards below do not change.",
898
899
  "- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, with kinds, events, totals_raw, first_height, last_height and last_bridge_event_id. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWS_TO walk passes through it.",
899
900
  "- Pool trace rule, for every trace: 1. Enter a `:Pool` on any edge. 2. Leave a `:Pool` only on `REMOVED_LIQUIDITY`, to the address the liquidity was paid to. 3. Never leave a `:Pool` on `FLOWS_TO`. 4. Across a swap, follow `SWAPPED` from payer to recipient, between two different addresses. Do not walk through the pool. Rug-pull probe: MATCH (victim:Address {address: $addr})-[paid:FLOWS_TO]->(pool:Pool)-[removal:REMOVED_LIQUIDITY]->(receiver:Address) WHERE NOT victim:Pool AND NOT victim:Protocol AND NOT victim:System AND NOT victim.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND receiver.address <> victim.address RETURN pool.address AS pool_address, receiver.address AS receiver_address, removal.usd AS removed_usd, removal.receiver_added_usd AS receiver_added_usd, removal.receiver_provided AS receiver_provided LIMIT 25.",
900
901
  "- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.",
@@ -905,7 +906,7 @@ const GRAPH_SCHEMA_HINTS = [
905
906
  "- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20",
906
907
  "- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count LIMIT 20",
907
908
  "- Anchor every topology read that filters on a link property: put an address in its pattern. Without one, a read starts from every link of the type it names, and you should not count on the filter to narrow that: WHERE x.strength = 'swap' on SWAPPED checks every SWAPPED link, LIMIT stops the read only after enough rows match, and a filter that matches few or none can run to the 60 s topology limit and fail with query_timeout. Example: MATCH (a:Address {address: $addr})-[x:SWAPPED]->(b:Address) WHERE x.swap_count >= 2 RETURN b.address, x.swap_count LIMIT 25. Pick an address with few links: degree_out and degree_in are a rough guide, because they count neighbours, not links, and an address with hundreds of thousands of neighbours can fail the same way. Discovery probes with LIMIT and no filter stay valid. The queries of one batch share a 100 s budget; USE facts queries stop at 30 s.",
908
- "- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
909
+ "- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool, Token and NFT labels, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
909
910
  "- All graph_query calls are read-only. Never use CREATE, INSERT, MERGE, SET, DELETE, REMOVE, DROP, DETACH, ADD, CONNECT, DISCONNECT, ALTER, TRUNCATE, GRANT, or REVOKE.",
910
911
  "- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly."
911
912
  ].join("\n");
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAkDa,kBAAgB;qBAYhB,2BAAyB;qBAUzB;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA0GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
1
+ {"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAkDa,kBAAgB;qBAYhB,2BAAyB;qBAUzB;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA2GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAkDa,kBAAgB;qBAYhB,2BAAyB;qBAUzB;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA0GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
1
+ {"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAkDa,kBAAgB;qBAYhB,2BAAyB;qBAUzB;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA2GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
@@ -218,7 +218,7 @@ const ROUTING_HANDOFF = "The layers hand each other keys, and each key keeps its
218
218
  * A whole-chain question is a time-bounded topology search, sent only when the
219
219
  * family of its link type is close to the head; a refusal names the family.
220
220
  */
221
- const ROUTING_NOT_SERVED = "A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. Read `Head` first (`USE chain MATCH (h:Head) RETURN h.behind_money, h.behind_dex`, under a second): a `FLOWS_TO` search is sent when `behind_money` is at most 100000; a `SWAPPED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY` search when `behind_dex` is at most 100000 (100000 blocks is a few hours: a robinhood block is about a tenth of a second apart from the next). When the family is further behind, the search cannot finish, so do not send it: say which family, how many blocks, and ask for an address. On a server whose Head has no `behind_money`, read `h.graph_blocks_behind` and hold it to the same limit. Never turn the block count into days or hours by head. To say how fresh the data is, give the newest `_utc` time you read on the layer you used: a family's lag counts its slowest layer, and the links of an address and the facts rows are often newer than it. If the server refuses the search with `anchor_missing` or `family_behind`, or stops it with `query_timeout`, it cannot search the whole chain now: say so in one line, name the family the refusal names, and ask for an address or drop the link type that is behind. A narrower window times out the same way, so do not send it again.";
221
+ const ROUTING_NOT_SERVED = "A question about the whole chain (recent activity, the biggest senders, a top list) is a `USE topology` search: bound it by time with `f.last_seen_timestamp >= <epoch ms>` on the `FLOWS_TO` link, then sort and `LIMIT`. A search has 10 s. Read `Head` first (`USE chain MATCH (h:Head) RETURN h.behind_money, h.behind_dex, h.behind_transfers`, under a second): a `FLOWS_TO` search is sent when `behind_money` is at most 100000; a `SWAPPED`, `ADDED_LIQUIDITY` or `REMOVED_LIQUIDITY` search when `behind_dex` is at most 100000; a `TRANSFERRED` search when `behind_transfers` is at most 100000 (100000 blocks is a few hours: a robinhood block is about a tenth of a second apart from the next). When the family is further behind, the search cannot finish, so do not send it: say which family, how many blocks, and ask for an address. On a server whose Head has no `behind_money`, read `h.graph_blocks_behind` and hold it to the same limit. On a server whose Head has no `behind_transfers`, it serves no `TRANSFERRED` link: send no `TRANSFERRED` search. Never turn the block count into days or hours by head. To say how fresh the data is, give the newest `_utc` time you read on the layer you used: a family's lag counts its slowest layer, and the links of an address and the facts rows are often newer than it. If the server refuses the search with `anchor_missing` or `family_behind`, or stops it with `query_timeout`, it cannot search the whole chain now: say so in one line, name the family the refusal names, and ask for an address or drop the link type that is behind. A narrower window times out the same way, so do not send it again.";
222
222
  /** What a refusal carries, and which part of it decides the move. */
223
223
  const MOVE_INTRO = "A refused, killed, busy or failed query comes back with `error_detail`: `code`, `rule`, `class`, `fix` and `example`. The `class` decides your next move.";
224
224
  /** The move for each class of the refusal envelope. */
@@ -826,7 +826,7 @@ function resolveMcpProxyMode(env = process.env) {
826
826
  if (raw === "no-workspace" || raw === "workspace-less") return "stateless";
827
827
  throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got "${raw}"`);
828
828
  }
829
- const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
829
+ const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool, Token and NFT labels, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for the chain node's own record of one known transaction, block, address or the head.";
830
830
  const TOOL_RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once before this tool. For a graph, alias the columns from_address and to_address; the host draws the picture from the answer itself. Never draw it again with another tool, such as a visualize or widget tool.";
831
831
  const KNOWN_PUBLIC_TOOL_DESCRIPTIONS = {
832
832
  meta_network_capabilities: "Return the current Chain Insights network and tool support matrix. Takes no arguments: send {}.",
@@ -890,7 +890,8 @@ const GRAPH_SCHEMA_HINTS = [
890
890
  "- (:Address)-[:OPERATED_BY]->(:Address) is the directed owner-to-operator edge: the transaction sender that moved the owner's tokens (ERC-20/721), or the event operator (ERC-1155); not the approved spender. Aggregate properties: tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp, and optional token_standard (ERC20/ERC721/ERC1155; absent when the pair is mixed-standard). Valuation fields: valuation_tracked_count, valued_count and the reason counters missing_valuation_price_count, unknown_quantity_count, unrepresentable_quantity_count and usd_range_count. valuation_complete is true when valued_count equals valuation_tracked_count, valuation_coverage_ratio is valued_count divided by valuation_tracked_count, and amount_usd_sum counts a transfer with no USD value as 0, so it is a floor unless valuation_complete is true. One edge per owner/operator pair; direct transfers with no operator create none; an owner never operates for itself. Topology-only, and a topology fact — not a risk label. Probe: MATCH (owner:Address)-[operation:OPERATED_BY]->(operator:Address {address: $addr}) RETURN owner.address AS owner, operation.tx_count AS tx_count ORDER BY operation.tx_count DESC LIMIT 10.",
891
891
  "- FLOWS_TO properties are tx_count, amount_usd_sum, first_seen_timestamp, last_seen_timestamp. pair_key and synced_through_height are internal sync bookkeeping, not to be queried. synced_through_height is also on the other summed links (SWAPPED, OPERATED_BY, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED, APPROVED, SPONSORED, BUNDLED, SIGNED_FOR) and pair_key on SWAPPED: bookkeeping there too. Tx ids of every transfer, internal ones included, come from USE facts TRANSFER. Averages come from inline arithmetic. FLOWS_TO into and out of pools is kept; the pool trace rule below governs a trace that reaches one.",
892
892
  "- Swaps, liquidity and bridges on USE topology are lifetime totals per pair; single events are USE facts rows. (:Address)-[:SWAPPED]->(:Address) joins a swap payer to a different recipient, one edge per payer, recipient, sold_asset and bought_asset, with swap_count, sold_amount_raw, bought_amount_raw, sold_usd, bought_usd, pools, families, strength, first_seen_height and last_seen_height. sold_usd and bought_usd sum the routes on the edge at the day price: a route side with no price adds 0, so 0 can mean no price. Do not read 0 as worth nothing. The USE facts SWAP row says which side had no price (sold_price_missing, bought_price_missing). strength is swap (the whole route is proven: payer, recipient, assets, exact raw amounts and conservation) or swap_like (the shape is a swap but the pool code is not proven). Today no route is swap: the swap reader reads transaction receipts only, with no execution trace, so every served route is swap_like, with reason unknown_pool_code and families unknown. A Uniswap V2 or V3 swap reads this way, and unknown does not mean the protocol is unsupported. A filter on strength = \"swap\" or on a known family matches nothing. A route that could not be paired (swap_unsplit) is never served: it has no payer, recipient or pool, makes no SWAPPED edge and no SWAP row, and exists in the warehouse only. Every Uniswap v4 swap is swap_unsplit today, by design, so v4 swaps are hidden. A self swap makes no edge, and a missing SWAPPED edge is not proof that no swap happened. Swap attribution is read from SWAPPED, the aggregate (strength, pools, families), or from the USE facts SWAP row, one route. FLOWS_TO carries value only.",
893
- "- Pool is a second label on an Address: the pool of a swap route or liquidity event, with liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.",
893
+ "- Pool is a second label on an Address: a pool contract the DEX indexer knows, traded or not (a bonding curve or an empty pair is a Pool too; a singleton pool manager never is). Once there is liquidity, it also carries liquidity_added_usd, liquidity_removed_usd, liquidity_net_usd, provider_count and receiver_count; a pool known only to the DEX indexer has none of them. (:Address)-[:ADDED_LIQUIDITY]->(:Pool) and (:Pool)-[:REMOVED_LIQUIDITY]->(:Address) join named providers and receivers to the pool, with event_count, totals_raw, usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw, receiver_added_usd (USD the receiver itself added to the same pool) and receiver_provided. A receiver's profit from a pool is usd minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.",
894
+ "- Token and NFT are second labels on an Address that mark a token contract: Token for every ERC20, ERC721, ERC1155 and ERC6909 contract the asset scan knows, NFT for the ERC721 and ERC1155 ones, and token_standard on the node says which. A token name from an explorer gives Token, not Protocol. They are marks, not stops: the pool trace rule and the guards below do not change.",
894
895
  "- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, with kinds, events, totals_raw, first_height, last_height and last_bridge_event_id. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWS_TO walk passes through it.",
895
896
  "- Pool trace rule, for every trace: 1. Enter a `:Pool` on any edge. 2. Leave a `:Pool` only on `REMOVED_LIQUIDITY`, to the address the liquidity was paid to. 3. Never leave a `:Pool` on `FLOWS_TO`. 4. Across a swap, follow `SWAPPED` from payer to recipient, between two different addresses. Do not walk through the pool. Rug-pull probe: MATCH (victim:Address {address: $addr})-[paid:FLOWS_TO]->(pool:Pool)-[removal:REMOVED_LIQUIDITY]->(receiver:Address) WHERE NOT victim:Pool AND NOT victim:Protocol AND NOT victim:System AND NOT victim.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND receiver.address <> victim.address RETURN pool.address AS pool_address, receiver.address AS receiver_address, removal.usd AS removed_usd, removal.receiver_added_usd AS receiver_added_usd, removal.receiver_provided AS receiver_provided LIMIT 25.",
896
897
  "- USE facts serves the single events. (payer:Address)-[s:SWAP]->(recipient:Address) is one row per swap route, self swaps included, with strength, reason, route_id and the sold_ and bought_ asset, amount and usd columns. (provider:Address)-[:LIQUIDITY_ADD]->(pool:Address) and (pool:Address)-[:LIQUIDITY_REMOVE]->(receiver:Address) are one row per liquidity event, with party_state and evidence_state. (sender:Address)-[c:BRIDGE_CROSSING]->(recipient:Address) is one row per bridge event. SWAP, LIQUIDITY_* and BRIDGE_CROSSING take the same rule as TRANSFER: an address pair with one day. USD comes from the daily price services, never from a swap: with no price, USD is empty and the matching price_missing column is true (price_missing on TRANSFER, sold_price_missing and bought_price_missing on SWAP, amount0_price_missing and amount1_price_missing on LIQUIDITY_*). block_timestamp on TRANSFER, SWAP and LIQUIDITY_* rows is epoch milliseconds, in filters and in results; BRIDGE_CROSSING has none.",
@@ -901,7 +902,7 @@ const GRAPH_SCHEMA_HINTS = [
901
902
  "- Start schema discovery with endpoint-safe property reads: MATCH (n:Address) RETURN n.address AS address, n.network AS network, n.labels AS labels, n.last_activity_timestamp AS last_activity_timestamp LIMIT 20",
902
903
  "- Relationship discovery: MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count LIMIT 20",
903
904
  "- Anchor every topology read that filters on a link property: put an address in its pattern. Without one, a read starts from every link of the type it names, and you should not count on the filter to narrow that: WHERE x.strength = 'swap' on SWAPPED checks every SWAPPED link, LIMIT stops the read only after enough rows match, and a filter that matches few or none can run to the 60 s topology limit and fail with query_timeout. Example: MATCH (a:Address {address: $addr})-[x:SWAPPED]->(b:Address) WHERE x.swap_count >= 2 RETURN b.address, x.swap_count LIMIT 25. Pick an address with few links: degree_out and degree_in are a rough guide, because they count neighbours, not links, and an address with hundreds of thousands of neighbours can fail the same way. Discovery probes with LIMIT and no filter stay valid. The queries of one batch share a 100 s budget; USE facts queries stop at 30 s.",
904
- "- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool label, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
905
+ "- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWS_TO/OPERATED_BY/LINKED graph with SWAPPED, ADDED_LIQUIDITY, REMOVED_LIQUIDITY, BRIDGED and the Pool, Token and NFT labels, unified recent+historical) and USE facts for bounded TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING rows and enrichment, and USE chain for one keyed lookup on the chain node: a transaction by tx_id, a block by block_height or block_hash, an address at one block, or the head. On topology, address is the node grain, not the topology name.",
905
906
  "- All graph_query calls are read-only. Never use CREATE, INSERT, MERGE, SET, DELETE, REMOVE, DROP, DETACH, ADD, CONNECT, DISCONNECT, ALTER, TRUNCATE, GRANT, or REVOKE.",
906
907
  "- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly."
907
908
  ].join("\n");