---
updatedAt: 2026-08-14T01:21:22.000Z
agentTools:
  projectIndex: https://readme.cipherowl.ai/llms.txt
---

# List risk changes in a pool

Time-ordered stream of risk change events for a pool, oldest first: one record per effective-level change detected by a scan (kind=SCAN, escalations and de-escalations included), per enrollment seed (kind=ENROLL), and per baseline move (kind=REBASELINE). Records for removed addresses are excluded. Records become visible a couple of minutes after they are detected; treat change_id as an idempotency key.

**Draining the feed:** Every non-empty page carries a next_page_token positioned after its last record, so a poller stores that token and resumes from it; an empty page (and its empty token) means you are caught up. Keep the last non-empty token across polls.

**since:** Optional coarse starting point for ad-hoc queries - records strictly newer than the given time. Records detected in the same write share one timestamp, so a since taken from a record you were served can skip that record's same-timestamp siblings unless you had already drained the feed; page_token is the exact resume position and is used instead of since whenever a non-empty page_token is supplied (an empty page_token counts as absent, so since still filters). A page_token that does not decode to a valid cursor, or that is bound to another endpoint or pool, is rejected with 400 rather than silently restarting or repositioning the feed, and a malformed since is rejected even when a page_token makes it stop filtering.

**Pagination:** Keyset pagination in detection order. Default 50 items per page, max 1000 (a page_size above the max is clamped to it; a negative page_size is rejected with 400).

# OpenAPI definition

