chain-insights 0.57.1 → 0.57.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/dist/cli.cjs CHANGED
@@ -225,11 +225,11 @@ async function printSubscriptionPassGuidance(pass) {
225
225
  function addAmlAddressRiskCommand(parent, networksCommand) {
226
226
  parent.addCommand(createCliCommand("aml-address-risk").description("CIA workflow: screen an address for AML risk, exchange behavior, and optional comparison with another address").requiredOption("--address <address>", "Full blockchain address to screen").requiredOption("--network <network>", `Network to query. Run \`${networksCommand}\` for supported networks.`).option("--compare-address <address>", "Optional second address to compare against the screened address").option("--json", "Print machine-readable JSON output").option("--version <version>", "AML tool contract version. Omit to use the latest version (currently v1)").addOption(new commander.Option("--tool-version <version>", "Internal alias for the AML tool version").hideHelp()).action(async (opts) => {
227
227
  try {
228
- const { resolveAmlAddressRiskVersion } = await Promise.resolve().then(() => require("./public-tools-3yTC17cA.cjs"));
228
+ const { resolveAmlAddressRiskVersion } = await Promise.resolve().then(() => require("./public-tools-Dj130EuJ.cjs"));
229
229
  const requestedVersion = opts.toolVersion ?? opts.version;
230
230
  resolveAmlAddressRiskVersion(requestedVersion);
231
231
  await withGraphMcpClient("chain-insights-cli-aml-address-risk", async (client) => {
232
- const { runAmlAddressRisk } = await Promise.resolve().then(() => require("./public-tools-3yTC17cA.cjs"));
232
+ const { runAmlAddressRisk } = await Promise.resolve().then(() => require("./public-tools-Dj130EuJ.cjs"));
233
233
  const result = await runAmlAddressRisk(client, {
234
234
  address: opts.address,
235
235
  network: opts.network,
package/dist/cli.mjs CHANGED
@@ -223,11 +223,11 @@ async function printSubscriptionPassGuidance(pass) {
223
223
  function addAmlAddressRiskCommand(parent, networksCommand) {
224
224
  parent.addCommand(createCliCommand("aml-address-risk").description("CIA workflow: screen an address for AML risk, exchange behavior, and optional comparison with another address").requiredOption("--address <address>", "Full blockchain address to screen").requiredOption("--network <network>", `Network to query. Run \`${networksCommand}\` for supported networks.`).option("--compare-address <address>", "Optional second address to compare against the screened address").option("--json", "Print machine-readable JSON output").option("--version <version>", "AML tool contract version. Omit to use the latest version (currently v1)").addOption(new Option("--tool-version <version>", "Internal alias for the AML tool version").hideHelp()).action(async (opts) => {
225
225
  try {
226
- const { resolveAmlAddressRiskVersion } = await import("./public-tools-CZyeptJQ.mjs");
226
+ const { resolveAmlAddressRiskVersion } = await import("./public-tools-B77ZT0jt.mjs");
227
227
  const requestedVersion = opts.toolVersion ?? opts.version;
228
228
  resolveAmlAddressRiskVersion(requestedVersion);
229
229
  await withGraphMcpClient("chain-insights-cli-aml-address-risk", async (client) => {
230
- const { runAmlAddressRisk } = await import("./public-tools-CZyeptJQ.mjs");
230
+ const { runAmlAddressRisk } = await import("./public-tools-B77ZT0jt.mjs");
231
231
  const result = await runAmlAddressRisk(client, {
232
232
  address: opts.address,
233
233
  network: opts.network,
@@ -853,11 +853,12 @@ const PICTURE_RULES = [
853
853
  `- Rows with from_address and to_address columns draw a graph of at most 5,000 addresses. Optional columns: ${GRAPH_OPTIONAL_COLUMNS.join(", ")}. A recipe that draws a graph returns all of them; a column it leaves out shows as Unavailable in the drawn link. Example: USE topology MATCH (a:Address {address: $addr})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool RETURN ${flowsRecipeReturn("a", "b")} ORDER BY f.last_seen_timestamp DESC LIMIT 25`,
854
854
  "- Rows with a day, date, hour, week, month or *_timestamp column, number columns, no address in any value and at least 3 distinct times draw a time series. Within one day, the transfers of a known pair: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.block_timestamp AS block_timestamp, t.amount_usd AS amount_usd LIMIT 200",
855
855
  "- Across days: facts reads one day at a time, so send graph_query_batch with one query per day (3 to 20 days), each query id the day as \"YYYY-MM-DD\", each returning one total row: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN count(t) AS tx_count, sum(t.amount_usd) AS amount_usd_sum. The view joins the days into one time series. Do not group by day or name several days in one read: both are refused.",
856
+ "- The balance of one address over time is a USE chain read, never topology or facts. a.balance is the native coin, ETH, in wei (10^18 wei is one ETH), as decimal text; no layer serves a token balance. A batch holds at most chain_admission.batch_max chain reads, so one chart holds at least 3 points and at most that many: days spread over the user's window, or with no window one day a week back from today, today last. First read the head: USE chain MATCH (h:Head) RETURN h.block_height, h.block_timestamp (H and T). Then read one block near the oldest day, guessed at H minus 864000 blocks a day: USE chain MATCH (b:Block {block_height: N}) RETURN b.block_height, b.block_timestamp (h0 and T0). Blocks per day B = (H - h0) x 86400000 / (T - T0). The block of a day is H - (T - E) x B / 86400000, where E is the last millisecond of that UTC day. Then send one graph_query_batch, each query id the day as \"YYYY-MM-DD\", each returning only the balance: USE chain MATCH (a:Address {address: $addr, at_block: N}) RETURN a.balance. For today, or a block nearer the head than chain_admission.at_block_min_depth, leave at_block out. The view joins the days into one time series. Answer in ETH and say only ETH is shown.",
856
857
  "- Any other rows draw a table.",
857
858
  "- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice.",
858
859
  "- Every drawn answer carries view_url, a page on the user's own PC. When the user asks to see the graph bigger, in a window, or full screen, or says the Open in browser button did nothing: if this host has a browser tool (Claude Desktop: the Claude_Browser navigate tool), open view_url there; otherwise give the user the view_url link as a plain link. Claude Desktop opens no local link from the button itself, only from its browser tool."
859
860
  ].join("\n");
860
- const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.";
861
+ const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day; the balance of one address over time: a graph_query_batch of USE chain Address reads at_block the end of each day, each query id the day), and any other rows draw a table.";
861
862
  const RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once (send {}) before the first graph_query or graph_query_batch, and follow what it returns. When the skill is loaded, do not call meta_rules.";
862
863
  const ANSWER_RULES = [
863
864
  "Answer rules, for every reply to the user:",
@@ -882,7 +883,7 @@ const GRAPH_SCHEMA_HINTS = [
882
883
  "- Address nodes carry address, network, labels, and the role flags is_exchange, is_scam, is_victim and is_sanctioned. (:Address)-[:LINKED]-(:Address) is an undirected ownership-overlay edge (basis derived/associated, plus confidence, source_event, declared_owner, owner_state) asserting the two addresses are controlled by the same actor. LINKED is served on the topology graph only. Enumerate LINKED neighbors with MATCH (a:Address {address: $addr})-[l:LINKED]-(b:Address) RETURN b.address, b.network, l.basis, l.confidence LIMIT 25.",
883
884
  "- Labels hold role words, never detector names: Exchange (plus the exchange name), Scam, Victim and Sanctioned (plus the entity name). Each role is also a node label and a flag on the Address: :Exchange and is_exchange follow the exchange label, :Scam and is_scam the risk label, :Victim and is_victim the protection label, :Sanctioned and is_sanctioned the sanctioned label. Each flag is present only when true and absent otherwise, never false: test IS NOT NULL or IS NULL, not = false. A withdrawn label removes its node label and its flag.",
884
885
  "- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.",
885
- "- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT owned:Pool AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.",
886
+ "- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND NOT owned:Pool AND NOT owned:Protocol AND NOT owned:System AND NOT owned.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.",
886
887
  "- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.",
887
888
  "- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.",
888
889
  "- Facts graph labels include Address; the TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING relationships each connect two Address nodes. Facts address keys match topology address values exactly.",
@@ -892,11 +893,11 @@ const GRAPH_SCHEMA_HINTS = [
892
893
  "- 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
894
  "- 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.",
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
- "- 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 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
+ "- 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.",
897
898
  "- A USE facts SWAP row carries no route: it has no pools and no families column. A read that names one of them, to return it, to filter on it or to order by it, is refused. The route of a swap stays a topology question. For the pools of the swaps of an address, read SWAPPED.pools and SWAPPED.families on USE topology, anchored on the payer or the recipient. SWAPPED has one link per payer, recipient, sold asset and bought asset, so its pools cover every route on the link, not one route.",
898
899
  "- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWS_TO traversal, exchange hot wallets are terminal endpoints only. Do not expand from, through, or classify exchange nodes as deposit, suspect, or intermediate candidates; filter every non-terminal node with is_exchange IS NULL. is_exchange is absent unless true, so a labelled node with no is_exchange is walked through, and is_scam, is_victim and is_sanctioned do not end a walk. At a Pool, follow the pool trace rule above.",
899
- "- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A fixed-hop walk adds WHERE NOT src:Pool AND NOT mid:Pool, each its own AND term, never inside an OR. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool instead of finding the route that avoids it.",
900
+ "- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A walk of two or more hops also never starts at or passes through a Protocol contract or a System address. System is the kind of the mint and burn addresses 0x0000000000000000000000000000000000000000 and 0x000000000000000000000000000000000000dead; Protocol is shared infrastructure such as a PoolManager, a router or a bridge (the PoolManager 0x8366a39cc670b4001a1121b8f6a443a643e40951 is one, and it is no Pool). Each links millions of unrelated wallets, so a route through one connects nothing. Its start and every address in its middle carry NOT x:Pool AND NOT x:Protocol AND NOT x:System AND NOT x.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"], each its own AND term, never inside an OR. The graph does not write the System kind yet, so keep the address test too; it becomes redundant once System is written. A one-hop read of such an address needs no guard. A fixed-hop walk adds these terms for src and mid in its WHERE. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool AND NOT via:Protocol AND NOT via:System AND NOT via.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool AND NOT via:Protocol AND NOT via:System AND NOT via.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool or a system address instead of finding the route that avoids it.",
900
901
  "- Call meta_schema {network} first when you need field names; it is cached for 24 hours. The two discovery reads below are the fallback.",
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",
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAwEa;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBAyGG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
1
+ {"version":3,"file":"mcp-proxy.d.cts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAwEa;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA0GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAwEa;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBAyGG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
1
+ {"version":3,"file":"mcp-proxy.d.mts","names":[],"sources":["../src/mcp/proxy.ts"],"mappings":";;qBAwEa;qBACA;;;;;wBAyBG;YAkBJ;wBAEI,oBAAoB,MAAK,OAAO,aAA2B;KAsCtE,iBAAiB,eAAe,EAAE;qBAgB1B;wBA0GG,2BAA2B,mBAAmB;;;;;;;;wBAomBxC,eAAe"}
@@ -849,11 +849,12 @@ const PICTURE_RULES = [
849
849
  `- Rows with from_address and to_address columns draw a graph of at most 5,000 addresses. Optional columns: ${GRAPH_OPTIONAL_COLUMNS.join(", ")}. A recipe that draws a graph returns all of them; a column it leaves out shows as Unavailable in the drawn link. Example: USE topology MATCH (a:Address {address: $addr})-[f:FLOWS_TO]->(b:Address) WHERE NOT a:Pool RETURN ${flowsRecipeReturn("a", "b")} ORDER BY f.last_seen_timestamp DESC LIMIT 25`,
850
850
  "- Rows with a day, date, hour, week, month or *_timestamp column, number columns, no address in any value and at least 3 distinct times draw a time series. Within one day, the transfers of a known pair: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.block_timestamp AS block_timestamp, t.amount_usd AS amount_usd LIMIT 200",
851
851
  "- Across days: facts reads one day at a time, so send graph_query_batch with one query per day (3 to 20 days), each query id the day as \"YYYY-MM-DD\", each returning one total row: USE facts MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN count(t) AS tx_count, sum(t.amount_usd) AS amount_usd_sum. The view joins the days into one time series. Do not group by day or name several days in one read: both are refused.",
852
+ "- The balance of one address over time is a USE chain read, never topology or facts. a.balance is the native coin, ETH, in wei (10^18 wei is one ETH), as decimal text; no layer serves a token balance. A batch holds at most chain_admission.batch_max chain reads, so one chart holds at least 3 points and at most that many: days spread over the user's window, or with no window one day a week back from today, today last. First read the head: USE chain MATCH (h:Head) RETURN h.block_height, h.block_timestamp (H and T). Then read one block near the oldest day, guessed at H minus 864000 blocks a day: USE chain MATCH (b:Block {block_height: N}) RETURN b.block_height, b.block_timestamp (h0 and T0). Blocks per day B = (H - h0) x 86400000 / (T - T0). The block of a day is H - (T - E) x B / 86400000, where E is the last millisecond of that UTC day. Then send one graph_query_batch, each query id the day as \"YYYY-MM-DD\", each returning only the balance: USE chain MATCH (a:Address {address: $addr, at_block: N}) RETURN a.balance. For today, or a block nearer the head than chain_admission.at_block_min_depth, leave at_block out. The view joins the days into one time series. Answer in ETH and say only ETH is shown.",
852
853
  "- Any other rows draw a table.",
853
854
  "- The host draws the picture from the answer itself. Do not draw it again with another tool, such as a visualize or widget tool: the user would see the same answer drawn twice.",
854
855
  "- Every drawn answer carries view_url, a page on the user's own PC. When the user asks to see the graph bigger, in a window, or full screen, or says the Open in browser button did nothing: if this host has a browser tool (Claude Desktop: the Claude_Browser navigate tool), open view_url there; otherwise give the user the view_url link as a plain link. Claude Desktop opens no local link from the button itself, only from its browser tool."
855
856
  ].join("\n");
856
- const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day), and any other rows draw a table.";
857
+ const PICTURE_HELP_LINE = "In hosts that draw views, a graph_query answer with from_address and to_address columns draws a graph, a day or time column with numbers draws a time series (across days: a graph_query_batch of one-day totals, each query id the day; the balance of one address over time: a graph_query_batch of USE chain Address reads at_block the end of each day, each query id the day), and any other rows draw a table.";
857
858
  const RULES_FIRST = "Rules first: if the chain-insights-cypher skill is not loaded in this chat, call meta_rules once (send {}) before the first graph_query or graph_query_batch, and follow what it returns. When the skill is loaded, do not call meta_rules.";
858
859
  const ANSWER_RULES = [
859
860
  "Answer rules, for every reply to the user:",
@@ -878,7 +879,7 @@ const GRAPH_SCHEMA_HINTS = [
878
879
  "- Address nodes carry address, network, labels, and the role flags is_exchange, is_scam, is_victim and is_sanctioned. (:Address)-[:LINKED]-(:Address) is an undirected ownership-overlay edge (basis derived/associated, plus confidence, source_event, declared_owner, owner_state) asserting the two addresses are controlled by the same actor. LINKED is served on the topology graph only. Enumerate LINKED neighbors with MATCH (a:Address {address: $addr})-[l:LINKED]-(b:Address) RETURN b.address, b.network, l.basis, l.confidence LIMIT 25.",
879
880
  "- Labels hold role words, never detector names: Exchange (plus the exchange name), Scam, Victim and Sanctioned (plus the entity name). Each role is also a node label and a flag on the Address: :Exchange and is_exchange follow the exchange label, :Scam and is_scam the risk label, :Victim and is_victim the protection label, :Sanctioned and is_sanctioned the sanctioned label. Each flag is present only when true and absent otherwise, never false: test IS NOT NULL or IS NULL, not = false. A withdrawn label removes its node label and its flag.",
880
881
  "- Address nodes carry base activity rollups: degree_in/degree_out/degree_total (distinct counterparty addresses), tx_in_count/tx_out_count/tx_total_count, total_in_usd/total_out_usd/total_volume_usd, net_flow_usd (in minus out; positive = net receiver) — all computed from external flows only — and first_activity_timestamp/last_activity_timestamp/activity_span_days, which include all flows (self-loops included). FLOWS_TO edges carry exactly tx_count, amount_usd_sum (total money flow, token and native value merged), first_seen_timestamp, last_seen_timestamp. Lifetime aggregates are the only serving window. Averages are computed inline (amount_usd_sum / toFloat(tx_count)). tx_count counts token and native transfers plus internal native transfers (a contract sending ETH during a call), and amount_usd_sum prices them all. USE facts TRANSFER lists all three, so the rows of a pair are the transfers its link counts, up to the height the link was built to. A link covers all time and a USE facts read covers one day: read the transfers of a pair one day at a time. A transaction anchor of a pair resolves through USE facts, on the UTC day of the first_seen_timestamp or last_seen_timestamp of the link: MATCH (a:Address {address: $from})-[t:TRANSFER]->(b:Address {address: $to}) WHERE t.block_date = \"YYYY-MM-DD\" RETURN t.tx_id, t.block_timestamp LIMIT 1.",
881
- "- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT owned:Pool AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.",
882
+ "- For actor-level exposure (AC11), UNION FLOWS_TO and SWAPPED reachability over one visible LINKED hop instead of expanding through the LINKED edge itself: MATCH (a:Address {address: $addr})-[:LINKED]-(owned:Address)-[r:FLOWS_TO|SWAPPED]-(b:Address) WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND NOT owned:Pool AND NOT owned:Protocol AND NOT owned:System AND NOT owned.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"] AND owned.address <> b.address AND a.address <> b.address RETURN owned.address, b.address, type(r), coalesce(r.amount_usd_sum, r.bought_usd) LIMIT 50.",
882
883
  "- Labels and per-label risk live on the address node: the labels array plus three parallel lists, label_risk_labels, label_risk_levels and label_risk_updated_timestamps, where entry i of each list is one label row. No overall risk score is served. USE facts serves bounded single-event rows (TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING edges) only; lifetime address metrics (degrees, totals, activity window) are node properties on USE topology.",
883
884
  "- (from:Address)-[t:TRANSFER]->(to:Address) on USE facts returns individual transfer rows, not aggregates, with properties amount, amount_usd, asset_symbol, asset_contract, tx_id, block_height, block_timestamp, event_index, edge_index, kind, price_usd, and price_missing. Every TRANSFER row has a kind: token, native or internal. An internal row is ETH a contract sends while it runs a call, such as the ETH leg of a wrap. Add t.kind = \"internal\" to the pair and the day to read only the internal rows. A facts read names an address pair with one day, whether it is a TRANSFER row-select or a count()/sum() aggregate: both endpoint addresses as {address: \"...\"} on from and to, with WHERE t.block_date = \"YYYY-MM-DD\". A transaction is read on USE chain by its tx_id, not on facts. t.block_timestamp bounds in epoch milliseconds may narrow the day to a time window. One address, a day alone, a window of days, a block range or a bare LIMIT is not enough. A facts read holds at most 200 rows in a reply, has one relationship and no hop, and takes no ORDER BY: sort the page yourself.",
884
885
  "- Facts graph labels include Address; the TRANSFER, SWAP, LIQUIDITY_ADD, LIQUIDITY_REMOVE and BRIDGE_CROSSING relationships each connect two Address nodes. Facts address keys match topology address values exactly.",
@@ -888,11 +889,11 @@ const GRAPH_SCHEMA_HINTS = [
888
889
  "- 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.",
889
890
  "- 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.",
890
891
  "- (: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.",
891
- "- 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 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.",
892
+ "- 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.",
892
893
  "- 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.",
893
894
  "- A USE facts SWAP row carries no route: it has no pools and no families column. A read that names one of them, to return it, to filter on it or to order by it, is refused. The route of a swap stays a topology question. For the pools of the swaps of an address, read SWAPPED.pools and SWAPPED.families on USE topology, anchored on the payer or the recipient. SWAPPED has one link per payer, recipient, sold asset and bought asset, so its pools cover every route on the link, not one route.",
894
895
  "- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWS_TO traversal, exchange hot wallets are terminal endpoints only. Do not expand from, through, or classify exchange nodes as deposit, suspect, or intermediate candidates; filter every non-terminal node with is_exchange IS NULL. is_exchange is absent unless true, so a labelled node with no is_exchange is walked through, and is_scam, is_victim and is_sanctioned do not end a walk. At a Pool, follow the pool trace rule above.",
895
- "- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A fixed-hop walk adds WHERE NOT src:Pool AND NOT mid:Pool, each its own AND term, never inside an OR. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool instead of finding the route that avoids it.",
896
+ "- Pool guard: a trace walks FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient without passing through the pool. A walk may end at a Pool, but never starts at one or passes through one: its start and every address in its middle stay off a Pool. A walk of two or more hops also never starts at or passes through a Protocol contract or a System address. System is the kind of the mint and burn addresses 0x0000000000000000000000000000000000000000 and 0x000000000000000000000000000000000000dead; Protocol is shared infrastructure such as a PoolManager, a router or a bridge (the PoolManager 0x8366a39cc670b4001a1121b8f6a443a643e40951 is one, and it is no Pool). Each links millions of unrelated wallets, so a route through one connects nothing. Its start and every address in its middle carry NOT x:Pool AND NOT x:Protocol AND NOT x:System AND NOT x.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"], each its own AND term, never inside an OR. The graph does not write the System kind yet, so keep the address test too; it becomes redundant once System is written. A one-hop read of such an address needs no guard. A fixed-hop walk adds these terms for src and mid in its WHERE. A quantified or shortest-path walk puts the guards inside the path pattern, on the start and on up to 4 guarded hops before one last hop: MATCH p = SHORTEST 1 (a:Address {address: $from} WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool AND NOT via:Protocol AND NOT via:System AND NOT via.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: $to}) RETURN [n IN nodes(p) | n.address] AS route LIMIT 5. ANY SHORTEST takes the same pattern. A route search asks for one path. An open target from one address: MATCH SHORTEST 1 (a:Address {address: $addr} WHERE NOT a:Pool AND NOT a:Protocol AND NOT a:System AND NOT a.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]) (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool AND NOT via:Protocol AND NOT via:System AND NOT via.address IN [\"0x0000000000000000000000000000000000000000\", \"0x000000000000000000000000000000000000dead\"]){0,4} ()-[:FLOWS_TO|SWAPPED]-(b:Address) RETURN b.address LIMIT 50. Use these shapes as written, changing only the addresses and the RETURN. A WHERE placed after a SHORTEST pattern runs after the shortest route is chosen, so it drops a route that crosses a pool or a system address instead of finding the route that avoids it.",
896
897
  "- Call meta_schema {network} first when you need field names; it is cached for 24 hours. The two discovery reads below are the fallback.",
897
898
  "- 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",
898
899
  "- 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",