chain-insights 0.74.1 → 0.75.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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.cjs +13 -13
  3. package/dist/cli.mjs +13 -13
  4. package/dist/{dex-tool-DLWnff1n.cjs → dex-tool-85_76Q8i.cjs} +5 -5
  5. package/dist/{dex-tool-DsaAIa-a.mjs → dex-tool-ib_ICiyV.mjs} +6 -6
  6. package/dist/dex-tool-ib_ICiyV.mjs.map +1 -0
  7. package/dist/{graph-schema-ClqjzgrM.mjs → graph-schema-Bxqwukqu.mjs} +5 -5
  8. package/dist/graph-schema-Bxqwukqu.mjs.map +1 -0
  9. package/dist/{graph-schema-BGbJumCY.cjs → graph-schema-DNJXDaTn.cjs} +4 -4
  10. package/dist/mcp-proxy.cjs +22 -22
  11. package/dist/mcp-proxy.mjs +22 -22
  12. package/dist/mcp-proxy.mjs.map +1 -1
  13. package/dist/{print-result-C3YazZBw.mjs → print-result-BtEAn2Wl.mjs} +2 -2
  14. package/dist/print-result-BtEAn2Wl.mjs.map +1 -0
  15. package/dist/{print-result-Cyqrlx8P.cjs → print-result-if-AbNLY.cjs} +1 -1
  16. package/dist/{public-tools-Bl_l_rKK.cjs → public-tools-COG3pCpD.cjs} +8 -8
  17. package/dist/{public-tools-CQjGt7_q.mjs → public-tools-DIJs4SHz.mjs} +9 -9
  18. package/dist/public-tools-DIJs4SHz.mjs.map +1 -0
  19. package/dist/{tool-visibility-BNwvcuVx.cjs → tool-visibility-DTAYvE9E.cjs} +1 -1
  20. package/dist/{tool-visibility-B6983rxo.mjs → tool-visibility-zcmwMSbn.mjs} +2 -2
  21. package/dist/{tool-visibility-B6983rxo.mjs.map → tool-visibility-zcmwMSbn.mjs.map} +1 -1
  22. package/dist/{transport-error-D4hmpkqL.mjs → transport-error-C7OQjmC7.mjs} +2 -2
  23. package/dist/{transport-error-D4hmpkqL.mjs.map → transport-error-C7OQjmC7.mjs.map} +1 -1
  24. package/dist/{transport-error-CgISST8l.cjs → transport-error-qDWQn8hb.cjs} +1 -1
  25. package/dist/{update-CZDHSi_u.mjs → update-DqBhBXFs.mjs} +2 -2
  26. package/dist/{update-CZDHSi_u.mjs.map → update-DqBhBXFs.mjs.map} +1 -1
  27. package/dist/{update-DGyRlm3J.cjs → update-NowrCmjR.cjs} +1 -1
  28. package/docs/graph-query-compatibility.md +13 -13
  29. package/docs/graph-tools.md +22 -22
  30. package/docs/mcp-proxy.md +1 -1
  31. package/package.json +1 -1
  32. package/skills/chain-insights-cypher/SKILL.md +27 -27
  33. package/skills/chain-insights-dex/SKILL.md +2 -2
  34. package/skills/chain-insights-dex/reference.md +1 -1
  35. package/skills/chain-insights-dex-gauge/SKILL.md +4 -4
  36. package/skills/chain-insights-dex-launchpad/SKILL.md +2 -2
  37. package/skills/chain-insights-virtuals/reference.md +5 -5
  38. package/dist/dex-tool-DsaAIa-a.mjs.map +0 -1
  39. package/dist/graph-schema-ClqjzgrM.mjs.map +0 -1
  40. package/dist/print-result-C3YazZBw.mjs.map +0 -1
  41. package/dist/public-tools-CQjGt7_q.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-schema-Bxqwukqu.mjs","names":["errorResult","isRecord"],"sources":["../src/mcp/flows.ts","../src/mcp/graph-schema-cache.ts","../src/mcp/graph-schema.ts"],"sourcesContent":["/**\n * graph_expand, the one dedicated tool of the local proxy, called by the Chain\n * Insights view (ui://chain-insights/view) when the investigator clicks. Its\n * _meta says visibility [\"app\"], so a host keeps it away from the model: the\n * model writes its own graph_query, and the view draws the rows by their column\n * names.\n *\n * Two forms, told apart by the arguments:\n *\n * - node, {network, address, in_offset?, out_offset?}: one address with up to\n * 250 newest senders and 250 newest receivers, a text summary and\n * chain-insights.flows.v1 in structuredContent. Composed from at most five\n * anchored graph_query reads on the topology graph: the node, then its\n * outgoing and its incoming FLOWED_TO, newest first, two reads a side because\n * a reply holds at most 200 rows.\n * - link, {network, from, to, day}: the transfers between two known addresses\n * on one UTC day, newest first, at most 50, a short text summary and\n * chain-insights.transfers.v1 in structuredContent. One anchored USE facts\n * graph_query read.\n * - link page, {network, from, to, day, limit, skip?, first_day?}: a page of\n * `limit` transfers (1 to 50) newest first, read from `day` back one UTC day\n * at a time, at most TRANSFERS_MAX_DAYS (a week of) reads a call, never before\n * first_day. The answer carries `next` ({day, skip}, or null when every\n * transfer back to first_day is listed) which the next call sends back as\n * day and skip. The view's \"More transfers\".\n * - details, {network, address, details: true}: the lifetime totals, counts,\n * role and labels of one address, chain-insights.address.v1, one anchored\n * topology read. The view fills its panel with it when the rows of a query\n * carried no totals.\n *\n * Every read goes through the proxy's remote client, the same path and payment\n * wrapping a graph_query call takes, and is billed as a graph_query. Every\n * argument is checked before the first read, so a refused call reaches no graph\n * endpoint and costs nothing.\n */\n\nexport const FLOWS_SCHEMA = 'chain-insights.flows.v1'\nexport const GRAPH_EXPAND_TOOL = 'graph_expand'\nexport const FLOWS_NETWORK = 'robinhood'\n\n/** How many senders and how many receivers one page shows, at most. */\nexport const FLOWS_PER_SIDE = 250\n/** The view data of one answer: at most this many addresses, the centre included... */\nexport const FLOWS_MAX_NODES = 500\n/**\n * ...and under this many characters of JSON. Hosts drop an app tool result\n * over about 150,000 characters.\n */\nexport const FLOWS_MAX_CHARS = 140_000\n/**\n * The graph endpoint cuts a reply at 200 rows, whatever the LIMIT, so one side\n * of a page is read in reads of at most this many rows.\n */\nexport const FLOWS_READ_ROWS = 200\n/** The text of a node answer holds at most this many lines. */\nexport const FLOWS_SUMMARY_MAX_LINES = 20\n/** Where a graph_expand page may start on each side, at most. */\nexport const FLOWS_MAX_OFFSET = 10_000\n\nexport const TRANSFERS_SCHEMA = 'chain-insights.transfers.v1'\n/** A link answer lists at most this many transfers... */\nexport const TRANSFERS_MAX = 50\n/**\n * ...from the rows one read brings back. A facts read takes no ORDER BY and\n * the server cuts a reply at 200 rows, so the read asks for the whole cap and\n * the newest rows are picked here.\n */\nexport const TRANSFERS_READ_ROWS = 200\n/** The text of a link answer names at most this many transfers. */\nexport const TRANSFERS_SUMMARY_ROWS = 5\n/** A paged link answer reads at most this many days (a week) in one call: a sparse link costs at most this many reads a page, and a week of it comes back at once. */\nexport const TRANSFERS_MAX_DAYS = 7\n/** Where a paged link answer stops reading back when the caller names no first_day. */\nexport const TRANSFERS_FLOOR_DAY = '2015-01-01'\n\nexport const ADDRESS_SCHEMA = 'chain-insights.address.v1'\n\nexport type FlowNode = {\n address: string\n role: string\n labels: string[]\n total_in_usd: number\n total_out_usd: number\n degree_in: number\n degree_out: number\n}\n\nexport type FlowEdge = {\n from: string\n to: string\n usd: number\n tx_count: number\n first_seen_ms: number\n last_seen_ms: number\n}\n\nexport type FlowCursor = { in_offset: number; out_offset: number }\n\n/** The links of the centre not loaded yet, past the cursor, on each side. */\nexport type FlowRemaining = { senders: number; receivers: number }\n\nexport type FlowView = {\n schema: typeof FLOWS_SCHEMA\n network: string\n center: string\n nodes: FlowNode[]\n edges: FlowEdge[]\n cursor: FlowCursor\n truncated: boolean\n /** Set on every node page. A view that does not know it ignores it. */\n remaining?: FlowRemaining\n}\n\ntype Row = Record<string, unknown>\n\nexport type FlowsPage = {\n center: Row\n senders: Row[]\n receivers: Row[]\n inOffset: number\n outOffset: number\n}\n\n/** One graph_query answer as the remote client returns it. */\nexport type GraphQueryAnswer = {\n content?: Array<{ type: string; text?: string }>\n structuredContent?: Record<string, unknown>\n isError?: boolean\n}\n\nexport type FlowsToolResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nexport type FlowsDependencies = {\n /** One graph_query call through the proxy's remote client. */\n graphQuery: (args: { network: string; query: string }) => Promise<GraphQueryAnswer>\n /**\n * The whole refusal of a failed read, text and structured facts\n * (chain-insights.refusal.v1: the free tier spent, a wallet that cannot\n * pay). When given it replaces describeFailure, so the view can say why in\n * the viewer's own words and time.\n */\n refusal?: (err: unknown) => Promise<FlowsToolResult>\n /** The text of a call that threw (payment required, transport failure). */\n describeFailure: (err: unknown) => string\n /** Why no read can run (the graph endpoint is not connected), checked after the arguments. */\n unavailable?: () => string | undefined\n now?: () => Date\n}\n\nexport const INVALID_ADDRESS_TEXT =\n 'invalid_address: a full 0x address of 40 hexadecimal characters is needed, for example 0x04911a118f11c75667e4d0dfb8e640af5a353550'\nexport const OFFSET_TOO_LARGE_TEXT = `invalid_offset: in_offset and out_offset must each be at most ${FLOWS_MAX_OFFSET}`\nexport const OFFSET_NEGATIVE_TEXT =\n 'invalid_offset: in_offset and out_offset must each be a whole number, 0 or more'\n\nexport const INVALID_DAY_TEXT =\n 'invalid_day: day must be a calendar date written YYYY-MM-DD (UTC), for example 2026-07-10'\nexport const BOTH_FORMS_TEXT =\n 'invalid_arguments: pass address (with in_offset and out_offset) to expand a node, or from, to and day to list the transfers of a link, not both'\nexport const INVALID_LIMIT_TEXT = 'invalid_limit: limit must be a whole number from 1 to 50'\nexport const INVALID_SKIP_TEXT = 'invalid_skip: skip must be a whole number of 0 or more'\nexport const INVALID_FIRST_DAY_TEXT =\n 'invalid_first_day: first_day must be a calendar date written YYYY-MM-DD (UTC), not after day'\nexport const DETAILS_FORM_TEXT =\n 'invalid_arguments: details takes address alone; pass no from, to, day or offsets with it'\n\nconst ADDRESS_PATTERN = /^0x[0-9a-f]{40}$/\nconst DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CODE_PREFIX = /^([a-z][a-z0-9_]*):\\s*/\n\n/**\n * The address lower-cased, or null when it is not 0x and 40 hexadecimal\n * characters. Only an address that passes is ever written into a read, so the\n * reads can carry it as a literal.\n */\nexport function normalizeFlowsAddress(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const address = raw.trim().toLowerCase()\n return ADDRESS_PATTERN.test(address) ? address : null\n}\n\n/**\n * The day as YYYY-MM-DD when it is a real calendar date, or null. Only a day\n * that passes is ever written into a read.\n */\nexport function normalizeFlowsDay(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const day = raw.trim()\n if (!DAY_PATTERN.test(day)) return null\n const time = Date.parse(`${day}T00:00:00Z`)\n if (Number.isNaN(time) || new Date(time).toISOString().slice(0, 10) !== day) return null\n return day\n}\n\n/**\n * The one facts read of a link: the transfers from one address to another on\n * one day. A facts read names the pair with one day, takes no ORDER BY and\n * holds at most 200 rows, so the read asks for the whole cap and the order is\n * made in buildTransfersView. Both addresses and the day have passed their\n * checks, so they are written into the read as literals.\n */\nexport function transfersReadFor(from: string, to: string, day: string): string {\n return (\n `USE facts MATCH (a:Address {address: \"${from}\"})-[t:TRANSFER]->(b:Address {address: \"${to}\"}) ` +\n `WHERE t.block_date = \"${day}\" ` +\n 'RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp, t.amount AS amount, ' +\n `t.asset_symbol AS asset_symbol, t.amount_usd AS amount_usd LIMIT ${TRANSFERS_READ_ROWS}`\n )\n}\n\n/** The UTC day before a YYYY-MM-DD day. */\nexport function dayBefore(day: string): string {\n return new Date(Date.parse(`${day}T00:00:00Z`) - 86_400_000).toISOString().slice(0, 10)\n}\n\n/**\n * The one topology read of a details call: the address alone, with its\n * lifetime rollups and activity span. The address has passed its check, so it\n * is written into the read as a literal.\n */\nexport function detailsReadFor(address: string): string {\n return (\n `USE topology MATCH (b:Address {address: \"${address}\"}) RETURN ${NODE_FIELDS}, ` +\n 'b.first_activity_timestamp AS first_activity_timestamp, b.last_activity_timestamp AS last_activity_timestamp LIMIT 1'\n )\n}\n\nexport type AddressView = FlowNode & {\n schema: typeof ADDRESS_SCHEMA\n network: string\n is_exchange: boolean\n is_sanctioned: boolean\n is_scam: boolean\n is_victim: boolean\n pool: boolean\n first_activity_timestamp: number | null\n last_activity_timestamp: number | null\n}\n\n/** chain-insights.address.v1 from the one row of a details read. */\nexport function buildAddressView(network: string, address: string, row: Row): AddressView {\n const node = flowsNodeOf({ ...row, address })\n return {\n schema: ADDRESS_SCHEMA,\n network,\n ...node,\n is_exchange: row.is_exchange === true,\n is_sanctioned: row.is_sanctioned === true,\n is_scam: row.is_scam === true,\n is_victim: row.is_victim === true,\n pool: row.pool === true,\n first_activity_timestamp: flowsNumberOrNull(row.first_activity_timestamp),\n last_activity_timestamp: flowsNumberOrNull(row.last_activity_timestamp),\n }\n}\n\n/** The one line of a details answer. */\nexport function addressSummary(view: AddressView): string {\n return (\n `${view.address} on ${view.network}: ${roleWord(view.role)}; lifetime ${formatFlowsUsd(view.total_in_usd)} in, ` +\n `${formatFlowsUsd(view.total_out_usd)} out; ${view.degree_in} senders and ${view.degree_out} receivers in the topology graph` +\n (view.labels.length ? `; labels ${view.labels.join(', ')}` : '') +\n '.'\n )\n}\n\nconst NODE_FIELDS =\n 'b.address AS address, b.labels AS labels, b.is_exchange AS is_exchange, ' +\n 'b.is_sanctioned AS is_sanctioned, b.is_scam AS is_scam, b.is_victim AS is_victim, b:Pool AS pool, ' +\n 'b.total_in_usd AS total_in_usd, b.total_out_usd AS total_out_usd, b.degree_in AS degree_in, b.degree_out AS degree_out'\nconst PARTY_FIELDS =\n NODE_FIELDS +\n ', f.amount_usd_sum AS usd, f.tx_count AS tx_count, ' +\n 'f.first_seen_timestamp AS first_seen, f.last_seen_timestamp AS last_seen'\n// Newest link first, the address breaking a tie, so a page and the next one\n// never share or skip a link.\nconst PAGE_ORDER = ' ORDER BY f.last_seen_timestamp DESC, b.address'\n\n/** One anchored read of a page, with the SKIP and LIMIT it carries. */\nexport type FlowsRead = { id: 'node' | 'out' | 'in'; query: string; skip: number; limit: number }\n\n/**\n * The anchored reads of one page: the node, then the receivers, then the\n * senders. Each side asks for one row more than it shows: the extra row says\n * another page exists. A reply holds at most FLOWS_READ_ROWS rows, so a side\n * is read in chunks on the same order, each starting where the last one ends.\n */\nexport function flowsReadsFor(address: string, inOffset: number, outOffset: number): FlowsRead[] {\n const side = (id: 'out' | 'in', offset: number, match: string): FlowsRead[] => {\n const reads: FlowsRead[] = []\n for (let done = 0; done < FLOWS_PER_SIDE + 1; done += FLOWS_READ_ROWS) {\n const skip = offset + done\n const limit = Math.min(FLOWS_READ_ROWS, FLOWS_PER_SIDE + 1 - done)\n reads.push({\n id,\n query: `USE topology MATCH ${match} RETURN ${PARTY_FIELDS}${PAGE_ORDER}${skip > 0 ? ` SKIP ${skip}` : ''} LIMIT ${limit}`,\n skip,\n limit,\n })\n }\n return reads\n }\n return [\n {\n id: 'node',\n query: `USE topology MATCH (b:Address {address: '${address}'}) RETURN ${NODE_FIELDS} LIMIT 1`,\n skip: 0,\n limit: 1,\n },\n ...side('out', outOffset, `(a:Address {address: '${address}'})-[f:FLOWED_TO]->(b:Address)`),\n ...side('in', inOffset, `(b:Address)-[f:FLOWED_TO]->(a:Address {address: '${address}'})`),\n ]\n}\n\n/** The role word of an address, first match wins. */\nexport function flowsRole(row: Row): string {\n if (row.is_exchange === true) return 'exchange'\n if (row.is_sanctioned === true) return 'sanctioned'\n if (row.is_scam === true) return 'scam'\n if (row.is_victim === true) return 'victim'\n if (row.pool === true) return 'pool'\n if (Array.isArray(row.labels) && row.labels.includes('SelfTrader')) return 'self_trader'\n return 'other'\n}\n\n/** The role as the text answers write it: a role code becomes words. */\nfunction roleWord(role: string): string {\n return role === 'self_trader' ? 'self-trading contract' : role\n}\n\nfunction flowsFloat(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction flowsInt(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : 0\n}\n\nfunction flowsNodeOf(row: Row): FlowNode {\n const labels = Array.isArray(row.labels)\n ? row.labels.filter((label): label is string => typeof label === 'string')\n : []\n return {\n address: typeof row.address === 'string' ? row.address : '',\n role: flowsRole(row),\n labels,\n total_in_usd: flowsFloat(row.total_in_usd),\n total_out_usd: flowsFloat(row.total_out_usd),\n degree_in: flowsInt(row.degree_in),\n degree_out: flowsInt(row.degree_out),\n }\n}\n\nfunction assembleFlowView(\n network: string,\n page: FlowsPage,\n senders: Row[],\n receivers: Row[],\n truncated: boolean\n): FlowView {\n const center = flowsNodeOf(page.center)\n const cursor = {\n in_offset: page.inOffset + senders.length,\n out_offset: page.outOffset + receivers.length,\n }\n const view: FlowView = {\n schema: FLOWS_SCHEMA,\n network,\n center: center.address,\n nodes: [center],\n edges: [],\n cursor,\n truncated,\n remaining: {\n senders: Math.max(0, center.degree_in - cursor.in_offset),\n receivers: Math.max(0, center.degree_out - cursor.out_offset),\n },\n }\n const seen = new Set([center.address])\n const add = (row: Row, incoming: boolean) => {\n const node = flowsNodeOf(row)\n if (!seen.has(node.address)) {\n seen.add(node.address)\n view.nodes.push(node)\n }\n view.edges.push({\n from: incoming ? node.address : center.address,\n to: incoming ? center.address : node.address,\n usd: flowsFloat(row.usd),\n tx_count: flowsInt(row.tx_count),\n first_seen_ms: flowsInt(row.first_seen),\n last_seen_ms: flowsInt(row.last_seen),\n })\n }\n for (const row of senders) add(row, true)\n for (const row of receivers) add(row, false)\n return view\n}\n\nfunction fitsFlowBudget(view: FlowView): boolean {\n return view.nodes.length <= FLOWS_MAX_NODES && JSON.stringify(view).length < FLOWS_MAX_CHARS\n}\n\n/**\n * How many senders and receivers stay after `drops` of the oldest links go,\n * one at a time from the longer side, the senders first on a tie.\n */\nfunction keptAfterDrops(senders: number, receivers: number, drops: number): [number, number] {\n let s = senders\n let r = receivers\n for (let left = Math.min(drops, s + r); left > 0; left -= 1) {\n if (s >= r) s -= 1\n else r -= 1\n }\n return [s, r]\n}\n\n/**\n * flows.v1 from one page: the centre first, then the senders and the\n * receivers, each newest first, at most FLOWS_PER_SIDE a side. When the view\n * data would hold more than FLOWS_MAX_NODES addresses or reach FLOWS_MAX_CHARS\n * characters, the oldest links go first, the view is marked truncated, and the\n * cursor still says where the next page starts.\n *\n * Each dropped link only makes the view smaller, so the fewest drops that fit\n * are found by halving: about ten views are measured for a full page, not one\n * per dropped link.\n */\nexport function buildFlowView(network: string, page: FlowsPage): FlowView {\n const overflow = page.senders.length > FLOWS_PER_SIDE || page.receivers.length > FLOWS_PER_SIDE\n const senders = page.senders.slice(0, FLOWS_PER_SIDE)\n const receivers = page.receivers.slice(0, FLOWS_PER_SIDE)\n const viewAfter = (drops: number) => {\n const [s, r] = keptAfterDrops(senders.length, receivers.length, drops)\n return assembleFlowView(\n network,\n page,\n senders.slice(0, s),\n receivers.slice(0, r),\n overflow || drops > 0\n )\n }\n // Dropping every link is the floor: that view is answered even when it does\n // not fit.\n let low = 0\n let high = senders.length + receivers.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (fitsFlowBudget(viewAfter(mid))) high = mid\n else low = mid + 1\n }\n return viewAfter(low)\n}\n\nexport function formatFlowsUsd(value: number): string {\n if (!(value > 0)) return '$0'\n if (value < 0.01) return 'under $0.01'\n const whole = Math.floor(value)\n const cents = Math.round((value - whole) * 100)\n if (cents === 100) return formatFlowsUsd(whole + 1)\n const grouped = whole.toFixed(0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\n return `$${grouped}.${String(cents).padStart(2, '0')}`\n}\n\nfunction formatFlowsAge(lastSeenMs: number, now: Date): string {\n if (lastSeenMs <= 0) return 'at an unknown time'\n const date = new Date(lastSeenMs).toISOString().slice(0, 10)\n const days = Math.trunc((now.getTime() - lastSeenMs) / 86_400_000)\n if (days < 1) return `${date} (less than a day ago)`\n if (days === 1) return `${date} (1 day ago)`\n return `${date} (${days} days ago)`\n}\n\n/** The role word, with up to three address labels beside it. */\nfunction roleText(node: FlowNode | undefined): string {\n if (!node) return 'other'\n const word = roleWord(node.role)\n if (node.labels.length === 0) return word\n return `${word} (${node.labels.slice(0, 3).join(', ')})`\n}\n\nfunction sideLines(title: string, parties: string[]): string[] {\n if (parties.length === 0) return [`${title}, newest first: none.`]\n const lines = [`${title}, newest first (${parties.length}):`]\n // A side with more than six counterparties lists two on a line, so a full\n // page stays within the line budget.\n const perLine = parties.length > 6 ? 2 : 1\n for (let start = 0; start < parties.length; start += perLine) {\n lines.push(`- ${parties.slice(start, start + perLine).join('; ')}`)\n }\n return lines\n}\n\n/**\n * The text of a node answer: the centre, its role and lifetime totals, then\n * every counterparty of the view with its USD, transaction count and last-seen\n * date and age. At most FLOWS_SUMMARY_MAX_LINES lines.\n */\nexport function flowsSummary(view: FlowView, now: Date): string {\n const nodes = new Map(view.nodes.map((node) => [node.address, node]))\n const center = nodes.get(view.center)\n const party = (address: string, edge: FlowEdge) =>\n `${address} ${roleText(nodes.get(address))}, ${formatFlowsUsd(edge.usd)} in ${edge.tx_count} tx, last seen ${formatFlowsAge(edge.last_seen_ms, now)}`\n const senders: string[] = []\n const receivers: string[] = []\n for (const edge of view.edges) {\n if (edge.to === view.center) senders.push(party(edge.from, edge))\n else receivers.push(party(edge.to, edge))\n }\n const lines = [\n `Money flows of ${view.center} on ${view.network}: ${roleText(center)}; lifetime ${formatFlowsUsd(center?.total_in_usd ?? 0)} in, ${formatFlowsUsd(center?.total_out_usd ?? 0)} out; ${center?.degree_in ?? 0} senders and ${center?.degree_out ?? 0} receivers in the topology graph.`,\n ...sideLines('Recent senders', senders),\n ...sideLines('Recent receivers', receivers),\n ]\n const body = lines.slice(0, FLOWS_SUMMARY_MAX_LINES - 2)\n const footer = pagingLines(view, senders.length, receivers.length, center)\n return [...body, ...footer].join('\\n')\n}\n\n/** Where this page sits, and that the picture loads the rest when an address is clicked. */\nfunction pagingLines(\n view: FlowView,\n shownSenders: number,\n shownReceivers: number,\n center: FlowNode | undefined\n): string[] {\n const totalIn = center?.degree_in ?? 0\n const totalOut = center?.degree_out ?? 0\n const { in_offset: nextIn, out_offset: nextOut } = view.cursor\n const range = (shown: number, next: number, total: number) =>\n shown === 0 ? `none of ${total}` : `${next - shown + 1} to ${next} of ${total}`\n const where = `Showing senders ${range(shownSenders, nextIn, totalIn)} and receivers ${range(shownReceivers, nextOut, totalOut)}, newest first.`\n if (!view.truncated && nextIn >= totalIn && nextOut >= totalOut) return [where]\n return [\n where,\n 'More senders and receivers load when an address in the picture is double-clicked.',\n ]\n}\n\nexport type TransferRow = {\n tx_id: string\n block_timestamp: number | null\n amount: number | string | null\n asset_symbol: string | null\n amount_usd: number | null\n}\n\nexport type TransfersCursor = { day: string; skip: number }\n\nexport type TransfersView = {\n schema: typeof TRANSFERS_SCHEMA\n network: string\n from: string\n to: string\n day: string\n transfers: TransferRow[]\n truncated: boolean\n /** Paged answers only: the earliest day read, how many days the call read, and where the next page starts (null: every transfer back to first_day is listed). */\n through_day?: string\n days_read?: number\n next?: TransfersCursor | null\n}\n\nexport type TransfersPageRequest = { skip: number; firstDay: string | null; limit: number }\n\n/** A finite number, or a decimal string read as one, or null. */\nfunction flowsNumberOrNull(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction transferOf(row: Row): TransferRow {\n const amount =\n typeof row.amount === 'number' && Number.isFinite(row.amount)\n ? row.amount\n : typeof row.amount === 'string' && row.amount.trim() !== ''\n ? row.amount\n : null\n return {\n tx_id: typeof row.tx_id === 'string' ? row.tx_id : '',\n block_timestamp: flowsNumberOrNull(row.block_timestamp),\n amount,\n asset_symbol:\n typeof row.asset_symbol === 'string' && row.asset_symbol !== '' ? row.asset_symbol : null,\n amount_usd: flowsNumberOrNull(row.amount_usd),\n }\n}\n\n/**\n * chain-insights.transfers.v1 from the rows of one facts read: newest first,\n * at most TRANSFERS_MAX, truncated when more rows than that came back. The\n * facts layer takes no ORDER BY, so the order is made here.\n */\nexport function buildTransfersView(\n network: string,\n from: string,\n to: string,\n day: string,\n rows: Row[]\n): TransfersView {\n const newestFirst = rows\n .map(transferOf)\n .sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity))\n return {\n schema: TRANSFERS_SCHEMA,\n network,\n from,\n to,\n day,\n transfers: newestFirst.slice(0, TRANSFERS_MAX),\n truncated: newestFirst.length > TRANSFERS_MAX,\n }\n}\n\nfunction transferLine(transfer: TransferRow): string {\n const time =\n transfer.block_timestamp === null\n ? 'at an unknown time'\n : `${new Date(transfer.block_timestamp).toISOString().slice(0, 19).replace('T', ' ')} UTC`\n const amount = transfer.amount === null ? 'an unknown amount' : String(transfer.amount)\n const asset = transfer.asset_symbol ? ` ${transfer.asset_symbol}` : ''\n const usd = transfer.amount_usd === null ? 'no USD price' : formatFlowsUsd(transfer.amount_usd)\n return `- ${time}, ${amount}${asset}, ${usd}, tx ${transfer.tx_id}`\n}\n\n/** The short text of a link answer: the pair, the day, the count and the newest few transfers. */\nexport function transfersSummary(view: TransfersView): string {\n const paged = view.through_day !== undefined\n const span = paged && view.through_day !== view.day ? `${view.through_day} to ${view.day}` : view.day\n const head = `Transfers from ${view.from} to ${view.to} on ${span} (UTC) on ${view.network}`\n if (view.transfers.length === 0) {\n return paged && view.next ? `${head}: none; earlier days remain before ${view.next.day}.` : `${head}: none.`\n }\n const count = view.truncated\n ? paged\n ? `${view.transfers.length}, and a day holds more than one read lists`\n : `the newest ${view.transfers.length}, and the day holds more`\n : `${view.transfers.length}`\n const lines = [\n `${head}: ${count}, newest first.`,\n ...view.transfers.slice(0, TRANSFERS_SUMMARY_ROWS).map(transferLine),\n ]\n const rest = view.transfers.length - TRANSFERS_SUMMARY_ROWS\n if (rest > 0) lines.push(`- and ${rest} more in the picture.`)\n if (paged && view.next) lines.push(`- earlier transfers remain, from ${view.next.day} back.`)\n return lines.join('\\n')\n}\n\nfunction errorResult(text: string): FlowsToolResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\n/** The result of a failed read: the structured refusal when the proxy builds one, else the text. */\nasync function failedRead(deps: FlowsDependencies, err: unknown): Promise<FlowsToolResult> {\n if (deps.refusal) {\n try {\n return await deps.refusal(err)\n } catch {\n // The refusal builder itself failed: the plain text still names the cause.\n }\n }\n return errorResult(deps.describeFailure(err))\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction answerText(answer: GraphQueryAnswer): string {\n return (answer.content ?? [])\n .map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))\n .filter(Boolean)\n .join('\\n')\n .trim()\n}\n\n/**\n * The refusal of a read that did not answer, naming its code first:\n * error_detail.code when the graph endpoint sent its typed refusal, the code\n * its text starts with otherwise, and graph_query_failed when it names none.\n * The view marks the address it was expanding.\n */\nexport function refusalText(answer: GraphQueryAnswer): string {\n const text = answerText(answer)\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n const detailCode = isRecord(detail) && typeof detail.code === 'string' ? detail.code : ''\n const textCode = CODE_PREFIX.exec(text)?.[1] ?? ''\n const code = detailCode || textCode || 'graph_query_failed'\n const rest = textCode === code ? text.replace(CODE_PREFIX, '') : text\n return rest ? `${code}: ${rest}` : `${code}: the graph endpoint refused the read`\n}\n\n/** The endpoint says it cut the reply short (facts.query.truncated). */\nfunction cutByEndpoint(answer: GraphQueryAnswer): boolean {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n return isRecord(query) && query.truncated === true\n}\n\nfunction rowsOf(answer: GraphQueryAnswer): Row[] | null {\n const facts = isRecord(answer.structuredContent) ? answer.structuredContent.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n const results = isRecord(query) ? query.results : undefined\n return Array.isArray(results) ? results.filter(isRecord) : null\n}\n\ntype Checked<T> = T | { error: string }\n\nfunction checkNetwork(args: Record<string, unknown>): Checked<string> {\n const rawNetwork = typeof args.network === 'string' ? args.network.trim().toLowerCase() : ''\n if (!rawNetwork) return { error: 'invalid_network: network is required; pass robinhood' }\n if (rawNetwork !== FLOWS_NETWORK) {\n return {\n error: `invalid_network: graph_expand reads the robinhood graph only; got \"${String(args.network).trim()}\"`,\n }\n }\n return FLOWS_NETWORK\n}\n\n/** The address of a node or of one end of a link; `name` says which end in a link refusal. */\nfunction checkAddress(raw: unknown, name?: string): Checked<string> {\n const address = normalizeFlowsAddress(raw)\n if (address) return address\n return {\n error: name\n ? INVALID_ADDRESS_TEXT.replace('invalid_address:', `invalid_address (${name}):`)\n : INVALID_ADDRESS_TEXT,\n }\n}\n\nfunction checkDay(raw: unknown): Checked<string> {\n const day = normalizeFlowsDay(raw)\n return day ?? { error: INVALID_DAY_TEXT }\n}\n\nfunction checkLimit(value: unknown): Checked<number> {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > TRANSFERS_MAX) {\n return { error: INVALID_LIMIT_TEXT }\n }\n return value\n}\n\nfunction checkSkip(value: unknown): Checked<number> {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) return { error: INVALID_SKIP_TEXT }\n return value\n}\n\nfunction checkFirstDay(value: unknown, day: string): Checked<string | null> {\n if (value === undefined || value === null) return null\n const first = normalizeFlowsDay(value)\n if (!first || first > day) return { error: INVALID_FIRST_DAY_TEXT }\n return first\n}\n\nfunction checkOffset(value: unknown): Checked<number> {\n if (value === undefined || value === null) return 0\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return { error: OFFSET_NEGATIVE_TEXT }\n }\n if (value > FLOWS_MAX_OFFSET) return { error: OFFSET_TOO_LARGE_TEXT }\n return value\n}\n\n/**\n * Read and answer one node page. The reads run one after another: the node\n * first, so an address the graph does not hold costs one read; then the\n * receivers and the senders, the chunks of a side joined in order. A chunk that\n * comes back short, and not cut by the endpoint, ends its side, so the next\n * chunk of that side is not read or billed. The first read that does not\n * answer ends the call.\n */\nasync function runNode(\n deps: FlowsDependencies,\n network: string,\n address: string,\n inOffset: number,\n outOffset: number\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const rows: Record<FlowsRead['id'], Row[]> = { node: [], out: [], in: [] }\n const ended = new Set<FlowsRead['id']>()\n for (const read of flowsReadsFor(address, inOffset, outOffset)) {\n if (ended.has(read.id)) continue\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: read.query })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const found = rowsOf(answer)\n if (!found) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n rows[read.id].push(...found)\n if (read.id === 'node' && found.length === 0) {\n return errorResult(`${address} is not in the ${network} topology graph`)\n }\n if (found.length < read.limit && !cutByEndpoint(answer)) ended.add(read.id)\n }\n const view = buildFlowView(network, {\n center: rows.node[0] ?? {},\n senders: rows.in,\n receivers: rows.out,\n inOffset,\n outOffset,\n })\n const now = deps.now ? deps.now() : new Date()\n return {\n content: [{ type: 'text', text: flowsSummary(view, now) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** Read and answer the transfers of one link on one day: a single facts read. */\nasync function runLink(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: transfersReadFor(from, to, day) })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const rows = rowsOf(answer)\n if (!rows) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n const view = buildTransfersView(network, from, to, day, rows)\n return {\n content: [{ type: 'text', text: transfersSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** One facts read of a day, as the paged walk needs it: the rows newest first, or the error that ends the call. */\nasync function readDay(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string\n): Promise<{ rows: TransferRow[]; cut: boolean } | { error: string }> {\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: transfersReadFor(from, to, day) })\n } catch (err) {\n return { error: deps.describeFailure(err) }\n }\n if (answer.isError === true) return { error: refusalText(answer) }\n const rows = rowsOf(answer)\n if (!rows) {\n return { error: 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows' }\n }\n const newestFirst = rows\n .map(transferOf)\n .sort((a, b) => (b.block_timestamp ?? -Infinity) - (a.block_timestamp ?? -Infinity))\n return { rows: newestFirst, cut: rows.length >= TRANSFERS_READ_ROWS }\n}\n\n/**\n * Read and answer one page of a link's transfers: from `day` back, one day a\n * read, until the page holds `limit` transfers, TRANSFERS_MAX_DAYS days were\n * read, or the day before first_day is reached. `skip` passes over the\n * transfers of `day` an earlier page listed. The first read that does not\n * answer ends the call.\n */\nasync function runLinkPage(\n deps: FlowsDependencies,\n network: string,\n from: string,\n to: string,\n day: string,\n page: TransfersPageRequest\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n const floor = page.firstDay ?? TRANSFERS_FLOOR_DAY\n const transfers: TransferRow[] = []\n let current = day\n let skip = page.skip\n let daysRead = 0\n let through = day\n let truncated = false\n let next: TransfersCursor | null = null\n for (;;) {\n if (current < floor) break\n if (daysRead >= TRANSFERS_MAX_DAYS) {\n next = { day: current, skip }\n break\n }\n const read = await readDay(deps, network, from, to, current)\n if ('error' in read) return errorResult(read.error)\n daysRead += 1\n through = current\n if (read.cut) truncated = true\n const rest = read.rows.slice(skip)\n const room = page.limit - transfers.length\n if (rest.length > room) {\n transfers.push(...rest.slice(0, room))\n next = { day: current, skip: skip + room }\n break\n }\n transfers.push(...rest)\n const earlier = dayBefore(current)\n if (transfers.length >= page.limit) {\n next = earlier < floor ? null : { day: earlier, skip: 0 }\n break\n }\n current = earlier\n skip = 0\n }\n const view: TransfersView = {\n schema: TRANSFERS_SCHEMA,\n network,\n from,\n to,\n day,\n transfers,\n truncated,\n through_day: through,\n days_read: daysRead,\n next,\n }\n return {\n content: [{ type: 'text', text: transfersSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\n/** Read and answer the details of one address: a single anchored topology read. */\nasync function runDetails(\n deps: FlowsDependencies,\n network: string,\n address: string\n): Promise<FlowsToolResult> {\n const unavailable = deps.unavailable?.()\n if (unavailable) return errorResult(unavailable)\n let answer: GraphQueryAnswer\n try {\n answer = await deps.graphQuery({ network, query: detailsReadFor(address) })\n } catch (err) {\n return failedRead(deps, err)\n }\n if (answer.isError === true) return errorResult(refusalText(answer))\n const rows = rowsOf(answer)\n if (!rows) {\n return errorResult(\n 'graph_query_failed: the graph endpoint answered without chain-insights.result.v1 rows'\n )\n }\n if (rows.length === 0) return errorResult(`${address} is not in the ${network} topology graph`)\n const view = buildAddressView(network, address, rows[0]!)\n return {\n content: [{ type: 'text', text: addressSummary(view) }],\n structuredContent: view as unknown as Record<string, unknown>,\n isError: false,\n }\n}\n\nfunction given(record: Record<string, unknown>, keys: string[]): boolean {\n return keys.some((key) => record[key] !== undefined && record[key] !== null)\n}\n\n/**\n * graph_expand, the node form {network, address, in_offset?, out_offset?} or the\n * link form {network, from, to, day}. Every argument is checked before the\n * first read.\n */\nexport async function handleGraphExpand(\n args: unknown,\n deps: FlowsDependencies\n): Promise<FlowsToolResult> {\n const record = isRecord(args) ? args : {}\n const network = checkNetwork(record)\n if (typeof network !== 'string') return errorResult(network.error)\n\n const isLink = given(record, ['from', 'to', 'day'])\n if (isLink && given(record, ['address', 'in_offset', 'out_offset', 'details']))\n return errorResult(BOTH_FORMS_TEXT)\n\n if (isLink) {\n const from = checkAddress(record.from, 'from')\n if (typeof from !== 'string') return errorResult(from.error)\n const to = checkAddress(record.to, 'to')\n if (typeof to !== 'string') return errorResult(to.error)\n const day = checkDay(record.day)\n if (typeof day !== 'string') return errorResult(day.error)\n // A limit, a skip or a first_day asks for a page across days; without them the call is the one-day read.\n if (given(record, ['limit', 'skip', 'first_day'])) {\n const limit = checkLimit(record.limit)\n if (typeof limit !== 'number') return errorResult(limit.error)\n const skip = checkSkip(record.skip)\n if (typeof skip !== 'number') return errorResult(skip.error)\n const firstDay = checkFirstDay(record.first_day, day)\n if (firstDay !== null && typeof firstDay !== 'string') return errorResult(firstDay.error)\n return runLinkPage(deps, network, from, to, day, { skip, firstDay, limit })\n }\n return runLink(deps, network, from, to, day)\n }\n\n if (record.details !== undefined && record.details !== null) {\n if (record.details !== true) return errorResult(DETAILS_FORM_TEXT)\n if (given(record, ['in_offset', 'out_offset'])) return errorResult(DETAILS_FORM_TEXT)\n const address = checkAddress(record.address)\n if (typeof address !== 'string') return errorResult(address.error)\n return runDetails(deps, network, address)\n }\n\n const address = checkAddress(record.address)\n if (typeof address !== 'string') return errorResult(address.error)\n const inOffset = checkOffset(record.in_offset)\n if (typeof inOffset !== 'number') return errorResult(inOffset.error)\n const outOffset = checkOffset(record.out_offset)\n if (typeof outOffset !== 'number') return errorResult(outOffset.error)\n return runNode(deps, network, address, inOffset, outOffset)\n}\n\nexport const GRAPH_EXPAND_TITLE = 'Expand a node or a link in the picture'\n\nexport const GRAPH_EXPAND_DESCRIPTION =\n 'Expand an open Chain Insights picture. Called by the view when the investigator clicks, never by the model. ' +\n 'A node, {network, address, in_offset?, out_offset?}, loads the newest senders and receivers of one address ' +\n '(up to 250 a side, from the offsets); billed as at most five graph_query topology reads. A link, {network, from, to, day}, ' +\n 'lists the transfers between two addresses on one UTC day (YYYY-MM-DD), newest first, at most 50; billed as ' +\n 'one graph_query facts read. A link page, {network, from, to, day, limit, skip?, first_day?}, lists `limit` (1-50) ' +\n 'transfers from `day` back across days, newest first, and answers a `next` cursor ({day, skip}) while older ones ' +\n 'remain; billed as at most seven facts reads (a week). Details, {network, address, details: true}, answer the lifetime totals, ' +\n 'counts, role and labels of one address; billed as one topology read. Read-only; an argument refused before a read costs nothing.'\n","import { createHash } from 'node:crypto'\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { CACHE_TTL_MS, chainInsightsHome } from './cache-location.js'\nimport type { GraphSchemaDocument } from './graph-schema.js'\n\n// The graph schema of one network on one endpoint, kept on disk beside the tool\n// list cache (src/mcp/schema-cache.ts) with the same home and the same 24 hour\n// lifetime: ~/.chain-insights/cache/schema-<network>-<endpoint hash>.json.\n//\n// A schema built while a read failed is kept for a short time only. Kept for\n// 24 hours, one refused or rate-limited read would hide a section of the schema\n// for a whole day.\n\n/** How long a schema with a failed read stays fresh. */\nexport const PARTIAL_SCHEMA_TTL_MS = 10 * 60 * 1000\n\ninterface GraphSchemaCacheFile {\n cached_at: number\n complete: boolean\n endpoint: string\n document: GraphSchemaDocument\n}\n\nfunction endpointHash(endpoint: string): string {\n return createHash('sha256').update(endpoint).digest('hex').slice(0, 12)\n}\n\n/** The cache file of a network on an endpoint. The network is an identifier already checked by the caller. */\nexport function graphSchemaCachePath(network: string, endpoint: string): string {\n return path.join(chainInsightsHome(), 'cache', `schema-${network}-${endpointHash(endpoint)}.json`)\n}\n\n/**\n * The cached schema, marked cached, or null when there is none, it is stale, or\n * the file cannot be read. A cache is derived data: a damaged file is a miss and\n * the next build replaces it.\n */\nexport async function loadGraphSchemaCache(\n network: string,\n endpoint: string,\n now: Date\n): Promise<GraphSchemaDocument | null> {\n let parsed: unknown\n try {\n parsed = JSON.parse(await readFile(graphSchemaCachePath(network, endpoint), 'utf8'))\n } catch {\n return null\n }\n const file = parsed as Partial<GraphSchemaCacheFile> | null\n if (\n !file ||\n typeof file.cached_at !== 'number' ||\n file.endpoint !== endpoint ||\n file.document?.schema !== 'chain-insights.graph-schema.v1'\n ) {\n return null\n }\n const lifetime = file.complete === true ? CACHE_TTL_MS : PARTIAL_SCHEMA_TTL_MS\n const age = now.getTime() - file.cached_at\n if (age < 0 || age > lifetime) return null\n return { ...file.document, cached: true }\n}\n\n/** `network` is the name the caller asked for, the same one a later load asks with. */\nexport async function saveGraphSchemaCache(\n network: string,\n document: GraphSchemaDocument,\n endpoint: string,\n complete: boolean,\n now: Date\n): Promise<void> {\n const target = graphSchemaCachePath(network, endpoint)\n const file: GraphSchemaCacheFile = {\n cached_at: now.getTime(),\n complete,\n endpoint,\n document: { ...document, cached: false },\n }\n await mkdir(path.dirname(target), { recursive: true })\n // Written beside the target and renamed, so a second proxy reading the file\n // never sees half of it.\n const scratch = `${target}.${process.pid}.tmp`\n await writeFile(scratch, JSON.stringify(file, null, 2) + '\\n', { mode: 0o600 })\n await rename(scratch, target)\n}\n","/**\n * meta_schema: the live graph schema of one network, read from the graph\n * endpoint and kept on disk for 24 hours.\n *\n * The graph serves four read-only catalog statements on `USE topology` through\n * the ordinary graph_query tool: `CALL db.labels()`, `CALL db.relationshipTypes()`,\n * `CALL db.propertyKeys()` and `SHOW INDEXES`. They name the labels, the link\n * types, every property key and the indexes. They do not say which keys belong to\n * which kind, so the main kinds are sampled with the two probe forms the server\n * admits (`keys(a)` on addresses, `keys(r)` on a link). The facts relationships\n * and the chain lookups come from the network's published capabilities.\n *\n * One build is one MCP session and runs its reads one after another: the public\n * endpoint allows about 30 requests a minute for each IP address, and opening a\n * session costs three of them. A read that fails leaves its section empty and\n * adds a note. It never fails the tool.\n */\n\nimport { findNetworkCapability, mirrorGraphNetworkCapabilities } from './capabilities.js'\nimport type { NetworkCapability } from './capabilities.js'\nimport { PaymentRequiredError } from './client.js'\nimport { refusalText, type GraphQueryAnswer } from './flows.js'\nimport { loadGraphSchemaCache, saveGraphSchemaCache } from './graph-schema-cache.js'\n\nexport const GRAPH_SCHEMA_ID = 'chain-insights.graph-schema.v1' as const\nexport const GRAPH_SCHEMA_SOURCE = 'live catalog' as const\nexport const META_SCHEMA_TITLE = 'Graph schema'\nexport const META_SCHEMA_DESCRIPTION =\n 'Return the live graph schema of one network: the labels, the link types, the field names of an address and of each main link, the indexes, the USE facts relationships with the TRANSFER columns, and the USE chain lookups. ' +\n 'Call it first when you need field names. It is cached for 24 hours; pass refresh=true to rebuild it.'\n\n/** Fields of an address come from this many sampled nodes, and of a link from this many sampled links. */\nexport const ADDRESS_SAMPLE_LIMIT = 20\nexport const LINK_SAMPLE_LIMIT = 5\n\n/** The link types an agent queries. Each is sampled only when the graph lists it. */\nexport const SAMPLED_LINK_TYPES = [\n 'FLOWED_TO',\n 'OPERATED_BY',\n 'LINKED',\n 'SWAPPED',\n 'BRIDGED',\n 'ADDED_LIQUIDITY',\n 'REMOVED_LIQUIDITY',\n] as const\n\n// The TRANSFER columns the cypher skill lists, in its order. tests/graph-schema.test.ts\n// holds this list to the skill and to the columns the graph server maps on TRANSFER\n// (tests/fixtures/facts-columns.json).\nexport const FACTS_TRANSFER_COLUMNS = [\n 'tx_id',\n 'block_date',\n 'block_height',\n 'block_timestamp',\n 'event_index',\n 'edge_index',\n 'kind',\n 'asset_contract',\n 'asset_symbol',\n 'amount',\n 'amount_usd',\n 'price_usd',\n 'price_missing',\n 'token_id',\n 'token_standard',\n 'operator_address',\n 'raw_amount',\n 'decimals',\n] as const\n\n// `CALL db.schema.visualization()` is not read. It bills about 427 units where\n// the four statements below bill 13 to 133 each, and it adds only which labels a\n// link type joins, which the cypher skill and the served hints already say.\nconst CATALOG_READS = {\n labels: { label: 'labels', query: 'USE topology CALL db.labels()' },\n linkTypes: { label: 'link types', query: 'USE topology CALL db.relationshipTypes()' },\n propertyKeys: { label: 'property keys', query: 'USE topology CALL db.propertyKeys()' },\n indexes: { label: 'indexes', query: 'USE topology SHOW INDEXES' },\n} as const\n\nexport function addressSampleQuery(): string {\n return `USE topology MATCH (a:Address) RETURN keys(a) AS keys LIMIT ${ADDRESS_SAMPLE_LIMIT}`\n}\n\nexport function linkSampleQuery(linkType: string): string {\n return `USE topology MATCH ()-[r:${linkType}]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}`\n}\n\nexport interface GraphSchemaIndex {\n name: string\n entity: 'node' | 'link'\n /** The labels of a node index, the link types of a link index. */\n types: string[]\n properties: string[]\n state: string\n}\n\nexport interface GraphSchemaDocument {\n schema: typeof GRAPH_SCHEMA_ID\n network: string\n built_at: string\n source: typeof GRAPH_SCHEMA_SOURCE\n cached: boolean\n /** Units the reads of the build billed, summed from the replies. A cached document keeps the units of its build. */\n billed_units: number\n layers: {\n topology: {\n labels: string[]\n link_types: string[]\n property_keys: string[]\n indexes: GraphSchemaIndex[]\n samples: Record<string, string[]>\n }\n facts: { relationships: string[]; transfer_columns: string[] }\n chain: { lookups: string[] }\n }\n notes: string[]\n}\n\n/** A refusal with its own words for the caller: a bad argument, an unserved network, an endpoint that is down. */\nexport class GraphSchemaError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'GraphSchemaError'\n }\n}\n\nexport interface GraphSchemaClient {\n callTool(request: { name: string; arguments: Record<string, unknown> }): Promise<unknown>\n}\n\ntype Row = Record<string, unknown>\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction sortedUnique(values: Iterable<string>): string[] {\n return [...new Set(values)].sort()\n}\n\nfunction stringList(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string' && entry !== '')\n : []\n}\n\nconst NETWORK_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/\n\n/**\n * The network as a lower-case identifier. It becomes part of a file name and of\n * every query, so only an identifier passes.\n */\nexport function normalizeSchemaNetwork(raw: unknown): string {\n const network = typeof raw === 'string' ? raw.trim().toLowerCase() : ''\n if (!network) {\n throw new GraphSchemaError(\n 'invalid_network: network is required. Pass the identifier of a served network, for example robinhood.'\n )\n }\n if (!NETWORK_PATTERN.test(network)) {\n throw new GraphSchemaError(\n `invalid_network: \"${String(raw).trim()}\" is not a network identifier. Pass the identifier of a served network, for example robinhood.`\n )\n }\n return network\n}\n\n// ─── Reading ──────────────────────────────────────────────────────────────────\n\nfunction queryBlock(structured: unknown): Row | null {\n const facts = isRecord(structured) ? structured.facts : undefined\n const query = isRecord(facts) ? facts.query : undefined\n return isRecord(query) ? query : null\n}\n\n/** The `facts.query` block of one graph_query reply, from structuredContent or from the JSON text. */\nfunction answerQuery(answer: GraphQueryAnswer): Row | null {\n const fromStructured = queryBlock(answer.structuredContent)\n if (fromStructured) return fromStructured\n for (const block of answer.content ?? []) {\n if (block.type !== 'text' || typeof block.text !== 'string') continue\n try {\n const parsed = queryBlock(JSON.parse(block.text))\n if (parsed) return parsed\n } catch {\n /* not JSON: try the next block */\n }\n }\n return null\n}\n\n// A failure after which every later read would fail the same way: the endpoint\n// limits requests, or it asks for payment. The build stops sending reads.\ntype Halt = 'rate_limit' | 'payment'\n\nfunction haltOfError(err: unknown): Halt | undefined {\n if (err instanceof PaymentRequiredError) return 'payment'\n const code = isRecord(err) ? err.code : undefined\n const message = err instanceof Error ? err.message : String(err)\n if (code === 429 || /\\b429\\b|too many requests|rate.?limit/i.test(message)) return 'rate_limit'\n if (/\\b402\\b|payment required/i.test(message)) return 'payment'\n return undefined\n}\n\nfunction haltOfAnswer(answer: GraphQueryAnswer): Halt | undefined {\n const detail = isRecord(answer.structuredContent)\n ? answer.structuredContent.error_detail\n : undefined\n return isRecord(detail) && detail.class === 'capacity' ? 'rate_limit' : undefined\n}\n\nexport interface BuildOptions {\n client: GraphSchemaClient\n /** A normalized network identifier. */\n network: string\n now?: () => Date\n /** A wait between two reads, in milliseconds. One build needs none: it stays under the rate limit. */\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface BuildResult {\n document: GraphSchemaDocument\n /** False when a read failed: the document has a section missing. */\n complete: boolean\n}\n\nfunction describeIndex(row: Row): GraphSchemaIndex | null {\n const name = typeof row.name === 'string' ? row.name : ''\n const entity =\n row.entityType === 'NODE' ? 'node' : row.entityType === 'RELATIONSHIP' ? 'link' : null\n const properties = stringList(row.properties)\n // A lookup index names no label and no property: it tells an agent nothing.\n if (!name || !entity || properties.length === 0) return null\n return {\n name,\n entity,\n types: stringList(row.labelsOrTypes),\n properties,\n state: typeof row.state === 'string' && row.state ? row.state : 'unknown',\n }\n}\n\nexport async function buildGraphSchema(options: BuildOptions): Promise<BuildResult> {\n const { client } = options\n const now = options.now ?? (() => new Date())\n const pauseMs = options.pauseMs ?? 0\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((done) => setTimeout(done, ms)))\n\n const problems: string[] = []\n const remarks: string[] = []\n let billed = 0\n let calls = 0\n let halted = false\n\n async function pace(): Promise<void> {\n if (calls > 0 && pauseMs > 0) await sleep(pauseMs)\n calls += 1\n }\n\n function fail(label: string, reason: string, halt?: Halt): null {\n problems.push(`${label} could not be read: ${reason}`)\n if (halt && !halted) {\n halted = true\n if (halt === 'payment') {\n problems.push(\n 'The endpoint asks for payment, so the reads after it were not sent. Fix the payment setup, then call again with refresh=true.'\n )\n } else {\n // The endpoint says how long to wait: \"retry after 23 seconds\".\n const wait = /retry after (\\d+) seconds?/i.exec(reason)?.[1]\n problems.push(\n `The endpoint is limiting requests, so the reads after it were not sent. Call again after ${wait ? `${wait} seconds` : 'a minute'} with refresh=true.`\n )\n }\n }\n return null\n }\n\n async function graphRead(network: string, label: string, query: string): Promise<Row[] | null> {\n if (halted) return null\n await pace()\n let answer: GraphQueryAnswer\n try {\n answer = (await client.callTool({\n name: 'graph_query',\n arguments: { network, query },\n })) as GraphQueryAnswer\n } catch (err) {\n return fail(label, err instanceof Error ? err.message : String(err), haltOfError(err))\n }\n const block = answerQuery(answer ?? {})\n if (block && typeof block.billable_units === 'number') billed += block.billable_units\n if (answer?.isError === true) {\n return fail(label, refusalText(answer), haltOfAnswer(answer))\n }\n if (!block || !Array.isArray(block.results)) {\n return fail(label, 'the reply holds no rows')\n }\n return block.results.filter(isRecord)\n }\n\n // ── The network: capabilities first, so an unserved name costs no graph read.\n let network = options.network\n let capability: NetworkCapability | undefined\n await pace()\n try {\n const answer = (await client.callTool({\n name: 'network_capabilities',\n arguments: {},\n })) as GraphQueryAnswer\n if (answer?.isError === true) {\n fail('network capabilities', refusalText(answer), haltOfAnswer(answer))\n } else {\n const facts = isRecord(answer?.structuredContent) ? answer.structuredContent.facts : undefined\n const capabilities = isRecord(facts) ? facts.capabilities : undefined\n const networks =\n isRecord(capabilities) && Array.isArray(capabilities.networks)\n ? capabilities.networks\n : null\n if (!networks) {\n fail('network capabilities', 'the reply lists no networks')\n } else {\n const document = mirrorGraphNetworkCapabilities({ networks })\n capability = findNetworkCapability(document, options.network)\n if (!capability) {\n const served = document.networks.map((entry) => entry.network).join(', ')\n throw new GraphSchemaError(\n `network_not_served: \"${options.network}\" is not a network this endpoint serves. Served networks: ${served || 'none'}.`\n )\n }\n network = capability.network\n }\n }\n } catch (err) {\n if (err instanceof GraphSchemaError) throw err\n fail('network capabilities', err instanceof Error ? err.message : String(err), haltOfError(err))\n }\n\n // ── Topology: the catalog, then a sample of the main kinds.\n const topology: GraphSchemaDocument['layers']['topology'] = {\n labels: [],\n link_types: [],\n property_keys: [],\n indexes: [],\n samples: {},\n }\n const topologyOff = capability?.layers?.topology?.enabled === false\n if (topologyOff) {\n remarks.push('The topology layer is off on this network.')\n } else {\n const labels = await graphRead(network, CATALOG_READS.labels.label, CATALOG_READS.labels.query)\n if (labels) topology.labels = sortedUnique(labels.flatMap((row) => stringList([row.label])))\n\n const linkTypes = await graphRead(\n network,\n CATALOG_READS.linkTypes.label,\n CATALOG_READS.linkTypes.query\n )\n if (linkTypes) {\n topology.link_types = sortedUnique(\n linkTypes.flatMap((row) => stringList([row.relationshipType]))\n )\n }\n\n const keys = await graphRead(\n network,\n CATALOG_READS.propertyKeys.label,\n CATALOG_READS.propertyKeys.query\n )\n if (keys) {\n topology.property_keys = sortedUnique(keys.flatMap((row) => stringList([row.propertyKey])))\n }\n\n const indexes = await graphRead(\n network,\n CATALOG_READS.indexes.label,\n CATALOG_READS.indexes.query\n )\n if (indexes) {\n topology.indexes = indexes\n .map(describeIndex)\n .filter((index): index is GraphSchemaIndex => index !== null)\n .sort((a, b) => a.name.localeCompare(b.name))\n }\n\n // An address is sampled unless the label list was read and has no Address.\n const labelsRead = labels !== null\n if (!labelsRead || topology.labels.includes('Address')) {\n const rows = await graphRead(network, 'the Address sample', addressSampleQuery())\n if (rows) topology.samples['Address'] = unionOfKeys(rows)\n }\n\n const sampled = SAMPLED_LINK_TYPES.filter((type) => topology.link_types.includes(type))\n if (linkTypes === null && !halted) {\n problems.push('The link samples were skipped: the link types are unknown.')\n }\n for (const type of sampled) {\n const rows = await graphRead(network, `the ${type} sample`, linkSampleQuery(type))\n if (rows) topology.samples[type] = unionOfKeys(rows)\n }\n\n const unsampled = topology.link_types.filter(\n (type) => !(SAMPLED_LINK_TYPES as readonly string[]).includes(type)\n )\n if (unsampled.length > 0) {\n remarks.push(\n `Link types with no sample: ${unsampled.join(', ')}. Read the keys of one with USE topology MATCH ()-[r:<TYPE>]->() RETURN keys(r) AS keys LIMIT ${LINK_SAMPLE_LIMIT}.`\n )\n }\n }\n\n // ── Facts and chain: what the network publishes.\n const facts: GraphSchemaDocument['layers']['facts'] = { relationships: [], transfer_columns: [] }\n const chain: GraphSchemaDocument['layers']['chain'] = { lookups: [] }\n if (capability) {\n if (capability.layers?.facts?.enabled === false) {\n remarks.push('The facts layer is off on this network.')\n } else {\n facts.relationships = stringList(capability.layers?.facts?.relationships)\n if (facts.relationships.includes('TRANSFER')) {\n facts.transfer_columns = [...FACTS_TRANSFER_COLUMNS]\n }\n }\n const admission = capability.chain_admission\n if (admission?.enabled === false) {\n remarks.push(`The chain layer is off (status ${admission.status ?? 'unknown'}).`)\n } else {\n chain.lookups = stringList(admission?.lookups)\n }\n }\n\n const words = [...topology.property_keys, ...Object.values(topology.samples).flat()]\n if (words.includes('synced_through_height') || words.includes('pair_key')) {\n remarks.push(\n 'synced_through_height and pair_key are sync bookkeeping: never filter or sort on them.'\n )\n }\n remarks.push('Fields come from a sample of the live graph. A rare field may be missing.')\n\n return {\n complete: problems.length === 0,\n document: {\n schema: GRAPH_SCHEMA_ID,\n network,\n built_at: now().toISOString(),\n source: GRAPH_SCHEMA_SOURCE,\n cached: false,\n billed_units: billed,\n layers: { topology, facts, chain },\n notes: [...problems, ...remarks],\n },\n }\n}\n\nfunction unionOfKeys(rows: Row[]): string[] {\n return sortedUnique(rows.flatMap((row) => stringList(row.keys)))\n}\n\n// ─── Reading from the cache, or building ──────────────────────────────────────\n\nexport interface GetGraphSchemaOptions {\n /** As the caller wrote it: normalized here. */\n network: unknown\n refresh?: boolean\n endpoint: string\n /**\n * Runs `fn` with one connected client. It is called once for a build and not at\n * all when the cache answers, so a cache hit opens no session.\n */\n withClient: <T>(fn: (client: GraphSchemaClient) => Promise<T>) => Promise<T>\n now?: () => Date\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\n// Builds in flight, by endpoint and network: two callers that ask for one\n// schema at once share one build and pay for it once.\nconst building = new Map<string, Promise<GraphSchemaDocument>>()\n\nexport async function getGraphSchema(options: GetGraphSchemaOptions): Promise<GraphSchemaDocument> {\n const network = normalizeSchemaNetwork(options.network)\n const now = options.now ?? (() => new Date())\n if (options.refresh !== true) {\n const cached = await loadGraphSchemaCache(network, options.endpoint, now())\n if (cached) return cached\n }\n const key = `${options.endpoint}\\n${network}`\n const running = building.get(key)\n if (running) return running\n const build = (async () => {\n const { document, complete } = await options.withClient((client) =>\n buildGraphSchema({\n client,\n network,\n now,\n ...(options.pauseMs === undefined ? {} : { pauseMs: options.pauseMs }),\n ...(options.sleep ? { sleep: options.sleep } : {}),\n })\n )\n try {\n await saveGraphSchemaCache(network, document, options.endpoint, complete, now())\n } catch (err) {\n document.notes.push(\n `The schema could not be saved to the cache: ${err instanceof Error ? err.message : String(err)}`\n )\n }\n return document\n })()\n building.set(key, build)\n try {\n return await build\n } finally {\n building.delete(key)\n }\n}\n\n// ─── Text ─────────────────────────────────────────────────────────────────────\n\nfunction listLine(title: string, values: string[], empty = 'none read'): string {\n return `${title}: ${values.length > 0 ? values.join(', ') : empty}`\n}\n\n/** The schema as the model and the terminal read it. */\nexport function formatGraphSchemaText(document: GraphSchemaDocument): string {\n const { topology, facts, chain } = document.layers\n const origin = document.cached\n ? 'from the cache, kept 24 hours; refresh=true rebuilds it'\n : `read now for ${document.billed_units} billed units and cached for 24 hours`\n const lines = [\n `Graph schema, network ${document.network}`,\n `Source: ${document.source}, built ${document.built_at} (${origin})`,\n '',\n 'USE topology',\n listLine('Labels', topology.labels),\n listLine('Link types', topology.link_types),\n listLine(`Property keys (${topology.property_keys.length})`, topology.property_keys),\n ]\n lines.push(topology.indexes.length > 0 ? 'Indexes:' : 'Indexes: none read')\n for (const index of topology.indexes) {\n lines.push(\n `- ${index.entity} ${index.types.join('|')}(${index.properties.join(', ')}) ${index.state}`\n )\n }\n const sampled = Object.entries(topology.samples)\n lines.push(sampled.length > 0 ? 'Fields, from a sample:' : 'Fields, from a sample: none read')\n for (const [kind, fields] of sampled) lines.push(`- ${kind}: ${fields.join(', ')}`)\n lines.push(\n '',\n 'USE facts',\n listLine('Relationships', facts.relationships),\n listLine('TRANSFER columns', facts.transfer_columns),\n '',\n 'USE chain',\n listLine('Lookups', chain.lookups)\n )\n if (document.notes.length > 0) {\n lines.push('', 'Notes:', ...document.notes.map((note) => `- ${note}`))\n }\n return lines.join('\\n')\n}\n\n// ─── The MCP tool ─────────────────────────────────────────────────────────────\n\nexport interface GraphSchemaDependencies {\n endpoint: string\n withClient: GetGraphSchemaOptions['withClient']\n /** The text of a failure that is not a GraphSchemaError: payment required, transport. */\n describeFailure: (err: unknown) => string\n now?: () => Date\n pauseMs?: number\n sleep?: (ms: number) => Promise<void>\n}\n\nexport type MetaSchemaResult = {\n content: Array<{ type: 'text'; text: string }>\n structuredContent?: Record<string, unknown>\n isError: boolean\n}\n\nfunction errorResult(text: string): MetaSchemaResult {\n return { content: [{ type: 'text', text }], isError: true }\n}\n\n/** meta_schema, {network, refresh?}. Every argument is checked before the first read. */\nexport async function handleMetaSchema(\n args: unknown,\n deps: GraphSchemaDependencies\n): Promise<MetaSchemaResult> {\n const record = isRecord(args) ? args : {}\n const refresh = record.refresh\n if (refresh !== undefined && refresh !== null && typeof refresh !== 'boolean') {\n return errorResult('invalid_refresh: refresh must be true or false')\n }\n try {\n const document = await getGraphSchema({\n network: record.network,\n refresh: refresh === true,\n endpoint: deps.endpoint,\n withClient: deps.withClient,\n ...(deps.now ? { now: deps.now } : {}),\n ...(deps.pauseMs === undefined ? {} : { pauseMs: deps.pauseMs }),\n ...(deps.sleep ? { sleep: deps.sleep } : {}),\n })\n return {\n content: [{ type: 'text', text: formatGraphSchemaText(document) }],\n structuredContent: document as unknown as Record<string, unknown>,\n isError: false,\n }\n } catch (err) {\n return errorResult(err instanceof GraphSchemaError ? err.message : deps.describeFailure(err))\n }\n}\n"],"mappings":"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"}
