chain-insights 0.37.0 → 0.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-tools-CDpFHbZ8.mjs","names":["isRecord","numberValue"],"sources":["../src/viz/graph-normalizer.ts","../src/investigation/risk-level.ts","../src/workspace/output-root.ts","../src/lib/usage-accumulator.ts","../src/investigation/public-tools.ts"],"sourcesContent":["const GRAPH_TYPE_LABELS = new Set([\n 'Address',\n 'Exchange',\n 'Miner',\n 'Validator',\n 'Hotkey',\n 'Subnet',\n 'IPAddress',\n])\nconst FLOW_EDGE_TYPES = new Set(['flows_to', 'transfer', 'transfer_to', 'transfers_to'])\n\ntype GraphRecord = Record<string, unknown>\n\nexport type NormalizedGraphPayload = {\n schema: 'chain-insights.graph.v1'\n nodes: GraphRecord[]\n edges: GraphRecord[]\n flows: unknown[]\n edge_anchors: unknown[]\n [key: string]: unknown\n}\n\nfunction isRecord(value: unknown): value is GraphRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction stringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.map(String).filter(Boolean) : []\n}\n\nfunction unique(values: string[]): string[] {\n return [...new Set(values)]\n}\n\nfunction lowerSnake(value: string): string {\n return value\n .trim()\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase()\n}\n\nfunction isGraphTypeLabel(label: string): boolean {\n return GRAPH_TYPE_LABELS.has(label)\n}\n\nfunction displayLabels(node: GraphRecord): string[] {\n return unique(stringArray(node['labels'])).filter((label) => !isGraphTypeLabel(label))\n}\n\nfunction systemLabels(node: GraphRecord): string[] {\n return unique([\n ...stringArray(node['system_labels']),\n ...stringArray(node['graph_labels']),\n ...stringArray(node['raw_labels']),\n ...stringArray(node['labels']).filter(isGraphTypeLabel),\n ])\n}\n\nfunction normalizeRoles(node: GraphRecord): string[] {\n const roles = stringArray(node['roles'])\n const role = typeof node['role'] === 'string' ? node['role'] : ''\n if (role === 'source_exchange') roles.push('exchange')\n else if (role) roles.push(role)\n\n const display = stringArray(node['labels']).map(lowerSnake)\n if (\n systemLabels(node).some((label) => lowerSnake(label) === 'exchange') ||\n display.includes('exchange')\n ) {\n roles.push('exchange')\n }\n\n return unique(roles.map(lowerSnake).filter(Boolean))\n}\n\nfunction normalizeNodeType(node: GraphRecord): string {\n if (typeof node['node_type'] === 'string' && node['node_type'].trim()) {\n return lowerSnake(node['node_type'])\n }\n if (typeof node['address'] === 'string' || typeof node['id'] === 'string') return 'address'\n const labels = systemLabels(node)\n if (labels.length > 0) return lowerSnake(labels[0]!)\n return 'unknown'\n}\n\nfunction normalizeNode(node: unknown): GraphRecord {\n if (!isRecord(node)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n [\n 'address_subtypes',\n 'entity_kind',\n 'graph_labels',\n 'labels',\n 'node_type',\n 'pattern_flags',\n 'raw_labels',\n 'role',\n 'roles',\n 'system_labels',\n 'type',\n ].includes(key)\n ) {\n continue\n }\n normalized[key] = value\n }\n\n const id =\n typeof node['id'] === 'string'\n ? node['id']\n : typeof node['address'] === 'string'\n ? node['address']\n : undefined\n if (id) normalized['id'] = id\n\n const address = typeof node['address'] === 'string' ? node['address'] : id\n if (address) normalized['address'] = address\n\n normalized['node_type'] = normalizeNodeType(node)\n normalized['labels'] = displayLabels(node)\n\n const addressSubtypes = unique(stringArray(node['address_subtypes']))\n if (addressSubtypes.length > 0) normalized['address_subtypes'] = addressSubtypes\n\n const roles = normalizeRoles(node)\n if (roles.length > 0) normalized['roles'] = roles\n\n if (\n !Array.isArray(node['flags']) &&\n Array.isArray(node['pattern_flags']) &&\n node['pattern_flags'].length > 0\n ) {\n normalized['flags'] = node['pattern_flags'].map(String)\n }\n\n return normalized\n}\n\nfunction normalizeEdgeType(edge: GraphRecord): string {\n const rawType =\n typeof edge['edge_type'] === 'string' && edge['edge_type'].trim()\n ? edge['edge_type']\n : typeof edge['type'] === 'string' && edge['type'].trim()\n ? edge['type']\n : typeof edge['relationship_type'] === 'string' && edge['relationship_type'].trim()\n ? edge['relationship_type']\n : 'related_to'\n const edgeType = lowerSnake(rawType)\n return FLOW_EDGE_TYPES.has(edgeType) ? 'flows_to' : edgeType\n}\n\nfunction normalizeEdge(edge: unknown): GraphRecord {\n if (!isRecord(edge)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(edge)) {\n if (['edge_type', 'from_address', 'relationship_type', 'to_address', 'type'].includes(key))\n continue\n normalized[key] = value\n }\n\n if (typeof normalized['source'] !== 'string' && typeof edge['from_address'] === 'string') {\n normalized['source'] = edge['from_address']\n }\n if (typeof normalized['target'] !== 'string' && typeof edge['to_address'] === 'string') {\n normalized['target'] = edge['to_address']\n }\n normalized['edge_type'] = normalizeEdgeType(edge)\n\n return normalized\n}\n\nexport function normalizeGraphPayload(payload: unknown): NormalizedGraphPayload {\n if (!isRecord(payload) || payload['schema'] !== 'chain-insights.graph.v1') {\n throw new Error('Unsupported graph payload schema')\n }\n\n return {\n ...payload,\n schema: 'chain-insights.graph.v1',\n nodes: Array.isArray(payload['nodes']) ? payload['nodes'].map(normalizeNode) : [],\n edges: Array.isArray(payload['edges']) ? payload['edges'].map(normalizeEdge) : [],\n flows: Array.isArray(payload['flows']) ? payload['flows'] : [],\n edge_anchors: Array.isArray(payload['edge_anchors']) ? payload['edge_anchors'] : [],\n }\n}\n","/**\n * Risk-level normalization shared by the AML tools and graph payload builders.\n *\n * The Chain Insights Graph serves the ML verdict verbatim, which historically\n * mixed cases (HIGH vs high) and — since the calibrated-scoring release —\n * includes an explicit abstention band: risk_level = UNSCORED means the model\n * had too little labeled graph context to stand behind a band. UNSCORED is a\n * \"no stance\" state and must never be rendered or reasoned about as low risk.\n */\n\nexport const RISK_SEVERITIES = ['low', 'medium', 'high', 'critical'] as const\nexport const UNSCORED_RISK_LEVEL = 'unscored'\n\n/** Lowercased, trimmed risk level; undefined for empty/unrecognized values. */\nexport function normalizeRiskLevel(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const normalized = value.trim().toLowerCase()\n if (!normalized) return undefined\n if (normalized === UNSCORED_RISK_LEVEL) return UNSCORED_RISK_LEVEL\n if ((RISK_SEVERITIES as readonly string[]).includes(normalized)) return normalized\n return undefined\n}\n\n/** True when the ML verdict explicitly abstained (no stance, not low). */\nexport function isUnscoredRiskLevel(value: unknown): boolean {\n return normalizeRiskLevel(value) === UNSCORED_RISK_LEVEL\n}\n\n/**\n * Severity ordering for comparisons: low(0) < medium(1) < high(2) <\n * critical(3). Unknown/unscored values rank -1 — they never win a\n * severity comparison, they abstain from it.\n */\nexport function riskSeverityRank(value: unknown): number {\n const normalized = normalizeRiskLevel(value)\n if (!normalized || normalized === UNSCORED_RISK_LEVEL) return -1\n return (RISK_SEVERITIES as readonly string[]).indexOf(normalized)\n}\n","import path from 'node:path'\nimport { findActiveWorkspace } from './active.js'\n\nexport const NO_WORKSPACE_ERROR = 'No Chain Insights workspace found. Run: cia init .'\n\nexport interface WorkspaceOutputPaths {\n root: string\n metadataDir: string\n schemaDir: string\n runtimeDir: string\n artifactsRoot: string\n entitiesRoot: string\n sessionsRoot: string\n publishedRoot: string\n reportsRoot: string\n reportGraphsRoot: string\n reportTablesRoot: string\n logsRoot: string\n}\n\nexport function requireWorkspaceRoot(startDir = process.cwd()): string {\n const workspace = findActiveWorkspace(startDir)\n if (!workspace) throw new Error(NO_WORKSPACE_ERROR)\n return workspace.root\n}\n\nexport function workspaceOutputPaths(workspaceRoot = requireWorkspaceRoot()): WorkspaceOutputPaths {\n const root = path.resolve(workspaceRoot)\n return {\n root,\n metadataDir: path.join(root, '.chain-insights'),\n schemaDir: path.join(root, '.chain-insights', 'schema'),\n runtimeDir: path.join(root, '.chain-insights', 'runtime'),\n artifactsRoot: path.join(root, 'artifacts'),\n entitiesRoot: path.join(root, 'entities'),\n sessionsRoot: path.join(root, 'sessions'),\n publishedRoot: path.join(root, 'published'),\n reportsRoot: path.join(root, 'reports'),\n reportGraphsRoot: path.join(root, 'reports', 'graphs'),\n reportTablesRoot: path.join(root, 'reports', 'tables'),\n logsRoot: path.join(root, '.chain-insights', 'runtime', 'logs'),\n }\n}\n","/**\n * Per-workflow billable-units accounting for the aml_* tools (internal feature:\n * usage-units-propagation). The `aml_*` workflow tools make MANY internal\n * graph_query_batch round trips to answer one high-level question; this\n * module totals what those round trips billed so the workflow response can\n * report a single `usage` block instead of exposing nothing at all.\n *\n * Capture point: aml_address_risk (and internal graph_query_batch helper\n * calls feeding its enrichment) wraps its `remoteClient` once via\n * `wrapClientForUsageTracking` before making any internal calls, then reads\n * the shared `UsageTotals` back out when building its response. Every\n * internal helper (callGraphBatch, probeSeedAddresses, ...) keeps calling\n * `client.callTool(...)` exactly as before -- the wrapper observes\n * graph_query_batch/graph_query responses in transit and never changes their\n * return value.\n *\n * Defensive by design: a backend that does not yet emit billing fields (or\n * an unparsable response) contributes 0 units but the call still counts\n * toward `query_count` -- this must never throw and never block a workflow.\n */\n\ninterface CallToolLike {\n callTool(...args: unknown[]): Promise<unknown>\n}\n\nexport interface UsageTotals {\n billable_units: number\n query_count: number\n truncated_queries: number\n}\n\nexport function createUsageAccumulator(): UsageTotals {\n return { billable_units: 0, query_count: 0, truncated_queries: 0 }\n}\n\n// Snapshot as a plain object for embedding in a response -- callers should\n// not hand out the live mutable accumulator.\nexport function usageBlock(totals: UsageTotals): UsageTotals {\n return {\n billable_units: totals.billable_units,\n query_count: totals.query_count,\n truncated_queries: totals.truncated_queries,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction numberValue(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\nfunction textOfToolResult(result: unknown): string {\n if (!isRecord(result)) return ''\n const content = result['content']\n if (!Array.isArray(content)) return ''\n return content\n .filter(\n (item): item is { type: 'text'; text: string } =>\n isRecord(item) && item['type'] === 'text' && typeof item['text'] === 'string'\n )\n .map((item) => item.text)\n .join('\\n')\n}\n\n// facts.query.* is the single-query shape (graph_query). facts.batch.* /\n// facts.queries[] is the batch shape (graph_query_batch). Both are read\n// defensively -- missing/malformed fields never throw, they just contribute\n// nothing to the total.\nfunction accumulateFacts(totals: UsageTotals, facts: unknown): void {\n if (!isRecord(facts)) return\n\n const query = facts['query']\n if (isRecord(query)) {\n const units = numberValue(query['billable_units'])\n if (units !== undefined) totals.billable_units += units\n if (query['truncated'] === true) totals.truncated_queries += 1\n }\n\n const batch = facts['batch']\n const batchUnits = isRecord(batch) ? numberValue(batch['billable_units']) : undefined\n const queries = Array.isArray(facts['queries'])\n ? (facts['queries'] as unknown[]).filter(isRecord)\n : undefined\n\n if (batchUnits !== undefined) {\n totals.billable_units += batchUnits\n } else if (queries) {\n // No batch-level total (older/mocked backend): fall back to summing\n // whatever per-query billable_units are present. Entries with no field\n // contribute 0, same as the single-query defensive path.\n for (const entry of queries) {\n const units = numberValue(entry['billable_units'])\n if (units !== undefined) totals.billable_units += units\n }\n }\n\n if (queries) {\n for (const entry of queries) {\n if (entry['truncated'] === true) totals.truncated_queries += 1\n }\n } else if (isRecord(batch) && batch['truncated'] === true) {\n totals.truncated_queries += 1\n }\n}\n\n// One graph_query_batch round trip is one query_count unit per Cypher\n// statement it carried (facts.queries.length) -- that is what \"one internal\n// graph query\" means for this tool. A round trip whose response could not be\n// attributed to individual statements (parse failure, missing facts.queries)\n// still represents one internal graph query having been issued, so it counts\n// as 1 rather than 0.\nfunction recordGraphQueryBatchResult(totals: UsageTotals, rawResult: unknown): void {\n let facts: unknown\n try {\n const text = textOfToolResult(rawResult).trim()\n if (text) facts = (JSON.parse(text) as { facts?: unknown }).facts\n else if (\n isRecord(rawResult) &&\n isRecord((rawResult as Record<string, unknown>)['structuredContent'])\n ) {\n facts = (\n (rawResult as Record<string, unknown>)['structuredContent'] as Record<string, unknown>\n )['facts']\n }\n } catch {\n facts = undefined\n }\n\n const queries =\n isRecord(facts) && Array.isArray(facts['queries']) ? (facts['queries'] as unknown[]) : undefined\n totals.query_count += queries ? queries.length : 1\n accumulateFacts(totals, facts)\n}\n\n// One graph_query round trip is exactly one internal graph query.\nfunction recordGraphQueryResult(totals: UsageTotals, rawResult: unknown): void {\n totals.query_count += 1\n let facts: unknown\n try {\n if (\n isRecord(rawResult) &&\n isRecord((rawResult as Record<string, unknown>)['structuredContent'])\n ) {\n facts = (\n (rawResult as Record<string, unknown>)['structuredContent'] as Record<string, unknown>\n )['facts']\n } else {\n const text = textOfToolResult(rawResult).trim()\n if (text) facts = (JSON.parse(text) as { facts?: unknown }).facts\n }\n } catch {\n facts = undefined\n }\n accumulateFacts(totals, facts)\n}\n\n// Wraps a Client-shaped object (real MCP SDK Client or a test double) so\n// every graph_query_batch/graph_query call it makes is observed and totaled\n// into `totals`, without changing what the call returns. Any other method\n// or property passes through untouched.\nexport function wrapClientForUsageTracking<T extends CallToolLike>(\n client: T,\n totals: UsageTotals\n): T {\n return new Proxy(client as object, {\n get(target, prop, receiver) {\n if (prop === 'callTool') {\n return async (...args: unknown[]) => {\n const result = await (target as CallToolLike).callTool(...args)\n const request = args[0]\n const toolName =\n isRecord(request) && typeof request['name'] === 'string' ? request['name'] : undefined\n if (toolName === 'graph_query_batch') {\n recordGraphQueryBatchResult(totals, result)\n } else if (toolName === 'graph_query') {\n recordGraphQueryResult(totals, result)\n }\n return result\n }\n }\n const value = Reflect.get(target, prop, target)\n return typeof value === 'function' ? value.bind(target) : value\n },\n }) as T\n}\n","import { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport type { ContentBlock } from '@modelcontextprotocol/sdk/types.js'\nimport { normalizeGraphPayload } from '../viz/graph-normalizer.js'\nimport { isUnscoredRiskLevel, normalizeRiskLevel, riskSeverityRank } from './risk-level.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport { GRAPH_TOOL_REQUEST_TIMEOUT_MS } from '../mcp/request-timeout.js'\nimport {\n createUsageAccumulator,\n usageBlock,\n wrapClientForUsageTracking,\n} from '../lib/usage-accumulator.js'\n\nexport { buildTruthProposal, resolveTruthIngressTlsPaths } from './truth-proposal.js'\nexport type { TruthProposalCommand } from './truth-proposal.js'\n\ntype RemoteToolResult = {\n content?: ContentBlock[]\n isError?: boolean\n}\n\ninterface ParsedGraphBatch {\n facts?: {\n queries?: Array<{\n id?: string\n ok?: boolean\n results?: Array<Record<string, unknown>>\n error?: string\n }>\n }\n}\n\ntype QueryFailure = {\n id: string\n error: string\n}\n\nexport interface AddressRiskOptions {\n address: string\n network: string\n compareAddress?: string\n writeArtifacts?: boolean\n}\n\nconst AML_ADDRESS_RISK_SUPPORTED_VERSIONS = ['v1'] as const\nexport type AmlAddressRiskVersion = (typeof AML_ADDRESS_RISK_SUPPORTED_VERSIONS)[number]\nexport const LATEST_AML_ADDRESS_RISK_VERSION: AmlAddressRiskVersion = 'v1'\n\nexport function resolveAmlAddressRiskVersion(requested?: string): AmlAddressRiskVersion {\n const normalized = requested?.trim().toLowerCase()\n if (!normalized) return LATEST_AML_ADDRESS_RISK_VERSION\n if (\n AML_ADDRESS_RISK_SUPPORTED_VERSIONS.includes(\n normalized as (typeof AML_ADDRESS_RISK_SUPPORTED_VERSIONS)[number]\n )\n ) {\n return normalized as AmlAddressRiskVersion\n }\n throw new Error(\n `Unsupported aml_address_risk version \"${requested?.trim() ?? ''}\". Supported versions: ${AML_ADDRESS_RISK_SUPPORTED_VERSIONS.join(', ')}.`\n )\n}\n\nfunction escapeCypherString(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"')\n}\n\nfunction textFromToolResult(result: RemoteToolResult): string {\n return (result.content ?? [])\n .filter((item): item is Extract<ContentBlock, { type: 'text' }> => item.type === 'text')\n .map((item) => item.text)\n .join('\\n')\n}\n\nfunction parseGraphBatchResult(result: RemoteToolResult): ParsedGraphBatch {\n const text = textFromToolResult(result).trim()\n if (!text) throw new Error('graph_query_batch returned no text content')\n const parsed = JSON.parse(text) as ParsedGraphBatch\n if (!parsed.facts?.queries)\n throw new Error('graph_query_batch response did not include facts.queries')\n return parsed\n}\n\nfunction topologyGraphQuery(query: string): string {\n const trimmed = query.trim()\n if (/^USE\\s+/i.test(trimmed)) return trimmed\n return `USE topology ${trimmed}`\n}\n\nfunction collectQueryFailure(\n failures: QueryFailure[],\n id: string,\n error: string | undefined\n): void {\n failures.push({ id, error: error || 'unknown error' })\n}\n\nfunction optionalResultsFor(\n batch: ParsedGraphBatch,\n id: string,\n failures: QueryFailure[]\n): Array<Record<string, unknown>> {\n const query = batch.facts?.queries?.find((entry) => entry.id === id)\n if (!query) return []\n if (query.ok === false) {\n collectQueryFailure(failures, id, query.error)\n return []\n }\n return query.results ?? []\n}\n\nfunction optionalResultsWithPrefix(\n batch: ParsedGraphBatch,\n prefix: string,\n failures: QueryFailure[]\n): Array<Record<string, unknown>> {\n return (batch.facts?.queries ?? [])\n .filter((entry) => entry.id?.startsWith(prefix))\n .flatMap((entry) => {\n if (entry.ok === false) {\n collectQueryFailure(failures, entry.id ?? prefix, entry.error)\n return []\n }\n return entry.results ?? []\n })\n}\n\nasync function callGraphBatch(\n remoteClient: Client,\n network: string,\n queries: Array<{ id: string; query: string }>\n): Promise<ParsedGraphBatch> {\n const result = (await remoteClient.callTool(\n {\n name: 'graph_query_batch',\n arguments: {\n network,\n queries: queries.map((query) => ({\n ...query,\n query: topologyGraphQuery(query.query),\n })),\n },\n },\n undefined,\n {\n timeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n maxTotalTimeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n }\n )) as RemoteToolResult\n if (result.isError) throw new Error(textFromToolResult(result) || 'graph_query_batch failed')\n return parseGraphBatchResult(result)\n}\n\nfunction addressProfileQuery(address: string): { id: string; query: string } {\n // risk_score/risk_level are always projected: topology is now the only ML\n // risk source (the retired facts_risk_scores_view never had a risk_level\n // column, so UNSCORED abstention was invisible until this move).\n const riskFields = ', a.risk_score AS live_risk_score, a.risk_level AS live_risk_level'\n // Per-label risk is the overlay graphsync materializes on the node, one\n // entry per current label row. DozerDB properties cannot hold a list of\n // maps, so it is three parallel lists: label_risk_labels, label_risk_levels\n // and label_risk_updated_timestamps. deriveLabelRows zips them back.\n // Missing/empty is \"no label-risk signal,\" never an error.\n const labelRiskFields =\n ', a.label_risk_labels AS label_risk_labels, a.label_risk_levels AS label_risk_levels, a.label_risk_updated_timestamps AS label_risk_updated_timestamps'\n return {\n id: 'address_profile',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n `RETURN a.address AS address, a.network AS network, a.labels AS display_labels, a.labels AS system_labels, a.is_exchange AS is_exchange${riskFields}${labelRiskFields}`,\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\n// Lifetime address features are materialized on the one USE topology node.\nfunction addressFeatureQuery(address: string): { id: string; query: string } {\n return {\n id: 'address_feature',\n query: [\n 'USE topology',\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n 'RETURN a.degree_in AS degree_in, a.degree_out AS degree_out, a.degree_total AS degree_total, a.tx_in_count AS tx_in_count, a.tx_out_count AS tx_out_count, a.tx_total_count AS tx_total_count, a.total_volume_usd AS total_volume_usd, a.total_in_usd AS total_in_usd, a.total_out_usd AS total_out_usd, a.net_flow_usd AS net_flow_usd, a.first_activity_timestamp AS first_activity_timestamp, a.last_activity_timestamp AS last_activity_timestamp, a.activity_span_days AS activity_span_days',\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\nfunction flowEdgeMap(variableName: string): string {\n return `{amount_usd_sum: ${variableName}.amount_usd_sum, tx_count: ${variableName}.tx_count, first_seen_timestamp: ${variableName}.first_seen_timestamp, last_seen_timestamp: ${variableName}.last_seen_timestamp}`\n}\n\nfunction pathNodeMap(variableName: string): string {\n // risk_score/risk_level are always projected on topology nodes.\n const riskFields = `, risk_score: ${variableName}.risk_score, risk_level: ${variableName}.risk_level`\n return `{address: ${variableName}.address, network: ${variableName}.network, labels: ${variableName}.labels, system_labels: ${variableName}.labels, is_exchange: ${variableName}.is_exchange${riskFields}}`\n}\n\n// money-trail enrichment: incident MONEY_TRAIL edges on the subject address\n// (either direction), and the TRAIL_ENDS_AT fan-out of whichever seed\n// generated the trail -- both are optional-results reads, so a graph with no\n// money-trail layer (or a failed query) never fails aml_address_risk itself.\nexport function moneyTrailIncidentQuery(address: string): string {\n return [\n 'USE topology',\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:MONEY_TRAIL]-()`,\n 'RETURN r.edge_class AS edge_class, r.value AS value, r.min_hop AS min_hop, r.seed_count AS seed_count, r.primary_seed AS primary_seed, r.generation AS generation, r.network AS network, r.first_ts AS first_ts, r.last_ts AS last_ts',\n 'LIMIT 200',\n ].join(' ')\n}\n\nexport function moneyTrailEndsQuery(seed: string): string {\n return [\n 'USE topology',\n `MATCH (s:Address {address: \"${escapeCypherString(seed)}\"})-[r:TRAIL_ENDS_AT]->(t:Address)`,\n 'RETURN t.address AS address, r.fact_type AS fact_type, r.direction AS direction, r.terminal_role AS terminal_role, r.hop AS hop, r.value AS value, r.generation AS generation',\n 'LIMIT 200',\n ].join(' ')\n}\n\n// Task 2 (trace fast path): a resolved trace target IS a money-trail seed\n// when it has outgoing TRAIL_ENDS_AT edges -- same shape as\n// moneyTrailEndsQuery, reused here under a probe-specific name so the\n// trace-tool call site reads as \"check whether this address is a seed\"\n// rather than \"fetch the ends fan of a known seed\" (they happen to be the\n// same read).\nexport function moneyTrailSeedProbeQuery(address: string): string {\n return moneyTrailEndsQuery(address)\n}\n\n// Precomputed MONEY_TRAIL corridor from a confirmed seed: bounded to 6 hops\n// (min_hop is a property baked onto the edge by the walk engine, not a\n// variable-length path depth) and excludes peripheral-only continuation --\n// peripheral is the \"touched funds\" floor (see MONEY_TRAIL_CLASS_RANK), not\n// a real trail hop, so it never extends the fast-path corridor.\nexport function moneyTrailCorridorQuery(seed: string): string {\n return [\n 'USE topology',\n `MATCH (s:Address {address: \"${escapeCypherString(seed)}\"})-[r:MONEY_TRAIL]->(t:Address)`,\n 'WHERE r.min_hop <= 6 AND r.edge_class <> \"peripheral\"',\n 'RETURN t.address AS address, r.edge_class AS edge_class, r.value AS value, r.min_hop AS min_hop, r.seed_count AS seed_count, r.primary_seed AS primary_seed, r.generation AS generation, r.network AS network',\n 'ORDER BY r.min_hop ASC',\n 'LIMIT 200',\n ].join(' ')\n}\n\nexport interface MoneyTrailBlock {\n on_trail: true\n class: string\n min_hop: number\n primary_seed: string\n generation: number\n nearest_trail_end?: { address: string; fact_type: string; value: string }\n}\n\nconst MONEY_TRAIL_CLASS_RANK: Record<string, number> = { transport: 3, holding: 2, peripheral: 1 }\n\nexport function buildMoneyTrailBlock(\n incidentRows: Array<Record<string, unknown>>,\n endRows: Array<Record<string, unknown>>\n): MoneyTrailBlock | undefined {\n if (incidentRows.length === 0) return undefined\n\n const winningClassRank = Math.max(\n ...incidentRows.map((row) => MONEY_TRAIL_CLASS_RANK[firstString(row['edge_class']) ?? ''] ?? 0)\n )\n const winningClass =\n Object.entries(MONEY_TRAIL_CLASS_RANK).find(([, rank]) => rank === winningClassRank)?.[0] ??\n firstString(incidentRows[0]?.['edge_class']) ??\n 'peripheral'\n const rowsOfWinningClass = incidentRows.filter(\n (row) => firstString(row['edge_class']) === winningClass\n )\n const bestRow = rowsOfWinningClass.reduce((best, row) => {\n const rowHop = numberValue(row['min_hop']) ?? Number.POSITIVE_INFINITY\n const bestHop = numberValue(best['min_hop']) ?? Number.POSITIVE_INFINITY\n return rowHop < bestHop ? row : best\n }, rowsOfWinningClass[0]!)\n\n const nearestEnd = endRows.reduce<Record<string, unknown> | undefined>((best, row) => {\n const rowValue = numberValue(row['value']) ?? Number.NEGATIVE_INFINITY\n const bestValue = best\n ? (numberValue(best['value']) ?? Number.NEGATIVE_INFINITY)\n : Number.NEGATIVE_INFINITY\n return !best || rowValue > bestValue ? row : best\n }, undefined)\n\n return {\n on_trail: true,\n class: winningClass,\n min_hop: numberValue(bestRow['min_hop']) ?? 0,\n primary_seed: firstString(bestRow['primary_seed']) ?? '',\n generation: numberValue(bestRow['generation']) ?? 0,\n ...(nearestEnd\n ? {\n nearest_trail_end: {\n address: firstString(nearestEnd['address']) ?? '',\n fact_type: firstString(nearestEnd['fact_type']) ?? '',\n value: String(nearestEnd['value'] ?? ''),\n },\n }\n : {}),\n }\n}\n\nexport function moneyTrailSummarySentence(block: MoneyTrailBlock): string {\n return block.class === 'peripheral'\n ? `This address touched money-trail funds (${block.class}, min hop ${block.min_hop}).`\n : `This address sits on a money trail (${block.class}, min hop ${block.min_hop}).`\n}\n\n// Pool guard (schema skill, \"Pool trace rule\"): a FLOWS_TO walk never starts\n// at a :Pool and never passes through one, and each guard is its own AND term\n// (inside an OR it would guard nothing). Fixed-hop form of\n// skills/chain-insights-cypher: `WHERE NOT src:Pool AND NOT mid:Pool`.\nfunction poolGuardTerms(nodeVariables: string[]): string {\n return nodeVariables.map((nodeVariable) => `NOT ${nodeVariable}:Pool`).join(' AND ')\n}\n\nfunction exchangeOutflowQueries(address: string): Array<{ id: string; query: string }> {\n return Array.from({ length: 3 }, (_, index) => exchangeOutflowQueryAtDepth(address, index + 1))\n}\n\nfunction exchangeOutflowQueryAtDepth(\n address: string,\n depth: number\n): { id: string; query: string } {\n const intermediateVariables = Array.from(\n { length: Math.max(depth - 1, 0) },\n (_, index) => `n${index + 1}`\n )\n const nodeVariables = ['a', ...intermediateVariables, 'exchange']\n const edgeVariables = Array.from({ length: depth }, (_, index) => `r${index + 1}`)\n const relationshipChain = edgeVariables\n .map((edgeVariable, index) => {\n const targetVariable =\n index === edgeVariables.length - 1 ? 'exchange' : intermediateVariables[index]!\n return `-[${edgeVariable}:FLOWS_TO]->(${targetVariable}:Address)`\n })\n .join('')\n const intermediatePredicates = intermediateVariables.map(\n (nodeVariable) => `${nodeVariable}.is_exchange IS NULL`\n )\n // Pool trace rule: the walk starts at the screened address and passes\n // through the intermediates, so none of them may be a :Pool. The exchange\n // end is the target, which may be one.\n const poolGuards = poolGuardTerms(['a', ...intermediateVariables])\n const depositVariable = nodeVariables[nodeVariables.length - 2]!\n const terminalEdgeVariable = edgeVariables[edgeVariables.length - 1]!\n return {\n id: `exchange_outflows_${depth}`,\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})${relationshipChain}`,\n `WHERE a.address <> exchange.address AND exchange.is_exchange IS NOT NULL${intermediatePredicates.length > 0 ? ` AND ${intermediatePredicates.join(' AND ')}` : ''} AND ${poolGuards}`,\n `RETURN \"outflow\" AS direction, exchange.address AS exchange_address, exchange.labels AS exchange_display_labels, exchange.labels AS exchange_system_labels, ${depositVariable}.address AS deposit_address, ${depth} AS hops, ${terminalEdgeVariable}.amount_usd_sum AS amount_usd_sum, ${terminalEdgeVariable}.tx_count AS tx_count, ${terminalEdgeVariable}.first_seen_timestamp AS first_seen_timestamp, ${terminalEdgeVariable}.last_seen_timestamp AS last_seen_timestamp, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(', ')}] AS addresses, [${nodeVariables.map((nodeVariable) => pathNodeMap(nodeVariable)).join(', ')}] AS path_nodes, [${edgeVariables.map(flowEdgeMap).join(', ')}] AS edge_props`,\n 'ORDER BY hops ASC',\n 'LIMIT 200',\n ].join(' '),\n }\n}\n\nfunction exchangeInflowQueries(address: string): Array<{ id: string; query: string }> {\n return Array.from({ length: 3 }, (_, index) => exchangeInflowQueryAtDepth(address, index + 1))\n}\n\nfunction exchangeInflowQueryAtDepth(address: string, depth: number): { id: string; query: string } {\n const intermediateVariables = Array.from(\n { length: Math.max(depth - 1, 0) },\n (_, index) => `n${index + 1}`\n )\n const nodeVariables = ['exchange', ...intermediateVariables, 'a']\n const edgeVariables = Array.from({ length: depth }, (_, index) => `r${index + 1}`)\n const relationshipChain = edgeVariables\n .map((edgeVariable, index) => {\n const targetVariable =\n index === edgeVariables.length - 1 ? 'a' : intermediateVariables[index]!\n return `-[${edgeVariable}:FLOWS_TO]->(${targetVariable}:Address)`\n })\n .join('')\n const intermediatePredicates = intermediateVariables.map(\n (nodeVariable) => `${nodeVariable}.is_exchange IS NULL`\n )\n // Pool trace rule: the trace starts at the screened address `a` and walks\n // back through the intermediates, so none of them may be a :Pool. The\n // exchange end is the target, which may be one.\n const poolGuards = poolGuardTerms(['a', ...intermediateVariables])\n const withdrawalVariable = nodeVariables[1]!\n const terminalEdgeVariable = edgeVariables[edgeVariables.length - 1]!\n return {\n id: `exchange_inflows_${depth}`,\n query: [\n `MATCH (exchange:Address)${relationshipChain}`,\n `WHERE a.address = \"${escapeCypherString(address)}\" AND a.address <> exchange.address AND exchange.is_exchange IS NOT NULL${intermediatePredicates.length > 0 ? ` AND ${intermediatePredicates.join(' AND ')}` : ''} AND ${poolGuards}`,\n `RETURN \"inflow\" AS direction, exchange.address AS exchange_address, exchange.labels AS exchange_display_labels, exchange.labels AS exchange_system_labels, ${withdrawalVariable}.address AS withdrawal_address, ${depth} AS hops, ${terminalEdgeVariable}.amount_usd_sum AS amount_usd_sum, ${terminalEdgeVariable}.tx_count AS tx_count, ${terminalEdgeVariable}.first_seen_timestamp AS first_seen_timestamp, ${terminalEdgeVariable}.last_seen_timestamp AS last_seen_timestamp, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(', ')}] AS addresses, [${nodeVariables.map((nodeVariable) => pathNodeMap(nodeVariable)).join(', ')}] AS path_nodes, [${edgeVariables.map(flowEdgeMap).join(', ')}] AS edge_props`,\n 'ORDER BY hops ASC',\n 'LIMIT 200',\n ].join(' '),\n }\n}\n\nfunction connectionProbeQuery(\n address: string,\n compareAddress: string\n): { id: string; query: string } {\n return {\n id: 'connection_probe',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:FLOWS_TO]-(b:Address {address: \"${escapeCypherString(compareAddress)}\"})`,\n 'RETURN [a.address, b.address] AS addresses, 1 AS hops',\n 'LIMIT 5',\n ].join(' '),\n }\n}\n\n// Address-grain has no separate identity-resolution lookup: existence of the\n// compare_address is checked directly against :Address, distinct from the\n// primary address_profile query so both existence checks can run in one batch.\nfunction compareAddressExistsQuery(address: string): { id: string; query: string } {\n return {\n id: 'compare_address_exists',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n 'RETURN a.address AS address',\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\n// Pool trace rule: the via_linked read is a two-hop walk through `owned`, so\n// `a` and `owned` carry the guard and the target `b` may be a pool. The direct\n// read is a counterparty listing of one address, not a trace, and stays\n// unguarded (ruling: a pool's own counterparty list is a listing).\n// AC11: FLOWS_TO reachability UNIONed over one -[:LINKED]- hop, so an\n// investigator surveying an address's exposure also sees exposure carried by\n// an address that is only ownership-LINKED to it (LINKED is undirected).\nfunction linkedExposureQueries(address: string): Array<{ id: string; query: string }> {\n return [\n {\n id: 'linked_exposure_direct',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:FLOWS_TO]-(b:Address)`,\n 'WHERE a.address <> b.address',\n `RETURN a.address AS subject_address, b.address AS counterparty_address, b.network AS counterparty_network, \"direct\" AS exposure_basis, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count`,\n 'LIMIT 200',\n ].join(' '),\n },\n {\n id: 'linked_exposure_via_linked',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[l:LINKED]-(owned:Address)-[r:FLOWS_TO]-(b:Address)`,\n 'WHERE NOT a:Pool AND NOT owned:Pool AND owned.address <> b.address AND a.address <> b.address',\n `RETURN a.address AS subject_address, b.address AS counterparty_address, b.network AS counterparty_network, \"via_linked\" AS exposure_basis, owned.address AS linked_via_address, l.basis AS link_basis, l.confidence AS link_confidence, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count`,\n 'LIMIT 200',\n ].join(' '),\n },\n ]\n}\n\n// AC5: LINKED ownership-overlay probe. On the single public robinhood graph,\n// :Address.network is a node PROPERTY (robinhood), never a second query\n// network: LINKED is the ownership edge within that one space, served on the\n// topology graph only, with no network switch.\nfunction crossSpaceLinkedQuery(address: string): { id: string; query: string } {\n return {\n id: 'cross_space_linked',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[l:LINKED]-(b:Address)`,\n 'WHERE a.network <> b.network',\n 'RETURN a.address AS address, a.network AS network, b.address AS linked_address, b.network AS linked_network, l.basis AS basis, l.confidence AS confidence, l.source_event AS source_event, l.declared_owner AS declared_owner',\n 'LIMIT 50',\n ].join(' '),\n }\n}\n\n// ── Pairwise route evidence ──\n// Undirected shortest route between two KNOWN identity endpoints. The\n// topology graph speaks ISO GQL, so both routes use one shortest path.\n// Exchange intermediates on a returned route are DISCLOSED in the evidence,\n// never silently filtered out.\n//\n// Pool trace rule (schema skill, \"Pool trace rule\"): a route never starts at\n// a :Pool and never passes through one, and it may end at one. It follows\n// FLOWS_TO and SWAPPED, so it crosses a swap from payer to recipient. The\n// guard sits inside the path pattern, so the search finds the shortest route\n// that avoids pools instead of dropping a shortest route that crosses one.\n//\n// The text is the guarded route contract (ruled 2026-09-28), served word for\n// word by skills/chain-insights-cypher, and graphrag-mcp rewrites exactly this\n// shape onto DozerDB's fast shortest-path search (data-pipeline\n// route_rewrite.go). Change the addresses and the RETURN only. The quantifier\n// bound covers the guarded middle; the last hop is the unguarded target end,\n// so a route is 1 to CONNECTION_ROUTE_DEPTH_BOUND + 1 hops. The `{0,...}`\n// lower bound keeps DozerDB off the slow undirected search.\n\nexport const CONNECTION_ROUTE_DEPTH_BOUND = 4\n// A route is the guarded middle plus the last hop to the target end.\nexport const CONNECTION_ROUTE_MAX_HOPS = CONNECTION_ROUTE_DEPTH_BOUND + 1\n\nexport function shouldIncludeRouteQueries(compareAddress: string | undefined): boolean {\n // GQL shortest-path traversal always fires when a compare address is given.\n return Boolean(compareAddress)\n}\n\nexport function connectionRouteQueries(\n address: string,\n compareAddress: string\n): Array<{ id: string; query: string }> {\n const routeQuery = (fromAddress: string, toAddress: string): string =>\n [\n `MATCH p = SHORTEST 1 (a:Address {address: \"${escapeCypherString(fromAddress)}\"} WHERE NOT a:Pool)`,\n ` (()-[:FLOWS_TO|SWAPPED]-(via:Address) WHERE NOT via:Pool){0,${CONNECTION_ROUTE_DEPTH_BOUND}}`,\n ` ()-[:FLOWS_TO|SWAPPED]-(b:Address {address: \"${escapeCypherString(toAddress)}\"}) RETURN p LIMIT 1`,\n ].join('')\n return [\n { id: 'connection_route_outbound', query: routeQuery(address, compareAddress) },\n { id: 'connection_route_inbound', query: routeQuery(compareAddress, address) },\n ]\n}\n\nexport interface RouteEvidenceSide {\n hops: number\n identities: string[]\n exchange_intermediates: string[]\n amount_usd_sum_total: number\n}\n\nfunction collectOrdered(\n value: unknown,\n matches: (candidate: Record<string, unknown>) => boolean\n): Array<Record<string, unknown>> {\n const collected: Array<Record<string, unknown>> = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const entry of node) walk(entry)\n return\n }\n if (node && typeof node === 'object') {\n const candidate = node as Record<string, unknown>\n if (matches(candidate)) collected.push(candidate)\n for (const nested of Object.values(candidate)) walk(nested)\n }\n }\n walk(value)\n return collected\n}\n\n// is_exchange on topology nodes is a MARKER, not a boolean: it usually\n// carries the exchange name (e.g. \"binance\"). Treat explicit falsy\n// encodings as not-exchange (a bare non-null check would falsely disclose\n// nodes serialized as false/\"false\"/0), and any other non-empty value as\n// exchange. isExchangeFlag() is NOT reused here — it is boolean-only and\n// would miss name-valued markers.\nexport function isExchangeMarker(value: unknown): boolean {\n if (value === null || value === undefined || value === false) return false\n if (typeof value === 'number') return value !== 0\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase()\n return (\n normalized !== '' && normalized !== 'false' && normalized !== '0' && normalized !== 'null'\n )\n }\n return value === true\n}\n\n// Shape-tolerant hydrated-path reader: walks the returned path value for\n// ordered address nodes and FLOWS_TO edge amounts without pinning the\n// backend's exact path envelope.\nexport function routeFromPathValue(value: unknown): RouteEvidenceSide | null {\n if (!value || typeof value !== 'object') return null\n const nodes = collectOrdered(value, (candidate) => typeof candidate['address'] === 'string')\n if (nodes.length < 2) return null\n const edges = collectOrdered(\n value,\n (candidate) => typeof candidate['amount_usd_sum'] === 'number' && !('address' in candidate)\n )\n const identities = nodes.map((node) => String(node['address']))\n const exchangeIntermediates = nodes\n .slice(1, -1)\n .filter((node) => isExchangeMarker(node['is_exchange']))\n .map((node) => String(node['address']))\n return {\n // Hop count is a structural property of the node sequence — never\n // derived from edge-object detection, which keys on numeric\n // amount_usd_sum and would under-report on partially hydrated edges.\n hops: nodes.length - 1,\n identities,\n exchange_intermediates: exchangeIntermediates,\n amount_usd_sum_total: edges.reduce((total, edge) => total + Number(edge['amount_usd_sum']), 0),\n }\n}\n\nexport interface RouteEvidence {\n search_strategy: 'any_shortest'\n route_rank_basis: 'hop_count'\n depth_bound: number\n route_found: boolean\n outbound: RouteEvidenceSide | null\n inbound: RouteEvidenceSide | null\n}\n\nexport function buildRouteEvidence(\n outboundRows: Array<Record<string, unknown>>,\n inboundRows: Array<Record<string, unknown>>\n): RouteEvidence {\n const sideFrom = (rows: Array<Record<string, unknown>>): RouteEvidenceSide | null => {\n const first = rows[0]\n if (!first) return null\n const pathValue = 'p' in first ? first['p'] : Object.values(first)[0]\n return routeFromPathValue(pathValue)\n }\n const outbound = sideFrom(outboundRows)\n const inbound = sideFrom(inboundRows)\n return {\n search_strategy: 'any_shortest',\n route_rank_basis: 'hop_count',\n depth_bound: CONNECTION_ROUTE_MAX_HOPS,\n route_found: outbound !== null || inbound !== null,\n outbound,\n inbound,\n }\n}\n\nfunction formatExchangeRows(rows: Array<Record<string, unknown>>): string[] {\n return rows.map((row) => {\n const direction = String(row['direction'] ?? 'flow')\n const exchange = String(row['exchange_address'] ?? '')\n const amount = row['amount_usd_sum'] ?? ''\n const hops = row['hops'] ?? ''\n return `- ${direction}: ${exchange} (${hops} hop(s), amount_usd_sum ${amount})`\n })\n}\n\nfunction numberValue(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n const parsed = numberValue(value)\n if (parsed !== undefined) return parsed\n }\n return undefined\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return undefined\n}\n\nfunction riskLevelFromScore(score: number): string {\n if (score >= 0.85) return 'critical'\n if (score >= 0.7) return 'high'\n if (score >= 0.4) return 'medium'\n return 'low'\n}\n\nfunction riskRecommendation(level: string): string {\n if (level === 'critical' || level === 'high') return 'Escalate for manual review.'\n if (level === 'medium') return 'Review exchange exposure and counterparties before clearing.'\n return 'No stored risk signal found; continue with normal monitoring.'\n}\n\nfunction riskDrivers(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>,\n exchangeRows: Array<Record<string, unknown>>\n): string[] {\n const drivers: string[] = []\n\n const riskLabels = labelRows\n .map((row) => firstString(row['label']))\n .filter((label): label is string => Boolean(label))\n if (riskLabels.length > 0) drivers.push(`Labels: ${[...new Set(riskLabels)].join('; ')}`)\n\n const outflowCount = exchangeRows.filter((row) => row['direction'] === 'outflow').length\n const inflowCount = exchangeRows.filter((row) => row['direction'] === 'inflow').length\n if (outflowCount > 0)\n drivers.push(`Forward bounded search reached ${outflowCount} exchange path(s).`)\n if (inflowCount > 0)\n drivers.push(`Backward bounded search found ${inflowCount} source exchange path(s).`)\n\n return [...new Set(drivers)]\n}\n\n// Derives the deterministic labelRows subset from the profile row's three\n// label_risk_* lists (topology), reproducing the retired\n// addressLabelRiskQuery's `ORDER BY label.updated_timestamp DESC LIMIT 10`\n// subset byte-for-byte: same sort, same cap. The lists are parallel: entry i\n// of each one describes the same label row. Missing/empty lists -> empty\n// array, same as \"no labels\" behaved under the old facts read.\nfunction deriveLabelRows(profile: Record<string, unknown>): Array<Record<string, unknown>> {\n // Dozer/Neo4j properties cannot store lists of maps. Sync therefore\n // materializes the three map fields as parallel primitive arrays.\n const labels = Array.isArray(profile['label_risk_labels'])\n ? (profile['label_risk_labels'] as unknown[]).map((value) => String(value))\n : []\n const riskLevels = Array.isArray(profile['label_risk_levels'])\n ? (profile['label_risk_levels'] as unknown[]).map((value) => String(value))\n : []\n const timestamps = Array.isArray(profile['label_risk_updated_timestamps'])\n ? (profile['label_risk_updated_timestamps'] as unknown[]).map((value) => numberValue(value))\n : []\n const rows = labels.map((label, index) => ({\n label,\n risk_level: riskLevels[index] ?? '',\n updated_timestamp: timestamps[index] ?? 0,\n }))\n return [...rows]\n .sort(\n (a, b) =>\n (numberValue(b['updated_timestamp']) ?? 0) - (numberValue(a['updated_timestamp']) ?? 0)\n )\n .slice(0, 10)\n}\n\nconst RISK_LEVEL_ORDER = ['critical', 'high', 'medium', 'low'] as const\n\nfunction strongestLabelRiskLevel(labelRows: Array<Record<string, unknown>>): string | undefined {\n const levels = labelRows\n .map((row) => firstString(row['risk_level'])?.toLowerCase())\n .filter((level): level is string =>\n Boolean(level && (RISK_LEVEL_ORDER as readonly string[]).includes(level))\n )\n if (levels.length === 0) return undefined\n return RISK_LEVEL_ORDER.find((candidate) => levels.includes(candidate))\n}\n\nfunction riskScoreSources(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>\n): Array<Record<string, unknown>> {\n const sources: Array<Record<string, unknown>> = []\n if (numberValue(profile['live_risk_score']) !== undefined) {\n sources.push({\n family: 'ml_risk_score',\n layer: 'topology',\n source: 'address_node',\n fields: ['risk_score', 'risk_level'],\n })\n }\n if (labelRows.length > 0) {\n sources.push({\n family: 'label_risk',\n layer: 'topology',\n source: 'address_node',\n labels: labelRows.map((row) => ({\n label: row['label'],\n risk_level: row['risk_level'],\n })),\n })\n }\n return sources\n}\n\nfunction terminalEdgeProperties(row: Record<string, unknown>): Record<string, unknown> | undefined {\n const edgeProps = Array.isArray(row['edge_props'])\n ? (row['edge_props'] as Array<Record<string, unknown>>)\n : []\n return edgeProps[edgeProps.length - 1]\n}\n\nfunction enrichExchangeRows(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>> {\n return rows.map((row) => {\n const terminal = terminalEdgeProperties(row)\n if (!terminal) return row\n return {\n ...row,\n amount_usd_sum: row['amount_usd_sum'] ?? terminal['amount_usd_sum'],\n tx_count: row['tx_count'] ?? terminal['tx_count'],\n first_seen_timestamp: row['first_seen_timestamp'] ?? terminal['first_seen_timestamp'],\n last_seen_timestamp: row['last_seen_timestamp'] ?? terminal['last_seen_timestamp'],\n }\n })\n}\n\nconst FALLBACK_SHARED_TX_COUNT = 1000\nconst FALLBACK_SHARED_USD_SUM = 5_000_000\nconst FALLBACK_SHARED_DAMPING = 0.1\nconst FALLBACK_VALUE_SATURATION_USD = 1_000_000\nconst FALLBACK_BASE_SCORE = 0.1\nconst FALLBACK_MAX_SCORE = 0.6\n\n/**\n * Score exchange exposure when no ML risk score exists. Topology-grain:\n * log-scaled USD volume across exchange rows, with shared/omnibus edges\n * (high tx_count or USD throughput) dampened, bounded in (0, 0.6] so a\n * fallback can never impersonate a high ML score band.\n */\nexport function exchangeExposureFallbackScore(\n exchangeRows: Array<Record<string, unknown>>\n): number {\n if (exchangeRows.length === 0) return 0\n let weightedUsd = 0\n for (const row of exchangeRows) {\n const usd = numberValue(row['amount_usd_sum']) ?? 0\n const txCount = numberValue(row['tx_count']) ?? 0\n const shared = txCount >= FALLBACK_SHARED_TX_COUNT || usd >= FALLBACK_SHARED_USD_SUM\n weightedUsd += shared ? usd * FALLBACK_SHARED_DAMPING : usd\n }\n const capped = Math.min(weightedUsd, FALLBACK_VALUE_SATURATION_USD)\n const factor = Math.log10(1 + capped) / Math.log10(1 + FALLBACK_VALUE_SATURATION_USD)\n return Math.min(\n FALLBACK_MAX_SCORE,\n FALLBACK_BASE_SCORE + (FALLBACK_MAX_SCORE - FALLBACK_BASE_SCORE) * factor\n )\n}\n\nexport function riskAssessment(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>,\n exchangeRows: Array<Record<string, unknown>>\n): Record<string, unknown> {\n const mlRiskScore = firstNumber(profile['live_risk_score'])\n // UNSCORED is the model's explicit abstention (calibrated-scoring release):\n // the score exists for transparency but carries no stance — deriving a\n // severity band from it would silently launder an abstention into a\n // confident verdict (typically \"low\"). Fall back to labels/exchange\n // exposure and say so.\n const mlAbstained = isUnscoredRiskLevel(profile['live_risk_level'])\n const labelRiskLevel = strongestLabelRiskLevel(labelRows)\n const usableMlScore = mlAbstained ? undefined : mlRiskScore\n const score = usableMlScore ?? exchangeExposureFallbackScore(exchangeRows)\n let level =\n labelRiskLevel ??\n (mlAbstained && exchangeRows.length === 0 ? 'unscored' : riskLevelFromScore(score))\n const drivers = riskDrivers(profile, labelRows, exchangeRows)\n if (mlAbstained)\n drivers.push(\n 'ml_abstained: ML verdict is UNSCORED (insufficient labeled graph context); level derived from labels/exchange exposure only'\n )\n // Labels are curated truth and stay first — but a lower-severity label\n // must never SUPPRESS a more severe usable ML band. Failing toward\n // \"looks safe\" is the one direction an AML triage tool may not fail.\n const usableMlBand = usableMlScore !== undefined ? riskLevelFromScore(usableMlScore) : undefined\n if (\n labelRiskLevel &&\n usableMlBand &&\n riskSeverityRank(usableMlBand) > riskSeverityRank(labelRiskLevel)\n ) {\n level = usableMlBand\n drivers.push(\n `ml_label_divergence: usable ML band ${usableMlBand} exceeds strongest label level ${labelRiskLevel}; reporting the more severe band — review the label`\n )\n }\n return {\n level,\n score,\n ...(mlRiskScore !== undefined ? { ml_risk_score: mlRiskScore } : {}),\n ...(mlAbstained ? { ml_verdict: 'unscored' } : {}),\n confidence:\n usableMlScore !== undefined || labelRiskLevel\n ? 'high'\n : exchangeRows.length > 0\n ? 'medium'\n : 'low',\n recommendation:\n level === 'unscored'\n ? 'Model abstained and no label/exchange signal found; gather more context before clearing.'\n : riskRecommendation(level),\n drivers,\n sources: riskScoreSources(profile, labelRows),\n }\n}\n\nfunction formatRiskScore(score: unknown): string {\n const parsed = numberValue(score)\n if (parsed === undefined) return String(score ?? 'unknown')\n return Number.isInteger(parsed) ? parsed.toString() : parsed.toFixed(2)\n}\n\nfunction stringArrayValue(value: unknown): string[] | undefined {\n if (Array.isArray(value)) return value.map(String)\n if (typeof value === 'string' && value.trim()) return [value]\n return undefined\n}\n\nfunction restoreSystemLabels(\n graph: Record<string, unknown>,\n rawNodes: Array<Record<string, unknown>>\n): Record<string, unknown> {\n if (!Array.isArray(graph['nodes'])) return graph\n const labelsByAddress = new Map(\n rawNodes\n .map(\n (node) =>\n [\n typeof node['address'] === 'string'\n ? node['address']\n : typeof node['id'] === 'string'\n ? node['id']\n : '',\n stringArrayValue(node['system_labels']),\n ] as const\n )\n .filter(\n (entry): entry is readonly [string, string[]] =>\n Boolean(entry[0]) && Array.isArray(entry[1]) && entry[1].length > 0\n )\n )\n return {\n ...graph,\n nodes: graph['nodes'].map((node) => {\n if (typeof node !== 'object' || node === null || Array.isArray(node)) return node\n const record = node as Record<string, unknown>\n const address =\n typeof record['address'] === 'string'\n ? record['address']\n : typeof record['id'] === 'string'\n ? record['id']\n : ''\n const systemLabels = labelsByAddress.get(address)\n return systemLabels ? { ...record, system_labels: systemLabels } : record\n }),\n }\n}\n\nfunction buildRiskGraph(\n address: string,\n profile: Record<string, unknown>,\n rows: Array<Record<string, unknown>>,\n network: string\n): Record<string, unknown> {\n const nodes = new Map<string, Record<string, unknown>>()\n nodes.set(address, {\n id: address,\n address,\n node_type: 'address',\n labels: stringArrayValue(profile['display_labels']) ?? [],\n ...(stringArrayValue(profile['system_labels'])\n ? { system_labels: stringArrayValue(profile['system_labels']) }\n : {}),\n ...(stringArrayValue(profile['member_addresses'])?.length\n ? { member_addresses: stringArrayValue(profile['member_addresses']) }\n : {}),\n ...(numberValue(profile['live_risk_score']) !== undefined\n ? { risk_score: numberValue(profile['live_risk_score']) }\n : {}),\n ...(firstString(profile['live_risk_level'])\n ? { risk_level: firstString(profile['live_risk_level']) }\n : {}),\n roles: ['subject'],\n })\n const edges: Array<Record<string, unknown>> = []\n const mergeNode = (entry: string, metadata?: Record<string, unknown>) => {\n const existing = nodes.get(entry) ?? {\n id: entry,\n address: entry,\n node_type: 'address',\n labels: [],\n }\n const labels = stringArrayValue(metadata?.['labels']) ?? existing['labels']\n const systemLabels = stringArrayValue(metadata?.['system_labels']) ?? existing['system_labels']\n const memberAddresses =\n stringArrayValue(metadata?.['member_addresses']) ?? existing['member_addresses']\n const riskScore = numberValue(metadata?.['risk_score']) ?? existing['risk_score']\n const riskLevel =\n normalizeRiskLevel(firstString(metadata?.['risk_level'])) ?? existing['risk_level']\n nodes.set(entry, {\n ...existing,\n labels,\n ...(systemLabels ? { system_labels: systemLabels } : {}),\n ...(Array.isArray(memberAddresses) && memberAddresses.length > 0\n ? { member_addresses: memberAddresses }\n : {}),\n ...(riskScore !== undefined ? { risk_score: riskScore } : {}),\n ...(riskLevel ? { risk_level: riskLevel } : {}),\n })\n }\n for (const row of rows) {\n const rawPath = Array.isArray(row['path']) ? row['path'] : row['addresses']\n const path = Array.isArray(rawPath) ? rawPath.map(String) : []\n const pathNodes = Array.isArray(row['path_nodes'])\n ? (row['path_nodes'] as Array<Record<string, unknown>>)\n : []\n for (let index = 0; index < path.length; index += 1) {\n const entry = path[index]!\n mergeNode(entry, pathNodes[index])\n }\n const exchange = typeof row['exchange_address'] === 'string' ? row['exchange_address'] : ''\n if (exchange) {\n const displayLabels = stringArrayValue(row['exchange_display_labels']) ?? []\n const systemLabels =\n stringArrayValue(row['exchange_system_labels']) ??\n stringArrayValue(row['exchange_labels']) ??\n []\n nodes.set(exchange, {\n id: exchange,\n address: exchange,\n node_type: 'address',\n labels: displayLabels,\n ...(systemLabels.length > 0 ? { system_labels: systemLabels } : {}),\n roles: ['exchange'],\n })\n }\n for (let index = 0; index < path.length - 1; index += 1) {\n const edgeProps = Array.isArray(row['edge_props'])\n ? (row['edge_props'] as Array<Record<string, unknown>>)\n : []\n const edge = edgeProps[index] ?? row\n edges.push({\n source: path[index],\n target: path[index + 1],\n edge_type: 'flows_to',\n usd_amount: edge['amount_usd_sum'] ?? 0,\n amount_usd_sum: edge['amount_usd_sum'] ?? 0,\n tx_count: edge['tx_count'] ?? 0,\n direction: row['direction'],\n })\n }\n }\n const rawNodes = [...nodes.values()]\n return restoreSystemLabels(\n normalizeGraphPayload({\n schema: 'chain-insights.graph.v1',\n nodes: rawNodes,\n edges,\n flows: [],\n edge_anchors: [],\n metadata: { address, network, generated_at: new Date().toISOString() },\n }),\n rawNodes\n )\n}\n\nexport async function addressRisk(\n remoteClient: Client,\n options: AddressRiskOptions\n): Promise<{\n summaryText: string\n structuredContent: Record<string, unknown>\n graphData: Record<string, unknown>\n}> {\n const address = options.address.trim()\n const network = options.network.trim()\n const compareInput = options.compareAddress?.trim() ?? ''\n if (!address) throw new Error('address is required')\n if (!network) throw new Error('network is required')\n\n // Every internal graph_query_batch round trip this workflow makes is\n // observed here and totaled into a usage block on the response (never\n // blocks/throws on a backend that doesn't emit billing fields yet).\n const usage = createUsageAccumulator()\n const trackedClient = wrapClientForUsageTracking(remoteClient, usage)\n\n // Address-grain has no separate resolution step for the SUBJECT address:\n // the input address IS the graph node key. Its existence is inferred\n // post-hoc from the address_profile row (below) instead of the pre-revert\n // pre-flight lookup -- deliberate: it saves a round trip on the hot path.\n // The COMPARE address keeps a pre-flight existence probe, because the\n // connection probe and shortest-path route queries must be SUPPRESSED (not\n // ignored) when the compare address does not exist -- pre-revert behavior\n // never issued route probes for an unresolved compare input.\n let compareUnresolved = false\n if (compareInput) {\n const compareBatch = await callGraphBatch(trackedClient, network, [\n compareAddressExistsQuery(compareInput),\n ])\n const compareRows = optionalResultsFor(compareBatch, 'compare_address_exists', [])\n compareUnresolved = !firstString(compareRows[0]?.['address'])\n }\n const compareAddress = compareInput && !compareUnresolved ? compareInput : ''\n\n const queries = [\n addressProfileQuery(address),\n addressFeatureQuery(address),\n { id: 'money_trail_incident', query: moneyTrailIncidentQuery(address) },\n ...exchangeOutflowQueries(address),\n ...exchangeInflowQueries(address),\n ...(compareAddress\n ? [connectionProbeQuery(address, compareAddress)]\n : [\n {\n id: 'connection_probe',\n query:\n 'MATCH (n:Address {address: \"__chain_insights_noop__\"}) RETURN n.address AS noop LIMIT 0',\n },\n ]),\n // Route evidence is additive: ISO GQL shortest-path traversal always\n // fires for a compare address whose existence probe succeeded (the\n // 1-hop probe above always runs).\n ...(shouldIncludeRouteQueries(compareAddress)\n ? connectionRouteQueries(address, compareAddress)\n : []),\n ]\n const batch = await callGraphBatch(trackedClient, network, queries)\n const partialQueryFailures: QueryFailure[] = []\n // Deliberate post-hoc existence inference (address grain): an empty\n // address_profile result means the subject :Address does not exist ->\n // report unresolved; this replaces the pre-revert identity pre-flight.\n const addressProfileRows = optionalResultsFor(batch, 'address_profile', partialQueryFailures)\n if (addressProfileRows.length === 0) {\n return {\n summaryText: `Address risk for ${network}:${address}\\n\\nUnresolved: no Address found for \"${address}\". The address may not have on-chain activity in this network.`,\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: 'aml_address_risk',\n facts: {\n subject: { network, addresses: [address] },\n unresolved: [address],\n usage: usageBlock(usage),\n },\n },\n graphData: {\n schema: 'chain-insights.graph.v1',\n nodes: [],\n edges: [],\n flows: [],\n edge_anchors: [],\n metadata: { address, network, generated_at: new Date().toISOString() },\n },\n }\n }\n const profile: Record<string, unknown> = {\n address,\n ...(addressProfileRows[0] ?? {}),\n ...(optionalResultsFor(batch, 'address_feature', partialQueryFailures)[0] ?? {}),\n }\n const labelRows = deriveLabelRows(profile)\n const outflows = enrichExchangeRows(\n optionalResultsWithPrefix(batch, 'exchange_outflows_', partialQueryFailures)\n )\n const inflows = enrichExchangeRows(\n optionalResultsWithPrefix(batch, 'exchange_inflows_', partialQueryFailures)\n )\n const connections = compareAddress\n ? optionalResultsFor(batch, 'connection_probe', partialQueryFailures)\n : []\n const routeEvidence = shouldIncludeRouteQueries(compareAddress)\n ? buildRouteEvidence(\n optionalResultsFor(batch, 'connection_route_outbound', partialQueryFailures),\n optionalResultsFor(batch, 'connection_route_inbound', partialQueryFailures)\n )\n : undefined\n const exchangeRows = [...outflows, ...inflows]\n // A hop-depth exchange_outflows_N/exchange_inflows_N query can fail\n // independently (e.g. the archive-tier query-memory limit on a deep\n // multi-hop search) while other hop depths succeed with zero rows. An\n // empty exchangeRows result must then read as \"search incomplete,\" not\n // \"no exchange exposure\" -- the two are not the same claim, and an AML\n // tool that silently reports a false-clean verdict on a partial search\n // failure is a real risk (found during MoA review, 2026-07-05).\n const exchangeSearchFailures = partialQueryFailures.filter(\n (failure) =>\n failure.id.startsWith('exchange_outflows_') || failure.id.startsWith('exchange_inflows_')\n )\n const exchangeSearchComplete = exchangeSearchFailures.length === 0\n // money-trail enrichment (optional): a preliminary block resolves the\n // primary_seed off the incident-only rows, then a second batch call fans\n // out TRAIL_ENDS_AT for that seed so the block can pick the highest-value\n // terminal fact. Both reads are optional-results -- a graph with no\n // money-trail layer never fails the tool.\n const moneyTrailIncidentRows = optionalResultsFor(\n batch,\n 'money_trail_incident',\n partialQueryFailures\n )\n const preliminaryMoneyTrail = buildMoneyTrailBlock(moneyTrailIncidentRows, [])\n let moneyTrailEndRows: Array<Record<string, unknown>> = []\n if (preliminaryMoneyTrail?.primary_seed) {\n try {\n const endsBatch = await callGraphBatch(trackedClient, network, [\n { id: 'money_trail_ends', query: moneyTrailEndsQuery(preliminaryMoneyTrail.primary_seed) },\n ])\n moneyTrailEndRows = optionalResultsFor(endsBatch, 'money_trail_ends', partialQueryFailures)\n } catch (error) {\n // Degrade, don't fail: a transport/parse error on the second batch call\n // (unlike a query-level ok:false, which optionalResultsFor already\n // absorbs) must not blow up the whole tool -- the incident rows are\n // already in hand, so the block still reports on_trail without\n // nearest_trail_end.\n collectQueryFailure(\n partialQueryFailures,\n 'money_trail_ends',\n error instanceof Error ? error.message : String(error)\n )\n }\n }\n const moneyTrail = buildMoneyTrailBlock(moneyTrailIncidentRows, moneyTrailEndRows)\n const graphData = buildRiskGraph(address, profile, exchangeRows, network)\n const risk = riskAssessment(profile, labelRows, exchangeRows)\n const liveRiskScore = numberValue(profile['live_risk_score'])\n const liveRiskLevel = firstString(profile['live_risk_level'])\n const liveNodeVerdict =\n liveRiskScore !== undefined || liveRiskLevel\n ? {\n ...(liveRiskScore !== undefined ? { risk_score: liveRiskScore } : {}),\n ...(liveRiskLevel ? { risk_level: liveRiskLevel } : {}),\n source: 'topology_node',\n }\n : undefined\n\n const lines = [\n `Address risk for ${network}:${address}`,\n '',\n `Risk: ${risk['level']} (${formatRiskScore(risk['score'])})`,\n `Confidence: ${risk['confidence']}`,\n `Recommendation: ${risk['recommendation']}`,\n ...(liveNodeVerdict\n ? [`Live node triage: ${liveRiskLevel ?? 'unknown'} (${formatRiskScore(liveRiskScore)})`]\n : []),\n `Graph degree: in ${profile['degree_in'] ?? 'unknown'}, out ${profile['degree_out'] ?? 'unknown'}.`,\n '',\n 'Exchange behavior',\n ...(exchangeRows.length > 0\n ? [\n formatExchangeRows(exchangeRows).join('\\n'),\n ...(exchangeSearchComplete\n ? []\n : [\n `(incomplete: ${exchangeSearchFailures.length} other hop-depth quer${exchangeSearchFailures.length === 1 ? 'y' : 'ies'} failed -- there may be more exchange exposure than shown here)`,\n ]),\n ]\n : [\n exchangeSearchComplete\n ? '- No exchange inflow/outflow paths found in bounded search.'\n : `- Exchange search incomplete: ${exchangeSearchFailures.length} hop-depth quer${exchangeSearchFailures.length === 1 ? 'y' : 'ies'} failed before returning a result. This is NOT a clean finding -- retry or narrow the search (see Partial query failures below).`,\n ]),\n ]\n if (Array.isArray(risk['drivers']) && risk['drivers'].length > 0) {\n lines.push('', 'Risk drivers', risk['drivers'].map((driver) => `- ${driver}`).join('\\n'))\n }\n if (moneyTrail) {\n lines.push('', moneyTrailSummarySentence(moneyTrail))\n }\n if (compareAddress) {\n lines.push(\n '',\n `Connection compare target: ${compareAddress}`,\n connections.length > 0\n ? `Connection paths found: ${connections.length}`\n : 'Connection paths found: 0'\n )\n }\n if (compareUnresolved) {\n lines.push(\n '',\n `Unresolved compare_address: no Address found for \"${compareInput}\"; comparison skipped.`\n )\n }\n if (partialQueryFailures.length > 0) {\n lines.push(\n '',\n 'Partial query failures',\n partialQueryFailures.map((failure) => `- ${failure.id}: ${failure.error}`).join('\\n')\n )\n }\n const summaryText = lines.join('\\n')\n const structuredContent = {\n schema: 'chain-insights.result.v1',\n tool: 'aml_address_risk',\n facts: {\n subject: {\n network,\n addresses: compareAddress ? [address, compareAddress] : [address],\n },\n risk: {\n ...risk,\n ...(liveNodeVerdict ? { live_node: liveNodeVerdict } : {}),\n },\n exchange_behavior: {\n outflows,\n inflows,\n search_status: exchangeSearchComplete ? 'complete' : 'incomplete',\n ...(exchangeSearchComplete\n ? {}\n : { failed_query_ids: exchangeSearchFailures.map((failure) => failure.id) }),\n },\n connection: compareAddress\n ? {\n compare_address: compareAddress,\n paths: connections,\n ...(routeEvidence ? { route_evidence: routeEvidence } : {}),\n }\n : undefined,\n money_trail: moneyTrail,\n unresolved: compareUnresolved ? [compareInput] : undefined,\n partial_query_errors: partialQueryFailures.length > 0 ? partialQueryFailures : undefined,\n usage: usageBlock(usage),\n },\n }\n const artifacts = options.writeArtifacts\n ? await writeAddressRiskArtifacts(\n network,\n address,\n compareAddress,\n graphData,\n exchangeRows,\n summaryText\n )\n : statelessArtifacts()\n const evidence = artifactEvidence(artifacts)\n\n return {\n summaryText,\n structuredContent: {\n ...structuredContent,\n artifacts,\n evidence: [\n ...evidence,\n {\n evidence_type: 'tool_summary',\n summary: `aml_address_risk ${address} completed for ${network}`,\n },\n ],\n },\n graphData,\n }\n}\n\n/**\n * Routes the public AML tool contract to the requested implementation. An\n * omitted version always follows the latest supported contract.\n */\nexport async function runAmlAddressRisk(\n remoteClient: Client,\n options: AddressRiskOptions,\n requestedVersion?: string\n) {\n const version = resolveAmlAddressRiskVersion(requestedVersion)\n switch (version) {\n case 'v1': {\n const result = await addressRisk(remoteClient, options)\n return {\n ...result,\n structuredContent: {\n ...result.structuredContent,\n tool_version: version,\n },\n }\n }\n }\n}\n\nfunction toCsvValue(value: unknown): string {\n return value === undefined || value === null ? '' : String(value)\n}\n\nfunction subjectNodeForExchangeRow(row: Record<string, unknown>, fallbackAddress: string): string {\n return String(\n row['direction'] === 'inflow'\n ? (row['withdrawal_address'] ?? row['deposit_address'] ?? fallbackAddress)\n : (row['deposit_address'] ?? row['withdrawal_address'] ?? fallbackAddress)\n )\n}\n\nfunction buildAddressRiskTableHtml(\n tool: string,\n network: string,\n rows: Array<Record<string, unknown>>,\n subject: string\n): string {\n const headers = [\n 'direction',\n 'exchange_address',\n 'subject_path_node',\n 'hops',\n 'amount_usd_sum',\n 'tx_count',\n ] as const\n const body = rows\n .map((row) => {\n const exchangeAddress = String(row['exchange_address'] ?? '')\n const subjectNode = subjectNodeForExchangeRow(row, subject)\n const rowValues = [\n row['direction'] ?? '',\n exchangeAddress,\n subjectNode,\n row['hops'] ?? '',\n row['amount_usd_sum'] ?? '',\n row['tx_count'] ?? '',\n ]\n return `<tr>${rowValues.map((value) => `<td>${htmlEscape(toCsvValue(value))}</td>`).join('')}</tr>`\n })\n .join('\\n')\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${htmlEscape(tool)} Risk Table</title>\n<style>\n :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui; background: #0b0d12; color: #f4f2ea; }\n body { margin: 0; background: #0b0d12; color: #f4f2ea; }\n main { padding: 24px; }\n h1 { font-size: 20px; margin: 0 0 8px; font-weight: 650; }\n table { border-collapse: collapse; width: 100%; min-width: 900px; }\n th, td { border-bottom: 1px solid rgba(255,255,255,.08); padding: 8px 10px; text-align: left; }\n th { position: sticky; top: 0; background: #161a24; color: #f2dda6; font-weight: 600; }\n</style>\n</head>\n<body>\n<main>\n <h1>${htmlEscape(tool)} Table</h1>\n <div>Network: <strong>${htmlEscape(network)}</strong></div>\n <div>Generated: <strong>${htmlEscape(new Date().toISOString())}</strong></div>\n <table>\n <thead><tr>${headers.map((header) => `<th>${htmlEscape(header)}</th>`).join('')}</tr></thead>\n <tbody>${body}</tbody>\n </table>\n</main>\n</body>\n</html>\n`\n}\n\nasync function writeAddressRiskArtifacts(\n network: string,\n address: string,\n compareAddress: string | undefined,\n graphData: Record<string, unknown>,\n exchangeRows: Array<Record<string, unknown>>,\n summaryText: string\n): Promise<Record<string, string>> {\n const paths = workspaceOutputPaths()\n await Promise.all([\n mkdir(paths.reportsRoot, { recursive: true }),\n mkdir(paths.reportGraphsRoot, { recursive: true }),\n mkdir(paths.reportTablesRoot, { recursive: true }),\n ])\n const safeNetwork = network.replace(/[^A-Za-z0-9._-]+/g, '_')\n const safeAddress = address.replace(/[^A-Za-z0-9._-]+/g, '_')\n const slug = `${new Date()\n .toISOString()\n .replace(/[-:]/g, '')\n .replace(/\\.\\d{3}Z$/, 'Z')}_aml_address_risk_${safeNetwork}_${safeAddress}`\n const graphPath = path.join(paths.reportGraphsRoot, `${slug}.graph.json`)\n const tableJsonPath = path.join(paths.reportTablesRoot, `${slug}.compact-evidence.json`)\n const csvPath = path.join(paths.reportTablesRoot, `${slug}.flows.csv`)\n const tableHtmlPath = path.join(paths.reportsRoot, `${slug}.table.html`)\n const reportPath = path.join(paths.reportsRoot, `${slug}.aml-address-report.md`)\n const graphHtmlPath = path.join(paths.reportsRoot, `${slug}.graph.html`)\n const { generateInlineGraphHtml } = await import('../viz/html-generator.js')\n const header = [\n 'direction',\n 'exchange_address',\n 'subject_path_node',\n 'hops',\n 'amount_usd_sum',\n 'tx_count',\n ]\n const csv =\n [\n header.join(','),\n ...exchangeRows.map((row) => {\n const exchangeAddress = String(row['exchange_address'] ?? '')\n const subjectPathNode = subjectNodeForExchangeRow(row, address)\n return [\n row['direction'] ?? '',\n exchangeAddress,\n subjectPathNode,\n row['hops'] ?? '',\n row['amount_usd_sum'] ?? '',\n row['tx_count'] ?? '',\n ]\n .map((value) => JSON.stringify(String(value)))\n .join(',')\n }),\n ].join('\\n') + '\\n'\n const evidence = {\n schema: 'chain-insights.trace.v1',\n tool: 'aml_address_risk',\n network,\n input: {\n address,\n ...(compareAddress ? { compare_address: compareAddress } : {}),\n },\n profile: {\n exchange_rows: exchangeRows.map((row) => ({\n direction: row['direction'],\n exchange_address: row['exchange_address'],\n subject_node: subjectNodeForExchangeRow(row, address),\n hops: row['hops'],\n amount_usd_sum: row['amount_usd_sum'],\n tx_count: row['tx_count'],\n })),\n report_summary: summaryText,\n },\n }\n await writeFile(graphPath, JSON.stringify(graphData, null, 2) + '\\n', { mode: 0o600 })\n await writeFile(tableJsonPath, JSON.stringify(evidence, null, 2) + '\\n', { mode: 0o600 })\n await writeFile(csvPath, csv, { mode: 0o600 })\n await writeFile(\n tableHtmlPath,\n buildAddressRiskTableHtml('aml_address_risk', network, exchangeRows, address),\n { mode: 0o600 }\n )\n await writeFile(graphHtmlPath, generateInlineGraphHtml(graphData), { mode: 0o600 })\n await writeFile(\n reportPath,\n [\n `# Address Risk Report (${network}:${address})`,\n `- Graph JSON: ${graphPath}`,\n `- Table JSON: ${tableJsonPath}`,\n `- CSV: ${csvPath}`,\n `- Report HTML: ${tableHtmlPath}`,\n `- Graph HTML: ${graphHtmlPath}`,\n '',\n summaryText,\n ].join('\\n'),\n { mode: 0o600 }\n )\n\n return {\n graph_json: graphPath,\n graph_html: graphHtmlPath,\n table_json: tableJsonPath,\n flows_csv: csvPath,\n table_html: tableHtmlPath,\n report_md: reportPath,\n }\n}\n\nfunction statelessArtifacts(): Record<string, unknown> {\n return {\n artifacts_written: false,\n artifact_mode: 'stateless',\n }\n}\n\nfunction artifactEvidence(artifacts: Record<string, unknown>): Array<Record<string, unknown>> {\n return Object.entries(artifacts)\n .filter(\n (entry): entry is [string, string] =>\n entry[0] !== 'artifact_mode' && typeof entry[1] === 'string' && entry[1].length > 0\n )\n .map(([kind, filePath]) => ({\n evidence_type: 'artifact_pointer',\n path: filePath,\n summary: `${kind} artifact`,\n }))\n}\n\nfunction htmlEscape(value: unknown): string {\n return String(value ?? '')\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;')\n}\n\n// Deterministic query-builder surface for the committed corpus and\n// query-with-a-documented-call-contract tests. Reflects the retained\n// aml_address_risk/graph query builders after the aml_trace_* cut.\n\n// pairAnchorQuery resolves a pair's first/last transaction id through the\n// facts lane (openspec dozerdb-flows-to-slim-schema: the topology edge\n// carries no tx anchors). order 'ASC' returns the first transfer between the\n// pair, 'DESC' the latest. Bounded: LIMIT 1 on an indexed endpoint pattern.\n// A pair with only internal native transfers has no TRANSFER row, so the anchor\n// query returns nothing for it: no MCP read lists internal transfers yet.\nexport function pairAnchorQuery(from: string, to: string, order: 'ASC' | 'DESC' = 'ASC'): string {\n return [\n 'USE facts',\n `MATCH (a:Address {address: \"${escapeCypherString(from)}\"})-[t:TRANSFER]->(b:Address {address: \"${escapeCypherString(to)}\"})`,\n 'RETURN t.tx_id AS tx_id, t.block_timestamp AS block_timestamp',\n `ORDER BY t.block_timestamp ${order} LIMIT 1`,\n ].join(' ')\n}\n\n// inlineAverageFlowQuery is the documented recipe for the retired\n// avg_tx_size_usd edge property: compute the average at read time from the\n// two fields the slim edge carries.\nexport function inlineAverageFlowQuery(address: string, limit = 25): { query: string } {\n return {\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:FLOWS_TO]->(b:Address)`,\n 'RETURN b.address AS neighbor, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count,',\n ' r.amount_usd_sum / toFloat(r.tx_count) AS avg_tx_size_usd',\n `ORDER BY r.amount_usd_sum DESC LIMIT ${limit}`,\n ].join(' '),\n }\n}\n\nexport const queryBuilderContract = {\n topologyGraphQuery,\n pairAnchorQuery,\n inlineAverageFlowQuery,\n addressProfileQuery,\n compareAddressExistsQuery,\n addressFeatureQuery,\n exchangeOutflowQueries,\n exchangeInflowQueries,\n connectionProbeQuery,\n connectionRouteQueries,\n linkedExposureQueries,\n crossSpaceLinkedQuery,\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
2
  const require_active = require("./active-Bc13hJIm.cjs");