```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "API ENDPOINTS",
    "description": "CipherOwl blockchain compliance and risk assessment APIs.",
    "version": "1.0"
  },
  "servers": [
    {
      "url": "https://svc.cipherowl.ai"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Continuous Monitoring",
      "description": "Monitor pools of blockchain addresses for risk changes. Create address pools, manage their membership, re-baseline accepted risk, and consume the pool's time-ordered feed of risk-change events (or one address's history). Members are identified by chain (e.g. `evm`) and address."
    }
  ],
  "paths": {
    "/api/monitoring/v1/pools/{poolId}/risk-changes": {
      "get": {
        "summary": "List risk changes in a pool",
        "description": "Time-ordered stream of risk change events for a pool, oldest first: one record per effective-level change detected by a scan (kind=SCAN, escalations and de-escalations included), per enrollment seed (kind=ENROLL), and per baseline move (kind=REBASELINE). Records for removed addresses are excluded. Records become visible a couple of minutes after they are detected; treat change_id as an idempotency key.\n\n**Draining the feed:** Every non-empty page carries a next_page_token positioned after its last record, so a poller stores that token and resumes from it; an empty page (and its empty token) means you are caught up. Keep the last non-empty token across polls.\n\n**since:** Optional coarse starting point for ad-hoc queries - records strictly newer than the given time. Records detected in the same write share one timestamp, so a since taken from a record you were served can skip that record's same-timestamp siblings unless you had already drained the feed; page_token is the exact resume position and is used instead of since whenever a non-empty page_token is supplied (an empty page_token counts as absent, so since still filters). A page_token that does not decode to a valid cursor, or that is bound to another endpoint or pool, is rejected with 400 rather than silently restarting or repositioning the feed, and a malformed since is rejected even when a page_token makes it stop filtering.\n\n**Pagination:** Keyset pagination in detection order. Default 50 items per page, max 1000 (a page_size above the max is clamped to it; a negative page_size is rejected with 400).",
        "operationId": "MonitoringService_ListRiskChanges",
        "responses": {
          "200": {
            "description": "A successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/v1ListRiskChangesResponse"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "poolId",
            "description": "Pool id, as returned by CreatePool/ListPools (a UUID). At most 64 bytes.",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageToken",
            "description": "Opaque token from a previous ListRiskChanges response for this pool —\ntokens are bound to the endpoint and pool that issued them. At most\n1024 bytes.",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "description": "Optional exclusive lower bound on detected_at (RFC 3339). A coarse\nstarting point for ad-hoc queries; ignored whenever a non-empty\npage_token is supplied, because the token is the exact resume position\n(an empty page_token counts as absent, so since still filters). \"Ignored\" means\nit stops filtering — a malformed value is still rejected.",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "tags": [
          "Continuous Monitoring"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "OAuth 2.0 access token obtained from /oauth/token using client credentials."
      }
    },
    "schemas": {
      "v1ListRiskChangesResponse": {
        "type": "object",
        "properties": {
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/v1RiskChangeRecord"
            }
          },
          "nextPageToken": {
            "type": "string",
            "description": "The resume position after the last record on this page. Unlike the\nsnapshot list endpoints, the feed sets this on EVERY non-empty page (a\ndurable cursor to come back to), and leaves it empty exactly when the\npage is empty - so it doubles as the \"you are caught up\" signal and\n`while next_page_token != \"\"` loops still terminate."
          }
        }
      },
      "v1RiskAssessment": {
        "type": "object",
        "properties": {
          "riskFound": {
            "type": "boolean",
            "description": "Convenience bool derived from risk_level: true iff risk_level != LOW."
          },
          "riskConfig": {
            "type": "string",
            "description": "The attributed risk config - normally the highest-level config that\nfound risk; on a scan record of a risky-to-LOW change, the previously\nrisky config. Empty for baselines (they record a level, not a config)\nand for members whose latest level is LOW."
          },
          "riskLevel": {
            "allOf": [
              {
                "$ref": "#/components/schemas/v1RiskLevel"
              }
            ],
            "description": "The effective risk level: the highest level among the pool's configs\nthat found risk; LOW means no risk found."
          }
        }
      },
      "v1RiskChangeKind": {
        "type": "string",
        "enum": [
          "RISK_CHANGE_KIND_UNSPECIFIED",
          "SCAN",
          "ENROLL",
          "REBASELINE"
        ],
        "default": "RISK_CHANGE_KIND_UNSPECIFIED",
        "description": "What kind of event a risk change record describes.\n\n - SCAN: An effective-level change detected by a periodic scan.\n - ENROLL: The enrollment seed: the level accepted as baseline when the address\nwas added.\n - REBASELINE: An operator baseline move (previous/current are baselines, not scan\nresults)."
      },
      "v1RiskChangeRecord": {
        "type": "object",
        "properties": {
          "poolId": {
            "type": "string"
          },
          "chain": {
            "type": "string",
            "description": "Canonical chain (e.g. evm)."
          },
          "address": {
            "type": "string",
            "description": "Normalized address."
          },
          "initialRisk": {
            "allOf": [
              {
                "$ref": "#/components/schemas/v1RiskAssessment"
              }
            ],
            "description": "The member's baseline (initial_risk_level) as it stands NOW, not as of\nthis record: it is a mutable member column that a re-baseline moves, so\nre-reading an older page after a re-baseline returns the new value.\nLevel only - baselines carry no attributed config. Carried here so a\nconsumer can answer \"how far has this address drifted from what we\nsigned off on\" without a second call."
          },
          "currentRisk": {
            "allOf": [
              {
                "$ref": "#/components/schemas/v1RiskAssessment"
              }
            ],
            "description": "Effective level after the change, with its attributed config."
          },
          "detectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the change was detected."
          },
          "previousRisk": {
            "allOf": [
              {
                "$ref": "#/components/schemas/v1RiskAssessment"
              }
            ],
            "description": "Effective level before the change (equals current_risk for ENROLL\nseeds). Level only - the record's attributed config is on current_risk."
          },
          "kind": {
            "$ref": "#/components/schemas/v1RiskChangeKind"
          },
          "changeId": {
            "type": "string",
            "description": "Stable record id; treat as an idempotency key when polling."
          }
        }
      },
      "v1RiskLevel": {
        "type": "string",
        "enum": [
          "RISK_LEVEL_UNSPECIFIED",
          "SEVERE",
          "HIGH",
          "MEDIUM",
          "LOW"
        ],
        "default": "RISK_LEVEL_UNSPECIFIED",
        "description": "Ordered risk levels: SEVERE > HIGH > MEDIUM > LOW. LOW is the \"no risk\nfound\" floor — it can never be bound to a risk config and never appears in\nopen_case_risk_levels."
      }
    }
  }
}
```