@@ -20,7 +20,7 @@ let node_crypto = require("node:crypto");
20
20
  * 250 newest senders and 250 newest receivers, a text summary and
21
21
  * chain-insights.flows.v1 in structuredContent. Composed from at most five
22
22
  * anchored graph_query reads on the topology graph: the node, then its
23
- * outgoing and its incoming FLOWS_TO, newest first, two reads a side because
23
+ * outgoing and its incoming FLOWED_TO, newest first, two reads a side because
24
24
  * a reply holds at most 200 rows.
25
25
  * - link, {network, from, to, day}: the transfers between two known addresses
26
26
  * on one UTC day, newest first, at most 50, a short text summary and
@@ -187,8 +187,8 @@ function flowsReadsFor(address, inOffset, outOffset) {
187
187
  skip: 0,
188
188
  limit: 1
189
189
  },
190
- ...side("out", outOffset, `(a:Address {address: '${address}'})-[f:FLOWS_TO]->(b:Address)`),
191
- ...side("in", inOffset, `(b:Address)-[f:FLOWS_TO]->(a:Address {address: '${address}'})`)
190
+ ...side("out", outOffset, `(a:Address {address: '${address}'})-[f:FLOWED_TO]->(b:Address)`),
191
+ ...side("in", inOffset, `(b:Address)-[f:FLOWED_TO]->(a:Address {address: '${address}'})`)
192
192
  ];