3
+ const require_request_timeout = require("./request-timeout-CYmQF8N7.cjs");
3
4
  let node_path = require("node:path");
4
5
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
5
6
  let node_fs_promises = require("node:fs/promises");
@@ -286,8 +287,6 @@ function wrapClientForUsageTracking(client, totals) {
286
287
  }
287
288
  //#endregion
288
289
  //#region src/investigation/public-tools.ts
289
- const GRAPH_QUERY_BATCH_TIMEOUT_SECONDS = 10;
290
- const GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS = 3e5;
291
290
  const AML_ADDRESS_RISK_SUPPORTED_VERSIONS = ["v1"];
292
291
  function resolveAmlAddressRiskVersion(requested) {
293
292
  const normalized = requested?.trim().toLowerCase();
@@ -345,12 +344,11 @@ async function callGraphBatch(remoteClient, network, queries) {
345
344
  queries: queries.map((query) => ({
346
345
  ...query,
347
346
  query: topologyGraphQuery(query.query)
348
- })),
349
- per_query_timeout_seconds: GRAPH_QUERY_BATCH_TIMEOUT_SECONDS
347
+ }))
350
348
  }
351
349
  }, void 0, {
352
- timeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS,
353
- maxTotalTimeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS
350
+ timeout: require_request_timeout.GRAPH_TOOL_REQUEST_TIMEOUT_MS,
351
+ maxTotalTimeout: require_request_timeout.GRAPH_TOOL_REQUEST_TIMEOUT_MS
354
352
  });
