chain-insights 0.2.20 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -18
- package/dist/cli.cjs +8 -8
- package/dist/cli.mjs +8 -8
- package/dist/mcp-proxy.cjs +4 -4
- package/dist/mcp-proxy.mjs +4 -4
- package/dist/{public-tools-BC1fi0DV.cjs → public-tools-q4NMdmDX.cjs} +227 -10
- package/dist/{public-tools-B13J0MJZ.mjs → public-tools-w7En2m3q.mjs} +228 -11
- package/dist/public-tools-w7En2m3q.mjs.map +1 -0
- package/docs/architecture.md +4 -0
- package/docs/mcp-proxy.md +41 -0
- package/package.json +2 -2
- package/dist/public-tools-B13J0MJZ.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
Chain Insights is an open-source AML investigation toolkit for AI agents and
|
|
6
6
|
analysts. Install it from npm to screen blockchain addresses, trace funds,
|
|
7
|
-
expand scam topologies, manage case evidence, and generate graph reports
|
|
8
|
-
Chain Insights graph intelligence.
|
|
7
|
+
expand scam topologies, manage case evidence, and generate graph reports.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Graph access is configuration-driven. The package defaults to a local GraphRAG
|
|
10
|
+
MCP endpoint for development; hosted endpoints are set explicitly with
|
|
11
|
+
`graphMcpEndpoint` or `CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT`.
|
|
13
12
|
|
|
14
13
|
## What You Can Do Today
|
|
15
14
|
|
|
@@ -53,26 +52,32 @@ cd ./chain-insights-investigations
|
|
|
53
52
|
cia init .
|
|
54
53
|
```
|
|
55
54
|
|
|
56
|
-
## Configure MCP
|
|
55
|
+
## Configure GraphRAG MCP Endpoint
|
|
57
56
|
|
|
58
|
-
`cia` uses `graphMcpEndpoint` for all GraphRAG MCP calls.
|
|
57
|
+
`cia` uses `graphMcpEndpoint` for all GraphRAG MCP calls. The npm package does
|
|
58
|
+
not hardcode a hosted endpoint. Configure the endpoint explicitly for the
|
|
59
|
+
environment you intend to use.
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Local development endpoint (default):
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
cia config set graphMcpEndpoint http://127.0.0.1:8012/mcp
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
Hosted staging endpoint for approved testers:
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
cia config set graphMcpEndpoint https://staging-mcp.chain-insights.ai/mcp
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Hosted access also needs an access mode, such as an approved access key or a
|
|
74
|
+
prepared wallet. Keep those credentials out of README examples; setup commands
|
|
75
|
+
live in [MCP proxy](docs/mcp-proxy.md).
|
|
76
|
+
|
|
77
|
+
Optional one-shot override from the environment:
|
|
73
78
|
|
|
74
79
|
```bash
|
|
75
|
-
export CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT=https://
|
|
80
|
+
export CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT=https://staging-mcp.chain-insights.ai/mcp
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
Validation rules:
|
|
@@ -91,15 +96,13 @@ Check the configured endpoint and current GraphRAG MCP capabilities:
|
|
|
91
96
|
|
|
92
97
|
```bash
|
|
93
98
|
cia config get graphMcpEndpoint
|
|
94
|
-
cia wallet balance
|
|
95
99
|
cia mcp networks
|
|
96
100
|
cia mcp tools --refresh
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
cases without a reachable GraphRAG MCP endpoint.
|
|
103
|
+
If network or tool discovery fails, check the endpoint and access mode first.
|
|
104
|
+
The CLI can still initialize workspaces and manage cases without a reachable
|
|
105
|
+
GraphRAG MCP endpoint.
|
|
103
106
|
|
|
104
107
|
Open a case and run a small investigation:
|
|
105
108
|
|
|
@@ -175,7 +178,8 @@ Graph queries must choose the right read layer explicitly:
|
|
|
175
178
|
| `facts` | Labels, features, risk scores, assets, and enrichment |
|
|
176
179
|
|
|
177
180
|
Use `graph_query_batch` when related reads should share one call and one
|
|
178
|
-
result envelope.
|
|
181
|
+
result envelope. Endpoint access and authentication are configured separately;
|
|
182
|
+
see [MCP proxy](docs/mcp-proxy.md).
|
|
179
183
|
|
|
180
184
|
## AML Tools
|
|
181
185
|
|
|
@@ -201,7 +205,7 @@ reports under the workspace instead of embedding large payloads in case notes.
|
|
|
201
205
|
| --- | --- |
|
|
202
206
|
| [Graph tools](docs/graph-tools.md) | GraphRAG MCP layers, `graph_query`, `graph_query_batch`, AML tool contracts, graph reports, evidence pointers |
|
|
203
207
|
| [Investigation workspaces](docs/investigation-workspaces.md) | `cia init`, case layout, evidence, dossiers, imports, templates, sessions, reports |
|
|
204
|
-
| [MCP proxy](docs/mcp-proxy.md) | Stdio proxy behavior, agent installers, local tools, auth modes, Inspector validation |
|
|
208
|
+
| [MCP proxy](docs/mcp-proxy.md) | Stdio proxy behavior, endpoint configuration, agent installers, local tools, auth modes, Inspector validation |
|
|
205
209
|
| [Architecture](docs/architecture.md) | Product layers, data flow, local storage, security model, config keys |
|
|
206
210
|
| [Development](docs/development.md) | Build, test, and local install commands |
|
|
207
211
|
| [Contributing](docs/contributing.md) | Development workflow, pull requests, release expectations |
|
package/dist/cli.cjs
CHANGED
|
@@ -378,7 +378,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
378
378
|
}));
|
|
379
379
|
return;
|
|
380
380
|
}
|
|
381
|
-
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-
|
|
381
|
+
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
382
382
|
const result = await addressRisk(client, {
|
|
383
383
|
address: opts.address,
|
|
384
384
|
network: opts.network,
|
|
@@ -406,7 +406,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
406
406
|
}));
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
|
-
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-
|
|
409
|
+
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
410
410
|
const caseId = opts.case ? await resolveCaseSelector(opts.case) : void 0;
|
|
411
411
|
const result = await trackFunds(client, config, {
|
|
412
412
|
trustedAddresses: opts.trustedAddresses,
|
|
@@ -429,7 +429,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
429
429
|
const { requireWorkspaceRoot } = await Promise.resolve().then(() => require("./output-root-YIbl6PwF.cjs")).then((n) => n.output_root_exports);
|
|
430
430
|
requireWorkspaceRoot();
|
|
431
431
|
await withGraphMcpClient("chain-insights-cli-scam-topology", async (client, config) => {
|
|
432
|
-
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-
|
|
432
|
+
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
433
433
|
const incidentTimestampMs = optionalNumber(opts.incidentTimestampMs);
|
|
434
434
|
if (incidentTimestampMs === void 0) throw new Error("incident-timestamp-ms is required");
|
|
435
435
|
const caseId = opts.case ? await resolveCaseSelector(opts.case) : void 0;
|
|
@@ -451,7 +451,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
451
451
|
})).addCommand(new commander.Command("stake-insights").description("Explain Bittensor staking behavior around an address, coldkey, or hotkey").requiredOption("--network <network>", "Network to query. Run `cia mcp networks` for supported networks.").option("--address <address>", "Full Bittensor address to inspect as either coldkey or hotkey").option("--coldkey <address>", "Full Bittensor coldkey address to inspect").option("--hotkey <address>", "Full Bittensor hotkey address to inspect").option("--netuid <number>", "Optional subnet netuid filter").option("--start-timestamp-ms <milliseconds>", "Optional inclusive lower activity timestamp bound").option("--end-timestamp-ms <milliseconds>", "Optional inclusive upper activity timestamp bound").option("--start-block <number>", "Optional start block. Current stake graph parity may require timestamp windows instead.").option("--end-block <number>", "Optional end block. Current stake graph parity may require timestamp windows instead.").option("--depth <number>", "Optional expansion depth limit, default 1, max 3").action(async (opts) => {
|
|
452
452
|
try {
|
|
453
453
|
await withGraphMcpClient("chain-insights-cli-stake-insights", async (client) => {
|
|
454
|
-
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-
|
|
454
|
+
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
455
455
|
const result = await stakeInsights(client, {
|
|
456
456
|
network: opts.network,
|
|
457
457
|
address: opts.address,
|
|
@@ -479,7 +479,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
479
479
|
assertPublicMcpToolName(tool);
|
|
480
480
|
await withGraphMcpClient("chain-insights-cli-call", async (client, config) => {
|
|
481
481
|
if (tool === "address_risk") {
|
|
482
|
-
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-
|
|
482
|
+
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
483
483
|
const result = await addressRisk(client, {
|
|
484
484
|
address: String(args["address"] ?? ""),
|
|
485
485
|
network: String(args["network"] ?? ""),
|
|
@@ -489,7 +489,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
489
489
|
return;
|
|
490
490
|
}
|
|
491
491
|
if (tool === "track_funds") {
|
|
492
|
-
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-
|
|
492
|
+
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
493
493
|
const result = await trackFunds(client, config, {
|
|
494
494
|
trustedAddresses: args["trusted_addresses"] ?? "",
|
|
495
495
|
untrustedAddresses: args["untrusted_addresses"],
|
|
@@ -504,7 +504,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
504
504
|
return;
|
|
505
505
|
}
|
|
506
506
|
if (tool === "scam_topology") {
|
|
507
|
-
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-
|
|
507
|
+
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
508
508
|
const victimAddress = String(args["victim_address"] ?? "").trim();
|
|
509
509
|
if (!victimAddress) throw new Error("victim_address is required");
|
|
510
510
|
const incidentTimestampMs = optionalNumberArg(args["incident_timestamp_ms"], "incident_timestamp_ms");
|
|
@@ -522,7 +522,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
522
522
|
return;
|
|
523
523
|
}
|
|
524
524
|
if (tool === "stake_insights") {
|
|
525
|
-
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-
|
|
525
|
+
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
526
526
|
const result = await stakeInsights(client, {
|
|
527
527
|
network: String(args["network"] ?? ""),
|
|
528
528
|
address: args["address"] === void 0 ? void 0 : String(args["address"]),
|
package/dist/cli.mjs
CHANGED
|
@@ -376,7 +376,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
376
376
|
}));
|
|
377
377
|
return;
|
|
378
378
|
}
|
|
379
|
-
const { addressRisk } = await import("./public-tools-
|
|
379
|
+
const { addressRisk } = await import("./public-tools-w7En2m3q.mjs");
|
|
380
380
|
const result = await addressRisk(client, {
|
|
381
381
|
address: opts.address,
|
|
382
382
|
network: opts.network,
|
|
@@ -404,7 +404,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
404
404
|
}));
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
|
-
const { trackFunds } = await import("./public-tools-
|
|
407
|
+
const { trackFunds } = await import("./public-tools-w7En2m3q.mjs");
|
|
408
408
|
const caseId = opts.case ? await resolveCaseSelector(opts.case) : void 0;
|
|
409
409
|
const result = await trackFunds(client, config, {
|
|
410
410
|
trustedAddresses: opts.trustedAddresses,
|
|
@@ -427,7 +427,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
427
427
|
const { requireWorkspaceRoot } = await import("./output-root-BRhzhhXZ.mjs").then((n) => n.t);
|
|
428
428
|
requireWorkspaceRoot();
|
|
429
429
|
await withGraphMcpClient("chain-insights-cli-scam-topology", async (client, config) => {
|
|
430
|
-
const { scamTopology } = await import("./public-tools-
|
|
430
|
+
const { scamTopology } = await import("./public-tools-w7En2m3q.mjs");
|
|
431
431
|
const incidentTimestampMs = optionalNumber(opts.incidentTimestampMs);
|
|
432
432
|
if (incidentTimestampMs === void 0) throw new Error("incident-timestamp-ms is required");
|
|
433
433
|
const caseId = opts.case ? await resolveCaseSelector(opts.case) : void 0;
|
|
@@ -449,7 +449,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
449
449
|
})).addCommand(new Command("stake-insights").description("Explain Bittensor staking behavior around an address, coldkey, or hotkey").requiredOption("--network <network>", "Network to query. Run `cia mcp networks` for supported networks.").option("--address <address>", "Full Bittensor address to inspect as either coldkey or hotkey").option("--coldkey <address>", "Full Bittensor coldkey address to inspect").option("--hotkey <address>", "Full Bittensor hotkey address to inspect").option("--netuid <number>", "Optional subnet netuid filter").option("--start-timestamp-ms <milliseconds>", "Optional inclusive lower activity timestamp bound").option("--end-timestamp-ms <milliseconds>", "Optional inclusive upper activity timestamp bound").option("--start-block <number>", "Optional start block. Current stake graph parity may require timestamp windows instead.").option("--end-block <number>", "Optional end block. Current stake graph parity may require timestamp windows instead.").option("--depth <number>", "Optional expansion depth limit, default 1, max 3").action(async (opts) => {
|
|
450
450
|
try {
|
|
451
451
|
await withGraphMcpClient("chain-insights-cli-stake-insights", async (client) => {
|
|
452
|
-
const { stakeInsights } = await import("./public-tools-
|
|
452
|
+
const { stakeInsights } = await import("./public-tools-w7En2m3q.mjs");
|
|
453
453
|
const result = await stakeInsights(client, {
|
|
454
454
|
network: opts.network,
|
|
455
455
|
address: opts.address,
|
|
@@ -477,7 +477,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
477
477
|
assertPublicMcpToolName(tool);
|
|
478
478
|
await withGraphMcpClient("chain-insights-cli-call", async (client, config) => {
|
|
479
479
|
if (tool === "address_risk") {
|
|
480
|
-
const { addressRisk } = await import("./public-tools-
|
|
480
|
+
const { addressRisk } = await import("./public-tools-w7En2m3q.mjs");
|
|
481
481
|
const result = await addressRisk(client, {
|
|
482
482
|
address: String(args["address"] ?? ""),
|
|
483
483
|
network: String(args["network"] ?? ""),
|
|
@@ -487,7 +487,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
487
487
|
return;
|
|
488
488
|
}
|
|
489
489
|
if (tool === "track_funds") {
|
|
490
|
-
const { trackFunds } = await import("./public-tools-
|
|
490
|
+
const { trackFunds } = await import("./public-tools-w7En2m3q.mjs");
|
|
491
491
|
const result = await trackFunds(client, config, {
|
|
492
492
|
trustedAddresses: args["trusted_addresses"] ?? "",
|
|
493
493
|
untrustedAddresses: args["untrusted_addresses"],
|
|
@@ -502,7 +502,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
502
502
|
return;
|
|
503
503
|
}
|
|
504
504
|
if (tool === "scam_topology") {
|
|
505
|
-
const { scamTopology } = await import("./public-tools-
|
|
505
|
+
const { scamTopology } = await import("./public-tools-w7En2m3q.mjs");
|
|
506
506
|
const victimAddress = String(args["victim_address"] ?? "").trim();
|
|
507
507
|
if (!victimAddress) throw new Error("victim_address is required");
|
|
508
508
|
const incidentTimestampMs = optionalNumberArg(args["incident_timestamp_ms"], "incident_timestamp_ms");
|
|
@@ -520,7 +520,7 @@ program.command("mcp").description("Interact with the Chain Insights MCP endpoin
|
|
|
520
520
|
return;
|
|
521
521
|
}
|
|
522
522
|
if (tool === "stake_insights") {
|
|
523
|
-
const { stakeInsights } = await import("./public-tools-
|
|
523
|
+
const { stakeInsights } = await import("./public-tools-w7En2m3q.mjs");
|
|
524
524
|
const result = await stakeInsights(client, {
|
|
525
525
|
network: String(args["network"] ?? ""),
|
|
526
526
|
address: args["address"] === void 0 ? void 0 : String(args["address"]),
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -995,7 +995,7 @@ async function createProxy() {
|
|
|
995
995
|
}],
|
|
996
996
|
isError: true
|
|
997
997
|
};
|
|
998
|
-
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-
|
|
998
|
+
const { addressRisk } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
999
999
|
const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-B3mkLP8Z.cjs"));
|
|
1000
1000
|
const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-XbN16DwU.cjs"));
|
|
1001
1001
|
const result = await addressRisk(remoteClient, {
|
|
@@ -1066,7 +1066,7 @@ async function createProxy() {
|
|
|
1066
1066
|
}],
|
|
1067
1067
|
isError: true
|
|
1068
1068
|
};
|
|
1069
|
-
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-
|
|
1069
|
+
const { trackFunds } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
1070
1070
|
const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-B3mkLP8Z.cjs"));
|
|
1071
1071
|
const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-XbN16DwU.cjs"));
|
|
1072
1072
|
const result = await trackFunds(remoteClient, config, {
|
|
@@ -1139,7 +1139,7 @@ async function createProxy() {
|
|
|
1139
1139
|
}],
|
|
1140
1140
|
isError: true
|
|
1141
1141
|
};
|
|
1142
|
-
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-
|
|
1142
|
+
const { scamTopology } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
1143
1143
|
const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-B3mkLP8Z.cjs"));
|
|
1144
1144
|
const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-XbN16DwU.cjs"));
|
|
1145
1145
|
const result = await scamTopology(remoteClient, config, {
|
|
@@ -1216,7 +1216,7 @@ async function createProxy() {
|
|
|
1216
1216
|
}],
|
|
1217
1217
|
isError: true
|
|
1218
1218
|
};
|
|
1219
|
-
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-
|
|
1219
|
+
const { stakeInsights } = await Promise.resolve().then(() => require("./public-tools-q4NMdmDX.cjs"));
|
|
1220
1220
|
const { writeGraphReport } = await Promise.resolve().then(() => require("./graph-reports-B3mkLP8Z.cjs"));
|
|
1221
1221
|
const { ensureArtifactServer } = await Promise.resolve().then(() => require("./artifact-server-XbN16DwU.cjs"));
|
|
1222
1222
|
const result = await stakeInsights(remoteClient, {
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -991,7 +991,7 @@ async function createProxy() {
|
|
|
991
991
|
}],
|
|
992
992
|
isError: true
|
|
993
993
|
};
|
|
994
|
-
const { addressRisk } = await import("./public-tools-
|
|
994
|
+
const { addressRisk } = await import("./public-tools-w7En2m3q.mjs");
|
|
995
995
|
const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
|
|
996
996
|
const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
|
|
997
997
|
const result = await addressRisk(remoteClient, {
|
|
@@ -1062,7 +1062,7 @@ async function createProxy() {
|
|
|
1062
1062
|
}],
|
|
1063
1063
|
isError: true
|
|
1064
1064
|
};
|
|
1065
|
-
const { trackFunds } = await import("./public-tools-
|
|
1065
|
+
const { trackFunds } = await import("./public-tools-w7En2m3q.mjs");
|
|
1066
1066
|
const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
|
|
1067
1067
|
const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
|
|
1068
1068
|
const result = await trackFunds(remoteClient, config, {
|
|
@@ -1135,7 +1135,7 @@ async function createProxy() {
|
|
|
1135
1135
|
}],
|
|
1136
1136
|
isError: true
|
|
1137
1137
|
};
|
|
1138
|
-
const { scamTopology } = await import("./public-tools-
|
|
1138
|
+
const { scamTopology } = await import("./public-tools-w7En2m3q.mjs");
|
|
1139
1139
|
const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
|
|
1140
1140
|
const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
|
|
1141
1141
|
const result = await scamTopology(remoteClient, config, {
|
|
@@ -1212,7 +1212,7 @@ async function createProxy() {
|
|
|
1212
1212
|
}],
|
|
1213
1213
|
isError: true
|
|
1214
1214
|
};
|
|
1215
|
-
const { stakeInsights } = await import("./public-tools-
|
|
1215
|
+
const { stakeInsights } = await import("./public-tools-w7En2m3q.mjs");
|
|
1216
1216
|
const { writeGraphReport } = await import("./graph-reports-BDELxmpi.mjs");
|
|
1217
1217
|
const { ensureArtifactServer } = await import("./artifact-server-CP6LXQ9d.mjs");
|
|
1218
1218
|
const result = await stakeInsights(remoteClient, {
|
|
@@ -1011,11 +1011,87 @@ function isExchangeFlag(value) {
|
|
|
1011
1011
|
if (typeof value === "number") return value === 1;
|
|
1012
1012
|
return false;
|
|
1013
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Address-type values that mark a node as part of the scam topology itself
|
|
1016
|
+
* (a written scam label or the protected victim role). Such nodes must never be
|
|
1017
|
+
* read back as exchange infrastructure, even when their label text contains a
|
|
1018
|
+
* brand or the word "exchange" — that text is our own output syncing back into
|
|
1019
|
+
* the graph, not authoritative exchange signal.
|
|
1020
|
+
*/
|
|
1021
|
+
const NON_EXCHANGE_ADDRESS_TYPES = new Set(["scam", "victim"]);
|
|
1022
|
+
/**
|
|
1023
|
+
* Determine whether a node's `address_type` marks it as scam- or victim-typed,
|
|
1024
|
+
* which disqualifies it from being treated as an exchange endpoint.
|
|
1025
|
+
*
|
|
1026
|
+
* @param addressType - The node's `address_type` property, if present.
|
|
1027
|
+
* @returns `true` when the type is a scam/victim role.
|
|
1028
|
+
*/
|
|
1029
|
+
function isScamOrVictimType(addressType) {
|
|
1030
|
+
if (addressType === void 0) return false;
|
|
1031
|
+
return NON_EXCHANGE_ADDRESS_TYPES.has(addressType.trim().toLowerCase());
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Detect an authoritative exchange label.
|
|
1035
|
+
*
|
|
1036
|
+
* Only an exact `exchange` token or a trailing `, exchange` registry suffix
|
|
1037
|
+
* (e.g. `"Binance, exchange"`) counts. Loose substring matching such as
|
|
1038
|
+
* `includes('exchange')` is deliberately rejected because scam labels written
|
|
1039
|
+
* by this tool (e.g. `"... -> Kucoin"`, subtype text containing "exchange")
|
|
1040
|
+
* sync back into the graph and would otherwise be mis-read as exchanges.
|
|
1041
|
+
*
|
|
1042
|
+
* @param labels - Node label strings from the graph.
|
|
1043
|
+
* @returns `true` when at least one label is an authoritative exchange marker.
|
|
1044
|
+
*/
|
|
1014
1045
|
function hasExchangeLabel(labels) {
|
|
1015
|
-
return labels.some((label) =>
|
|
1046
|
+
return labels.some((label) => {
|
|
1047
|
+
const normalized = label.trim().toLowerCase();
|
|
1048
|
+
return normalized === "exchange" || /(^|,)\s*exchange$/.test(normalized);
|
|
1049
|
+
});
|
|
1016
1050
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1051
|
+
/**
|
|
1052
|
+
* Decide whether a node is an exchange endpoint that terminates traversal.
|
|
1053
|
+
*
|
|
1054
|
+
* Exchange status keys off the authoritative `is_exchange` flag first, then an
|
|
1055
|
+
* exact `exchange` registry label. A node whose `address_type` is SCAM or
|
|
1056
|
+
* VICTIM is never an exchange endpoint, regardless of its label or role text,
|
|
1057
|
+
* because that signal originates from this tool's own labels rather than from
|
|
1058
|
+
* exchange enrichment.
|
|
1059
|
+
*
|
|
1060
|
+
* @param labels - Destination node label strings.
|
|
1061
|
+
* @param isExchange - The node's authoritative `is_exchange` property.
|
|
1062
|
+
* @param roles - Enrichment role strings for the node.
|
|
1063
|
+
* @param addressType - The node's `address_type` property, if present.
|
|
1064
|
+
* @returns `true` when the node should be treated as an exchange endpoint.
|
|
1065
|
+
*/
|
|
1066
|
+
function isExchangeEndpoint(labels, isExchange, roles, addressType) {
|
|
1067
|
+
if (isScamOrVictimType(addressType)) return false;
|
|
1068
|
+
return isExchangeFlag(isExchange) || hasExchangeLabel(labels) || roles.some((role) => role.trim().toLowerCase() === "exchange");
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Transaction-count threshold above which a deposit edge is treated as shared
|
|
1072
|
+
* exchange infrastructure rather than a scammer-dedicated cash-out address.
|
|
1073
|
+
*/
|
|
1074
|
+
const SHARED_EXCHANGE_DEPOSIT_TX_COUNT = 1e3;
|
|
1075
|
+
/**
|
|
1076
|
+
* USD-volume threshold above which a deposit edge is treated as shared exchange
|
|
1077
|
+
* infrastructure rather than a scammer-dedicated cash-out address.
|
|
1078
|
+
*/
|
|
1079
|
+
const SHARED_EXCHANGE_DEPOSIT_USD_SUM = 5e6;
|
|
1080
|
+
/**
|
|
1081
|
+
* Decide whether a penultimate-to-exchange edge represents shared exchange
|
|
1082
|
+
* deposit infrastructure (an omnibus or routing address many users fund)
|
|
1083
|
+
* rather than a scammer-dedicated cash-out address.
|
|
1084
|
+
*
|
|
1085
|
+
* A single scammer's cash-out deposit for one incident does not aggregate
|
|
1086
|
+
* thousands of transfers or tens of millions of USD; an edge that does is
|
|
1087
|
+
* exchange-side infrastructure and must not be auto-labeled scam.
|
|
1088
|
+
*
|
|
1089
|
+
* @param edge - A `terminal_exchange` topology edge (deposit -> exchange).
|
|
1090
|
+
* @returns `true` when the edge's `tx_count` or `amount_usd_sum` exceeds the
|
|
1091
|
+
* shared-infrastructure thresholds.
|
|
1092
|
+
*/
|
|
1093
|
+
function isSharedExchangeDeposit(edge) {
|
|
1094
|
+
return edge.tx_count !== void 0 && edge.tx_count >= SHARED_EXCHANGE_DEPOSIT_TX_COUNT || edge.amount_usd_sum !== void 0 && edge.amount_usd_sum >= SHARED_EXCHANGE_DEPOSIT_USD_SUM;
|
|
1019
1095
|
}
|
|
1020
1096
|
function isGenericContextLabel(label) {
|
|
1021
1097
|
const normalized = label.trim().toLowerCase();
|
|
@@ -1032,6 +1108,10 @@ function traversalProjection() {
|
|
|
1032
1108
|
"dst.labels AS dst_labels",
|
|
1033
1109
|
"src.is_exchange AS src_is_exchange",
|
|
1034
1110
|
"dst.is_exchange AS dst_is_exchange",
|
|
1111
|
+
"src.address_type AS src_address_type",
|
|
1112
|
+
"dst.address_type AS dst_address_type",
|
|
1113
|
+
"src.address_subtypes AS src_address_subtypes",
|
|
1114
|
+
"dst.address_subtypes AS dst_address_subtypes",
|
|
1035
1115
|
"r.amount_sum AS amount_sum",
|
|
1036
1116
|
"r.amount_usd_sum AS amount_usd_sum",
|
|
1037
1117
|
"r.tx_count AS tx_count",
|
|
@@ -1090,9 +1170,14 @@ function edgeFromRow(row, graphScope, hop, context) {
|
|
|
1090
1170
|
const dstLabels = stringArray(row["dst_labels"]);
|
|
1091
1171
|
const srcRoles = stringArray(row["src_roles"]);
|
|
1092
1172
|
const dstRoles = stringArray(row["dst_roles"]);
|
|
1093
|
-
const
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1173
|
+
const srcAddressType = stringValue$1(row["src_address_type"]);
|
|
1174
|
+
const dstAddressType = stringValue$1(row["dst_address_type"]);
|
|
1175
|
+
const srcAddressSubtypes = stringArray(row["src_address_subtypes"]);
|
|
1176
|
+
const dstAddressSubtypes = stringArray(row["dst_address_subtypes"]);
|
|
1177
|
+
const srcIsExchange = isExchangeEndpoint(srcLabels, row["src_is_exchange"], srcRoles, srcAddressType);
|
|
1178
|
+
const dstIsExchange = isExchangeEndpoint(dstLabels, row["dst_is_exchange"], dstRoles, dstAddressType);
|
|
1179
|
+
const dstIsScamTyped = isScamOrVictimType(dstAddressType);
|
|
1180
|
+
const genericLabeledBoundary = dstLabels.length > 0 && !dstIsExchange && !dstIsScamTyped;
|
|
1096
1181
|
return {
|
|
1097
1182
|
relation: dstIsExchange ? "terminal_exchange" : genericLabeledBoundary ? "context_boundary" : hop === 1 ? "seed_outflow" : "traversal_edge",
|
|
1098
1183
|
src,
|
|
@@ -1112,7 +1197,11 @@ function edgeFromRow(row, graphScope, hop, context) {
|
|
|
1112
1197
|
src_labels: srcLabels,
|
|
1113
1198
|
dst_labels: dstLabels,
|
|
1114
1199
|
src_is_exchange: srcIsExchange,
|
|
1115
|
-
dst_is_exchange: dstIsExchange
|
|
1200
|
+
dst_is_exchange: dstIsExchange,
|
|
1201
|
+
...srcAddressType !== void 0 ? { src_address_type: srcAddressType } : {},
|
|
1202
|
+
...dstAddressType !== void 0 ? { dst_address_type: dstAddressType } : {},
|
|
1203
|
+
...srcAddressSubtypes.length > 0 ? { src_address_subtypes: srcAddressSubtypes } : {},
|
|
1204
|
+
...dstAddressSubtypes.length > 0 ? { dst_address_subtypes: dstAddressSubtypes } : {}
|
|
1116
1205
|
};
|
|
1117
1206
|
}
|
|
1118
1207
|
function edgeKey(edge) {
|
|
@@ -1318,6 +1407,126 @@ function labelForSubtype(subtype) {
|
|
|
1318
1407
|
case "exchange_deposit_candidate": return "Scam exchange deposit candidate";
|
|
1319
1408
|
}
|
|
1320
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Per-hop multiplicative decay applied to a candidate's base confidence. Each
|
|
1412
|
+
* additional hop from the seed multiplies confidence by this factor, so deeper
|
|
1413
|
+
* addresses score strictly lower than closer ones at equal value.
|
|
1414
|
+
*/
|
|
1415
|
+
const SCAM_TOPOLOGY_HOP_DECAY = .85;
|
|
1416
|
+
/**
|
|
1417
|
+
* Floor on the hop-decay multiplier so very deep chains do not collapse to
|
|
1418
|
+
* zero; keeps confidence bounded and positive while still ranking deep edges
|
|
1419
|
+
* below shallow ones.
|
|
1420
|
+
*/
|
|
1421
|
+
const SCAM_TOPOLOGY_MIN_HOP_FACTOR = .25;
|
|
1422
|
+
/**
|
|
1423
|
+
* Native/USD value at or above which the value factor saturates to its maximum
|
|
1424
|
+
* contribution. Chosen so a large incident-scale transfer earns full value
|
|
1425
|
+
* weight while small dust transfers earn little.
|
|
1426
|
+
*/
|
|
1427
|
+
const SCAM_TOPOLOGY_VALUE_SATURATION = 1e5;
|
|
1428
|
+
/**
|
|
1429
|
+
* Fraction of confidence governed by carried value (the remainder is the fixed
|
|
1430
|
+
* base). A high-value edge keeps near-base confidence; a dust edge is damped.
|
|
1431
|
+
*/
|
|
1432
|
+
const SCAM_TOPOLOGY_VALUE_WEIGHT = .5;
|
|
1433
|
+
/**
|
|
1434
|
+
* Confidence threshold at or above which a close-hop candidate is auto-promoted
|
|
1435
|
+
* to `promote_confirmed` instead of `review_required`. Tuned so that only a
|
|
1436
|
+
* near-full-value, close-hop core reaches it: a hop-1 edge carrying
|
|
1437
|
+
* incident-scale value retains its full base confidence (victim-seeded base is
|
|
1438
|
+
* 0.72), while dust or deeper edges fall below the bar and stay review-only.
|
|
1439
|
+
*/
|
|
1440
|
+
const SCAM_TOPOLOGY_PROMOTE_CONFIDENCE = .72;
|
|
1441
|
+
/**
|
|
1442
|
+
* Maximum hop distance eligible for auto-promotion. Only the close-hop core of
|
|
1443
|
+
* a topology can promote automatically; the diluted tail stays review-only.
|
|
1444
|
+
*/
|
|
1445
|
+
const SCAM_TOPOLOGY_PROMOTE_MAX_HOP = 2;
|
|
1446
|
+
/**
|
|
1447
|
+
* Choose the value used for confidence scoring.
|
|
1448
|
+
*
|
|
1449
|
+
* Deep-hop `amount_usd_sum` is frequently inconsistent with the native amount
|
|
1450
|
+
* (e.g. hundreds of tokens reported as a few dollars) because price coverage is
|
|
1451
|
+
* missing at depth or the price join is wrong. Trusting such USD would deflate
|
|
1452
|
+
* confidence for genuinely high-value transfers, so the native `amount_sum` is
|
|
1453
|
+
* always preferred when present and positive. USD is used only as a fallback
|
|
1454
|
+
* when no usable native amount exists. Native units are consistent within a
|
|
1455
|
+
* single asset's topology, so value comparisons across edges remain meaningful.
|
|
1456
|
+
*
|
|
1457
|
+
* @param amountSum - Native transferred amount on the edge, if present.
|
|
1458
|
+
* @param amountUsdSum - Reported USD value on the edge, if present.
|
|
1459
|
+
* @returns A positive scoring value, or `undefined` when neither amount is
|
|
1460
|
+
* usable.
|
|
1461
|
+
*/
|
|
1462
|
+
function reliableScoringValue(amountSum, amountUsdSum) {
|
|
1463
|
+
const native = amountSum !== void 0 && Number.isFinite(amountSum) && amountSum > 0 ? amountSum : void 0;
|
|
1464
|
+
if (native !== void 0) return native;
|
|
1465
|
+
return amountUsdSum !== void 0 && Number.isFinite(amountUsdSum) && amountUsdSum > 0 ? amountUsdSum : void 0;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Compute a bounded [0, 1] value factor from a reliable scoring value using a
|
|
1469
|
+
* logarithmic scale, so confidence increases monotonically with carried value
|
|
1470
|
+
* and saturates for incident-scale transfers.
|
|
1471
|
+
*
|
|
1472
|
+
* @param value - A non-negative scoring value, or `undefined`.
|
|
1473
|
+
* @returns A factor in [0, 1]; `0` when no value is available.
|
|
1474
|
+
*/
|
|
1475
|
+
function valueFactor(value) {
|
|
1476
|
+
if (value === void 0 || value <= 0) return 0;
|
|
1477
|
+
return Math.log10(1 + Math.min(value, SCAM_TOPOLOGY_VALUE_SATURATION)) / Math.log10(100001);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Confidence model for a topology edge: a base confidence that decays
|
|
1481
|
+
* multiplicatively with hop distance and scales with carried value.
|
|
1482
|
+
*
|
|
1483
|
+
* The result is `base * hopFactor * (1 - VALUE_WEIGHT + VALUE_WEIGHT * valueFactor)`,
|
|
1484
|
+
* where `hopFactor = max(MIN_HOP_FACTOR, HOP_DECAY^(hop - 1))`. It is bounded in
|
|
1485
|
+
* `(0, base]`, strictly decreasing in `hop`, and increasing in carried value, so
|
|
1486
|
+
* a hop-1 high-value edge always outranks a deeper low-value edge. Carried value
|
|
1487
|
+
* is read from the native amount when available (see {@link reliableScoringValue}),
|
|
1488
|
+
* so unreliable deep-hop USD pricing cannot distort the score.
|
|
1489
|
+
*
|
|
1490
|
+
* @param edge - The topology edge being scored.
|
|
1491
|
+
* @param baseConfidence - The relation-and-role base confidence in `(0, 1]`.
|
|
1492
|
+
* @returns A confidence score in `(0, 1]`.
|
|
1493
|
+
*/
|
|
1494
|
+
function decayedConfidence(edge, baseConfidence) {
|
|
1495
|
+
const hop = Number.isFinite(edge.hop) && edge.hop > 0 ? edge.hop : 1;
|
|
1496
|
+
const hopFactor = Math.max(SCAM_TOPOLOGY_MIN_HOP_FACTOR, Math.pow(SCAM_TOPOLOGY_HOP_DECAY, hop - 1));
|
|
1497
|
+
const value = reliableScoringValue(edge.amount_sum, edge.amount_usd_sum);
|
|
1498
|
+
const valueScale = 1 - SCAM_TOPOLOGY_VALUE_WEIGHT + SCAM_TOPOLOGY_VALUE_WEIGHT * valueFactor(value);
|
|
1499
|
+
const confidence = baseConfidence * hopFactor * valueScale;
|
|
1500
|
+
return Math.min(baseConfidence, Math.max(0, confidence));
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Decide the promotion tier for a scored candidate. Only a close-hop,
|
|
1504
|
+
* high-confidence core auto-promotes to `promote_confirmed`; everything else
|
|
1505
|
+
* stays `review_required` for human triage.
|
|
1506
|
+
*
|
|
1507
|
+
* @param edge - The topology edge backing the candidate.
|
|
1508
|
+
* @param confidence - The candidate's decayed confidence score.
|
|
1509
|
+
* @returns The promotion status for the candidate.
|
|
1510
|
+
*/
|
|
1511
|
+
function promotionTier(edge, confidence) {
|
|
1512
|
+
const hop = Number.isFinite(edge.hop) && edge.hop > 0 ? edge.hop : 1;
|
|
1513
|
+
return confidence >= SCAM_TOPOLOGY_PROMOTE_CONFIDENCE && hop <= SCAM_TOPOLOGY_PROMOTE_MAX_HOP ? "promote_confirmed" : "review_required";
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Build a scam label candidate from a topology edge with hop-and-value decayed
|
|
1517
|
+
* confidence and an automatic promotion tier.
|
|
1518
|
+
*
|
|
1519
|
+
* @param address - The candidate address.
|
|
1520
|
+
* @param subtype - The candidate scam subtype.
|
|
1521
|
+
* @param evidence - Structured evidence for the candidate.
|
|
1522
|
+
* @param edge - The topology edge backing the candidate.
|
|
1523
|
+
* @param baseConfidence - The relation-and-role base confidence in `(0, 1]`.
|
|
1524
|
+
* @returns A scored label candidate.
|
|
1525
|
+
*/
|
|
1526
|
+
function makeScoredCandidate(address, subtype, evidence, edge, baseConfidence) {
|
|
1527
|
+
const confidence = decayedConfidence(edge, baseConfidence);
|
|
1528
|
+
return makeCandidate(address, subtype, evidence, confidence, promotionTier(edge, confidence));
|
|
1529
|
+
}
|
|
1321
1530
|
function makeCandidate(address, subtype, evidence, confidence, promotionStatus) {
|
|
1322
1531
|
return {
|
|
1323
1532
|
address,
|
|
@@ -1429,7 +1638,7 @@ function classifyTopology(seeds, edges) {
|
|
|
1429
1638
|
seed_role: edge.seed_role
|
|
1430
1639
|
});
|
|
1431
1640
|
addRole(rolesByAddress, edge.src, "laundering_intermediate");
|
|
1432
|
-
mergeCandidate(candidates,
|
|
1641
|
+
mergeCandidate(candidates, makeScoredCandidate(edge.src, "laundering_intermediate", edgeEvidence(edge, "Address sends into an exchange-deposit cluster reached from a known scam topology seed."), edge, edge.seed_role === "scammer" ? .78 : .64));
|
|
1433
1642
|
continue;
|
|
1434
1643
|
}
|
|
1435
1644
|
if (edge.relation === "terminal_exchange") {
|
|
@@ -1488,7 +1697,15 @@ function classifyTopology(seeds, edges) {
|
|
|
1488
1697
|
seed_role: edge.seed_role
|
|
1489
1698
|
});
|
|
1490
1699
|
addRole(rolesByAddress, edge.src, "exchange_deposit_candidate");
|
|
1491
|
-
|
|
1700
|
+
if (isSharedExchangeDeposit(edge)) pushSafetyDecision(safetyDecisions, {
|
|
1701
|
+
address: edge.src,
|
|
1702
|
+
decision: "do_not_label_shared_exchange_deposit",
|
|
1703
|
+
reason: "Penultimate address shows shared exchange-deposit throughput (high tx_count or USD volume); treated as exchange-adjacent context, not an automatic scam candidate.",
|
|
1704
|
+
tx_count: edge.tx_count,
|
|
1705
|
+
amount_usd_sum: edge.amount_usd_sum,
|
|
1706
|
+
seed_address: edge.seed_address
|
|
1707
|
+
});
|
|
1708
|
+
else mergeCandidate(candidates, makeScoredCandidate(edge.src, "exchange_deposit_candidate", edgeEvidence(edge, "Address is the penultimate hop before an exchange endpoint."), edge, edge.seed_role === "scammer" ? .8 : .68));
|
|
1492
1709
|
}
|
|
1493
1710
|
continue;
|
|
1494
1711
|
}
|
|
@@ -1533,7 +1750,7 @@ function classifyTopology(seeds, edges) {
|
|
|
1533
1750
|
seed_role: edge.seed_role
|
|
1534
1751
|
});
|
|
1535
1752
|
addRole(rolesByAddress, edge.dst, "laundering_intermediate");
|
|
1536
|
-
mergeCandidate(candidates,
|
|
1753
|
+
mergeCandidate(candidates, makeScoredCandidate(edge.dst, "laundering_intermediate", edgeEvidence(edge, "Address appears on an outward path from a known scam topology seed."), edge, edge.seed_role === "scammer" ? .85 : .72));
|
|
1537
1754
|
}
|
|
1538
1755
|
return {
|
|
1539
1756
|
labelCandidates: [...candidates.values()].sort((a, b) => b.confidence_score - a.confidence_score || a.address.localeCompare(b.address)),
|