193
193
  }
194
194
  /** The role word of an address, first match wins. */
@@ -861,7 +861,7 @@ const ADDRESS_SAMPLE_LIMIT = 20;
861
861
  const LINK_SAMPLE_LIMIT = 5;
862
862
  /** The link types an agent queries. Each is sampled only when the graph lists it. */
863
863
  const SAMPLED_LINK_TYPES = [
864
- "FLOWS_TO",
864
+ "FLOWED_TO",
865
865
  "OPERATED_BY",
866
866
  "LINKED",
867
867
  "SWAPPED",
@@ -1,15 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
3
- const require_print_result = require("./print-result-Cyqrlx8P.cjs");
3
+ const require_print_result = require("./print-result-if-AbNLY.cjs");
4
4
  const require_rules = require("./rules-BD-cNHMX.cjs");
5
5
  const require_client = require("./client-C9uwHnBS.cjs");
6
- const require_tool_visibility = require("./tool-visibility-BNwvcuVx.cjs");
6
+ const require_tool_visibility = require("./tool-visibility-DTAYvE9E.cjs");
7
7
  const require_usage_status = require("./usage-status-DonwoLR3.cjs");
8
8
  const require_subscription_status = require("./subscription-status-9tMfD90J.cjs");
9
9
  const require_capabilities = require("./capabilities-Ds3320iS.cjs");
10
- const require_graph_schema = require("./graph-schema-BGbJumCY.cjs");
10
+ const require_graph_schema = require("./graph-schema-DNJXDaTn.cjs");
11
11
  const require_action_log = require("./action-log-HniMMF7f.cjs");
12
- const require_dex_tool = require("./dex-tool-DLWnff1n.cjs");
12
+ const require_dex_tool = require("./dex-tool-85_76Q8i.cjs");
13
13
  const require_dex_registry = require("./dex-registry-Chs79Ukc.cjs");
14
14
  let node_url = require("node:url");
15
15
  let node_path = require("node:path");
@@ -240,7 +240,7 @@ const ROUTING_HANDOFF = "The layers hand each other keys, and each key keeps its
240
240
  * A whole-chain question is a time-bounded topology search, sent only when the
241
241
  * family of its link type is close to the head; a refusal names the family.
242
242
  */
243
- 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.";
243
+ 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 `FLOWED_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 `FLOWED_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.";
244
244
  /** What a refusal carries, and which part of it decides the move. */
245
245
  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.";
246
246
  /** The move for each class of the refusal envelope. */
@@ -511,7 +511,7 @@ async function startViewServer(options) {
511
511
  };
512
512
  }