355
353
  if (result.isError) throw new Error(textFromToolResult(result) || "graph_query_batch failed");
356
354
  return parseGraphBatchResult(result);
@@ -0,0 +1,11 @@
1
+ //#region src/mcp/request-timeout.ts
2
+ const GRAPH_TOOL_REQUEST_TIMEOUT_MS = 3e5;
3
+ function graphToolRequestOptions(tool) {
4
+ if (tool === "graph_query" || tool === "graph_query_batch") return {
5
+ timeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,
6
+ maxTotalTimeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS
7
+ };
8
+ }
9
+ //#endregion
10
+ exports.GRAPH_TOOL_REQUEST_TIMEOUT_MS = GRAPH_TOOL_REQUEST_TIMEOUT_MS;
11
+ exports.graphToolRequestOptions = graphToolRequestOptions;
@@ -0,0 +1,12 @@
1
+ //#region src/mcp/request-timeout.ts
2
+ const GRAPH_TOOL_REQUEST_TIMEOUT_MS = 3e5;
3
+ function graphToolRequestOptions(tool) {
4
+ if (tool === "graph_query" || tool === "graph_query_batch") return {
5
+ timeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,
6
+ maxTotalTimeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS
7
+ };
8
+ }
9
+ //#endregion
10
+ export { GRAPH_TOOL_REQUEST_TIMEOUT_MS, graphToolRequestOptions };
11
+
12
+ //# sourceMappingURL=request-timeout-lBBe0mGf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-timeout-lBBe0mGf.mjs","names":[],"sources":["../src/mcp/request-timeout.ts"],"sourcesContent":["import type { Client } from '@modelcontextprotocol/sdk/client/index.js'\n\n// Above the graph server's 100 s batch budget and the 125 s edge limit.\n// The SDK default is 60 s and fails a batch the server is still answering.\nexport const GRAPH_TOOL_REQUEST_TIMEOUT_MS = 5 * 60 * 1000\n\nexport function graphToolRequestOptions(tool: string): Parameters<Client['callTool']>[2] {\n if (tool === 'graph_query' || tool === 'graph_query_batch') {\n return {\n timeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n maxTotalTimeout: GRAPH_TOOL_REQUEST_TIMEOUT_MS,\n }\n }\n return undefined\n}\n"],"mappings":";AAIA,MAAa,gCAAgC;AAE7C,SAAgB,wBAAwB,MAAiD;CACvF,IAAI,SAAS,iBAAiB,SAAS,qBACrC,OAAO;EACL,SAAS;EACT,iBAAiB;CACnB;AAGJ"}
@@ -131,6 +131,19 @@ Edge aggregate properties (as the live backend provides them):
131
131
  | `first_seen_timestamp` / `last_seen_timestamp` | First and last transfer time (Unix milliseconds). |
