@chainpatrol/mcp 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4468 @@
1
+ // src/prompts.ts
2
+ var ORG_ARGUMENT = {
3
+ name: "org",
4
+ description: "Organization slug to run against, e.g. `morpho`.",
5
+ required: true
6
+ };
7
+ var PROMPTS = [
8
+ {
9
+ name: "org_healthcheck",
10
+ title: "Organization healthcheck",
11
+ description: "Audit one organization across the whole pipeline \u2014 detection, reviewing, blocklisting, takedowns \u2014 and report what needs attention.",
12
+ arguments: [ORG_ARGUMENT],
13
+ tools: [
14
+ "healthchecks_list",
15
+ "detection_configs_list",
16
+ "operations_queues_snapshot",
17
+ "organization_reports_list",
18
+ "metrics_breakdown"
19
+ ],
20
+ template: `Run a full healthcheck on the ChainPatrol organization \`{{org}}\`, covering the pipeline end to end: detection -> reviewing -> blocklisting -> takedowns.
21
+
22
+ **How to run it**
23
+
24
+ 1. Call \`healthchecks_list\` first. It returns every check the platform exposes, which are implemented, and each one's default thresholds. Run every implemented check \u2014 they are independent, so fire them concurrently. The list describes the platform, not this session: if a check has no matching tool available to you, skip it and say so in the summary rather than reporting it as a failure.
25
+ 2. Alongside those, gather the qualitative signals the automated checks do not cover:
26
+ - \`detection_configs_list\` \u2014 what is enabled, disabled, or not configured at all.
27
+ - \`operations_queues_snapshot\` \u2014 the raw review and takedown queue counts.
28
+ - \`organization_reports_list\` with \`reportedByCustomer: true\` \u2014 threats the customer found that detection missed. Each one is a detection gap worth naming.
29
+ - \`metrics_breakdown\` by day \u2014 volume shifts that no single threshold catches.
30
+
31
+ **How to report it**
32
+
33
+ Narrate as you go; do not batch everything to the end. Before each check, one short sentence saying what you are checking. As soon as it returns, one line starting with a status word and ending in the number that matters:
34
+
35
+ - \`DONE\` \u2014 ran cleanly, nothing to flag
36
+ - \`WARN\` \u2014 a soft signal worth a human's eye
37
+ - \`FAIL\` \u2014 a concrete failure to act on
38
+
39
+ Then close with a Summary: one line per check, followed by a "Top issues" list in priority order, each with its concrete next action.`
40
+ },
41
+ {
42
+ name: "trend_search",
43
+ title: "Trend search",
44
+ description: "Compare a recent window against a baseline for one organization to surface spikes \u2014 new attack channels, campaigns, or targeting \u2014 before they become healthcheck failures.",
45
+ arguments: [
46
+ ORG_ARGUMENT,
47
+ {
48
+ name: "days",
49
+ description: "Length of the current window in days. Defaults to 7.",
50
+ required: false
51
+ }
52
+ ],
53
+ tools: ["metrics_breakdown", "metrics_found", "organization_brands_list"],
54
+ template: `Search for trends in the ChainPatrol organization \`{{org}}\` over the last {{days}} days.
55
+
56
+ Trend search asks what has *changed*, so it compares rates rather than grading absolute counts against thresholds. A trend is worth reporting even when the pipeline is keeping up with it.
57
+
58
+ **Windows**
59
+
60
+ Use two non-overlapping windows: a current window of {{days}} days, and a baseline of the prior 90 days ending the day before the current window starts. For every bucket:
61
+
62
+ current_per_day = current_count / current_window_days
63
+ baseline_per_day = baseline_count / baseline_window_days
64
+ ratio = current_per_day / max(baseline_per_day, epsilon)
65
+
66
+ Flag a bucket when \`ratio >= 2\` **and** \`current_count >= 5\`. The floor keeps a single new report on a quiet organization from reading as a spike.
67
+
68
+ **Run these three concurrently**
69
+
70
+ 1. **By asset type** \u2014 \`metrics_breakdown\` for both windows, broken out by asset type. Catches a new attack channel.
71
+ 2. **Overall volume** \u2014 \`metrics_found\` daily for roughly six weeks, so the baseline and the spike are visible in one series.
72
+ 3. **By sub-brand** \u2014 \`metrics_breakdown\` for both windows by brand, and \`organization_brands_list\` to name them. Employee brands spiking usually means targeted impersonation.
73
+
74
+ **Report**
75
+
76
+ For each flagged bucket: the bucket, current vs. baseline per-day rate, the ratio, and the absolute count. Then say what it most likely means and what to do about it. If nothing clears both thresholds, say so plainly and give the largest ratio you saw.`
77
+ },
78
+ {
79
+ name: "cs_weekly_health",
80
+ title: "Weekly customer-success sweep",
81
+ description: "The packaged weekly customer health review: threats found, summary metrics, and whether the detectors are actually producing.",
82
+ arguments: [ORG_ARGUMENT],
83
+ tools: ["metrics_summary", "metrics_found", "detection_configs_validate"],
84
+ template: `Produce the weekly customer-success health package for \`{{org}}\`, covering the current week to date.
85
+
86
+ Gather concurrently:
87
+
88
+ 1. \`metrics_summary\` for the week \u2014 the headline numbers.
89
+ 2. \`metrics_found\` for the week \u2014 what was discovered, and through which channel.
90
+ 3. \`detection_configs_validate\` over a 168-hour lookback with a minimum of 1 result \u2014 which detectors produced nothing and are therefore silently broken.
91
+
92
+ Report the headline metrics first, then any detector that failed validation, and finish with the one thing most worth a human's attention this week. If every detector passed and the metrics look ordinary, say that in a sentence rather than padding the report.`
93
+ }
94
+ ];
95
+ function renderPrompt(prompt, args) {
96
+ const defaults = { days: "7" };
97
+ return prompt.template.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
98
+ const provided = args[name];
99
+ if (typeof provided === "string" && provided !== "") return provided;
100
+ if (typeof provided === "number" || typeof provided === "boolean") {
101
+ return provided.toString();
102
+ }
103
+ const fallback = defaults[name];
104
+ if (fallback !== void 0) return fallback;
105
+ const argument = prompt.arguments.find((item) => item.name === name);
106
+ if (argument?.required) {
107
+ throw new Error(`Prompt "${prompt.name}" requires the "${name}" argument.`);
108
+ }
109
+ return "";
110
+ });
111
+ }
112
+ function selectPrompts(prompts, available) {
113
+ const exposed = new Set(available);
114
+ return prompts.filter((prompt) => prompt.tools.every((tool) => exposed.has(tool)));
115
+ }
116
+
117
+ // src/resources.ts
118
+ function buildResources(manifest2, tools = manifest2.tools) {
119
+ const exposed = new Set(tools.map((tool) => tool.name));
120
+ const resources = [];
121
+ for (const definition of manifest2.enums) {
122
+ const usedBy = definition.usedBy.filter((site) => exposed.has(toolNameOf(site)));
123
+ if (usedBy.length === 0) continue;
124
+ resources.push({
125
+ uri: `chainpatrol://enums/${definition.name}`,
126
+ name: definition.name,
127
+ title: `Accepted values: ${definition.name}`,
128
+ description: `Every value accepted for ${definition.name} (${definition.values.length} total). Referenced by ${usedBy.join(", ")}.`,
129
+ mimeType: "text/plain",
130
+ text: definition.values.join("\n")
131
+ });
132
+ }
133
+ for (const glossary of manifest2.glossaries) {
134
+ if (!exposed.has(toolNameOf(glossary.title))) continue;
135
+ const name = glossary.uri.split("/").pop() ?? glossary.uri;
136
+ resources.push({
137
+ uri: glossary.uri,
138
+ name,
139
+ title: `Guide: ${name.replace(/-/g, " ")}`,
140
+ description: `What each value means, for ${glossary.title}.`,
141
+ mimeType: "text/markdown",
142
+ text: glossary.text
143
+ });
144
+ }
145
+ resources.sort((a, b) => a.uri.localeCompare(b.uri));
146
+ return resources;
147
+ }
148
+ function toolNameOf(site) {
149
+ const dot = site.indexOf(".");
150
+ return dot === -1 ? site : site.slice(0, dot);
151
+ }
152
+
153
+ // src/invoker.ts
154
+ var ToolInvocationError = class extends Error {
155
+ status;
156
+ details;
157
+ constructor(message, options) {
158
+ super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
159
+ this.name = "ToolInvocationError";
160
+ this.status = options?.status;
161
+ this.details = options?.details;
162
+ }
163
+ };
164
+
165
+ // src/server.ts
166
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
167
+ import {
168
+ CallToolRequestSchema,
169
+ GetPromptRequestSchema,
170
+ ListPromptsRequestSchema,
171
+ ListResourcesRequestSchema,
172
+ ListToolsRequestSchema,
173
+ ReadResourceRequestSchema
174
+ } from "@modelcontextprotocol/sdk/types.js";
175
+
176
+ // src/generated/tools.json
177
+ var tools_default = {
178
+ manifestVersion: 1,
179
+ tools: [
180
+ {
181
+ name: "asset_changelog",
182
+ procedure: "assetChangelog",
183
+ method: "POST",
184
+ path: "/asset/changelog",
185
+ pathParams: [],
186
+ title: "Asset changelog",
187
+ description: "Asset changelog. Assets can be changed for various reasons, such as false positives, false negatives or normal reviews.",
188
+ tags: [
189
+ "asset"
190
+ ],
191
+ readOnly: true,
192
+ deprecated: false,
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ type: {
197
+ type: "string",
198
+ description: "Asset type One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
199
+ },
200
+ content: {
201
+ type: "string",
202
+ description: "Asset content"
203
+ },
204
+ fromStatus: {
205
+ type: "string",
206
+ enum: [
207
+ "UNKNOWN",
208
+ "ALLOWED",
209
+ "BLOCKED"
210
+ ],
211
+ description: "Status of the changed assets to retrieve"
212
+ },
213
+ toStatus: {
214
+ type: "string",
215
+ enum: [
216
+ "UNKNOWN",
217
+ "ALLOWED",
218
+ "BLOCKED"
219
+ ],
220
+ description: "Status of the changed assets to retrieve"
221
+ },
222
+ startDate: {
223
+ allOf: [
224
+ {
225
+ type: "string",
226
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
227
+ },
228
+ {
229
+ type: "string",
230
+ format: "date-time"
231
+ }
232
+ ],
233
+ description: "The start date to list items from. This should be in the format `YYYY-MM-DD` and is inclusive."
234
+ },
235
+ endDate: {
236
+ allOf: [
237
+ {
238
+ type: "string",
239
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
240
+ },
241
+ {
242
+ type: "string",
243
+ format: "date-time"
244
+ }
245
+ ],
246
+ description: "The end date to list items from. This should be in the format `YYYY-MM-DD` and is inclusive."
247
+ }
248
+ },
249
+ description: "List asset request body\n\nDefaults to getting all the updates in the last 1 day.\n\nYou can also choose a `startDate` and `endDate` for the range of asset updates, most \ntimestamp formats should work, we use [Luxon](https://moment.github.io/luxon/#/parsing) \nfor parsing the dates."
250
+ }
251
+ },
252
+ {
253
+ name: "asset_check",
254
+ procedure: "assetCheck",
255
+ method: "POST",
256
+ path: "/asset/check",
257
+ pathParams: [],
258
+ title: "Check asset",
259
+ description: "Answers 'is this asset known to be malicious?'. Takes one piece of content \u2014 a URL, domain, social handle, app listing, or wallet address \u2014 and returns its status (`BLOCKED`, `ALLOWED`, or `UNKNOWN`), the source that decided it, the reason, and the per-source breakdown. Use it when you have a specific asset in hand. To search the blocklist by pattern instead, use `/asset/search`; to find whether a report already exists for an asset, use `/reports/search`.",
260
+ tags: [
261
+ "asset"
262
+ ],
263
+ readOnly: true,
264
+ deprecated: false,
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ content: {
269
+ type: "string",
270
+ description: "Asset content. Could be a domain, URL, or crypto address."
271
+ }
272
+ },
273
+ required: [
274
+ "content"
275
+ ],
276
+ description: "Check asset request body"
277
+ }
278
+ },
279
+ {
280
+ name: "asset_details",
281
+ procedure: "assetDetails",
282
+ method: "POST",
283
+ path: "/asset/details",
284
+ pathParams: [],
285
+ title: "Get Asset Details",
286
+ description: "Deprecated \u2014 prefer `/asset/check`, which returns the same status plus the deciding source and per-source breakdown. Answers 'what is this asset's status, and which report set it?'. Returns the status, the reason (`report` for a ChainPatrol report, `eth-phishing-detect` for that list), and the id and link of the report behind the latest status change.",
287
+ tags: [
288
+ "asset"
289
+ ],
290
+ readOnly: true,
291
+ deprecated: true,
292
+ inputSchema: {
293
+ type: "object",
294
+ properties: {
295
+ content: {
296
+ type: "string",
297
+ description: "Asset content"
298
+ }
299
+ },
300
+ required: [
301
+ "content"
302
+ ],
303
+ description: "Get asset details request body"
304
+ }
305
+ },
306
+ {
307
+ name: "asset_list",
308
+ procedure: "assetList",
309
+ method: "POST",
310
+ path: "/asset/list",
311
+ pathParams: [],
312
+ title: "List assets",
313
+ description: "Answers 'which assets of this type does ChainPatrol hold at this status?'. Returns a paginated list for one asset type, defaulting to `BLOCKED`, optionally narrowed to a date range. Built for bulk export and sync \u2014 pass `next_page` from the previous response to continue. To check one specific asset, use `/asset/check` instead; for an organization's own monitored assets, use `/organization/assets`.",
314
+ tags: [
315
+ "asset"
316
+ ],
317
+ readOnly: true,
318
+ deprecated: false,
319
+ inputSchema: {
320
+ type: "object",
321
+ properties: {
322
+ type: {
323
+ type: "string",
324
+ description: "Asset type One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
325
+ },
326
+ status: {
327
+ type: "string",
328
+ enum: [
329
+ "UNKNOWN",
330
+ "ALLOWED",
331
+ "BLOCKED"
332
+ ],
333
+ default: "BLOCKED",
334
+ description: "Status of the assets to retrieve"
335
+ },
336
+ startDate: {
337
+ type: "string",
338
+ description: "The start date to list assets from. This should be in the format `YYYY-MM-DD` and is inclusive."
339
+ },
340
+ endDate: {
341
+ type: "string",
342
+ description: "The end date to list assets from. This should be in the format `YYYY-MM-DD` and is inclusive."
343
+ },
344
+ per_page: {
345
+ type: "integer",
346
+ minimum: 1,
347
+ maximum: 1e4,
348
+ default: 100,
349
+ description: "The number of assets to return per page"
350
+ },
351
+ next_page: {
352
+ anyOf: [
353
+ {
354
+ anyOf: [
355
+ {
356
+ not: {}
357
+ },
358
+ {
359
+ type: "string"
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ type: "null"
365
+ }
366
+ ],
367
+ description: "Cursor for fetching the next page of results"
368
+ }
369
+ },
370
+ required: [
371
+ "type"
372
+ ],
373
+ description: "List asset request body\n\nDefaults to getting all the updates in the last 1 day.\n\nYou can also choose a `startDate` and `endDate` for the range of asset updates, most \ntimestamp formats should work, we use [Luxon](https://moment.github.io/luxon/#/parsing) \nfor parsing the dates."
374
+ }
375
+ },
376
+ {
377
+ name: "asset_parse",
378
+ procedure: "assetParse",
379
+ method: "POST",
380
+ path: "/asset/parse",
381
+ pathParams: [],
382
+ title: "Parse Asset",
383
+ description: "Parse and normalize asset content to extract structured information",
384
+ tags: [
385
+ "asset"
386
+ ],
387
+ readOnly: true,
388
+ deprecated: false,
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ content: {
393
+ type: "string",
394
+ minLength: 1,
395
+ description: "The asset content to parse (URL, address, etc.)"
396
+ }
397
+ },
398
+ required: [
399
+ "content"
400
+ ],
401
+ description: "Parse asset request body"
402
+ }
403
+ },
404
+ {
405
+ name: "asset_scan_live",
406
+ procedure: "assetScanLive",
407
+ method: "POST",
408
+ path: "/asset/scan",
409
+ pathParams: [],
410
+ title: "Start an asset scan with optional webhook callback",
411
+ description: "Initiate an asset scan. Optionally provide an organization slug for brand impersonation detection and a callback URL to receive results when the scan completes.",
412
+ tags: [
413
+ "asset"
414
+ ],
415
+ readOnly: false,
416
+ deprecated: false,
417
+ inputSchema: {
418
+ type: "object",
419
+ properties: {
420
+ asset: {
421
+ type: "string",
422
+ description: "The asset URL or content to scan"
423
+ },
424
+ organizationSlug: {
425
+ type: "string",
426
+ description: "Optional: Org slug for brand impersonation & visual similarity rules"
427
+ },
428
+ mode: {
429
+ type: "string",
430
+ enum: [
431
+ "fast",
432
+ "full"
433
+ ],
434
+ default: "full",
435
+ description: "Scan mode"
436
+ },
437
+ callbackUrl: {
438
+ type: "string",
439
+ format: "uri",
440
+ description: "HTTPS URL to receive scan results when complete"
441
+ }
442
+ },
443
+ required: [
444
+ "asset"
445
+ ]
446
+ }
447
+ },
448
+ {
449
+ name: "asset_search",
450
+ procedure: "assetSearch",
451
+ method: "POST",
452
+ path: "/asset/search",
453
+ pathParams: [],
454
+ title: "Search asset",
455
+ description: "Search for an asset by content or ID and get its status, associated reports, and takedown details",
456
+ tags: [
457
+ "asset"
458
+ ],
459
+ readOnly: true,
460
+ deprecated: false,
461
+ inputSchema: {
462
+ type: "object",
463
+ properties: {
464
+ content: {
465
+ type: "string",
466
+ description: "Asset content to search for (URL, domain, address, etc.)"
467
+ },
468
+ assetId: {
469
+ type: "integer",
470
+ description: "Asset ID to search for"
471
+ },
472
+ assetIds: {
473
+ type: "array",
474
+ items: {
475
+ type: "integer"
476
+ },
477
+ description: "Multiple asset IDs to search for in bulk"
478
+ }
479
+ },
480
+ description: "Asset search request body"
481
+ }
482
+ },
483
+ {
484
+ name: "asset_submit",
485
+ procedure: "assetSubmit",
486
+ method: "POST",
487
+ path: "/asset/submit",
488
+ pathParams: [],
489
+ title: "Submit assets to process and assign to organization(s)",
490
+ description: "Submit a list of assets such as domains, social profiles, or blockchain addresses. Automation will classify and assign them to appropriate organization(s).",
491
+ tags: [
492
+ "asset"
493
+ ],
494
+ readOnly: false,
495
+ deprecated: false,
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ assets: {
500
+ type: "array",
501
+ items: {
502
+ type: "string"
503
+ },
504
+ maxItems: 100,
505
+ description: "List of assets to classify, such as domains, social profiles, or blockchain addresses."
506
+ },
507
+ organizationSlug: {
508
+ type: "string",
509
+ description: "Organization slug to classify assets for. If not provided, the classification will be done for all organizations."
510
+ }
511
+ },
512
+ required: [
513
+ "assets"
514
+ ]
515
+ }
516
+ },
517
+ {
518
+ name: "asset_unblock",
519
+ procedure: "assetUnblock",
520
+ method: "POST",
521
+ path: "/asset/unblock",
522
+ pathParams: [],
523
+ title: "Unblock an asset",
524
+ description: "Unblock an asset if it belongs to your organization, or create a dispute if it doesn't. Requires API key.",
525
+ tags: [
526
+ "asset"
527
+ ],
528
+ readOnly: false,
529
+ deprecated: false,
530
+ inputSchema: {
531
+ type: "object",
532
+ properties: {
533
+ content: {
534
+ type: "string",
535
+ minLength: 1
536
+ },
537
+ reason: {
538
+ type: "string"
539
+ }
540
+ },
541
+ required: [
542
+ "content"
543
+ ]
544
+ }
545
+ },
546
+ {
547
+ name: "blocklist_confirm",
548
+ procedure: "blocklistConfirm",
549
+ method: "POST",
550
+ path: "/asset/blocklist/confirm",
551
+ pathParams: [],
552
+ title: "Confirm blocklist prefix matches",
553
+ description: "Given a list of 4-byte (8-hex-char) hash prefixes and the algorithm version, returns the full SHA-256 hashes of blocked expressions whose prefixes match, grouped by prefix. Used by the browser extension to resolve true positives after a local prefix hit.",
554
+ tags: [
555
+ "asset"
556
+ ],
557
+ readOnly: false,
558
+ deprecated: false,
559
+ inputSchema: {
560
+ type: "object",
561
+ properties: {
562
+ algoVersion: {
563
+ type: "integer",
564
+ exclusiveMinimum: 0
565
+ },
566
+ prefixes: {
567
+ type: "array",
568
+ items: {
569
+ type: "string",
570
+ pattern: "^[0-9a-f]{8}$"
571
+ },
572
+ minItems: 1,
573
+ maxItems: 200
574
+ }
575
+ },
576
+ required: [
577
+ "algoVersion",
578
+ "prefixes"
579
+ ],
580
+ description: "Blocklist confirm request"
581
+ }
582
+ },
583
+ {
584
+ name: "blocklist_version",
585
+ procedure: "blocklistVersion",
586
+ method: "GET",
587
+ path: "/asset/blocklist/version",
588
+ pathParams: [],
589
+ title: "Get blocklist version",
590
+ description: "Returns the current blocklist snapshot and recent-additions feed metadata (tokens, counts, absolute CDN URLs) and the canonicalization algorithm version. Used by the browser extension to discover and cache blocklist artifacts.",
591
+ tags: [
592
+ "asset"
593
+ ],
594
+ readOnly: true,
595
+ deprecated: false,
596
+ acceptsNoInput: true,
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {}
600
+ }
601
+ },
602
+ {
603
+ name: "detection_configs_create",
604
+ procedure: "detectionConfigsCreate",
605
+ method: "POST",
606
+ path: "/detection/configs/create",
607
+ pathParams: [],
608
+ title: "Create threat detection config",
609
+ description: "Create a config that makes a detection source run for an organization. Sources that are disabled by default have no config, so they cannot be enabled through /detection/configs/update until one is created. A source can have several configs, each with its own query, schedule or brand.",
610
+ tags: [
611
+ "detection"
612
+ ],
613
+ readOnly: false,
614
+ deprecated: false,
615
+ inputSchema: {
616
+ type: "object",
617
+ properties: {
618
+ slug: {
619
+ type: "string",
620
+ minLength: 1,
621
+ description: "Organization slug. Defaults to the organization your API key is scoped to, so you only need this when authenticating with a key that spans organizations."
622
+ },
623
+ source: {
624
+ type: "string",
625
+ description: "Detection source key, from `GET /detection/sources`. The source must support the `organization` scope; global-only sources cannot be configured per organization. One of 55 values, e.g. meta_ads_search, telegram_channels_search, telegram_user_search, telegram_channels_search_vetric, telegram_user_search_vetric, facebook_page_search_vetric, facebook_user_search_vetric, instagram_account_search_vetric. Full list: resource chainpatrol://enums/source"
626
+ },
627
+ status: {
628
+ type: "string",
629
+ enum: [
630
+ "ENABLED",
631
+ "DEPRECATED_EVALUATE",
632
+ "DISABLED"
633
+ ],
634
+ default: "ENABLED",
635
+ description: "Status to create the config with. Defaults to `ENABLED`."
636
+ },
637
+ title: {
638
+ type: "string",
639
+ description: "Optional label for this config"
640
+ },
641
+ description: {
642
+ anyOf: [
643
+ {
644
+ type: "string"
645
+ },
646
+ {
647
+ type: "null"
648
+ }
649
+ ],
650
+ description: "Optional human-readable description of what the config does"
651
+ },
652
+ cron: {
653
+ anyOf: [
654
+ {
655
+ type: "string"
656
+ },
657
+ {
658
+ type: "null"
659
+ }
660
+ ],
661
+ description: "Optional custom CRON schedule. Only accepted for sources that run on a schedule; the source's default schedule is used when omitted."
662
+ },
663
+ config: {
664
+ type: "object",
665
+ additionalProperties: {},
666
+ description: "Source-specific configuration, validated against that source's `configSchema` from `GET /detection/sources`. When omitted the source's schema defaults are applied."
667
+ },
668
+ brandId: {
669
+ anyOf: [
670
+ {
671
+ type: "integer",
672
+ exclusiveMinimum: 0
673
+ },
674
+ {
675
+ type: "null"
676
+ }
677
+ ],
678
+ description: "Optional brand to scope this config to. The brand must belong to the same organization. Omit for an organization-wide config."
679
+ }
680
+ },
681
+ required: [
682
+ "source"
683
+ ],
684
+ description: "Create a threat detection config\n\nCreates the config row that makes a detection source run for your organization. Sources that are disabled by default have no config row, so they cannot be enabled through `/detection/configs/update` until one exists.\n\nA source can have several configs, each scanning with its own query, schedule or brand. Use `title` to tell them apart."
685
+ }
686
+ },
687
+ {
688
+ name: "detection_configs_delete",
689
+ procedure: "detectionConfigsDelete",
690
+ method: "POST",
691
+ path: "/detection/configs/delete",
692
+ pathParams: [],
693
+ title: "Delete threat detection config",
694
+ description: "Delete one detection config so it stops scanning. Other configs sharing the same source keep running. Deleting the last config for a source that is enabled by default is not permanent, since automation re-creates it; disable it through /detection/configs/update instead.",
695
+ tags: [
696
+ "detection"
697
+ ],
698
+ readOnly: false,
699
+ deprecated: false,
700
+ inputSchema: {
701
+ type: "object",
702
+ properties: {
703
+ slug: {
704
+ type: "string",
705
+ minLength: 1,
706
+ description: "Organization slug. Defaults to the organization your API key is scoped to, so you only need this when authenticating with a key that spans organizations."
707
+ },
708
+ configId: {
709
+ type: "integer",
710
+ exclusiveMinimum: 0,
711
+ description: "ID of the config to delete, from `/detection/configs/list`"
712
+ }
713
+ },
714
+ required: [
715
+ "configId"
716
+ ],
717
+ description: "Delete a threat detection config\n\nDeletes one detection config so it stops scanning. Deleting a config that shares a source with others leaves the others running.\n\nDeleting the *last* config for a source that is enabled by default for your organization is not permanent: automation re-creates a default config for that source on its next run. To stop such a source durably, set `status` to `DISABLED` through `/detection/configs/update` instead, which automation will not override."
718
+ }
719
+ },
720
+ {
721
+ name: "detection_configs_list",
722
+ procedure: "detectionConfigsList",
723
+ method: "POST",
724
+ path: "/detection/configs/list",
725
+ pathParams: [],
726
+ title: "List threat detection configs for organization",
727
+ description: "List all threat detection sources for an organization. Each source includes its individual configurations with details like schedule, status, and source-specific settings.",
728
+ tags: [
729
+ "detection"
730
+ ],
731
+ readOnly: true,
732
+ deprecated: false,
733
+ inputSchema: {
734
+ type: "object",
735
+ properties: {
736
+ slug: {
737
+ type: "string",
738
+ description: "Organization slug"
739
+ }
740
+ },
741
+ required: [
742
+ "slug"
743
+ ]
744
+ }
745
+ },
746
+ {
747
+ name: "detection_configs_run",
748
+ procedure: "detectionConfigsRun",
749
+ method: "POST",
750
+ path: "/detection/configs/run",
751
+ pathParams: [],
752
+ title: "Run detection configs on demand",
753
+ description: "Run one or many organization detection configs and return run status per config.",
754
+ tags: [
755
+ "detection"
756
+ ],
757
+ readOnly: false,
758
+ deprecated: false,
759
+ inputSchema: {
760
+ type: "object",
761
+ properties: {
762
+ slug: {
763
+ type: "string",
764
+ minLength: 1
765
+ },
766
+ configId: {
767
+ type: "integer",
768
+ exclusiveMinimum: 0
769
+ },
770
+ source: {
771
+ type: "string",
772
+ minLength: 1
773
+ },
774
+ includeDisabled: {
775
+ type: "boolean",
776
+ default: false
777
+ }
778
+ },
779
+ required: [
780
+ "slug"
781
+ ]
782
+ }
783
+ },
784
+ {
785
+ name: "detection_configs_update",
786
+ procedure: "detectionConfigsUpdate",
787
+ method: "POST",
788
+ path: "/detection/configs/update",
789
+ pathParams: [],
790
+ title: "Update threat detection config",
791
+ description: "Update an organization threat detection config status, schedule, and config fields.",
792
+ tags: [
793
+ "detection"
794
+ ],
795
+ readOnly: false,
796
+ deprecated: false,
797
+ inputSchema: {
798
+ type: "object",
799
+ properties: {
800
+ slug: {
801
+ type: "string",
802
+ minLength: 1
803
+ },
804
+ configId: {
805
+ type: "integer",
806
+ exclusiveMinimum: 0
807
+ },
808
+ status: {
809
+ type: "string",
810
+ enum: [
811
+ "ENABLED",
812
+ "DEPRECATED_EVALUATE",
813
+ "DISABLED"
814
+ ]
815
+ },
816
+ title: {
817
+ type: "string"
818
+ },
819
+ description: {
820
+ anyOf: [
821
+ {
822
+ type: "string"
823
+ },
824
+ {
825
+ type: "null"
826
+ }
827
+ ]
828
+ },
829
+ cron: {
830
+ anyOf: [
831
+ {
832
+ type: "string"
833
+ },
834
+ {
835
+ type: "null"
836
+ }
837
+ ]
838
+ },
839
+ config: {
840
+ type: "object",
841
+ additionalProperties: {}
842
+ },
843
+ mergeConfig: {
844
+ type: "boolean",
845
+ default: false
846
+ },
847
+ brandId: {
848
+ anyOf: [
849
+ {
850
+ type: "integer",
851
+ exclusiveMinimum: 0
852
+ },
853
+ {
854
+ type: "null"
855
+ }
856
+ ],
857
+ description: "Optional brand connection for this detection config. Pass null to clear the link. Only valid for org-scoped configs; the brand must belong to the same organization."
858
+ }
859
+ },
860
+ required: [
861
+ "slug",
862
+ "configId"
863
+ ]
864
+ }
865
+ },
866
+ {
867
+ name: "detection_configs_validate",
868
+ procedure: "detectionConfigsValidate",
869
+ method: "POST",
870
+ path: "/detection/configs/validate",
871
+ pathParams: [],
872
+ title: "Validate detection config health",
873
+ description: "Validate detection configs by checking recent detection results, optionally running each config first.",
874
+ tags: [
875
+ "detection"
876
+ ],
877
+ readOnly: true,
878
+ deprecated: false,
879
+ inputSchema: {
880
+ type: "object",
881
+ properties: {
882
+ slug: {
883
+ type: "string",
884
+ minLength: 1
885
+ },
886
+ source: {
887
+ type: "string"
888
+ },
889
+ minResults: {
890
+ type: "integer",
891
+ minimum: 0,
892
+ default: 1
893
+ },
894
+ lookbackHours: {
895
+ type: "integer",
896
+ minimum: 1,
897
+ default: 168
898
+ },
899
+ runBeforeValidate: {
900
+ type: "boolean",
901
+ default: false
902
+ },
903
+ includeDisabled: {
904
+ type: "boolean",
905
+ default: false
906
+ }
907
+ },
908
+ required: [
909
+ "slug"
910
+ ]
911
+ }
912
+ },
913
+ {
914
+ name: "detection_drift",
915
+ procedure: "detectionDrift",
916
+ method: "POST",
917
+ path: "/detection/drift",
918
+ pathParams: [],
919
+ title: "Get detection drift signals",
920
+ description: "Analyze organization detection configs and surface zero-result, noisy-source, and stale-query drift signals.",
921
+ tags: [
922
+ "detection"
923
+ ],
924
+ readOnly: true,
925
+ deprecated: false,
926
+ inputSchema: {
927
+ type: "object",
928
+ properties: {
929
+ slug: {
930
+ type: "string",
931
+ minLength: 1
932
+ },
933
+ lookbackHours: {
934
+ type: "integer",
935
+ exclusiveMinimum: 0,
936
+ default: 168
937
+ },
938
+ startDate: {
939
+ type: "string",
940
+ format: "date-time"
941
+ },
942
+ endDate: {
943
+ type: "string",
944
+ format: "date-time"
945
+ },
946
+ source: {
947
+ type: "string",
948
+ minLength: 1
949
+ },
950
+ configIds: {
951
+ type: "array",
952
+ items: {
953
+ type: "integer",
954
+ exclusiveMinimum: 0
955
+ }
956
+ },
957
+ includeDisabled: {
958
+ type: "boolean",
959
+ default: false
960
+ },
961
+ thresholds: {
962
+ type: "object",
963
+ properties: {
964
+ zeroResultsMaxHours: {
965
+ type: "integer",
966
+ exclusiveMinimum: 0,
967
+ default: 72
968
+ },
969
+ noisyResultsPerDay: {
970
+ type: "number",
971
+ exclusiveMinimum: 0,
972
+ default: 100
973
+ },
974
+ noisyAllowedRatioThreshold: {
975
+ type: "number",
976
+ minimum: 0,
977
+ maximum: 1,
978
+ default: 0.6
979
+ },
980
+ staleConfigDays: {
981
+ type: "integer",
982
+ exclusiveMinimum: 0,
983
+ default: 30
984
+ }
985
+ }
986
+ }
987
+ },
988
+ required: [
989
+ "slug"
990
+ ]
991
+ }
992
+ },
993
+ {
994
+ name: "detection_list",
995
+ procedure: "detectionList",
996
+ method: "POST",
997
+ path: "/detection/list",
998
+ pathParams: [],
999
+ title: "List threat detection results for organization",
1000
+ description: "List threat detection results for an organization using API key authentication. Returns human-readable confidence levels (none, low, medium, high) and report status. Supports filtering by source, confidence level, asset status, and asset type. Includes pagination and search capabilities.",
1001
+ tags: [
1002
+ "detection"
1003
+ ],
1004
+ readOnly: true,
1005
+ deprecated: false,
1006
+ inputSchema: {
1007
+ type: "object",
1008
+ properties: {
1009
+ slug: {
1010
+ type: "string",
1011
+ description: "Organization slug"
1012
+ },
1013
+ cursor: {
1014
+ type: "number",
1015
+ description: "Cursor for pagination"
1016
+ },
1017
+ limit: {
1018
+ type: "number",
1019
+ minimum: 1,
1020
+ maximum: 100,
1021
+ default: 50,
1022
+ description: "Number of results to return"
1023
+ },
1024
+ filters: {
1025
+ type: "array",
1026
+ description: "Filters to apply to the results. Each clause filters on one property. Accepted values per property \u2014 `source`: One of 55 values, e.g. meta_ads_search, telegram_channels_search, telegram_user_search, telegram_channels_search_vetric, telegram_user_search_vetric, facebook_page_search_vetric, facebook_user_search_vetric, instagram_account_search_vetric. Full list: resource chainpatrol://enums/source; `confidence`: none, low, medium, high; `liveness`: UNKNOWN, ALIVE, DEAD; `watchlist`: ENABLED, DISABLED; `assetStatus`: UNKNOWN, ALLOWED, BLOCKED; `reported`: reported, not_reported; `assetType`: One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type; `brand`: number values; `deleted`: string values; `countryCode`: string values.",
1027
+ items: {
1028
+ type: "object",
1029
+ properties: {
1030
+ property: {
1031
+ type: "string",
1032
+ enum: [
1033
+ "source",
1034
+ "confidence",
1035
+ "liveness",
1036
+ "watchlist",
1037
+ "assetStatus",
1038
+ "reported",
1039
+ "assetType",
1040
+ "brand",
1041
+ "deleted",
1042
+ "countryCode"
1043
+ ]
1044
+ },
1045
+ operator: {
1046
+ type: "string",
1047
+ enum: [
1048
+ "in",
1049
+ "notIn"
1050
+ ]
1051
+ },
1052
+ value: {
1053
+ type: "array",
1054
+ items: {
1055
+ anyOf: [
1056
+ {
1057
+ type: "number"
1058
+ },
1059
+ {
1060
+ type: "string"
1061
+ }
1062
+ ]
1063
+ }
1064
+ }
1065
+ },
1066
+ required: [
1067
+ "property",
1068
+ "operator",
1069
+ "value"
1070
+ ]
1071
+ }
1072
+ },
1073
+ query: {
1074
+ type: "string",
1075
+ default: "",
1076
+ description: "Search query for threat content"
1077
+ },
1078
+ startDate: {
1079
+ type: "string",
1080
+ format: "date-time",
1081
+ description: "Start date for filtering results"
1082
+ },
1083
+ endDate: {
1084
+ type: "string",
1085
+ format: "date-time",
1086
+ description: "End date for filtering results"
1087
+ }
1088
+ },
1089
+ required: [
1090
+ "slug"
1091
+ ]
1092
+ }
1093
+ },
1094
+ {
1095
+ name: "detection_sources_list",
1096
+ procedure: "detectionSourcesList",
1097
+ method: "GET",
1098
+ path: "/detection/sources",
1099
+ pathParams: [],
1100
+ title: "List available threat detection sources",
1101
+ description: "List the catalog of threat detection sources ChainPatrol can run, including the scopes each source supports and the JSON Schema its config object must satisfy. Sources that do not apply to the organization's industry are omitted.",
1102
+ tags: [
1103
+ "detection"
1104
+ ],
1105
+ readOnly: true,
1106
+ deprecated: false,
1107
+ inputSchema: {
1108
+ type: "object",
1109
+ properties: {
1110
+ slug: {
1111
+ type: "string",
1112
+ minLength: 1,
1113
+ description: "Organization slug. Defaults to the organization your API key is scoped to, so you only need this when authenticating with a key that spans organizations. Sources that do not apply to the organization's industry are omitted."
1114
+ }
1115
+ },
1116
+ description: "List available threat detection sources\n\nReturns the catalog of detection sources ChainPatrol can run, including the scopes each source supports and the JSON Schema its `config` object must satisfy. Use this to discover valid `source` keys before creating or updating a detection config."
1117
+ }
1118
+ },
1119
+ {
1120
+ name: "dispute_create",
1121
+ procedure: "disputeCreate",
1122
+ method: "POST",
1123
+ path: "/dispute/create",
1124
+ pathParams: [],
1125
+ title: "Create a dispute",
1126
+ description: "Create a new dispute for an asset via the external API. Requires API key.",
1127
+ tags: [
1128
+ "dispute"
1129
+ ],
1130
+ readOnly: false,
1131
+ deprecated: false,
1132
+ inputSchema: {
1133
+ type: "object",
1134
+ properties: {
1135
+ content: {
1136
+ type: "string",
1137
+ minLength: 1
1138
+ },
1139
+ email: {
1140
+ type: "string",
1141
+ format: "email"
1142
+ },
1143
+ description: {
1144
+ type: "string"
1145
+ }
1146
+ },
1147
+ required: [
1148
+ "content"
1149
+ ]
1150
+ }
1151
+ },
1152
+ {
1153
+ name: "get_organization_reports",
1154
+ procedure: "getOrganizationReports",
1155
+ method: "POST",
1156
+ path: "/public/getOrganizationReports",
1157
+ pathParams: [],
1158
+ title: "Get reports for an organization",
1159
+ description: "Get reports for an organization based on organization slug and filters",
1160
+ tags: [
1161
+ "public"
1162
+ ],
1163
+ readOnly: true,
1164
+ deprecated: false,
1165
+ inputSchema: {
1166
+ type: "object",
1167
+ properties: {
1168
+ slug: {
1169
+ type: "string"
1170
+ },
1171
+ limit: {
1172
+ type: "number",
1173
+ minimum: 1,
1174
+ maximum: 20
1175
+ },
1176
+ cursor: {
1177
+ anyOf: [
1178
+ {
1179
+ type: "number"
1180
+ },
1181
+ {
1182
+ type: "null"
1183
+ }
1184
+ ]
1185
+ },
1186
+ status: {
1187
+ type: "string",
1188
+ enum: [
1189
+ "TODO",
1190
+ "IN_PROGRESS",
1191
+ "CLOSED"
1192
+ ]
1193
+ },
1194
+ searchQuery: {
1195
+ type: "string"
1196
+ },
1197
+ reporterQuery: {
1198
+ type: "string"
1199
+ },
1200
+ excludeAutomation: {
1201
+ type: "boolean",
1202
+ default: false
1203
+ },
1204
+ reporterKind: {
1205
+ type: "string",
1206
+ enum: [
1207
+ "human",
1208
+ "automation"
1209
+ ]
1210
+ },
1211
+ reviewerKind: {
1212
+ type: "string",
1213
+ enum: [
1214
+ "human",
1215
+ "automation"
1216
+ ]
1217
+ },
1218
+ onlyRejected: {
1219
+ type: "boolean",
1220
+ default: false
1221
+ },
1222
+ onlyFavorited: {
1223
+ type: "boolean",
1224
+ default: false
1225
+ },
1226
+ reviewStatuses: {
1227
+ type: "array",
1228
+ items: {
1229
+ type: "string",
1230
+ enum: [
1231
+ "APPROVE",
1232
+ "REJECT",
1233
+ "SKIP",
1234
+ "ESCALATE"
1235
+ ]
1236
+ }
1237
+ },
1238
+ assetTypes: {
1239
+ type: "array",
1240
+ items: {
1241
+ type: "string",
1242
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
1243
+ }
1244
+ },
1245
+ reviewedByUserId: {
1246
+ anyOf: [
1247
+ {
1248
+ type: "number"
1249
+ },
1250
+ {
1251
+ type: "null"
1252
+ }
1253
+ ]
1254
+ },
1255
+ startDate: {
1256
+ type: "string",
1257
+ format: "date-time"
1258
+ },
1259
+ endDate: {
1260
+ type: "string",
1261
+ format: "date-time"
1262
+ },
1263
+ updatedAtStartDate: {
1264
+ type: "string",
1265
+ format: "date-time"
1266
+ },
1267
+ updatedAtEndDate: {
1268
+ type: "string",
1269
+ format: "date-time"
1270
+ },
1271
+ brandIds: {
1272
+ type: "array",
1273
+ items: {
1274
+ type: "number"
1275
+ }
1276
+ },
1277
+ threatActorIds: {
1278
+ type: "array",
1279
+ items: {
1280
+ type: "number"
1281
+ }
1282
+ },
1283
+ reportedByCustomer: {
1284
+ type: "boolean"
1285
+ },
1286
+ countryCodes: {
1287
+ type: "array",
1288
+ items: {
1289
+ type: "string",
1290
+ minLength: 2,
1291
+ maxLength: 2
1292
+ }
1293
+ },
1294
+ registrars: {
1295
+ type: "array",
1296
+ items: {
1297
+ type: "string"
1298
+ }
1299
+ },
1300
+ hasMxRecords: {
1301
+ type: "boolean"
1302
+ },
1303
+ sources: {
1304
+ type: "array",
1305
+ items: {
1306
+ type: "string",
1307
+ enum: [
1308
+ "APP",
1309
+ "API",
1310
+ "CANARY_TOKEN",
1311
+ "AUTO_DETECTION",
1312
+ "ASSET_MANAGEMENT"
1313
+ ]
1314
+ }
1315
+ },
1316
+ needsCustomerReview: {
1317
+ type: "boolean",
1318
+ description: "Filter to reports waiting on the organization's own approval \u2014 the same queue the Review page shows a customer admin. `true` returns only reports with at least one pending proposal that is the customer's to action; `false` returns only reports that are not. A pending proposal counts when ChainPatrol staff escalated it to the customer and the customer has not answered yet, or when the organization submitted the report itself. Obligatory Organization Admin Approval does not widen this: an untriaged proposal is still waiting on ChainPatrol review, and reaches the customer only once staff escalate it."
1319
+ }
1320
+ },
1321
+ required: [
1322
+ "slug",
1323
+ "limit"
1324
+ ]
1325
+ }
1326
+ },
1327
+ {
1328
+ name: "get_public_organization_metrics",
1329
+ procedure: "getPublicOrganizationMetrics",
1330
+ method: "POST",
1331
+ path: "/public/getOrganizationMetrics",
1332
+ pathParams: [],
1333
+ title: "Get public metrics for organizations",
1334
+ description: "Get public metrics for one or more organizations",
1335
+ tags: [
1336
+ "public"
1337
+ ],
1338
+ readOnly: true,
1339
+ deprecated: false,
1340
+ inputSchema: {
1341
+ type: "object",
1342
+ properties: {
1343
+ organizationSlug: {
1344
+ type: "string"
1345
+ },
1346
+ brandSlug: {
1347
+ type: "string"
1348
+ },
1349
+ startDate: {
1350
+ type: "string",
1351
+ format: "date-time"
1352
+ },
1353
+ endDate: {
1354
+ type: "string",
1355
+ format: "date-time"
1356
+ }
1357
+ },
1358
+ required: [
1359
+ "organizationSlug"
1360
+ ]
1361
+ }
1362
+ },
1363
+ {
1364
+ name: "healthchecks_assets_dead_asset_spike",
1365
+ procedure: "healthchecksAssetsDeadAssetSpike",
1366
+ method: "POST",
1367
+ path: "/healthchecks/assets/dead-asset-spike",
1368
+ pathParams: [],
1369
+ title: "Healthcheck: spike in recently dead assets",
1370
+ description: "Compares the count of `DETECTED_AS_DEAD` LivenessStatusEvent rows in the current window against the baseline rate from the prior `baselineDays`. Severity fires only when the current count clears `minSpikeCount` (suppresses noise when the org has near-zero baseline activity) and exceeds the configured multiplier. Useful for catching liveness-checker regressions after platform changes (captcha rollouts, anti-bot updates) which falsely mark live assets as dead.",
1371
+ tags: [
1372
+ "healthchecks"
1373
+ ],
1374
+ readOnly: true,
1375
+ deprecated: false,
1376
+ inputSchema: {
1377
+ type: "object",
1378
+ properties: {
1379
+ slug: {
1380
+ type: "string",
1381
+ minLength: 1
1382
+ },
1383
+ windowHours: {
1384
+ type: "integer",
1385
+ exclusiveMinimum: 0,
1386
+ default: 24
1387
+ },
1388
+ baselineDays: {
1389
+ type: "integer",
1390
+ exclusiveMinimum: 0,
1391
+ default: 7
1392
+ },
1393
+ warnMultiplier: {
1394
+ type: "number",
1395
+ exclusiveMinimum: 0,
1396
+ default: 2
1397
+ },
1398
+ failMultiplier: {
1399
+ type: "number",
1400
+ exclusiveMinimum: 0,
1401
+ default: 4
1402
+ },
1403
+ minSpikeCount: {
1404
+ type: "integer",
1405
+ minimum: 1,
1406
+ default: 10
1407
+ }
1408
+ },
1409
+ required: [
1410
+ "slug"
1411
+ ]
1412
+ }
1413
+ },
1414
+ {
1415
+ name: "healthchecks_detections_silent_configs",
1416
+ procedure: "healthchecksDetectionsSilentConfigs",
1417
+ method: "POST",
1418
+ path: "/healthchecks/detections/silent-configs",
1419
+ pathParams: [],
1420
+ title: "Healthcheck: detection configs producing recent results",
1421
+ description: "Validates that each enabled detection config has produced at least the configured minimum number of results within the lookback window. Returns the uniform healthcheck result shape.",
1422
+ tags: [
1423
+ "healthchecks"
1424
+ ],
1425
+ readOnly: true,
1426
+ deprecated: false,
1427
+ inputSchema: {
1428
+ type: "object",
1429
+ properties: {
1430
+ slug: {
1431
+ type: "string",
1432
+ minLength: 1
1433
+ },
1434
+ source: {
1435
+ type: "string"
1436
+ },
1437
+ minResults: {
1438
+ type: "integer",
1439
+ minimum: 0,
1440
+ default: 1
1441
+ },
1442
+ lookbackHours: {
1443
+ type: "integer",
1444
+ minimum: 1,
1445
+ default: 168
1446
+ },
1447
+ runBeforeValidate: {
1448
+ type: "boolean",
1449
+ default: false
1450
+ },
1451
+ includeDisabled: {
1452
+ type: "boolean",
1453
+ default: false
1454
+ }
1455
+ },
1456
+ required: [
1457
+ "slug"
1458
+ ]
1459
+ }
1460
+ },
1461
+ {
1462
+ name: "healthchecks_list",
1463
+ procedure: "healthchecksList",
1464
+ method: "POST",
1465
+ path: "/healthchecks/list",
1466
+ pathParams: [],
1467
+ title: "List available healthchecks",
1468
+ description: "Return the registry of healthchecks ChainPatrol exposes today, including planned checks that are not yet implemented on the backend. Clients should iterate this list and only call endpoints where `implemented` is true.",
1469
+ tags: [
1470
+ "healthchecks"
1471
+ ],
1472
+ readOnly: true,
1473
+ deprecated: false,
1474
+ inputSchema: {
1475
+ type: "object",
1476
+ properties: {}
1477
+ }
1478
+ },
1479
+ {
1480
+ name: "healthchecks_missing_contact_url",
1481
+ procedure: "healthchecksMissingContactUrl",
1482
+ method: "POST",
1483
+ path: "/healthchecks/organization/missing-contact-url",
1484
+ pathParams: [],
1485
+ title: "Healthcheck: missing main contact URL / communication channel",
1486
+ description: "Flags organizations that have no contactUrl configured. The contact URL is the main communication channel (typically Slack or Telegram) used to reach the customer.",
1487
+ tags: [
1488
+ "healthchecks"
1489
+ ],
1490
+ readOnly: true,
1491
+ deprecated: false,
1492
+ inputSchema: {
1493
+ type: "object",
1494
+ properties: {
1495
+ slug: {
1496
+ type: "string",
1497
+ minLength: 1
1498
+ }
1499
+ },
1500
+ required: [
1501
+ "slug"
1502
+ ]
1503
+ }
1504
+ },
1505
+ {
1506
+ name: "healthchecks_reviewing_backlog",
1507
+ procedure: "healthchecksReviewingBacklog",
1508
+ method: "POST",
1509
+ path: "/healthchecks/reviewing/backlog",
1510
+ pathParams: [],
1511
+ title: "Healthcheck: pile-up of unreviewed proposals",
1512
+ description: "Counts proposals in PENDING review state for the org and grades severity (ok/warn/fail) against the configurable thresholds.",
1513
+ tags: [
1514
+ "healthchecks"
1515
+ ],
1516
+ readOnly: true,
1517
+ deprecated: false,
1518
+ inputSchema: {
1519
+ type: "object",
1520
+ properties: {
1521
+ slug: {
1522
+ type: "string",
1523
+ minLength: 1
1524
+ },
1525
+ warnThreshold: {
1526
+ type: "integer",
1527
+ minimum: 0,
1528
+ default: 50
1529
+ },
1530
+ failThreshold: {
1531
+ type: "integer",
1532
+ minimum: 0,
1533
+ default: 100
1534
+ }
1535
+ },
1536
+ required: [
1537
+ "slug"
1538
+ ]
1539
+ }
1540
+ },
1541
+ {
1542
+ name: "healthchecks_reviewing_old_proposals",
1543
+ procedure: "healthchecksReviewingOldProposals",
1544
+ method: "POST",
1545
+ path: "/healthchecks/reviewing/old-proposals",
1546
+ pathParams: [],
1547
+ title: "Healthcheck: proposals waiting too long in review",
1548
+ description: "Counts PENDING proposals older than the warn / fail age thresholds (default 7 / 14 days) and lists the oldest offenders.",
1549
+ tags: [
1550
+ "healthchecks"
1551
+ ],
1552
+ readOnly: true,
1553
+ deprecated: false,
1554
+ inputSchema: {
1555
+ type: "object",
1556
+ properties: {
1557
+ slug: {
1558
+ type: "string",
1559
+ minLength: 1
1560
+ },
1561
+ warnAgeHours: {
1562
+ type: "integer",
1563
+ exclusiveMinimum: 0,
1564
+ default: 168
1565
+ },
1566
+ failAgeHours: {
1567
+ type: "integer",
1568
+ exclusiveMinimum: 0,
1569
+ default: 336
1570
+ }
1571
+ },
1572
+ required: [
1573
+ "slug"
1574
+ ]
1575
+ }
1576
+ },
1577
+ {
1578
+ name: "healthchecks_takedowns_automation_off",
1579
+ procedure: "healthchecksTakedownsAutomationOff",
1580
+ method: "POST",
1581
+ path: "/healthchecks/takedowns/automation-off",
1582
+ pathParams: [],
1583
+ title: "Healthcheck: automated takedowns disabled for too long",
1584
+ description: "Flags orgs where takedown service is enabled but `isAutomatedTakedownsActive` has been off for longer than the configured age threshold (default warn 30 days, fail 60 days). Age is derived from the most recent SERVICES_AUTOMATED_TAKEDOWNS_UPDATED entry in OrganizationEvent, falling back to Organization.updatedAt. Orgs without takedown service enabled at all are excluded.",
1585
+ tags: [
1586
+ "healthchecks"
1587
+ ],
1588
+ readOnly: true,
1589
+ deprecated: false,
1590
+ inputSchema: {
1591
+ type: "object",
1592
+ properties: {
1593
+ slug: {
1594
+ type: "string",
1595
+ minLength: 1
1596
+ },
1597
+ warnAgeHours: {
1598
+ type: "integer",
1599
+ minimum: 0,
1600
+ default: 720
1601
+ },
1602
+ failAgeHours: {
1603
+ type: "integer",
1604
+ minimum: 0,
1605
+ default: 1440
1606
+ }
1607
+ },
1608
+ required: [
1609
+ "slug"
1610
+ ]
1611
+ }
1612
+ },
1613
+ {
1614
+ name: "healthchecks_takedowns_cancelled_count",
1615
+ procedure: "healthchecksTakedownsCancelledCount",
1616
+ method: "POST",
1617
+ path: "/healthchecks/takedowns/cancelled-count",
1618
+ pathParams: [],
1619
+ title: "Healthcheck: excess CANCELLED takedowns over a rolling window",
1620
+ description: "Counts transitions into the CANCELLED status from the TakedownEvent log within the configured lookback window. Cancellations should be rare (typically 0-2 per week); a higher count usually signals a quality problem in the proposal funnel or a misuse of the CANCELLED status.",
1621
+ tags: [
1622
+ "healthchecks"
1623
+ ],
1624
+ readOnly: true,
1625
+ deprecated: false,
1626
+ inputSchema: {
1627
+ type: "object",
1628
+ properties: {
1629
+ slug: {
1630
+ type: "string",
1631
+ minLength: 1
1632
+ },
1633
+ lookbackHours: {
1634
+ type: "integer",
1635
+ exclusiveMinimum: 0,
1636
+ default: 168
1637
+ },
1638
+ warnThreshold: {
1639
+ type: "integer",
1640
+ minimum: 0,
1641
+ default: 3
1642
+ },
1643
+ failThreshold: {
1644
+ type: "integer",
1645
+ minimum: 0,
1646
+ default: 10
1647
+ }
1648
+ },
1649
+ required: [
1650
+ "slug"
1651
+ ]
1652
+ }
1653
+ },
1654
+ {
1655
+ name: "healthchecks_takedowns_in_progress_volume",
1656
+ procedure: "healthchecksTakedownsInProgressVolume",
1657
+ method: "POST",
1658
+ path: "/healthchecks/takedowns/in-progress-volume",
1659
+ pathParams: [],
1660
+ title: "Healthcheck: pile-up of IN_PROGRESS takedowns",
1661
+ description: "Counts takedowns currently IN_PROGRESS for the org regardless of age. Complements `takedowns.stale-in-progress` (which only flags items past a staleness threshold) \u2014 a high overall pile-up signals submission-format or vendor-side issues even when individual items are not yet stale.",
1662
+ tags: [
1663
+ "healthchecks"
1664
+ ],
1665
+ readOnly: true,
1666
+ deprecated: false,
1667
+ inputSchema: {
1668
+ type: "object",
1669
+ properties: {
1670
+ slug: {
1671
+ type: "string",
1672
+ minLength: 1
1673
+ },
1674
+ warnThreshold: {
1675
+ type: "integer",
1676
+ minimum: 0,
1677
+ default: 30
1678
+ },
1679
+ failThreshold: {
1680
+ type: "integer",
1681
+ minimum: 0,
1682
+ default: 75
1683
+ }
1684
+ },
1685
+ required: [
1686
+ "slug"
1687
+ ]
1688
+ }
1689
+ },
1690
+ {
1691
+ name: "healthchecks_takedowns_stale_in_progress",
1692
+ procedure: "healthchecksTakedownsStaleInProgress",
1693
+ method: "POST",
1694
+ path: "/healthchecks/takedowns/stale-in-progress",
1695
+ pathParams: [],
1696
+ title: "Healthcheck: stuck IN_PROGRESS takedowns",
1697
+ description: "Counts takedowns that have sat in IN_PROGRESS past the staleness threshold (default 7 days) and lists the oldest offenders.",
1698
+ tags: [
1699
+ "healthchecks"
1700
+ ],
1701
+ readOnly: true,
1702
+ deprecated: false,
1703
+ inputSchema: {
1704
+ type: "object",
1705
+ properties: {
1706
+ slug: {
1707
+ type: "string",
1708
+ minLength: 1
1709
+ },
1710
+ staleThresholdHours: {
1711
+ type: "integer",
1712
+ exclusiveMinimum: 0,
1713
+ default: 168
1714
+ },
1715
+ warnThreshold: {
1716
+ type: "integer",
1717
+ minimum: 0,
1718
+ default: 1
1719
+ },
1720
+ failThreshold: {
1721
+ type: "integer",
1722
+ minimum: 0,
1723
+ default: 5
1724
+ }
1725
+ },
1726
+ required: [
1727
+ "slug"
1728
+ ]
1729
+ }
1730
+ },
1731
+ {
1732
+ name: "healthchecks_takedowns_todo_volume",
1733
+ procedure: "healthchecksTakedownsTodoVolume",
1734
+ method: "POST",
1735
+ path: "/healthchecks/takedowns/todo-volume",
1736
+ pathParams: [],
1737
+ title: "Healthcheck: pile-up of TODO takedowns",
1738
+ description: "Counts takedowns sitting in TODO status for the org. A backlog typically signals either an automation gap on a new threat surface, or manual-filing capacity issues on the takedown team.",
1739
+ tags: [
1740
+ "healthchecks"
1741
+ ],
1742
+ readOnly: true,
1743
+ deprecated: false,
1744
+ inputSchema: {
1745
+ type: "object",
1746
+ properties: {
1747
+ slug: {
1748
+ type: "string",
1749
+ minLength: 1
1750
+ },
1751
+ warnThreshold: {
1752
+ type: "integer",
1753
+ minimum: 0,
1754
+ default: 50
1755
+ },
1756
+ failThreshold: {
1757
+ type: "integer",
1758
+ minimum: 0,
1759
+ default: 100
1760
+ }
1761
+ },
1762
+ required: [
1763
+ "slug"
1764
+ ]
1765
+ }
1766
+ },
1767
+ {
1768
+ name: "metrics_breakdown",
1769
+ procedure: "metricsBreakdown",
1770
+ method: "POST",
1771
+ path: "/metrics/breakdown",
1772
+ pathParams: [],
1773
+ title: "Get organization metrics breakdown",
1774
+ description: "Get blocked threat counts grouped by day, asset type, or brand.",
1775
+ tags: [
1776
+ "metrics"
1777
+ ],
1778
+ readOnly: true,
1779
+ deprecated: false,
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {
1783
+ slug: {
1784
+ type: "string",
1785
+ minLength: 1
1786
+ },
1787
+ by: {
1788
+ type: "string",
1789
+ enum: [
1790
+ "day",
1791
+ "type",
1792
+ "brand"
1793
+ ]
1794
+ },
1795
+ startDate: {
1796
+ type: "string",
1797
+ format: "date-time"
1798
+ },
1799
+ endDate: {
1800
+ type: "string",
1801
+ format: "date-time"
1802
+ },
1803
+ brandIds: {
1804
+ type: "array",
1805
+ items: {
1806
+ type: "integer",
1807
+ exclusiveMinimum: 0
1808
+ }
1809
+ }
1810
+ },
1811
+ required: [
1812
+ "slug",
1813
+ "by"
1814
+ ]
1815
+ }
1816
+ },
1817
+ {
1818
+ name: "metrics_found",
1819
+ procedure: "metricsFound",
1820
+ method: "POST",
1821
+ path: "/metrics/found",
1822
+ pathParams: [],
1823
+ title: "Get found threats count",
1824
+ description: "Return the default 'found' metric for an organization, defined as customer-facing new threats.",
1825
+ tags: [
1826
+ "metrics"
1827
+ ],
1828
+ readOnly: true,
1829
+ deprecated: false,
1830
+ inputSchema: {
1831
+ type: "object",
1832
+ properties: {
1833
+ slug: {
1834
+ type: "string",
1835
+ minLength: 1
1836
+ },
1837
+ startDate: {
1838
+ type: "string",
1839
+ format: "date-time"
1840
+ },
1841
+ endDate: {
1842
+ type: "string",
1843
+ format: "date-time"
1844
+ },
1845
+ brandIds: {
1846
+ type: "array",
1847
+ items: {
1848
+ type: "integer",
1849
+ exclusiveMinimum: 0
1850
+ }
1851
+ }
1852
+ },
1853
+ required: [
1854
+ "slug",
1855
+ "startDate",
1856
+ "endDate"
1857
+ ]
1858
+ }
1859
+ },
1860
+ {
1861
+ name: "metrics_found_by_source",
1862
+ procedure: "metricsFoundBySource",
1863
+ method: "POST",
1864
+ path: "/metrics/found-by-source",
1865
+ pathParams: [],
1866
+ title: "Get currently blocked threats by first-report source",
1867
+ description: "Count distinct currently blocked assets whose earliest report for the organization falls in the date range, split into customer vs ChainPatrol (staff + automation). Blocked is current status or pending BLOCKED, not blockedAt. This does not equal /metrics/found.",
1868
+ tags: [
1869
+ "metrics"
1870
+ ],
1871
+ readOnly: true,
1872
+ deprecated: false,
1873
+ inputSchema: {
1874
+ type: "object",
1875
+ properties: {
1876
+ slug: {
1877
+ type: "string",
1878
+ minLength: 1,
1879
+ description: "Organization slug"
1880
+ },
1881
+ startDate: {
1882
+ type: "string",
1883
+ format: "date-time",
1884
+ description: "Only include assets whose earliest report was created on or after this date. A date-only value (YYYY-MM-DD) starts at midnight UTC"
1885
+ },
1886
+ endDate: {
1887
+ type: "string",
1888
+ format: "date-time",
1889
+ description: "Only include assets whose earliest report was created on or before this date. A date-only value (YYYY-MM-DD) covers the whole day in UTC; pass a full timestamp for a precise cut-off"
1890
+ },
1891
+ brandIds: {
1892
+ type: "array",
1893
+ items: {
1894
+ type: "integer",
1895
+ exclusiveMinimum: 0
1896
+ },
1897
+ description: "Only include assets belonging to these brands"
1898
+ }
1899
+ },
1900
+ required: [
1901
+ "slug",
1902
+ "startDate",
1903
+ "endDate"
1904
+ ]
1905
+ }
1906
+ },
1907
+ {
1908
+ name: "metrics_summary",
1909
+ procedure: "metricsSummary",
1910
+ method: "POST",
1911
+ path: "/metrics/summary",
1912
+ pathParams: [],
1913
+ title: "Get organization metrics summary",
1914
+ description: "Get customer-facing organization metrics and blocked threat breakdowns for a date range.",
1915
+ tags: [
1916
+ "metrics"
1917
+ ],
1918
+ readOnly: true,
1919
+ deprecated: false,
1920
+ inputSchema: {
1921
+ type: "object",
1922
+ properties: {
1923
+ slug: {
1924
+ type: "string",
1925
+ minLength: 1
1926
+ },
1927
+ startDate: {
1928
+ type: "string",
1929
+ format: "date-time"
1930
+ },
1931
+ endDate: {
1932
+ type: "string",
1933
+ format: "date-time"
1934
+ },
1935
+ brandIds: {
1936
+ type: "array",
1937
+ items: {
1938
+ type: "integer",
1939
+ exclusiveMinimum: 0
1940
+ }
1941
+ }
1942
+ },
1943
+ required: [
1944
+ "slug"
1945
+ ]
1946
+ }
1947
+ },
1948
+ {
1949
+ name: "metrics_takedown_success_rate",
1950
+ procedure: "metricsTakedownSuccessRate",
1951
+ method: "POST",
1952
+ path: "/metrics/takedown-success-rate",
1953
+ pathParams: [],
1954
+ title: "Get organization takedown success rate by asset type",
1955
+ description: "Get the share of takedowns that ended in a removal, broken down by asset type, for a date range. The range bounds when a takedown was first submitted to a provider, so takedowns opened but never filed are excluded; outcomes are read as of now, and takedowns still being worked count against the rate.",
1956
+ tags: [
1957
+ "metrics"
1958
+ ],
1959
+ readOnly: true,
1960
+ deprecated: false,
1961
+ inputSchema: {
1962
+ type: "object",
1963
+ properties: {
1964
+ slug: {
1965
+ type: "string",
1966
+ minLength: 1,
1967
+ description: "Organization slug"
1968
+ },
1969
+ startDate: {
1970
+ type: "string",
1971
+ format: "date-time",
1972
+ description: "Only include takedowns first submitted to a provider on or after this date. A date-only value (YYYY-MM-DD) starts at midnight UTC"
1973
+ },
1974
+ endDate: {
1975
+ type: "string",
1976
+ format: "date-time",
1977
+ description: "Only include takedowns first submitted to a provider on or before this date. A date-only value (YYYY-MM-DD) covers the whole day in UTC; pass a full timestamp for a precise cut-off"
1978
+ },
1979
+ blockLabel: {
1980
+ type: "string",
1981
+ description: 'Only include assets blocked with this review label, e.g. "Brand Impersonation", "Employee Impersonation" or "Targeting Org Users"'
1982
+ },
1983
+ brandType: {
1984
+ type: "string",
1985
+ enum: [
1986
+ "INDIVIDUAL",
1987
+ "ORGANIZATION",
1988
+ "PRODUCT"
1989
+ ],
1990
+ description: "Only include assets belonging to brands of this type"
1991
+ },
1992
+ countryCodes: {
1993
+ type: "array",
1994
+ items: {
1995
+ type: "string",
1996
+ minLength: 2,
1997
+ maxLength: 2
1998
+ },
1999
+ description: "Only include assets scanned from these ISO 3166-1 alpha-2 countries"
2000
+ },
2001
+ brandIds: {
2002
+ type: "array",
2003
+ items: {
2004
+ type: "integer",
2005
+ exclusiveMinimum: 0
2006
+ },
2007
+ description: "Only include assets belonging to these brands"
2008
+ }
2009
+ },
2010
+ required: [
2011
+ "slug"
2012
+ ]
2013
+ }
2014
+ },
2015
+ {
2016
+ name: "metrics_takedown_time",
2017
+ procedure: "metricsTakedownTime",
2018
+ method: "POST",
2019
+ path: "/metrics/takedown-time",
2020
+ pathParams: [],
2021
+ title: "Get organization time-to-takedown metrics",
2022
+ description: "Get the median time from takedown start to completion, broken down by asset type, for a date range.",
2023
+ tags: [
2024
+ "metrics"
2025
+ ],
2026
+ readOnly: true,
2027
+ deprecated: false,
2028
+ inputSchema: {
2029
+ type: "object",
2030
+ properties: {
2031
+ slug: {
2032
+ type: "string",
2033
+ minLength: 1,
2034
+ description: "Organization slug"
2035
+ },
2036
+ startDate: {
2037
+ type: "string",
2038
+ format: "date-time",
2039
+ description: "Only include takedowns completed on or after this date. A date-only value (YYYY-MM-DD) starts at midnight UTC"
2040
+ },
2041
+ endDate: {
2042
+ type: "string",
2043
+ format: "date-time",
2044
+ description: "Only include takedowns completed on or before this date. A date-only value (YYYY-MM-DD) covers the whole day in UTC; pass a full timestamp for a precise cut-off"
2045
+ },
2046
+ blockLabel: {
2047
+ type: "string",
2048
+ description: 'Only include assets blocked with this review label, e.g. "Brand Impersonation", "Employee Impersonation" or "Targeting Org Users"'
2049
+ },
2050
+ brandType: {
2051
+ type: "string",
2052
+ enum: [
2053
+ "INDIVIDUAL",
2054
+ "ORGANIZATION",
2055
+ "PRODUCT"
2056
+ ],
2057
+ description: "Only include assets belonging to brands of this type"
2058
+ },
2059
+ countryCodes: {
2060
+ type: "array",
2061
+ items: {
2062
+ type: "string",
2063
+ minLength: 2,
2064
+ maxLength: 2
2065
+ },
2066
+ description: "Only include assets scanned from these ISO 3166-1 alpha-2 countries"
2067
+ },
2068
+ brandIds: {
2069
+ type: "array",
2070
+ items: {
2071
+ type: "integer",
2072
+ exclusiveMinimum: 0
2073
+ },
2074
+ description: "Only include assets belonging to these brands"
2075
+ }
2076
+ },
2077
+ required: [
2078
+ "slug"
2079
+ ]
2080
+ }
2081
+ },
2082
+ {
2083
+ name: "metrics_takedown_time_by_provider",
2084
+ procedure: "metricsTakedownTimeByProvider",
2085
+ method: "POST",
2086
+ path: "/metrics/takedown-time/by-provider",
2087
+ pathParams: [],
2088
+ title: "Get organization time-to-takedown metrics by provider",
2089
+ description: "Get the median time from takedown start to completion, broken down by the provider a takedown was filed against, for a date range. Each takedown is counted once per provider role it carries (hosting provider, domain registrar, TLD registrar).",
2090
+ tags: [
2091
+ "metrics"
2092
+ ],
2093
+ readOnly: true,
2094
+ deprecated: false,
2095
+ inputSchema: {
2096
+ type: "object",
2097
+ properties: {
2098
+ slug: {
2099
+ type: "string",
2100
+ minLength: 1,
2101
+ description: "Organization slug"
2102
+ },
2103
+ startDate: {
2104
+ type: "string",
2105
+ format: "date-time",
2106
+ description: "Only include takedowns completed on or after this date. A date-only value (YYYY-MM-DD) starts at midnight UTC"
2107
+ },
2108
+ endDate: {
2109
+ type: "string",
2110
+ format: "date-time",
2111
+ description: "Only include takedowns completed on or before this date. A date-only value (YYYY-MM-DD) covers the whole day in UTC; pass a full timestamp for a precise cut-off"
2112
+ },
2113
+ roles: {
2114
+ type: "array",
2115
+ items: {
2116
+ type: "string",
2117
+ enum: [
2118
+ "HOSTING_PROVIDER",
2119
+ "DOMAIN_REGISTRAR",
2120
+ "TLD_REGISTRAR"
2121
+ ]
2122
+ },
2123
+ description: "Only break down these provider roles. Omit for every role. A takedown filed against both a host and a registrar is counted once under each"
2124
+ },
2125
+ blockLabel: {
2126
+ type: "string",
2127
+ description: 'Only include assets blocked with this review label, e.g. "Brand Impersonation", "Employee Impersonation" or "Targeting Org Users"'
2128
+ },
2129
+ brandType: {
2130
+ type: "string",
2131
+ enum: [
2132
+ "INDIVIDUAL",
2133
+ "ORGANIZATION",
2134
+ "PRODUCT"
2135
+ ],
2136
+ description: "Only include assets belonging to brands of this type"
2137
+ },
2138
+ countryCodes: {
2139
+ type: "array",
2140
+ items: {
2141
+ type: "string",
2142
+ minLength: 2,
2143
+ maxLength: 2
2144
+ },
2145
+ description: "Only include assets scanned from these ISO 3166-1 alpha-2 countries"
2146
+ },
2147
+ brandIds: {
2148
+ type: "array",
2149
+ items: {
2150
+ type: "integer",
2151
+ exclusiveMinimum: 0
2152
+ },
2153
+ description: "Only include assets belonging to these brands"
2154
+ }
2155
+ },
2156
+ required: [
2157
+ "slug"
2158
+ ]
2159
+ }
2160
+ },
2161
+ {
2162
+ name: "metrics_takedown_time_trend",
2163
+ procedure: "metricsTakedownTimeTrend",
2164
+ method: "POST",
2165
+ path: "/metrics/takedown-time/trend",
2166
+ pathParams: [],
2167
+ title: "Get organization time-to-takedown trends",
2168
+ description: "Get the median time from takedown start to completion bucketed by the month a takedown completed, over a trailing window of whole months, together with the month-over-month change. Returns one series per requested asset type, or a single pooled series.",
2169
+ tags: [
2170
+ "metrics"
2171
+ ],
2172
+ readOnly: true,
2173
+ deprecated: false,
2174
+ inputSchema: {
2175
+ type: "object",
2176
+ properties: {
2177
+ slug: {
2178
+ type: "string",
2179
+ minLength: 1,
2180
+ description: "Organization slug"
2181
+ },
2182
+ months: {
2183
+ type: "integer",
2184
+ minimum: 2,
2185
+ maximum: 36,
2186
+ default: 12,
2187
+ description: "Size of the trailing window in whole months, including the current month"
2188
+ },
2189
+ assetTypes: {
2190
+ type: "array",
2191
+ items: {
2192
+ type: "string",
2193
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
2194
+ },
2195
+ maxItems: 5,
2196
+ description: "Break the trend down by these asset types, one series each. Omit for a single series pooled across every asset type"
2197
+ },
2198
+ blockLabel: {
2199
+ type: "string",
2200
+ description: 'Only include assets blocked with this review label, e.g. "Brand Impersonation", "Employee Impersonation" or "Targeting Org Users"'
2201
+ },
2202
+ brandType: {
2203
+ type: "string",
2204
+ enum: [
2205
+ "INDIVIDUAL",
2206
+ "ORGANIZATION",
2207
+ "PRODUCT"
2208
+ ],
2209
+ description: "Only include assets belonging to brands of this type"
2210
+ },
2211
+ countryCodes: {
2212
+ type: "array",
2213
+ items: {
2214
+ type: "string",
2215
+ minLength: 2,
2216
+ maxLength: 2
2217
+ },
2218
+ description: "Only include assets scanned from these ISO 3166-1 alpha-2 countries"
2219
+ },
2220
+ brandIds: {
2221
+ type: "array",
2222
+ items: {
2223
+ type: "integer",
2224
+ exclusiveMinimum: 0
2225
+ },
2226
+ description: "Only include assets belonging to these brands"
2227
+ }
2228
+ },
2229
+ required: [
2230
+ "slug"
2231
+ ]
2232
+ }
2233
+ },
2234
+ {
2235
+ name: "operations_queues_snapshot",
2236
+ procedure: "operationsQueuesSnapshot",
2237
+ method: "POST",
2238
+ path: "/operations/queues/snapshot",
2239
+ pathParams: [],
2240
+ title: "Get operations queue snapshot",
2241
+ description: "Return pending review and takedown queue state with SLA and age buckets for operational monitoring.",
2242
+ tags: [
2243
+ "operations"
2244
+ ],
2245
+ readOnly: true,
2246
+ deprecated: false,
2247
+ inputSchema: {
2248
+ type: "object",
2249
+ properties: {
2250
+ slug: {
2251
+ type: "string",
2252
+ minLength: 1
2253
+ },
2254
+ all: {
2255
+ type: "boolean",
2256
+ default: false
2257
+ },
2258
+ windowHours: {
2259
+ type: "integer",
2260
+ exclusiveMinimum: 0,
2261
+ default: 168
2262
+ }
2263
+ }
2264
+ }
2265
+ },
2266
+ {
2267
+ name: "organization_asset_groups_create",
2268
+ procedure: "organizationAssetGroupsCreate",
2269
+ method: "POST",
2270
+ path: "/organization/asset-groups",
2271
+ pathParams: [],
2272
+ title: "Create asset group",
2273
+ description: "Create a new asset group for organizing your organization's assets.",
2274
+ tags: [
2275
+ "organization"
2276
+ ],
2277
+ readOnly: false,
2278
+ deprecated: false,
2279
+ inputSchema: {
2280
+ type: "object",
2281
+ properties: {
2282
+ name: {
2283
+ type: "string",
2284
+ minLength: 1,
2285
+ maxLength: 255,
2286
+ description: "Name for the new group"
2287
+ }
2288
+ },
2289
+ required: [
2290
+ "name"
2291
+ ],
2292
+ description: "Create a new asset group\n\nCreates a new asset group for organizing your organization's assets."
2293
+ }
2294
+ },
2295
+ {
2296
+ name: "organization_asset_groups_delete",
2297
+ procedure: "organizationAssetGroupsDelete",
2298
+ method: "DELETE",
2299
+ path: "/organization/asset-groups/{groupId}",
2300
+ pathParams: [
2301
+ "groupId"
2302
+ ],
2303
+ title: "Delete asset group",
2304
+ description: "Delete an asset group. Assets in this group will become ungrouped.",
2305
+ tags: [
2306
+ "organization"
2307
+ ],
2308
+ readOnly: false,
2309
+ deprecated: false,
2310
+ inputSchema: {
2311
+ type: "object",
2312
+ properties: {
2313
+ groupId: {
2314
+ type: "integer",
2315
+ exclusiveMinimum: 0,
2316
+ description: "ID of the group to delete"
2317
+ }
2318
+ },
2319
+ required: [
2320
+ "groupId"
2321
+ ],
2322
+ description: "Delete an asset group\n\nDeletes an asset group. Assets in this group will become ungrouped."
2323
+ }
2324
+ },
2325
+ {
2326
+ name: "organization_asset_groups_list",
2327
+ procedure: "organizationAssetGroupsList",
2328
+ method: "GET",
2329
+ path: "/organization/asset-groups",
2330
+ pathParams: [],
2331
+ title: "List organization asset groups",
2332
+ description: "List all asset groups belonging to your organization with asset counts.",
2333
+ tags: [
2334
+ "organization"
2335
+ ],
2336
+ readOnly: true,
2337
+ deprecated: false,
2338
+ inputSchema: {
2339
+ type: "object",
2340
+ properties: {},
2341
+ description: "List organization asset groups\n\nReturns all asset groups belonging to the organization associated with your API key."
2342
+ }
2343
+ },
2344
+ {
2345
+ name: "organization_asset_groups_update",
2346
+ procedure: "organizationAssetGroupsUpdate",
2347
+ method: "PATCH",
2348
+ path: "/organization/asset-groups/{groupId}",
2349
+ pathParams: [
2350
+ "groupId"
2351
+ ],
2352
+ title: "Update asset group",
2353
+ description: "Rename an existing asset group.",
2354
+ tags: [
2355
+ "organization"
2356
+ ],
2357
+ readOnly: false,
2358
+ deprecated: false,
2359
+ inputSchema: {
2360
+ type: "object",
2361
+ properties: {
2362
+ groupId: {
2363
+ type: "integer",
2364
+ exclusiveMinimum: 0,
2365
+ description: "ID of the group to update"
2366
+ },
2367
+ name: {
2368
+ type: "string",
2369
+ minLength: 1,
2370
+ maxLength: 255,
2371
+ description: "New name for the group"
2372
+ }
2373
+ },
2374
+ required: [
2375
+ "groupId",
2376
+ "name"
2377
+ ],
2378
+ description: "Update an asset group\n\nRename an existing asset group."
2379
+ }
2380
+ },
2381
+ {
2382
+ name: "organization_assets_add",
2383
+ procedure: "organizationAssetsAdd",
2384
+ method: "POST",
2385
+ path: "/organization/assets",
2386
+ pathParams: [],
2387
+ title: "Add assets to organization allowlist",
2388
+ description: "Batch add multiple assets to your organization's allowlist. Each asset will be automatically parsed, classified, and approved as ALLOWED status. Supports optional group assignment.",
2389
+ tags: [
2390
+ "organization"
2391
+ ],
2392
+ readOnly: false,
2393
+ deprecated: false,
2394
+ inputSchema: {
2395
+ type: "object",
2396
+ properties: {
2397
+ assets: {
2398
+ type: "array",
2399
+ items: {
2400
+ type: "object",
2401
+ properties: {
2402
+ content: {
2403
+ type: "string",
2404
+ description: "Asset content (URL, address, handle, etc.)"
2405
+ },
2406
+ name: {
2407
+ type: "string",
2408
+ description: "Optional display name for the asset"
2409
+ },
2410
+ description: {
2411
+ type: "string",
2412
+ description: "Optional description for the asset"
2413
+ },
2414
+ groupId: {
2415
+ type: "integer",
2416
+ exclusiveMinimum: 0,
2417
+ description: "Optional group ID to assign the asset to"
2418
+ }
2419
+ },
2420
+ required: [
2421
+ "content"
2422
+ ]
2423
+ },
2424
+ minItems: 1,
2425
+ maxItems: 1e3,
2426
+ description: "Array of assets to add (max 1000 per request)"
2427
+ }
2428
+ },
2429
+ required: [
2430
+ "assets"
2431
+ ],
2432
+ description: "Add assets to organization allowlist\n\nBatch add multiple assets to your organization's allowlist. \nEach asset will be automatically parsed and classified.\nAssets will be auto-approved as ALLOWED status."
2433
+ }
2434
+ },
2435
+ {
2436
+ name: "organization_assets_delete",
2437
+ procedure: "organizationAssetsDelete",
2438
+ method: "DELETE",
2439
+ path: "/organization/assets/{assetId}",
2440
+ pathParams: [
2441
+ "assetId"
2442
+ ],
2443
+ title: "Remove asset from organization",
2444
+ description: "Remove an asset from your organization's allowlist. This performs a soft delete.",
2445
+ tags: [
2446
+ "organization"
2447
+ ],
2448
+ readOnly: false,
2449
+ deprecated: false,
2450
+ inputSchema: {
2451
+ type: "object",
2452
+ properties: {
2453
+ assetId: {
2454
+ type: "integer",
2455
+ exclusiveMinimum: 0,
2456
+ description: "ID of the asset to remove"
2457
+ }
2458
+ },
2459
+ required: [
2460
+ "assetId"
2461
+ ],
2462
+ description: "Remove an asset from the organization\n\nThis removes the asset from the organization's allowlist."
2463
+ }
2464
+ },
2465
+ {
2466
+ name: "organization_assets_list",
2467
+ procedure: "organizationAssetsList",
2468
+ method: "GET",
2469
+ path: "/organization/assets",
2470
+ pathParams: [],
2471
+ title: "List organization assets",
2472
+ description: "List all assets belonging to your organization with optional filtering by type, group, and search query. Supports pagination.",
2473
+ tags: [
2474
+ "organization"
2475
+ ],
2476
+ readOnly: true,
2477
+ deprecated: false,
2478
+ inputSchema: {
2479
+ type: "object",
2480
+ properties: {
2481
+ slug: {
2482
+ type: "string",
2483
+ description: "Organization slug. Optional for organization-scoped API keys, which resolve the organization from the key itself. Required when your credentials can reach more than one organization."
2484
+ },
2485
+ type: {
2486
+ type: "string",
2487
+ description: "Filter by asset type (URL, ADDRESS, etc.) One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
2488
+ },
2489
+ groupId: {
2490
+ type: "integer",
2491
+ description: "Filter by group ID. Use -1 for ungrouped assets only. Omit for all assets."
2492
+ },
2493
+ query: {
2494
+ type: "string",
2495
+ description: "Search query to filter assets by content"
2496
+ },
2497
+ per_page: {
2498
+ type: "integer",
2499
+ minimum: 1,
2500
+ maximum: 1e3,
2501
+ default: 100,
2502
+ description: "The number of assets to return per page (max 1000)"
2503
+ },
2504
+ next_page: {
2505
+ anyOf: [
2506
+ {
2507
+ anyOf: [
2508
+ {
2509
+ not: {}
2510
+ },
2511
+ {
2512
+ type: "string"
2513
+ }
2514
+ ]
2515
+ },
2516
+ {
2517
+ type: "null"
2518
+ }
2519
+ ],
2520
+ description: "Cursor for fetching the next page of results"
2521
+ }
2522
+ },
2523
+ description: "List organization assets request body\n\nReturns all assets belonging to the organization identified by `slug`, or \u2014\nwhen `slug` is omitted \u2014 the organization associated with your API key.\nSupports filtering by type, group, and search query."
2524
+ }
2525
+ },
2526
+ {
2527
+ name: "organization_assets_update",
2528
+ procedure: "organizationAssetsUpdate",
2529
+ method: "PATCH",
2530
+ path: "/organization/assets/{assetId}",
2531
+ pathParams: [
2532
+ "assetId"
2533
+ ],
2534
+ title: "Update organization asset",
2535
+ description: "Update an asset's name, description, or group assignment. The asset must belong to your organization.",
2536
+ tags: [
2537
+ "organization"
2538
+ ],
2539
+ readOnly: false,
2540
+ deprecated: false,
2541
+ inputSchema: {
2542
+ type: "object",
2543
+ properties: {
2544
+ assetId: {
2545
+ type: "integer",
2546
+ exclusiveMinimum: 0,
2547
+ description: "ID of the asset to update"
2548
+ },
2549
+ name: {
2550
+ type: "string",
2551
+ description: "New display name for the asset"
2552
+ },
2553
+ description: {
2554
+ description: "New description for the asset",
2555
+ anyOf: [
2556
+ {
2557
+ type: "string"
2558
+ },
2559
+ {
2560
+ type: "null"
2561
+ }
2562
+ ]
2563
+ },
2564
+ groupId: {
2565
+ anyOf: [
2566
+ {
2567
+ type: "integer",
2568
+ exclusiveMinimum: 0
2569
+ },
2570
+ {
2571
+ type: "null"
2572
+ }
2573
+ ],
2574
+ description: "New group ID (null to ungroup)"
2575
+ }
2576
+ },
2577
+ required: [
2578
+ "assetId"
2579
+ ],
2580
+ description: "Update an organization asset\n\nUpdate the name, description, or group assignment of an asset."
2581
+ }
2582
+ },
2583
+ {
2584
+ name: "organization_brands_list",
2585
+ procedure: "organizationBrandsList",
2586
+ method: "GET",
2587
+ path: "/organization/brands",
2588
+ pathParams: [],
2589
+ title: "List organization brands",
2590
+ description: "List every brand belonging to the organization associated with your API key. Returns parent brands (`type: ORGANIZATION`), individual / employee brands (`type: INDIVIDUAL`), and product brands (`type: PRODUCT`) in one call, including configuration fields: `brandColors` (HEX strings), `includedTerms`, `excludedTerms`, `tickers`, `avatarUrl`, `websiteUrl`, and `twitterHandle`. Each brand also reports `useOrganizationDocs` (when true, the brand inherits its LOA/POA from the org), a `legalDocuments` block with the brand-level Letter of Authorization and Power of Attorney attachments (each `{ present, fileName, fileUrl }`), and `trademarkRegistrations` \u2014 every brand-level trademark on file with its issuing office, registration number, and (when uploaded) a `certificateFileUrl` link to the certificate PDF. Soft-deleted brands are excluded. Brand counts per organization are typically <500, so the endpoint returns the full set in one response (no pagination).",
2591
+ tags: [
2592
+ "organization"
2593
+ ],
2594
+ readOnly: true,
2595
+ deprecated: false,
2596
+ inputSchema: {
2597
+ type: "object",
2598
+ properties: {},
2599
+ description: "List organization brands\n\nReturns all brands belonging to the organization associated with your API key,\nincluding sub-brands (`ORGANIZATION`), individual / employee brands\n(`INDIVIDUAL`), and product brands (`PRODUCT`). Soft-deleted brands are\nexcluded."
2600
+ }
2601
+ },
2602
+ {
2603
+ name: "organization_create",
2604
+ procedure: "organizationCreate",
2605
+ method: "POST",
2606
+ path: "/organization/immunefi-create",
2607
+ pathParams: [],
2608
+ title: "Create a new baseline organization (Immunefi only)",
2609
+ description: "Create a new organization with baseline configuration and return an API key for accessing ChainPatrol services. This endpoint requires authentication with a valid Immunefi API key. Organization slugs will be automatically prefixed with 'immunefi-' (or 'immunefi-test-' when using a test API key) to prevent name clashes. The endpoint validates that the requested slug doesn't conflict with any existing organizations (checking bare slug, 'immunefi-{slug}', and 'immunefi-test-{slug}' variations). The organization will be created with standard monitoring and detection features enabled. Returns the organization details, API key, owner information, and direct dashboard URL.",
2610
+ tags: [
2611
+ "organization",
2612
+ "immunefi"
2613
+ ],
2614
+ readOnly: false,
2615
+ deprecated: false,
2616
+ inputSchema: {
2617
+ type: "object",
2618
+ properties: {
2619
+ name: {
2620
+ type: "string",
2621
+ minLength: 1,
2622
+ maxLength: 255
2623
+ },
2624
+ slug: {
2625
+ type: "string",
2626
+ minLength: 1,
2627
+ maxLength: 100,
2628
+ pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
2629
+ },
2630
+ websiteUrl: {
2631
+ type: "string",
2632
+ format: "uri"
2633
+ },
2634
+ description: {
2635
+ type: "string",
2636
+ maxLength: 1e3
2637
+ },
2638
+ ownerEmail: {
2639
+ type: "string",
2640
+ format: "email"
2641
+ },
2642
+ ownerFullName: {
2643
+ type: "string",
2644
+ minLength: 1,
2645
+ maxLength: 255
2646
+ }
2647
+ },
2648
+ required: [
2649
+ "name",
2650
+ "slug",
2651
+ "ownerEmail",
2652
+ "ownerFullName"
2653
+ ]
2654
+ }
2655
+ },
2656
+ {
2657
+ name: "organization_metrics_get",
2658
+ procedure: "organizationMetricsGet",
2659
+ method: "GET",
2660
+ path: "/organization/metrics",
2661
+ pathParams: [],
2662
+ title: "Get organization metrics",
2663
+ description: "Get metrics for your organization with optional date range and brand filtering. Organization is determined from your API key.",
2664
+ tags: [
2665
+ "organization"
2666
+ ],
2667
+ readOnly: true,
2668
+ deprecated: false,
2669
+ inputSchema: {
2670
+ type: "object",
2671
+ properties: {
2672
+ organizationSlug: {
2673
+ type: "string",
2674
+ description: "Organization slug. Required when authenticating with a user session (Bearer token) and `slugs`/`allMyOrgs` are not supplied; ignored when using an org-scoped API key (org is derived from the key). Mutually exclusive with `slugs` and `allMyOrgs`."
2675
+ },
2676
+ slugs: {
2677
+ type: "array",
2678
+ items: {
2679
+ type: "string"
2680
+ },
2681
+ maxItems: 500,
2682
+ description: "Multi-org form. Each slug is auth-checked individually (org-scoped API keys may only pass their own org; sessions and user API keys must have membership or staff role per slug). When supplied with more than one slug, the response includes a `perOrg` breakdown plus `metrics` rolled up across all of them. Capped at 500 slugs per call. Mutually exclusive with `organizationSlug` and `allMyOrgs`."
2683
+ },
2684
+ allMyOrgs: {
2685
+ type: "boolean",
2686
+ description: "Shortcut for 'every org the caller is authorized to read'. Reuses the same filter logic as `/user/orgs`: org-scoped API keys are rejected (they're pinned to one org \u2014 use the default form); user API keys / Bearer sessions resolve to the user's memberships (or every active org for staff). Combine with `subscriptionStatus` and `services` to narrow (e.g. only paying customers with takedowns enabled). The resolved org set is still capped at 500 and a 413 is returned if exceeded. Mutually exclusive with `organizationSlug`, `slugs`, and `brandSlug`."
2687
+ },
2688
+ subscriptionStatus: {
2689
+ type: "array",
2690
+ items: {
2691
+ type: "string",
2692
+ enum: [
2693
+ "PROSPECT",
2694
+ "POC",
2695
+ "ACTIVE",
2696
+ "INTEGRATION",
2697
+ "INACTIVE"
2698
+ ]
2699
+ },
2700
+ minItems: 1,
2701
+ description: "Only meaningful with `allMyOrgs: true`. Defaults to the same set `/user/orgs` uses (ACTIVE/POC/PROSPECT/INTEGRATION); pass INACTIVE explicitly to include churned organizations."
2702
+ },
2703
+ services: {
2704
+ type: "object",
2705
+ properties: {
2706
+ reporting: {
2707
+ type: "object",
2708
+ properties: {
2709
+ active: {
2710
+ type: "boolean"
2711
+ }
2712
+ }
2713
+ },
2714
+ reviewing: {
2715
+ type: "object",
2716
+ properties: {
2717
+ active: {
2718
+ type: "boolean"
2719
+ }
2720
+ }
2721
+ },
2722
+ protection: {
2723
+ type: "object",
2724
+ properties: {
2725
+ active: {
2726
+ type: "boolean"
2727
+ }
2728
+ }
2729
+ },
2730
+ takedowns: {
2731
+ type: "object",
2732
+ properties: {
2733
+ active: {
2734
+ type: "boolean"
2735
+ },
2736
+ automated: {
2737
+ type: "boolean"
2738
+ }
2739
+ }
2740
+ },
2741
+ detection: {
2742
+ type: "object",
2743
+ properties: {
2744
+ active: {
2745
+ type: "boolean"
2746
+ }
2747
+ }
2748
+ },
2749
+ darkWebMonitoring: {
2750
+ type: "object",
2751
+ properties: {
2752
+ active: {
2753
+ type: "boolean"
2754
+ }
2755
+ }
2756
+ }
2757
+ },
2758
+ description: "Only meaningful with `allMyOrgs: true`. Same shape as `/user/orgs`'s `services` filter \u2014 every service accepts `{ active }`, and **only `takedowns` additionally accepts `automated`** (the one service where manual-vs-automated changes real platform behavior). E.g. `{ takedowns: { active: true, automated: false } }` for 'orgs with takedowns enabled but automation off'."
2759
+ },
2760
+ brandSlug: {
2761
+ type: "string"
2762
+ },
2763
+ startDate: {
2764
+ type: "string",
2765
+ format: "date-time"
2766
+ },
2767
+ endDate: {
2768
+ type: "string",
2769
+ format: "date-time"
2770
+ },
2771
+ include: {
2772
+ type: "array",
2773
+ items: {
2774
+ type: "string",
2775
+ enum: [
2776
+ "reports",
2777
+ "newThreats",
2778
+ "threatsWatchlisted",
2779
+ "takedownsFiled",
2780
+ "takedownsInProgress",
2781
+ "takedownsCompleted",
2782
+ "takedownsCancelled",
2783
+ "domainThreats",
2784
+ "twitterThreats",
2785
+ "telegramThreats",
2786
+ "otherThreats",
2787
+ "blockedByType",
2788
+ "blockedByDay"
2789
+ ]
2790
+ },
2791
+ description: "Subset of metrics to compute. When omitted or empty, every metric is computed (legacy behavior). Each value in `include` costs roughly one aggregate query, so narrow the list when an agent only needs a couple of numbers \u2014 this is the main lever for keeping big-org calls inside the 30s function cap. Fields not in `include` come back as `null`. `takedownsInProgress` and `takedownsCancelled` count takedowns currently in that status whose `updatedAt` falls in the window (same windowing as `takedownsFiled`), so they reconcile with the takedowns list filtered by status over the same date range."
2792
+ }
2793
+ }
2794
+ }
2795
+ },
2796
+ {
2797
+ name: "organization_proposals_review",
2798
+ procedure: "organizationProposalsReview",
2799
+ method: "POST",
2800
+ path: "/organization/proposals/{proposalId}/review",
2801
+ pathParams: [
2802
+ "proposalId"
2803
+ ],
2804
+ title: "Review an organization proposal",
2805
+ description: "Records your organization's decision on a proposal that is waiting on you \u2014 one ChainPatrol escalated to you, or one on a report you submitted yourself. These are exactly the proposals your Review page shows and the ones `GET /organization/reports?needsCustomerReview=true` counts; find their IDs in `proposals[].id` on that endpoint. `APPROVE` blocks the asset and takes a `label` (plus a `brandId` for labels that name a specific brand). `REJECT` takes a `rejectReason`. `WATCHLIST` rejects the proposal and parks the asset so it is re-evaluated if it comes back alive.\n\nPass `assetId` alongside `proposalId`; it must match the proposal's current asset or the request returns `409`, which keeps a caller from actioning a proposal it never read.\n\nApprovals are held to the same safety checks as the Review page, with no override: an asset that any enabled legitimacy rule marks as legitimate cannot be approved here, and an allowlisted asset cannot be blocked here. Both return `403` and must be handled on the Review page by a person.\n\nRate limited to 10 reviews per organization per day, since a review is not undoable and looping over a queue is easy to do by accident. Exceeding it returns `429`. The limit is shared across all of the organization's API credentials and does not apply to reviews made from the Review page.",
2806
+ tags: [
2807
+ "organization"
2808
+ ],
2809
+ readOnly: false,
2810
+ deprecated: false,
2811
+ inputSchema: {
2812
+ type: "object",
2813
+ properties: {
2814
+ proposalId: {
2815
+ type: "integer",
2816
+ exclusiveMinimum: 0,
2817
+ description: "ID of the proposal to review. Read it from `proposals[].id` on `GET /organization/reports`."
2818
+ },
2819
+ assetId: {
2820
+ type: "integer",
2821
+ exclusiveMinimum: 0,
2822
+ description: "ID of the asset this proposal is for, from `proposals[].asset.id` on `GET /organization/reports`. Required, and must match the proposal's current asset \u2014 a mismatch returns 409. This proves the request was built from a proposal you actually read, rather than from a guessed or stale ID."
2823
+ },
2824
+ slug: {
2825
+ type: "string",
2826
+ description: "Organization slug. Optional for organization-scoped API keys, which resolve the organization from the key itself. Required when your credentials can reach more than one organization."
2827
+ },
2828
+ decision: {
2829
+ type: "string",
2830
+ enum: [
2831
+ "APPROVE",
2832
+ "REJECT",
2833
+ "WATCHLIST"
2834
+ ],
2835
+ description: "What to do with the proposal. `APPROVE` blocks the asset, `REJECT` declines it, and `WATCHLIST` declines it but parks the asset so it is re-evaluated if it comes back alive. `WATCHLIST` is recorded as a rejection with the asset watchlisted \u2014 the same write the Review page performs."
2836
+ },
2837
+ label: {
2838
+ type: "string",
2839
+ enum: [
2840
+ "Brand Impersonation",
2841
+ "Employee Impersonation",
2842
+ "Fake Employee",
2843
+ "Targeting Org Users",
2844
+ "General Phishing",
2845
+ "C2 Server",
2846
+ "False Positive",
2847
+ "Organization Member Impersonation",
2848
+ "Targeting Organization"
2849
+ ],
2850
+ description: "Why the asset is being blocked. Full guidance: resource chainpatrol://glossary/proposal-labels"
2851
+ },
2852
+ brandId: {
2853
+ type: "integer",
2854
+ exclusiveMinimum: 0,
2855
+ description: "Brand the asset impersonates. Optional for most labels, but required for `Brand Impersonation` and `Employee Impersonation`, which name a specific impersonation target. List your brands with `GET /organization/brands`."
2856
+ },
2857
+ rejectReason: {
2858
+ type: "string",
2859
+ enum: [
2860
+ "irrelevant",
2861
+ "no_malicious_activity",
2862
+ "insufficient_evidence",
2863
+ "not_targeting_org",
2864
+ "decayed"
2865
+ ],
2866
+ description: "Why the proposal is being rejected. Full guidance: resource chainpatrol://glossary/proposal-reject-reasons"
2867
+ },
2868
+ watchlistReason: {
2869
+ type: "string",
2870
+ enum: [
2871
+ "DEAD",
2872
+ "PARKING",
2873
+ "NOT_ENOUGH_EVIDENCE",
2874
+ "OTHER"
2875
+ ],
2876
+ description: "Why the asset is being parked. Full guidance: resource chainpatrol://glossary/proposal-watchlist-reasons"
2877
+ },
2878
+ note: {
2879
+ type: "string",
2880
+ maxLength: 1e3,
2881
+ description: "Free-text detail recorded on the review, alongside the structured reason. Required when `watchlistReason` is `OTHER`."
2882
+ }
2883
+ },
2884
+ required: [
2885
+ "proposalId",
2886
+ "assetId",
2887
+ "decision"
2888
+ ],
2889
+ description: "Review a proposal on your organization's behalf\n\nRecords your organization's decision on a proposal ChainPatrol escalated to you\nand you have not answered yet. These are exactly the proposals your Review page\nshows, and the ones counted by\n`GET /organization/reports?needsCustomerReview=true`.\n\nProposals on reports your organization sourced itself are not reviewable here:\nChainPatrol triages those and staff approval blocks the asset on its own, so\nthere is no decision for you to record. They return `403`."
2890
+ }
2891
+ },
2892
+ {
2893
+ name: "organization_reports_list",
2894
+ procedure: "organizationReportsList",
2895
+ method: "GET",
2896
+ path: "/organization/reports",
2897
+ pathParams: [],
2898
+ title: "List organization reports",
2899
+ description: "Answers 'which of my organization's reports match these filters?'. Returns full report records \u2014 proposals, assets, scans, reporter, SLA \u2014 with filtering and pagination, newest first. The organization comes from your API key, or pass `slug` when your credentials can reach more than one. To check whether a report already exists for specific assets before filing a new one, use `POST /reports/search`, which looks reports up by asset content in a single batched call.",
2900
+ tags: [
2901
+ "organization"
2902
+ ],
2903
+ readOnly: true,
2904
+ deprecated: false,
2905
+ inputSchema: {
2906
+ type: "object",
2907
+ properties: {
2908
+ limit: {
2909
+ type: "number",
2910
+ minimum: 1,
2911
+ maximum: 20
2912
+ },
2913
+ cursor: {
2914
+ anyOf: [
2915
+ {
2916
+ type: "number"
2917
+ },
2918
+ {
2919
+ type: "null"
2920
+ }
2921
+ ]
2922
+ },
2923
+ status: {
2924
+ type: "string",
2925
+ enum: [
2926
+ "TODO",
2927
+ "IN_PROGRESS",
2928
+ "CLOSED"
2929
+ ]
2930
+ },
2931
+ searchQuery: {
2932
+ type: "string"
2933
+ },
2934
+ reporterQuery: {
2935
+ type: "string"
2936
+ },
2937
+ reporterKind: {
2938
+ type: "string",
2939
+ enum: [
2940
+ "human",
2941
+ "automation"
2942
+ ]
2943
+ },
2944
+ reviewerKind: {
2945
+ type: "string",
2946
+ enum: [
2947
+ "human",
2948
+ "automation"
2949
+ ]
2950
+ },
2951
+ reviewedByUserId: {
2952
+ anyOf: [
2953
+ {
2954
+ type: "number"
2955
+ },
2956
+ {
2957
+ type: "null"
2958
+ }
2959
+ ]
2960
+ },
2961
+ startDate: {
2962
+ type: "string",
2963
+ format: "date-time"
2964
+ },
2965
+ endDate: {
2966
+ type: "string",
2967
+ format: "date-time"
2968
+ },
2969
+ updatedAtStartDate: {
2970
+ type: "string",
2971
+ format: "date-time"
2972
+ },
2973
+ updatedAtEndDate: {
2974
+ type: "string",
2975
+ format: "date-time"
2976
+ },
2977
+ registrars: {
2978
+ type: "array",
2979
+ items: {
2980
+ type: "string"
2981
+ }
2982
+ },
2983
+ hasMxRecords: {
2984
+ type: "boolean"
2985
+ },
2986
+ slug: {
2987
+ type: "string",
2988
+ description: "Organization slug. Optional for organization-scoped API keys, which resolve the organization from the key itself. Required when your credentials can reach more than one organization."
2989
+ },
2990
+ excludeAutomation: {
2991
+ type: "boolean",
2992
+ default: false
2993
+ },
2994
+ onlyRejected: {
2995
+ type: "boolean",
2996
+ default: false
2997
+ },
2998
+ onlyFavorited: {
2999
+ type: "boolean",
3000
+ default: false
3001
+ },
3002
+ reportedByCustomer: {
3003
+ type: "boolean"
3004
+ },
3005
+ needsCustomerReview: {
3006
+ type: "boolean",
3007
+ description: "Filter to reports waiting on your organization's own approval \u2014 the same queue your Review page shows. `true` returns only reports with at least one pending proposal that is yours to action; `false` returns only reports that are not. A pending proposal counts when ChainPatrol staff escalated it to you and you have not answered yet. Answering means approving, rejecting, or escalating it back to ChainPatrol. Two things do not put a report here: reports your organization sourced itself (ChainPatrol triages those and staff approval blocks the asset without you, though you still see them in the unfiltered list), and Obligatory Organization Admin Approval (a proposal ChainPatrol has not triaged yet is still waiting on our review, and reaches you only once staff escalate it)."
3008
+ },
3009
+ reviewStatuses: {
3010
+ type: "array",
3011
+ items: {
3012
+ type: "string",
3013
+ enum: [
3014
+ "APPROVE",
3015
+ "REJECT",
3016
+ "SKIP",
3017
+ "ESCALATE"
3018
+ ]
3019
+ }
3020
+ },
3021
+ assetTypes: {
3022
+ type: "array",
3023
+ items: {
3024
+ type: "string",
3025
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
3026
+ }
3027
+ },
3028
+ brandIds: {
3029
+ type: "array",
3030
+ items: {
3031
+ type: "number"
3032
+ }
3033
+ },
3034
+ countryCodes: {
3035
+ type: "array",
3036
+ items: {
3037
+ type: "string",
3038
+ minLength: 2,
3039
+ maxLength: 2
3040
+ }
3041
+ },
3042
+ sources: {
3043
+ type: "array",
3044
+ items: {
3045
+ type: "string",
3046
+ enum: [
3047
+ "APP",
3048
+ "API",
3049
+ "CANARY_TOKEN",
3050
+ "AUTO_DETECTION",
3051
+ "ASSET_MANAGEMENT"
3052
+ ]
3053
+ }
3054
+ },
3055
+ threatActorIds: {
3056
+ type: "array",
3057
+ items: {
3058
+ type: "number"
3059
+ }
3060
+ }
3061
+ },
3062
+ required: [
3063
+ "limit"
3064
+ ]
3065
+ }
3066
+ },
3067
+ {
3068
+ name: "report_create",
3069
+ procedure: "reportCreate",
3070
+ method: "POST",
3071
+ path: "/report/create",
3072
+ pathParams: [],
3073
+ title: "Create a report",
3074
+ description: "Create a new report on ChainPatrol for a particular organization",
3075
+ tags: [
3076
+ "report"
3077
+ ],
3078
+ readOnly: false,
3079
+ deprecated: false,
3080
+ inputSchema: {
3081
+ type: "object",
3082
+ properties: {
3083
+ organizationSlug: {
3084
+ type: "string",
3085
+ description: "Organization slug used to identify the organization on ChainPatrol"
3086
+ },
3087
+ discordGuildId: {
3088
+ type: "string",
3089
+ description: "Discord Guild (Server) ID linked to the organization on ChainPatrol"
3090
+ },
3091
+ telegramGroupId: {
3092
+ type: "string",
3093
+ description: "Telegram Group ID linked to the organization on ChainPatrol"
3094
+ },
3095
+ title: {
3096
+ type: "string",
3097
+ minLength: 3,
3098
+ description: "Title of the report"
3099
+ },
3100
+ description: {
3101
+ type: "string",
3102
+ description: "Description of the report. Supports markdown"
3103
+ },
3104
+ contactInfo: {
3105
+ type: "string",
3106
+ description: "Optional reporter contact info. If this value is a phone number, include a country code in E.164 format (e.g. +14155552671)."
3107
+ },
3108
+ attachmentUrls: {
3109
+ type: "array",
3110
+ items: {
3111
+ type: "string",
3112
+ format: "uri"
3113
+ },
3114
+ description: "URLs of images to attach to the report"
3115
+ },
3116
+ externalSubmissionLink: {
3117
+ type: "string",
3118
+ format: "uri",
3119
+ description: "Link to the external submission (e.g. Telegram message link)"
3120
+ },
3121
+ userAgent: {
3122
+ type: "string",
3123
+ description: "User agent string from the reporter's browser"
3124
+ },
3125
+ referrer: {
3126
+ type: "string",
3127
+ description: "Referrer URL from the reporter's browser"
3128
+ },
3129
+ assets: {
3130
+ type: "array",
3131
+ items: {
3132
+ type: "object",
3133
+ properties: {
3134
+ content: {
3135
+ type: "string",
3136
+ description: "Asset content. If providing a phone number, include a country code in E.164 format (e.g. +14155552671)."
3137
+ },
3138
+ status: {
3139
+ type: "string",
3140
+ enum: [
3141
+ "UNKNOWN",
3142
+ "ALLOWED",
3143
+ "BLOCKED"
3144
+ ],
3145
+ default: "BLOCKED",
3146
+ description: "Proposed asset status (defaults to BLOCKED if not provided)"
3147
+ },
3148
+ reporterConfidence: {
3149
+ type: "string",
3150
+ enum: [
3151
+ "HIGH",
3152
+ "LOW"
3153
+ ],
3154
+ description: "Reporter confidence level (applies special handling for trusted reporters and superusers only)"
3155
+ },
3156
+ brandSlug: {
3157
+ type: "string",
3158
+ minLength: 1,
3159
+ description: "Optional suggested brand slug for this proposed blocked asset"
3160
+ },
3161
+ enrichments: {
3162
+ type: "array",
3163
+ items: {
3164
+ type: "object",
3165
+ properties: {
3166
+ type: {
3167
+ type: "string",
3168
+ description: "Enrichment type One of 39 values, e.g. content_and_metadata, browser_capture, ownership_and_registration, geolocation_and_ip, dns, tls, tcp, post. Full list: resource chainpatrol://enums/asset_scan_enrichment_type"
3169
+ },
3170
+ source: {
3171
+ type: "string",
3172
+ minLength: 1,
3173
+ description: "Enrichment source (e.g., extension)"
3174
+ },
3175
+ output: {
3176
+ type: "object",
3177
+ additionalProperties: {},
3178
+ description: "Enrichment output data"
3179
+ }
3180
+ },
3181
+ required: [
3182
+ "type",
3183
+ "source"
3184
+ ]
3185
+ },
3186
+ description: "Enrichments to attach to this asset"
3187
+ }
3188
+ },
3189
+ required: [
3190
+ "content"
3191
+ ]
3192
+ }
3193
+ },
3194
+ rawAssetsInput: {
3195
+ type: "string"
3196
+ },
3197
+ externalReporter: {
3198
+ type: "object",
3199
+ properties: {
3200
+ avatarUrl: {
3201
+ type: "string",
3202
+ format: "uri",
3203
+ description: "URL of the external avatar"
3204
+ },
3205
+ platformIdentifier: {
3206
+ type: "string",
3207
+ description: "Unique identifier on the external platform"
3208
+ },
3209
+ platform: {
3210
+ type: "string",
3211
+ description: "External platform's name"
3212
+ },
3213
+ displayName: {
3214
+ type: "string",
3215
+ description: "User's public (mutable) display name"
3216
+ }
3217
+ },
3218
+ required: [
3219
+ "platformIdentifier",
3220
+ "platform",
3221
+ "displayName"
3222
+ ]
3223
+ }
3224
+ },
3225
+ required: [
3226
+ "assets"
3227
+ ]
3228
+ }
3229
+ },
3230
+ {
3231
+ name: "report_search_external",
3232
+ procedure: "reportSearchExternal",
3233
+ method: "POST",
3234
+ path: "/reports/search",
3235
+ pathParams: [],
3236
+ title: "Look up reports by asset content",
3237
+ description: "Answers 'do reports already exist for these specific assets?'. Takes a batch of asset contents, normalizes each one the way ChainPatrol normalizes assets, and returns the reports covering them along with which of your assets each report matched. Use it to avoid filing a duplicate before calling `POST /report/create`. To browse or filter an organization's reports instead \u2014 by status, asset type, brand, date, review state \u2014 use `GET /organization/reports`, which returns the full report records with pagination.",
3238
+ tags: [
3239
+ "reports"
3240
+ ],
3241
+ readOnly: true,
3242
+ deprecated: false,
3243
+ inputSchema: {
3244
+ type: "object",
3245
+ properties: {
3246
+ assetContents: {
3247
+ type: "array",
3248
+ items: {
3249
+ type: "string"
3250
+ },
3251
+ description: "Asset contents to look up \u2014 URLs, handles, addresses. Each is normalized the way ChainPatrol normalizes assets, so `https://Bad.Site/` matches a report holding the canonical form."
3252
+ },
3253
+ slug: {
3254
+ type: "string",
3255
+ description: "Organization slug. Optional for organization-scoped API keys, which resolve the organization from the key itself. Required when your credentials can reach more than one organization."
3256
+ },
3257
+ reportedByCustomer: {
3258
+ type: "boolean"
3259
+ },
3260
+ limit: {
3261
+ type: "integer",
3262
+ minimum: 1,
3263
+ maximum: 100,
3264
+ default: 50,
3265
+ description: "Maximum number of reports to return, newest first. Defaults to 50, maximum 100. Compare with `totalCount` to tell whether the results were truncated."
3266
+ }
3267
+ },
3268
+ required: [
3269
+ "assetContents"
3270
+ ]
3271
+ }
3272
+ },
3273
+ {
3274
+ name: "scan_results",
3275
+ procedure: "scanResults",
3276
+ method: "POST",
3277
+ path: "/scan/result",
3278
+ pathParams: [],
3279
+ title: "Get scan results by scan ID",
3280
+ description: "Get detailed scan results including enrichments, rules/checks results, and labels for a specific asset scan.",
3281
+ tags: [
3282
+ "scan"
3283
+ ],
3284
+ readOnly: true,
3285
+ deprecated: false,
3286
+ inputSchema: {
3287
+ type: "object",
3288
+ properties: {
3289
+ scanId: {
3290
+ type: "number",
3291
+ description: "The ID of the asset scan to retrieve results for"
3292
+ },
3293
+ workflowId: {
3294
+ type: "string",
3295
+ description: "Optional Temporal workflow ID to check if scan is still processing"
3296
+ }
3297
+ },
3298
+ required: [
3299
+ "scanId"
3300
+ ]
3301
+ }
3302
+ },
3303
+ {
3304
+ name: "takedowns_list",
3305
+ procedure: "takedownsList",
3306
+ method: "POST",
3307
+ path: "/takedowns/list",
3308
+ pathParams: [],
3309
+ title: "List takedowns",
3310
+ description: "List takedowns for an organization. Authenticate with an API key (the org is derived from the key) or with a user session (pass `organizationSlug` in the request body).",
3311
+ tags: [
3312
+ "takedowns"
3313
+ ],
3314
+ readOnly: true,
3315
+ deprecated: false,
3316
+ inputSchema: {
3317
+ type: "object",
3318
+ properties: {
3319
+ organizationSlug: {
3320
+ type: "string",
3321
+ description: "Organization slug. Required when authenticating with a user session (Bearer token); ignored when using an API key (org is derived from the key)."
3322
+ },
3323
+ query: {
3324
+ type: "string",
3325
+ description: "Search query to filter takedowns by asset content"
3326
+ },
3327
+ startDate: {
3328
+ type: "string",
3329
+ description: "The start date to list takedowns from. This should be in the format `YYYY-MM-DD` and is inclusive."
3330
+ },
3331
+ endDate: {
3332
+ type: "string",
3333
+ description: "The end date to list takedowns to. This should be in the format `YYYY-MM-DD` and is inclusive."
3334
+ },
3335
+ assetType: {
3336
+ type: "array",
3337
+ items: {
3338
+ type: "string",
3339
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
3340
+ },
3341
+ description: "Filter by asset types"
3342
+ },
3343
+ takedownStatus: {
3344
+ type: "array",
3345
+ items: {
3346
+ type: "string",
3347
+ enum: [
3348
+ "TODO",
3349
+ "IN_PROGRESS",
3350
+ "COMPLETED",
3351
+ "CANCELLED",
3352
+ "PENDING_RETRACTION",
3353
+ "RETRACTION_SENT",
3354
+ "RETRACTED",
3355
+ "PENDING_INPUT",
3356
+ "PENDING_EVIDENCE"
3357
+ ]
3358
+ },
3359
+ description: "Filter by takedown status"
3360
+ },
3361
+ livenessStatus: {
3362
+ type: "array",
3363
+ items: {
3364
+ type: "string",
3365
+ enum: [
3366
+ "UNKNOWN",
3367
+ "ALIVE",
3368
+ "DEAD"
3369
+ ]
3370
+ },
3371
+ description: "Filter by liveness status"
3372
+ },
3373
+ brandIds: {
3374
+ type: "array",
3375
+ items: {
3376
+ type: "integer",
3377
+ exclusiveMinimum: 0
3378
+ },
3379
+ description: "Filter by brand IDs"
3380
+ },
3381
+ assigneeIds: {
3382
+ type: "array",
3383
+ items: {
3384
+ type: "integer",
3385
+ exclusiveMinimum: 0
3386
+ },
3387
+ description: "Filter by takedown assignee user IDs"
3388
+ },
3389
+ startedAtStartDate: {
3390
+ type: "string",
3391
+ description: "Inclusive start of the `takedown started at` date range, in YYYY-MM-DD or ISO 8601. A takedown's started-at is the earliest IN_PROGRESS status-change event."
3392
+ },
3393
+ startedAtEndDate: {
3394
+ type: "string",
3395
+ description: "Inclusive end of the `takedown started at` date range. Defaults to the current time when `startedAtStartDate` is provided alone."
3396
+ },
3397
+ hideAutomatedTakedowns: {
3398
+ type: "boolean",
3399
+ description: "Hide takedowns whose target asset type (or content) is handled by the automated platform-takedown pipeline (e.g. Telegram, Medium, *.webflow.io)."
3400
+ },
3401
+ hideAutomatedLivenessChecks: {
3402
+ type: "boolean",
3403
+ description: "Hide takedowns whose asset type (or content) is checked for liveness automatically (e.g. Twitter, Bluesky, *.gitbook.io)."
3404
+ },
3405
+ sorting: {
3406
+ type: "array",
3407
+ items: {
3408
+ type: "object",
3409
+ properties: {
3410
+ key: {
3411
+ type: "string",
3412
+ enum: [
3413
+ "updatedAt",
3414
+ "createdAt",
3415
+ "takedownStatus",
3416
+ "takedownUpdatedAt",
3417
+ "assigneeId",
3418
+ "brandId"
3419
+ ],
3420
+ description: "Field to sort by"
3421
+ },
3422
+ direction: {
3423
+ type: "string",
3424
+ enum: [
3425
+ "asc",
3426
+ "desc"
3427
+ ],
3428
+ description: "Sort direction"
3429
+ }
3430
+ },
3431
+ required: [
3432
+ "key",
3433
+ "direction"
3434
+ ]
3435
+ },
3436
+ description: "Sorting configuration"
3437
+ },
3438
+ per_page: {
3439
+ type: "integer",
3440
+ minimum: 1,
3441
+ maximum: 100,
3442
+ default: 10,
3443
+ description: "The number of takedowns to return per page"
3444
+ },
3445
+ next_page: {
3446
+ anyOf: [
3447
+ {
3448
+ anyOf: [
3449
+ {
3450
+ not: {}
3451
+ },
3452
+ {
3453
+ type: "string"
3454
+ }
3455
+ ]
3456
+ },
3457
+ {
3458
+ type: "null"
3459
+ }
3460
+ ],
3461
+ description: "Cursor for fetching the next page of results"
3462
+ }
3463
+ },
3464
+ description: "List takedowns request body\n\nDefaults to getting all takedowns in the last 30 days.\n\nYou can also choose a `startDate` and `endDate` for the range of takedown updates, most \ntimestamp formats should work, we use [Luxon](https://moment.github.io/luxon/#/parsing) \nfor parsing the dates."
3465
+ }
3466
+ },
3467
+ {
3468
+ name: "threats_list",
3469
+ procedure: "threatsList",
3470
+ method: "POST",
3471
+ path: "/threats/list",
3472
+ pathParams: [],
3473
+ title: "List threats",
3474
+ description: "List threats for an organization using API key authentication",
3475
+ tags: [
3476
+ "threats"
3477
+ ],
3478
+ readOnly: true,
3479
+ deprecated: false,
3480
+ inputSchema: {
3481
+ type: "object",
3482
+ properties: {
3483
+ query: {
3484
+ type: "string",
3485
+ description: "Search query to filter threats by content"
3486
+ },
3487
+ startDate: {
3488
+ type: "string",
3489
+ description: "The start date to list threats from. This should be in the format `YYYY-MM-DD` and is inclusive."
3490
+ },
3491
+ endDate: {
3492
+ type: "string",
3493
+ description: "The end date to list threats to. This should be in the format `YYYY-MM-DD` and is inclusive."
3494
+ },
3495
+ assetType: {
3496
+ type: "array",
3497
+ items: {
3498
+ type: "string",
3499
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
3500
+ },
3501
+ description: "Filter by asset types"
3502
+ },
3503
+ sorting: {
3504
+ type: "array",
3505
+ items: {
3506
+ type: "object",
3507
+ properties: {
3508
+ key: {
3509
+ type: "string",
3510
+ description: "Field to sort by"
3511
+ },
3512
+ direction: {
3513
+ type: "string",
3514
+ enum: [
3515
+ "asc",
3516
+ "desc"
3517
+ ],
3518
+ description: "Sort direction"
3519
+ }
3520
+ },
3521
+ required: [
3522
+ "key",
3523
+ "direction"
3524
+ ]
3525
+ },
3526
+ description: "Sorting configuration"
3527
+ },
3528
+ per_page: {
3529
+ type: "integer",
3530
+ minimum: 1,
3531
+ maximum: 100,
3532
+ default: 10,
3533
+ description: "The number of threats to return per page"
3534
+ },
3535
+ next_page: {
3536
+ anyOf: [
3537
+ {
3538
+ anyOf: [
3539
+ {
3540
+ not: {}
3541
+ },
3542
+ {
3543
+ type: "string"
3544
+ }
3545
+ ]
3546
+ },
3547
+ {
3548
+ type: "null"
3549
+ }
3550
+ ],
3551
+ description: "Cursor for fetching the next page of results"
3552
+ }
3553
+ },
3554
+ description: "List threats request body\n\nDefaults to getting all threats in the last 1 day.\n\nYou can also choose a `startDate` and `endDate` for the range of threat updates, most \ntimestamp formats should work, we use [Luxon](https://moment.github.io/luxon/#/parsing) \nfor parsing the dates."
3555
+ }
3556
+ },
3557
+ {
3558
+ name: "user_me",
3559
+ procedure: "userMe",
3560
+ method: "POST",
3561
+ path: "/user/me",
3562
+ pathParams: [],
3563
+ title: "Get current user",
3564
+ description: "Get details of currently logged in user",
3565
+ tags: [
3566
+ "user"
3567
+ ],
3568
+ readOnly: true,
3569
+ deprecated: false,
3570
+ acceptsNoInput: true,
3571
+ inputSchema: {
3572
+ type: "object",
3573
+ properties: {}
3574
+ }
3575
+ },
3576
+ {
3577
+ name: "user_me_update",
3578
+ procedure: "userMeUpdate",
3579
+ method: "POST",
3580
+ path: "/user/me/update",
3581
+ pathParams: [],
3582
+ title: "Update current user",
3583
+ description: "Update the currently authenticated user's profile and settings",
3584
+ tags: [
3585
+ "user"
3586
+ ],
3587
+ readOnly: false,
3588
+ deprecated: false,
3589
+ inputSchema: {
3590
+ type: "object",
3591
+ properties: {
3592
+ name: {
3593
+ type: "string",
3594
+ minLength: 1
3595
+ },
3596
+ avatarUrl: {
3597
+ anyOf: [
3598
+ {
3599
+ type: "string",
3600
+ format: "uri"
3601
+ },
3602
+ {
3603
+ type: "null"
3604
+ }
3605
+ ]
3606
+ },
3607
+ protectionConfig: {
3608
+ type: "object",
3609
+ properties: {
3610
+ linkMonitoring: {
3611
+ type: "boolean"
3612
+ },
3613
+ socialMediaScanning: {
3614
+ type: "boolean"
3615
+ },
3616
+ historyScanning: {
3617
+ type: "boolean"
3618
+ }
3619
+ }
3620
+ }
3621
+ }
3622
+ }
3623
+ },
3624
+ {
3625
+ name: "user_org_get",
3626
+ procedure: "userOrgGet",
3627
+ method: "GET",
3628
+ path: "/user/orgs/{slug}",
3629
+ pathParams: [
3630
+ "slug"
3631
+ ],
3632
+ title: "Get an organization the caller can access",
3633
+ description: "Fetch a single organization by slug. Returns the same per-organization shape as `/user/orgs` (id, name, slug, avatarUrl, subscriptionStatus, services flags, integration connection status, `obligatoryAdminApproval`, `legalDocuments`, and `trademarkRegistrations`). The `integrations` object shows which third-party integrations are connected: `slack`, `discord`, `vercel`, `intercom`, and `moderation` are booleans; `telegram` is `{ connected, groupCount }`. The `legalDocuments` object reports whether the org's Letter of Authorization and Power of Attorney are on file along with a direct `fileUrl` to view each when present. `trademarkRegistrations` returns every org-level trademark on file with its issuing office, registration number, and (when uploaded) a `certificateFileUrl` link to the certificate PDF \u2014 an empty array means no trademarks are recorded for the org. Access requires the caller to have permission for the org: org-scoped API keys must match the slug; user sessions and user-scoped API keys need an active OrganizationMembership unless the caller is staff. Soft-deleted organizations are excluded.",
3634
+ tags: [
3635
+ "user"
3636
+ ],
3637
+ readOnly: true,
3638
+ deprecated: false,
3639
+ inputSchema: {
3640
+ type: "object",
3641
+ properties: {
3642
+ slug: {
3643
+ type: "string",
3644
+ minLength: 1,
3645
+ description: "Organization slug to fetch (must be one the caller can access)"
3646
+ }
3647
+ },
3648
+ required: [
3649
+ "slug"
3650
+ ],
3651
+ description: "Get a single organization by slug.\n\nReturns the same per-organization shape as `/user/orgs`. Access is gated by\nthe same rules: org-scoped API keys must match the slug; user sessions (and\nuser-scoped API keys) need an active OrganizationMembership unless the user\nhas a staff role."
3652
+ }
3653
+ },
3654
+ {
3655
+ name: "user_orgs",
3656
+ procedure: "userOrgs",
3657
+ method: "POST",
3658
+ path: "/user/orgs",
3659
+ pathParams: [],
3660
+ title: "Get user organizations",
3661
+ description: "List organizations accessible to the current user along with each org's subscription status, which services (reporting, reviewing, protection, takedowns, detection, dark web monitoring) are enabled, integration connection status (Slack, Discord, Telegram, Vercel, Intercom, Moderation API), whether Obligatory Organization Admin Approval is enabled, the org-level Letter of Authorization / Power of Attorney attachments, and every org-level trademark registration on file. Each service exposes an `active` flag; **only `takedowns` additionally exposes `automated`**. The `integrations` object shows which third-party integrations are connected: `slack`, `discord`, `vercel`, `intercom`, and `moderation` are booleans; `telegram` is `{ connected, groupCount }` since an org can have multiple Telegram groups. The `legalDocuments` block has `letterOfAuthorization` and `powerOfAttorney` entries, each with `{ present, fileName, fileUrl }`. `trademarkRegistrations` is the full list of org-level trademarks with their issuing office, registration number, and (when uploaded) a `certificateFileUrl` link \u2014 use it to identify which customers have or have not added trademarks. Optional `subscriptionStatus`, `services`, and `obligatoryAdminApproval` filters narrow the result server-side. `subscriptionStatus` defaults to the live set (PROSPECT/POC/ACTIVE/INTEGRATION); pass INACTIVE explicitly to include churned organizations.",
3662
+ tags: [
3663
+ "user"
3664
+ ],
3665
+ readOnly: true,
3666
+ deprecated: false,
3667
+ inputSchema: {
3668
+ type: "object",
3669
+ properties: {
3670
+ query: {
3671
+ type: "string",
3672
+ default: ""
3673
+ },
3674
+ subscriptionStatus: {
3675
+ type: "array",
3676
+ items: {
3677
+ type: "string",
3678
+ enum: [
3679
+ "PROSPECT",
3680
+ "POC",
3681
+ "ACTIVE",
3682
+ "INTEGRATION",
3683
+ "INACTIVE"
3684
+ ]
3685
+ },
3686
+ minItems: 1,
3687
+ description: "Statuses to include. Defaults to the live set (PROSPECT/POC/ACTIVE/INTEGRATION); pass INACTIVE explicitly to see churned organizations."
3688
+ },
3689
+ services: {
3690
+ type: "object",
3691
+ properties: {
3692
+ reporting: {
3693
+ type: "object",
3694
+ properties: {
3695
+ active: {
3696
+ type: "boolean"
3697
+ }
3698
+ }
3699
+ },
3700
+ reviewing: {
3701
+ type: "object",
3702
+ properties: {
3703
+ active: {
3704
+ type: "boolean"
3705
+ }
3706
+ }
3707
+ },
3708
+ protection: {
3709
+ type: "object",
3710
+ properties: {
3711
+ active: {
3712
+ type: "boolean"
3713
+ }
3714
+ }
3715
+ },
3716
+ takedowns: {
3717
+ type: "object",
3718
+ properties: {
3719
+ active: {
3720
+ type: "boolean"
3721
+ },
3722
+ automated: {
3723
+ type: "boolean"
3724
+ }
3725
+ }
3726
+ },
3727
+ detection: {
3728
+ type: "object",
3729
+ properties: {
3730
+ active: {
3731
+ type: "boolean"
3732
+ }
3733
+ }
3734
+ },
3735
+ darkWebMonitoring: {
3736
+ type: "object",
3737
+ properties: {
3738
+ active: {
3739
+ type: "boolean"
3740
+ }
3741
+ }
3742
+ }
3743
+ }
3744
+ },
3745
+ obligatoryAdminApproval: {
3746
+ type: "object",
3747
+ properties: {
3748
+ active: {
3749
+ type: "boolean"
3750
+ },
3751
+ assetTypes: {
3752
+ type: "array",
3753
+ items: {
3754
+ type: "string",
3755
+ description: "One of 92 values, e.g. URL, PAGE, ADDRESS, DISCORD, LINKEDIN, TWITTER, FACEBOOK, YOUTUBE. Full list: resource chainpatrol://enums/asset_type"
3756
+ },
3757
+ minItems: 1
3758
+ }
3759
+ }
3760
+ },
3761
+ pendingServiceApproval: {
3762
+ type: "object",
3763
+ properties: {
3764
+ active: {
3765
+ type: "boolean"
3766
+ },
3767
+ services: {
3768
+ type: "array",
3769
+ items: {
3770
+ type: "string",
3771
+ enum: [
3772
+ "protection",
3773
+ "takedowns"
3774
+ ]
3775
+ },
3776
+ minItems: 1
3777
+ }
3778
+ }
3779
+ }
3780
+ }
3781
+ }
3782
+ },
3783
+ {
3784
+ name: "validate",
3785
+ procedure: "validate",
3786
+ method: "GET",
3787
+ path: "/validate",
3788
+ pathParams: [],
3789
+ title: "Validate API key",
3790
+ description: "Validates if the provided API key is valid for Zapier integration",
3791
+ tags: [
3792
+ "auth"
3793
+ ],
3794
+ readOnly: true,
3795
+ deprecated: false,
3796
+ acceptsNoInput: true,
3797
+ inputSchema: {
3798
+ type: "object",
3799
+ properties: {}
3800
+ }
3801
+ },
3802
+ {
3803
+ name: "webhook_config",
3804
+ procedure: "webhookConfig",
3805
+ method: "POST",
3806
+ path: "/webhook/config",
3807
+ pathParams: [],
3808
+ title: "Configure webhook for organization",
3809
+ description: "Create and configure a webhook for an organization to receive real-time notifications for asset status updates and threat detections. Requires a valid API key with access to the specified organization.",
3810
+ tags: [
3811
+ "webhook"
3812
+ ],
3813
+ readOnly: false,
3814
+ deprecated: false,
3815
+ inputSchema: {
3816
+ type: "object",
3817
+ properties: {
3818
+ organizationSlug: {
3819
+ type: "string",
3820
+ description: "Organization slug to configure webhooks for"
3821
+ },
3822
+ subscriberUrl: {
3823
+ type: "string",
3824
+ format: "uri",
3825
+ description: "HTTPS URL where webhook events will be delivered"
3826
+ },
3827
+ description: {
3828
+ type: "string",
3829
+ description: "Optional description for the webhook"
3830
+ },
3831
+ active: {
3832
+ type: "boolean",
3833
+ default: true,
3834
+ description: "Whether the webhook should be active"
3835
+ },
3836
+ triggers: {
3837
+ type: "array",
3838
+ items: {
3839
+ type: "string",
3840
+ enum: [
3841
+ "DEBUG_TEST",
3842
+ "ASSET_STATUS_UPDATED",
3843
+ "ORGANIZATION_THREAT_DETECTION_ADDED",
3844
+ "ASSET_STATUS_ESCALATED"
3845
+ ]
3846
+ },
3847
+ minItems: 1,
3848
+ description: "Array of webhook events to subscribe to"
3849
+ }
3850
+ },
3851
+ required: [
3852
+ "organizationSlug",
3853
+ "subscriberUrl",
3854
+ "triggers"
3855
+ ]
3856
+ }
3857
+ }
3858
+ ],
3859
+ excluded: [
3860
+ {
3861
+ path: "/cloudflare/webhook",
3862
+ method: "POST",
3863
+ reason: "Inbound webhook receiver \u2014 Cloudflare calls this, users never do. Exposing it would let an agent forge takedown-status callbacks."
3864
+ },
3865
+ {
3866
+ path: "/github/webhook",
3867
+ method: "POST",
3868
+ reason: "Inbound webhook receiver \u2014 GitHub calls this, users never do. Exposing it would let an agent forge repository events."
3869
+ },
3870
+ {
3871
+ path: "/internal/asset/list/eth-phishing-detect",
3872
+ method: "GET",
3873
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3874
+ },
3875
+ {
3876
+ path: "/internal/getAutoDashboardTelegramGroups",
3877
+ method: "POST",
3878
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3879
+ },
3880
+ {
3881
+ path: "/internal/getDiscordConfig",
3882
+ method: "POST",
3883
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3884
+ },
3885
+ {
3886
+ path: "/internal/getDiscordGuildStatus",
3887
+ method: "POST",
3888
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3889
+ },
3890
+ {
3891
+ path: "/internal/getIntercomOrganization",
3892
+ method: "POST",
3893
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3894
+ },
3895
+ {
3896
+ path: "/internal/getOrganizationMetrics",
3897
+ method: "POST",
3898
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3899
+ },
3900
+ {
3901
+ path: "/internal/getSlackOrganizations",
3902
+ method: "POST",
3903
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3904
+ },
3905
+ {
3906
+ path: "/internal/getTakedown",
3907
+ method: "POST",
3908
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3909
+ },
3910
+ {
3911
+ path: "/internal/getTelegramOrganization",
3912
+ method: "POST",
3913
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3914
+ },
3915
+ {
3916
+ path: "/internal/notion/create-crm-page",
3917
+ method: "POST",
3918
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3919
+ },
3920
+ {
3921
+ path: "/internal/reports/search",
3922
+ method: "POST",
3923
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3924
+ },
3925
+ {
3926
+ path: "/internal/subscribeToTakedown",
3927
+ method: "POST",
3928
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3929
+ },
3930
+ {
3931
+ path: "/internal/telegram/group",
3932
+ method: "POST",
3933
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3934
+ },
3935
+ {
3936
+ path: "/internal/telegram/user",
3937
+ method: "POST",
3938
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3939
+ },
3940
+ {
3941
+ path: "/internal/updateDiscordConfig",
3942
+ method: "POST",
3943
+ reason: "Internal endpoint \u2014 ChainPatrol-owned plumbing (Slack/Telegram bots, CRM sync) rather than public API, and out of scope for the MCP surface by design."
3944
+ },
3945
+ {
3946
+ path: "/notion/webhook",
3947
+ method: "POST",
3948
+ reason: "Inbound webhook receiver \u2014 Notion calls this, users never do. Exposing it would let an agent forge CRM automation events."
3949
+ }
3950
+ ],
3951
+ enums: [
3952
+ {
3953
+ name: "asset_scan_enrichment_type",
3954
+ values: [
3955
+ "content_and_metadata",
3956
+ "browser_capture",
3957
+ "ownership_and_registration",
3958
+ "geolocation_and_ip",
3959
+ "dns",
3960
+ "tls",
3961
+ "tcp",
3962
+ "post",
3963
+ "profile",
3964
+ "twitter_followers",
3965
+ "cast",
3966
+ "farcaster_profile",
3967
+ "channel",
3968
+ "miniapp",
3969
+ "telegram_profile",
3970
+ "instagram_profile",
3971
+ "facebook_profile",
3972
+ "youtube_video",
3973
+ "youtube_channel",
3974
+ "apple_app_store_listing",
3975
+ "google_play_store_listing",
3976
+ "mozilla_addon",
3977
+ "reddit_subreddit",
3978
+ "email_validation",
3979
+ "phone_lookup",
3980
+ "bilibili_video",
3981
+ "bilibili_profile",
3982
+ "dailymotion_video",
3983
+ "vimeo_video",
3984
+ "vimeo_profile",
3985
+ "stackblitz_page",
3986
+ "leetcode_page",
3987
+ "npm_package",
3988
+ "pypi_package",
3989
+ "rust_package",
3990
+ "linkedin_profile",
3991
+ "meta_ad",
3992
+ "google_ad",
3993
+ "contract_deployer"
3994
+ ],
3995
+ usedBy: [
3996
+ "report_create.type"
3997
+ ]
3998
+ },
3999
+ {
4000
+ name: "asset_type",
4001
+ values: [
4002
+ "URL",
4003
+ "PAGE",
4004
+ "ADDRESS",
4005
+ "DISCORD",
4006
+ "LINKEDIN",
4007
+ "TWITTER",
4008
+ "FACEBOOK",
4009
+ "YOUTUBE",
4010
+ "REDDIT",
4011
+ "TELEGRAM",
4012
+ "GOOGLE_APP_STORE",
4013
+ "APPLE_APP_STORE",
4014
+ "AMAZON_APP_STORE",
4015
+ "MICROSOFT_APP_STORE",
4016
+ "TIKTOK",
4017
+ "INSTAGRAM",
4018
+ "THREADS",
4019
+ "MEDIUM",
4020
+ "CHROME_WEB_STORE",
4021
+ "MOZILLA_ADDONS",
4022
+ "OPERA_ADDONS",
4023
+ "EMAIL",
4024
+ "PATREON",
4025
+ "OPENSEA",
4026
+ "FARCASTER",
4027
+ "IPFS",
4028
+ "GOOGLE_FORM",
4029
+ "WHATSAPP",
4030
+ "DISCORD_USER",
4031
+ "QUORA",
4032
+ "GITHUB",
4033
+ "TEACHABLE",
4034
+ "SUBSTACK",
4035
+ "DEBANK",
4036
+ "TAWK_TO",
4037
+ "JOTFORM",
4038
+ "PRIMAL",
4039
+ "BLUESKY",
4040
+ "SNAPCHAT",
4041
+ "DESO",
4042
+ "PINTEREST",
4043
+ "FLICKR",
4044
+ "GALXE",
4045
+ "VELOG",
4046
+ "NPM",
4047
+ "PYPI",
4048
+ "HEX",
4049
+ "DOCKER_HUB",
4050
+ "VOCAL_MEDIA",
4051
+ "TECKFINE",
4052
+ "TENDERLY",
4053
+ "HACKMD",
4054
+ "ETSY",
4055
+ "ZAZZLE",
4056
+ "BASENAME",
4057
+ "BILIBILI_TV",
4058
+ "VIMEO",
4059
+ "DAILYMOTION",
4060
+ "PHONE_NUMBER",
4061
+ "SLACK",
4062
+ "CALENDLY",
4063
+ "NGROK",
4064
+ "RARIBLE",
4065
+ "RUST_PACKAGE",
4066
+ "FLATHUB",
4067
+ "VIDLII",
4068
+ "VEVIOZ",
4069
+ "ISSUU",
4070
+ "SOUNDCLOUD",
4071
+ "ZAPPER",
4072
+ "REDNOTE",
4073
+ "SAMSUNG_APP_STORE",
4074
+ "HUAWEI_APP_STORE",
4075
+ "XIAOMI_APP_STORE",
4076
+ "TENCENT_APP_STORE",
4077
+ "OPPO_APP_STORE",
4078
+ "VIVO_APP_STORE",
4079
+ "F_DROID",
4080
+ "GOOGLE_AD",
4081
+ "BING_AD",
4082
+ "TWITCH",
4083
+ "BEHANCE",
4084
+ "ZORA",
4085
+ "META_AD",
4086
+ "SIGNAL",
4087
+ "DEVIANTART",
4088
+ "BANDCAMP",
4089
+ "ARCHIVE_ORG",
4090
+ "FIVE_HUNDRED_PX",
4091
+ "LUMA",
4092
+ "SMARTMONEYMATCH",
4093
+ "APK_GOLD"
4094
+ ],
4095
+ usedBy: [
4096
+ "asset_changelog.type",
4097
+ "asset_list.type",
4098
+ "detection_list.value",
4099
+ "get_organization_reports.assetTypes",
4100
+ "metrics_takedown_time_trend.assetTypes",
4101
+ "organization_assets_list.type",
4102
+ "organization_reports_list.assetTypes",
4103
+ "takedowns_list.assetType",
4104
+ "threats_list.assetType",
4105
+ "user_orgs.assetTypes"
4106
+ ]
4107
+ },
4108
+ {
4109
+ name: "source",
4110
+ values: [
4111
+ "meta_ads_search",
4112
+ "telegram_channels_search",
4113
+ "telegram_user_search",
4114
+ "telegram_channels_search_vetric",
4115
+ "telegram_user_search_vetric",
4116
+ "facebook_page_search_vetric",
4117
+ "facebook_user_search_vetric",
4118
+ "instagram_account_search_vetric",
4119
+ "twitter_search_vetric",
4120
+ "linkedin_people_search_vetric",
4121
+ "linkedin_company_search_vetric",
4122
+ "meta_ads_search_vetric",
4123
+ "tik_tok_video_search",
4124
+ "tik_tok_user_search",
4125
+ "tik_tok_user_search_vetric",
4126
+ "tik_tok_video_search_vetric",
4127
+ "blocklist",
4128
+ "apple_app_store",
4129
+ "google_ads_search",
4130
+ "mozilla_addon_search",
4131
+ "reddit_subreddit_search",
4132
+ "asset_check",
4133
+ "twitter_post_search",
4134
+ "medium_tag_rss",
4135
+ "twitter_search",
4136
+ "yahoo_search",
4137
+ "duck_duck_go_search",
4138
+ "bing_search",
4139
+ "guestbook",
4140
+ "certstream",
4141
+ "external",
4142
+ "google_search",
4143
+ "dns_twist",
4144
+ "twitter",
4145
+ "twitter_username_monitor",
4146
+ "urlscan",
4147
+ "urlscan_hostname_search",
4148
+ "youtube_search",
4149
+ "google_play_search",
4150
+ "dexscreener_search",
4151
+ "blocked_ip_scan",
4152
+ "blocked_bilibili_suggested",
4153
+ "grok_post_search",
4154
+ "grok_user_search",
4155
+ "google_lens_image_search",
4156
+ "yandex_search",
4157
+ "yandex_image_search",
4158
+ "linkedin_employee_detection",
4159
+ "linkedin_post_search",
4160
+ "linkedin_company_search",
4161
+ "bing_ads_search",
4162
+ "tik_tok_ads_search",
4163
+ "dnsdb",
4164
+ "daily_motion_search",
4165
+ "watchlist"
4166
+ ],
4167
+ usedBy: [
4168
+ "detection_configs_create.source",
4169
+ "detection_list.value"
4170
+ ]
4171
+ }
4172
+ ],
4173
+ glossaries: [
4174
+ {
4175
+ uri: "chainpatrol://glossary/proposal-labels",
4176
+ title: "organization_proposals_review.label",
4177
+ text: "Why the asset is being blocked. Required when `decision` is `APPROVE`, ignored otherwise. One of: `Brand Impersonation`, `Employee Impersonation`, `Fake Employee`, `Targeting Org Users`, `General Phishing`, `C2 Server`, `False Positive`, `Organization Member Impersonation`, `Targeting Organization`.\n\n- `Brand Impersonation` \u2014 The asset is directly using the Brand's name, logo, or other trademarks\n- `Employee Impersonation` \u2014 The asset is impersonating a specific employee of the organization\n- `Fake Employee` \u2014 The asset claims to work at the company but is not directly impersonating a specific person\n- `Targeting Org Users` \u2014 A scam the org wants taken down that falls outside direct impersonation\n- `General Phishing` \u2014 This asset is trying to steal user funds\n- `C2 Server` \u2014 Command and Control server providing backend infrastructure for phishing operations\n- `False Positive` \u2014 This asset was incorrectly flagged and should be allowed\n- `Organization Member Impersonation` \u2014 The asset is impersonating a member or employee of the organization\n- `Targeting Organization` \u2014 A scam the org wants taken down that falls outside direct impersonation"
4178
+ },
4179
+ {
4180
+ uri: "chainpatrol://glossary/proposal-reject-reasons",
4181
+ title: "organization_proposals_review.rejectReason",
4182
+ text: "Why the proposal is being rejected. Required when `decision` is `REJECT`, ignored otherwise. These are the same reasons the Review page offers, and they feed ChainPatrol's detection-tuning breakdown \u2014 pick the closest one rather than relying on `note`."
4183
+ },
4184
+ {
4185
+ uri: "chainpatrol://glossary/proposal-watchlist-reasons",
4186
+ title: "organization_proposals_review.watchlistReason",
4187
+ text: "Why the asset is being parked. Required when `decision` is `WATCHLIST`, ignored otherwise. Use `OTHER` for anything not covered, in which case `note` becomes required and is treated as the reason. Note that the watchlist only covers asset types that can be re-checked for revival (URL, PAGE, EMAIL, and the social platforms); watchlisting any other type is rejected, so reject the proposal instead."
4188
+ }
4189
+ ]
4190
+ };
4191
+
4192
+ // src/server.ts
4193
+ var manifest = tools_default;
4194
+ function selectTools(only) {
4195
+ if (!only || only.length === 0) return manifest.tools;
4196
+ const wanted = new Set(only);
4197
+ const selected = manifest.tools.filter((tool) => wanted.has(tool.name));
4198
+ const unknown = only.filter((name) => !manifest.tools.some((t) => t.name === name));
4199
+ if (unknown.length > 0) {
4200
+ throw new Error(
4201
+ `Unknown MCP tool name(s): ${unknown.join(", ")}. Run with no filter to expose all ${manifest.tools.length} tools.`
4202
+ );
4203
+ }
4204
+ return selected;
4205
+ }
4206
+ var MAX_RESULT_CHARS = 1e5;
4207
+ function toToolResult(value) {
4208
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
4209
+ const text = serialized.length <= MAX_RESULT_CHARS ? serialized : `${serialized.slice(0, MAX_RESULT_CHARS)}
4210
+
4211
+ [Truncated: showing ${MAX_RESULT_CHARS} of ${serialized.length} characters, so this is no longer parseable JSON. Narrow the filters or request a smaller page to see a complete result.]`;
4212
+ return {
4213
+ content: [{ type: "text", text }]
4214
+ };
4215
+ }
4216
+ function toErrorResult(error) {
4217
+ const message = error instanceof ToolInvocationError || error instanceof Error ? error.message : String(error);
4218
+ return {
4219
+ isError: true,
4220
+ content: [{ type: "text", text: message }]
4221
+ };
4222
+ }
4223
+ function createChainPatrolMcpServer(options) {
4224
+ const tools = selectTools(options.only);
4225
+ const resources = buildResources(manifest, tools);
4226
+ const prompts = selectPrompts(
4227
+ PROMPTS,
4228
+ tools.map((tool) => tool.name)
4229
+ );
4230
+ const byName = new Map(tools.map((tool) => [tool.name, tool]));
4231
+ const server = new Server(
4232
+ {
4233
+ name: options.serverName ?? "chainpatrol",
4234
+ version: options.version ?? "0.0.0"
4235
+ },
4236
+ {
4237
+ capabilities: { tools: {}, resources: {}, prompts: {} },
4238
+ instructions: "ChainPatrol threat intelligence and brand protection. Every public API capability is available here as a tool: checking and searching assets, reports, proposals, detections, takedowns, metrics, healthchecks and organization management.\n\nMost tools are scoped to one organization. Credentials can reach more than one, and there is no implicit default, so call `user_orgs` first and pass the slug explicitly.\n\nWhere a parameter's description points at a `chainpatrol://` resource, read it before guessing a value."
4239
+ }
4240
+ );
4241
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
4242
+ tools: tools.map((tool) => ({
4243
+ name: tool.name,
4244
+ title: tool.title,
4245
+ description: tool.description,
4246
+ inputSchema: tool.inputSchema,
4247
+ annotations: {
4248
+ title: tool.title,
4249
+ readOnlyHint: tool.readOnly,
4250
+ /*
4251
+ * `destructiveHint` is only meaningful when `readOnlyHint` is false, and
4252
+ * it does not mean "irreversible" — per the MCP spec, `false` asserts
4253
+ * the tool performs *only additive* updates, and the default is `true`.
4254
+ *
4255
+ * The public API's mutations block and unblock assets, delete detection
4256
+ * configs and asset groups, remove allowlist entries, review proposals
4257
+ * and replace webhook configuration. Those modify or remove existing
4258
+ * state, so claiming additive-only would suppress the confirmation
4259
+ * hosts raise for exactly this kind of call. Whether staff can restore
4260
+ * the data afterwards is a different question from what the hint asks.
4261
+ */
4262
+ ...tool.readOnly ? {} : { destructiveHint: true },
4263
+ ...tool.deprecated ? { deprecated: true } : {}
4264
+ }
4265
+ }))
4266
+ }));
4267
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4268
+ const tool = byName.get(request.params.name);
4269
+ if (!tool) {
4270
+ return toErrorResult(
4271
+ new Error(
4272
+ `Unknown tool "${request.params.name}". Call tools/list for the current set.`
4273
+ )
4274
+ );
4275
+ }
4276
+ try {
4277
+ const args = request.params.arguments ?? {};
4278
+ return toToolResult(await options.invoker(tool, args));
4279
+ } catch (error) {
4280
+ return toErrorResult(error);
4281
+ }
4282
+ });
4283
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({
4284
+ resources: resources.map(({ uri, name, title, description, mimeType }) => ({
4285
+ uri,
4286
+ name,
4287
+ title,
4288
+ description,
4289
+ mimeType
4290
+ }))
4291
+ }));
4292
+ server.setRequestHandler(ReadResourceRequestSchema, (request) => {
4293
+ const resource = resources.find((item) => item.uri === request.params.uri);
4294
+ if (!resource) {
4295
+ throw new Error(`Unknown resource: ${request.params.uri}`);
4296
+ }
4297
+ return {
4298
+ contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.text }]
4299
+ };
4300
+ });
4301
+ server.setRequestHandler(ListPromptsRequestSchema, () => ({
4302
+ prompts: prompts.map(({ name, title, description, arguments: args }) => ({
4303
+ name,
4304
+ title,
4305
+ description,
4306
+ arguments: args
4307
+ }))
4308
+ }));
4309
+ server.setRequestHandler(GetPromptRequestSchema, (request) => {
4310
+ const prompt = prompts.find((item) => item.name === request.params.name);
4311
+ if (!prompt) {
4312
+ throw new Error(`Unknown prompt: ${request.params.name}`);
4313
+ }
4314
+ return {
4315
+ description: prompt.description,
4316
+ messages: [
4317
+ {
4318
+ role: "user",
4319
+ content: {
4320
+ type: "text",
4321
+ text: renderPrompt(prompt, request.params.arguments ?? {})
4322
+ }
4323
+ }
4324
+ ]
4325
+ };
4326
+ });
4327
+ return server;
4328
+ }
4329
+
4330
+ // src/http-invoker.ts
4331
+ var DEFAULT_BASE_URL = "https://app.chainpatrol.io";
4332
+ var DEFAULT_TIMEOUT_MS = 6e4;
4333
+ function trimTrailingSlashes(url) {
4334
+ return url.replace(/\/+$/, "");
4335
+ }
4336
+ function appendQueryValue(search, key, value) {
4337
+ if (value === void 0 || value === null) return;
4338
+ if (Array.isArray(value)) {
4339
+ for (const item of value) appendQueryValue(search, key, item);
4340
+ return;
4341
+ }
4342
+ if (typeof value === "object") {
4343
+ search.append(key, JSON.stringify(value));
4344
+ return;
4345
+ }
4346
+ const serialized = scalarToString(value);
4347
+ if (serialized === void 0 || serialized === "") return;
4348
+ search.append(key, serialized);
4349
+ }
4350
+ function scalarToString(value) {
4351
+ if (typeof value === "string") return value;
4352
+ if (typeof value === "number" || typeof value === "bigint") {
4353
+ return Number.isNaN(value) ? void 0 : value.toString();
4354
+ }
4355
+ if (typeof value === "boolean") return value ? "true" : "false";
4356
+ return void 0;
4357
+ }
4358
+ function applyPathParams(tool, args) {
4359
+ const rest = { ...args };
4360
+ let path = tool.path;
4361
+ for (const name of tool.pathParams) {
4362
+ const value = scalarToString(rest[name]);
4363
+ if (value === void 0 || value === "") {
4364
+ throw new ToolInvocationError(
4365
+ `${tool.name}: "${name}" is required and must be a string or number \u2014 it identifies the resource in the request path.`
4366
+ );
4367
+ }
4368
+ path = path.replace(`{${name}}`, encodeURIComponent(value));
4369
+ delete rest[name];
4370
+ }
4371
+ return { path, rest };
4372
+ }
4373
+ function acceptsBody(method) {
4374
+ return method === "POST" || method === "PATCH" || method === "PUT";
4375
+ }
4376
+ function createHttpInvoker(options) {
4377
+ const baseUrl = trimTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
4378
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4379
+ const doFetch = options.fetchImpl ?? fetch;
4380
+ return async (tool, args) => {
4381
+ const credential = await options.credential();
4382
+ const headers = {
4383
+ accept: "application/json",
4384
+ "user-agent": "chainpatrol-mcp"
4385
+ };
4386
+ if (credential.kind === "api-key") {
4387
+ headers["x-api-key"] = credential.value;
4388
+ } else {
4389
+ headers["authorization"] = `Bearer ${credential.value}`;
4390
+ }
4391
+ const { path, rest } = applyPathParams(tool, args);
4392
+ let url = `${baseUrl}/api/v2${path}`;
4393
+ let body;
4394
+ if (acceptsBody(tool.method)) {
4395
+ headers["content-type"] = "application/json";
4396
+ body = JSON.stringify(rest);
4397
+ } else {
4398
+ const search = new URLSearchParams();
4399
+ for (const [key, value] of Object.entries(rest)) {
4400
+ appendQueryValue(search, key, value);
4401
+ }
4402
+ const queryString = search.toString();
4403
+ if (queryString) url = `${url}?${queryString}`;
4404
+ }
4405
+ let response;
4406
+ try {
4407
+ response = await doFetch(url, {
4408
+ method: tool.method,
4409
+ headers,
4410
+ body,
4411
+ signal: AbortSignal.timeout(timeoutMs)
4412
+ });
4413
+ } catch (error) {
4414
+ if (error instanceof DOMException && error.name === "TimeoutError") {
4415
+ throw new ToolInvocationError(
4416
+ `${tool.name} timed out after ${timeoutMs}ms. Narrow the date range or filters and try again.`
4417
+ );
4418
+ }
4419
+ throw new ToolInvocationError(
4420
+ `Could not reach ChainPatrol at ${baseUrl}. Check network access and the configured API URL.`,
4421
+ { cause: error }
4422
+ );
4423
+ }
4424
+ const text = await response.text();
4425
+ let parsed;
4426
+ try {
4427
+ parsed = text.length > 0 ? JSON.parse(text) : null;
4428
+ } catch {
4429
+ parsed = text;
4430
+ }
4431
+ if (!response.ok) {
4432
+ const message = parsed?.message ?? `${tool.name} failed with HTTP ${response.status}.`;
4433
+ throw new ToolInvocationError(message, {
4434
+ status: response.status,
4435
+ details: parsed
4436
+ });
4437
+ }
4438
+ return parsed;
4439
+ };
4440
+ }
4441
+
4442
+ // src/tools-filter.ts
4443
+ var TOOLS_ENV_VAR = "CHAINPATROL_MCP_TOOLS";
4444
+ function toolsFromEnvironment(env = process.env) {
4445
+ const raw = env[TOOLS_ENV_VAR]?.trim();
4446
+ if (!raw) return void 0;
4447
+ const names = raw.split(",").map((name) => name.trim()).filter(Boolean);
4448
+ return names.length > 0 ? names : void 0;
4449
+ }
4450
+
4451
+ // src/version.ts
4452
+ var PACKAGE_VERSION = "1.10.0";
4453
+
4454
+ export {
4455
+ PROMPTS,
4456
+ renderPrompt,
4457
+ selectPrompts,
4458
+ buildResources,
4459
+ ToolInvocationError,
4460
+ manifest,
4461
+ MAX_RESULT_CHARS,
4462
+ createChainPatrolMcpServer,
4463
+ createHttpInvoker,
4464
+ TOOLS_ENV_VAR,
4465
+ toolsFromEnvironment,
4466
+ PACKAGE_VERSION
4467
+ };
4468
+ //# sourceMappingURL=chunk-AHMNJBKL.js.map