513
513
  Object.freeze(["from_address", "to_address"]);
514
- /** What a FLOWS_TO link carries: the link panel of the view shows each one. */
514
+ /** What a FLOWED_TO link carries: the link panel of the view shows each one. */
515
515
  const GRAPH_LINK_COLUMNS = Object.freeze([
516
516
  "amount_usd_sum",
517
517
  "tx_count",
@@ -534,7 +534,7 @@ const GRAPH_OPTIONAL_COLUMNS = Object.freeze([
534
534
  ...GRAPH_NODE_COLUMNS
535
535
  ]);
536
536
  Object.freeze([...GRAPH_LINK_COLUMNS, ...GRAPH_NODE_COLUMNS]);
537
- /** The `RETURN` tail of a FLOWS_TO recipe over link `f` between `from` and `to`. */
537
+ /** The `RETURN` tail of a FLOWED_TO recipe over link `f` between `from` and `to`. */
538
538
  function flowsRecipeReturn(from, to, link = "f") {
539
539
  return `${from}.address AS from_address, ${to}.address AS to_address, ${link}.amount_usd_sum AS amount_usd_sum, ${link}.tx_count AS tx_count, ${link}.first_seen_timestamp AS first_seen_timestamp, ${link}.last_seen_timestamp AS last_seen_timestamp, ${from}.labels AS from_labels, ${to}.labels AS to_labels, ${from}.is_exchange AS from_is_exchange, ${to}.is_exchange AS to_is_exchange`;
540
540
  }
@@ -717,7 +717,7 @@ function resolveMcpProxyMode(env = process.env) {
717
717
  if (raw === "no-workspace" || raw === "workspace-less") return "stateless";
718
718
  throw new Error(`CHAIN_INSIGHTS_MCP_PROXY_MODE must be workspace or stateless; got "${raw}"`);
719
719
  }
720
- 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.";
720
+ const GRAPH_LAYERS_TEXT = "Use USE topology for topology (address/FLOWED_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.";
721
721
  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.";
722
722
  const GRAPH_QUERY_DEX_DOOR_SENTENCE = "A question that names an exchange and no address starts with meta_dex_lookup, never with a scan of :Protocol, :Pool or labels.";
723
723
  const GRAPH_QUERY_ROLE_MARKS_SENTENCE = ":Sanctioned (is_sanctioned) means the address is on a sanctions list. :IssuerFrozen (is_issuer_frozen) means an issuer froze this wallet; the token in labels says which. Neither is :Scam.";
@@ -743,7 +743,7 @@ const EMPTY_INPUT_SCHEMA = zod.strictObject({});
743
743
  const REMOTE_GRAPH_TOOL_REQUEST_TIMEOUT_MS = 9e5;
744
744
  const PICTURE_RULES = [
745
745
  "Pictures: hosts that draw views, such as Claude Desktop and the Codex app, draw a graph_query answer from its column names. No other tool is needed. Choose the picture by the question: who sent to whom, flows and routes are a graph; how something changed over time is a time series; a list, a lookup or the details of one thing is a table.",
746
- `- 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`,
746
+ `- 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:FLOWED_TO]->(b:Address) WHERE NOT a:Pool RETURN ${flowsRecipeReturn("a", "b")} ORDER BY f.last_seen_timestamp DESC LIMIT 25`,
747
747
  "- 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",
748
748
  "- 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.",
749
749
  "- 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.",
@@ -775,28 +775,28 @@ const GRAPH_SCHEMA_HINTS = [
775
775
  "- An Address carries a kind from chain facts. topology carries :Account or :Contract as a second label on an Address (:Contract:SmartAccount for a contract wallet), with is_contract kept beside :Contract, and an address that only received has none. facts serves no kind, and a facts read that names :Account or :Contract is refused: ask USE topology or USE chain. chain Address returns is_contract, nonce and delegated_to at the newest block, or at the block named by at_block. block_timestamp is epoch milliseconds on every layer.",
776
776
  "- Address nodes carry address, network, labels, and the role flags is_exchange, is_scam, is_victim, is_sanctioned and is_issuer_frozen. (: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.",
777
777
  "- Labels hold role words, never detector names: Exchange (plus the exchange name), Scam, Victim, Sanctioned (plus the entity name) and IssuerFrozen (plus the token symbol). 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, :IssuerFrozen and is_issuer_frozen the issuer_frozen label: an issuer froze this wallet; the token in labels says which. 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.",
778
- "- 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.",
779
- "- 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.",
778
+ "- 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). FLOWED_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.",
779
+ "- For actor-level exposure (AC11), UNION FLOWED_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:FLOWED_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.",
780
780
  "- 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.",
781
781
  "- (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.",
782
782
  "- 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.",
783
- "- Topology relationships include FLOWS_TO, SWAPPED, OPERATED_BY, LINKED, and RISK_PROXIMITY between Address nodes, ADDED_LIQUIDITY and REMOVED_LIQUIDITY to and from Pool nodes, and BRIDGED to and from Chain nodes.",
783
+ "- Topology relationships include FLOWED_TO, SWAPPED, OPERATED_BY, LINKED, and RISK_PROXIMITY between Address nodes, ADDED_LIQUIDITY and REMOVED_LIQUIDITY to and from Pool nodes, and BRIDGED to and from Chain nodes.",
784
784
  "- (: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.",
785
- "- 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, ADDED_LIQUIDITY, REMOVED_LIQUIDITY and BRIDGED: 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.",
786
- "- 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.",
785
+ "- FLOWED_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, ADDED_LIQUIDITY, REMOVED_LIQUIDITY and BRIDGED: bookkeeping there too. Tx ids of every transfer, internal ones included, come from USE facts TRANSFER. Averages come from inline arithmetic. FLOWED_TO into and out of pools is kept; the pool trace rule below governs a trace that reaches one.",
786
+ "- 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. FLOWED_TO carries value only.",
787
787
  "- 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, one link per party, pool and asset, with asset (the token contract in full, or NATIVE for the coin), event_count, amount_raw_sum (the exact total of that asset in base units, as text), usd, unpriced_count, first_seen_height and last_seen_height; REMOVED_LIQUIDITY adds fees_raw_sum (exact, that asset only), receiver_added_usd (USD the receiver itself added to the same pool, over every asset) and receiver_provided. asset_key is bookkeeping, never named in a query. A party with two assets in one pool has two links: sum usd over them for the pool total, and take receiver_added_usd once, as max(). A receiver's profit from a pool is that sum minus receiver_added_usd: receiver_provided false is a drain by a stranger, true with a large profit is the creator's rug pull.",
788
788
  "- 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.",
789
- "- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, one link per address, Chain node, direction, event kind and asset, with kind, asset (the token contract in full, NATIVE for the coin, or UNKNOWN when the bridge named no local contract), events, amount_raw_sum (the exact total of that kind and asset in base units, as text), first_height, last_height and last_bridge_event_id. asset_key is bookkeeping, never named in a query. There is no kinds list and no totals_raw map: an address that used two kinds or two assets has one link for each. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWS_TO walk passes through it.",
790
- "- 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, sum(removal.usd) AS removed_usd, max(removal.receiver_added_usd) AS receiver_added_usd, max(removal.receiver_provided) AS receiver_provided LIMIT 25.",
789
+ "- (:Address)-[:BRIDGED]->(:Chain) is outbound bridge use and (:Chain)-[:BRIDGED]->(:Address) inbound, one link per address, Chain node, direction, event kind and asset, with kind, asset (the token contract in full, NATIVE for the coin, or UNKNOWN when the bridge named no local contract), events, amount_raw_sum (the exact total of that kind and asset in base units, as text), first_height, last_height and last_bridge_event_id. asset_key is bookkeeping, never named in a query. There is no kinds list and no totals_raw map: an address that used two kinds or two assets has one link for each. A Chain node (network, address) is the remote bridge endpoint, never an Address, so no FLOWED_TO walk passes through it.",
790
+ "- 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 `FLOWED_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:FLOWED_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, sum(removal.usd) AS removed_usd, max(removal.receiver_added_usd) AS receiver_added_usd, max(removal.receiver_provided) AS receiver_provided LIMIT 25.",
791
791
  "- 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.",
792
792
  "- 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.",
793
- "- 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.",
794
- "- 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.",
793
+ "- Traversal rule: for BFS, fixed-hop fallback, shortest-path, or manual FLOWED_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.",
794
+ "- Pool guard: a trace walks FLOWED_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\"]) (()-[:FLOWED_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} ()-[:FLOWED_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\"]) (()-[:FLOWED_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} ()-[:FLOWED_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.",
795
795
  "- Call meta_schema {network} first when you need field names; it is cached for 24 hours. The two discovery reads below are the fallback.",
796
796
  "- 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",
797
- "- 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",
797
+ "- Relationship discovery: MATCH (:Address)-[r:FLOWED_TO]->(:Address) RETURN r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count LIMIT 20",
798
798
  "- 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.",
799
- "- 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.",
799
+ "- graph_query uses the active Chain Insights graph endpoint. Select the graph with USE topology for topology (address/FLOWED_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.",
800
800
  "- 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.",
801
801
  "- Use USE facts graph patterns for fact and enrichment reads. Do not query internal table namespaces directly."
802
802
  ].join("\n");
@@ -1124,7 +1124,7 @@ function registerLocalPrompts(server) {
1124
1124
  query,
1125
1125
  "```",
1126
1126
  "",
1127
- `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`
1127
+ `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWED_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`
1128
1128
  ].join("\n"), "Graph query"));
1129
1129
  server.registerPrompt("graph-query-batch", {
1130
1130
  title: "Graph Query Batch",
@@ -1142,7 +1142,7 @@ function registerLocalPrompts(server) {
1142
1142
  "```",
1143
1143
  per_query_timeout_seconds ? `per_query_timeout_seconds: ${per_query_timeout_seconds}` : "",
1144
1144
  "",
1145
- `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`
1145
+ `${GRAPH_LAYERS_TEXT} If you need schema context, first run small discovery queries such as MATCH (a:Address) RETURN a.address AS address, keys(a) AS address_properties LIMIT 5 and MATCH (:Address)-[r:FLOWED_TO]->(:Address) RETURN keys(r) AS flow_properties LIMIT 5. Return the full address when available; never shorten addresses with ellipses.`
1146
1146
  ].filter(Boolean).join("\n"), "Graph query batch"));
1147
1147
  server.registerPrompt("wallet-balance", {
1148
1148
  title: "Wallet Balance",