132
132
  | `token_standard` | `ERC20`, `ERC721`, or `ERC1155` when the pair's transfers share one unambiguous standard. Absent when mixed. Optional — do not assume it is present. |
133
133
  | `owner_address` / `operator_address` / `pair_id` | Endpoint identity copied onto the edge. |
134
+ | `valuation_tracked_count` | Transfers on the pair that are checked for a USD value. It can be lower than `tx_count`. |
135
+ | `valued_count` | Of those, transfers that got a USD value. |
136
+ | `missing_valuation_price_count` | Of those, transfers with no price for their day. |
137
+ | `unknown_quantity_count` | Of those, transfers whose token decimals are unknown, so the amount is unknown. |
138
+ | `unrepresentable_quantity_count` | Of those, transfers whose amount does not fit the warehouse number format. |
139
+ | `usd_range_count` | Of those, transfers whose USD value is 10^20 or more. No USD value is stored. |
140
+ | `valuation_complete` | `true` when `valued_count` equals `valuation_tracked_count`. |
141
+ | `valuation_coverage_ratio` | `valued_count` divided by `valuation_tracked_count`. 0 when none is tracked. |
142
+
143
+ `amount_usd_sum` counts a transfer with no USD value as 0, so it is a floor
144
+ unless `valuation_complete` is true. The valuation counters do not add up to
145
+ `valuation_tracked_count`: a transfer that fails a basic check is tracked, is
146
+ not valued, and sits in none of the four reason counters.
134
147
 
135
148
  The canonical probe is point-anchored and sub-second on the hosted endpoint:
136
149
  given one operator address, its top owners. Scope comes from the tool's
@@ -157,9 +170,9 @@ victim, or risk label. Confirm with money-flow and label context before
157
170
  acting.
158
171
 
159
172
  The whole-graph high-fan-in sweep — every operator grouped by distinct owner
160
- count — is a valid shape but a heavy one: at millions of edges it exceeds the
161
- hosted per-query timeout (10 seconds by default), and a sweep that times out
162
- can still consume the metered seconds. Scope both endpoints by the network
173
+ count — is a valid shape but a heavy one: at millions of edges it can exceed
174
+ the per-query limit (60 seconds by default), and a sweep that times out can
175
+ still consume the metered seconds. Scope both endpoints by the network
163
176
  property (this match has no exact-address key, so the shared-graph rule
164
177
  applies), bound it by a recent `last_seen_timestamp` window — recompute the
165
178
  cutoff rather than copying a literal, for example now minus 7 days in Unix
@@ -200,7 +213,10 @@ An approval is a fact, not proof of malicious intent.
200
213
 
201
214
  `(:Address)-[:DEPLOYED_CONTRACT]->(:Address)` joins a deployer to the contract
202
215
  it created, one link per creation. `kind` says how it was created.
203
- `source_event` is `contract_creation`.
216
+ `confidence_score` is the indexer's confidence in that reading, and it is 1 on
217
+ every creation today. `call_type` and `amount` are set only when the creation
218
+ was funded: the funding call (`CREATE` or `CREATE2`) and the native amount
219
+ sent, as decimal text. `source_event` is `contract_creation`.
204
220
 
205
221
  ### `SPONSORED`, `BUNDLED`, `SIGNED_FOR` — smart accounts (topology only)
206
222
 
@@ -210,7 +226,9 @@ it created, one link per creation. `kind` says how it was created.
210
226
  - `BUNDLED`: a bundler to the smart account whose user operations it
211
227
  submitted. The same properties as `SPONSORED`.
212
228
  - `SIGNED_FOR`: a signing key to the smart account it signs for.
213
- `first_height`, `last_height`, `source_event` `account_signer`.
229
+ `operations` (user operations the key signed for the account),
230
+ `failed_operations` (of those, the ones that failed), `first_height`,
231
+ `last_height`, `source_event` `account_signer`.
214
232
 
215
233
  ### `SIGNED_AUTHORIZATION` — the EIP-7702 link (topology only)
216
234
 
@@ -238,9 +256,9 @@ only.
238
256
  ### `LINKED` and `BRIDGED` properties
239
257
 
240
258
  `LINKED` carries `basis`, `confidence`, `source_event` `account_owner`,
241
- `declared_owner` and `last_height`, the block height of the newest owner
242
- action. `BRIDGED.totals_raw` is keyed by event kind and asset, so one sum has
243
- one unit.
259
+ `declared_owner`, `owner_state` (`added` on every link served: a removed owner
260
+ has no link) and `last_height`, the block height of the newest owner action.
261
+ `BRIDGED.totals_raw` is keyed by event kind and asset, so one sum has one unit.
244
262
 
245
263
  ### Traversal (the expanded surface)
246
264
 
@@ -276,9 +294,11 @@ bounded so an admitted query cannot become an unbounded graph walk:
276
294
  | Non-GQL path operators/functions | forbidden | Legacy path functions and starred path algorithms return a dialect error |
277
295
  | `UNWIND` literal list length | ≤ 1000 | `UNWIND [ …1001 items… ] AS x` → _UNWIND list of 1001 items exceeds the maximum of 1000_ |
278
296
 
279
- Always add an explicit upper hop bound and a `LIMIT`. Writes/DDL (`CREATE`,
280
- `MERGE`, `SET`, `DELETE`, `DROP`, `CALL`, …) are always rejected — the surface is
281
- read-only.
297
+ Always add an explicit upper hop bound and a `LIMIT`. A topology read that
298
+ filters on a link property also needs an address in its pattern: see
299
+ [Topology limits and errors](graph-tools.md#topology-limits-and-errors).
300
+ Writes/DDL (`CREATE`, `MERGE`, `SET`, `DELETE`, `DROP`, `CALL`, …) are always
301
+ rejected — the surface is read-only.
282
302
 
283
303
  ## `facts` — compiled Cypher subset
284
304
 
@@ -59,7 +59,18 @@ of truth.
59
59
  liquidity event. It carries the pool's liquidity totals.
60
60
  - `SWAPPED` joins two different addresses. A self swap is a `SWAP` row only.
61
61
  `strength` is `swap` (the whole route is proven) or `swap_like` (the shape
62
- is a swap, the protocol is unidentified).
62
+ is a swap, the pool's code is not proven).
63
+ - Today no route is `swap`. The swap reader reads transaction receipts only,
64
+ with no execution trace, and the proof needs the trace. Every served route
65
+ is `swap_like`, with `reason` `unknown_pool_code` and `families` `unknown`.
66
+ A Uniswap V2 or V3 swap reads this way. `unknown` does not mean the
67
+ protocol is unsupported. A filter on `strength = 'swap'` or on a known
68
+ family matches nothing.
69
+ - A route that could not be paired (`swap_unsplit`) is never served. It has
70
+ no payer, recipient or pool. It makes no `SWAPPED` edge and no `SWAP` row,
71
+ and it exists in the warehouse only. Every Uniswap v4 swap reads this way
72
+ today, by design, so v4 swaps are hidden. The share of `swap_unsplit` rises
73
+ with v4 use.
63
74
  - `REMOVED_LIQUIDITY` carries `receiver_added_usd` and `receiver_provided`.
64
75
  A receiver's profit from a pool is `usd` minus `receiver_added_usd`.
65
76
  - `SWAP` and `LIQUIDITY_*` rows need an address on either endpoint or a
@@ -67,8 +78,13 @@ of truth.
67
78
  a `tx_id` equality.
68
79
  - `block_timestamp` on `SWAP` and `LIQUIDITY_*` rows is epoch milliseconds,
69
80
  in filters and in results, as on `TRANSFER`.
70
- - USD comes from the daily price services, never from a swap. With no price,
71
- USD is empty and the matching `…price_missing` property is true.
81
+ - On a facts row, USD comes from the daily price services, never from a swap.
82
+ With no price, USD is empty and the matching `…price_missing` property is
83
+ true.
84
+ - `SWAPPED` sums its routes, and a route side with no price adds 0 to
85
+ `sold_usd` or `bought_usd`. So 0 can mean no price. Do not read 0 as worth
86
+ nothing. The facts `SWAP` row says which side had no price:
87
+ `sold_price_missing` and `bought_price_missing`.
72
88
  - Swap attribution is read from `SWAPPED`, the aggregate (`strength`, pools,
73
89
  families), or from the facts `SWAP` row, one route. `FLOWS_TO` carries value
74
90
  only.
@@ -160,6 +176,12 @@ and `:Sanctioned`. Each role also has a flag on the node.
160
176
  endpoint, a `tx_id` (the `0x` transaction hash on EVM networks), or a bare
161
177
  `block_date` bound, which `block_timestamp` bounds in epoch milliseconds
162
178
  may narrow to a time window.
179
+ - An address-only `TRANSFER` read covers a recent window (90 days today).
180
+ Add a bare `block_date` bound to read further back.
181
+ - `TRANSFER` lists token and native transfers. It lists no internal native
182
+ transfer, a contract sending ETH during a call. `FLOWS_TO` `tx_count`
183
+ counts those too, so a pair can have a `tx_count` above 0 and no
184
+ `TRANSFER` row. No MCP read lists internal native transfers yet.
163
185
  - Use `meta_usage_status` through Chain Insights before public hosted reads
164
186
  when you need the caller's remaining free-tier allowance.
165
187
  - Hosted endpoints can expose a public free tier for graph_query. The default
@@ -173,7 +195,8 @@ and `:Sanctioned`. Each role also has a flag on the node.
173
195
  - Use single bounded `graph_query` calls for public no-wallet free-tier usage. Use
174
196
  `graph_query_batch` for related reads that should share one paid call; public
175
197
  free-tier access does not include batches.
176
- - `per_query_timeout_seconds` is optional and capped at `10` by default.
198
+ - `per_query_timeout_seconds` is optional and capped at `60` by default (`30` for
199
+ `USE facts`). It can lower a limit. It cannot raise one.
177
200
  - Returned rows live in `structuredContent.facts`.
178
201
 
179
202
  Agent installers ship three skills:
@@ -230,7 +253,8 @@ Batch result facts include:
230
253
  "count": 2,
231
254
  "completed": 2,
232
255
  "failed": 0,
233
- "per_query_timeout_seconds": 10,
256
+ "live_tier_timeout_seconds": 60,
257
+ "starrocks_tier_timeout_seconds": 30,
234
258
  "total_query_elapsed_ms": 1345,
235
259
  "billable_seconds": 2,
236
260
  "estimated_usdc": "0.02"
@@ -244,10 +268,16 @@ Hosted `USE topology` queries share one graph database, so every query is
244
268
  bounded:
245
269
 
246
270
  - **Time:** the graph database stops a query when its time budget ends
247
- (10 seconds by default, or your lower `per_query_timeout_seconds`).
271
+ (60 seconds by default, or your lower `per_query_timeout_seconds`).
248
272
  - **Concurrency:** at most 4 topology queries run at once on the hosted
249
273
  endpoint. A query waits for a free slot inside its own time budget.
250
274
  - **Memory:** a query that grows past the per-query memory limit is stopped.
275
+ - **Batch:** the queries of one batch run one after another and share a
276
+ 100-second budget. A query that would start with less than 1 second left
277
+ does not run. A topology query then fails with `query_timeout`. The results
278
+ of earlier queries still come back. `USE facts` queries stop at 30 seconds.
279
+ A client that sets its own request timeout needs at least 100 seconds for a
280
+ batch and 65 seconds for one query. `cia` waits 5 minutes.
251
281
 
252
282
  A stopped query fails with one of these codes. The code starts the error text
253
283
  and is also returned as `refusal_code` in the response metadata.
@@ -264,6 +294,33 @@ are the most common cause of `query_timeout`: one hop through a hub can touch
264
294
  millions of edges. A route or an open target keeps its one guarded shape: change
265
295
  only its addresses, never its `{0,4}` bound.
266
296
 
297
+ A topology read that filters on a link property needs an address anchor. With
298
+ no address in its pattern, the read starts from every link of the type it
299
+ names. Do not count on the filter to narrow that. `WHERE x.strength = 'swap'`
300
+ on `SWAPPED` checks every `SWAPPED` link. `LIMIT` stops the read only after it
301
+ has found enough rows, so a filter that matches few or none can run to the
302
+ 60-second limit and fail with `query_timeout`. A discovery probe with no
303
+ filter, such as
304
+ `MATCH (:Address)-[r:FLOWS_TO]->(:Address) RETURN r.tx_count LIMIT 20`, finds
305
+ its rows at once and stays valid.
306
+
307
+ Put an address in the pattern, and pick one with few links. Read `degree_out`
308
+ and `degree_in` on the node first. They are a rough guide, not a guarantee:
309
+ they count neighbours, not links, and one pair of addresses can hold many
310
+ `SWAPPED` links. An address with hundreds of thousands of neighbours can fail
311
+ the same way. The queries of one batch share a 100-second budget.
312
+
313
+ Anchored on one address:
314
+
315
+ ```cypher
316
+ USE topology
317
+ MATCH (a:Address {address: "0x…"})-[x:SWAPPED]->(b:Address)
318
+ WHERE x.swap_count >= 2
319
+ RETURN b.address AS recipient, x.swap_count AS swap_count
320
+ ORDER BY x.swap_count DESC
321
+ LIMIT 25
322
+ ```
323
+
267
324
  ## Operator topology recipe
268
325
 
269
326
  `OPERATED_BY` is the owner-to-operator topology edge. The operator is the
@@ -289,8 +346,8 @@ Notes:
289
346
  - Zero rows is a healthy result — the address simply has no mediated
290
347
  transfers.
291
348
  - Whole-graph high-fan-in sweeps (every operator grouped by distinct owner)
292
- are valid but heavy; at millions of edges they exceed the hosted 10-second
293
- per-query budget. See `docs/graph-query-compatibility.md` for the
349
+ are valid but heavy; at millions of edges they can exceed the 60-second
350
+ per-query limit. See `docs/graph-query-compatibility.md` for the
294
351
  time-bounded sweep shape.
295
352
  - Confirm any lead with `FLOWS_TO` money-flow context and address labels
296
353
  before drawing conclusions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chain-insights",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
4
4
  "description": "AML investigation CLI and MCP proxy for blockchain risk screening and Chain Insights Graph access",
5
5
  "homepage": "https://chain-insights.ai",
6
6
  "repository": {